在UNIX中通过recv/send交换数据时,如何正确使用缓冲区



我正在进行一个简单的聊天,必须将一个成员制作的短信发送给所有其他成员。每个人都必须收到的信息格式是"[IP]:你好!"。此外,当有人连接或断开连接时,服务器必须通知每个人:"[IP]已连接"one_answers"[IP]isdoosconnected"。

下面是一段代码,用于实现服务器的这一功能。你可以从"问题在这里"的行开始查看评论:

while (true)
{
// select() for reading
static constexpr int bufferSize = 1024;
static char buffer[bufferSize];
// this is for getting IP-address of sender
static sockaddr_in sockAddr;
static socklen_t sockAddrSize;
if (FD_ISSET(masterSocket, &set)) // masterSocket is a socket that establishes connections
{
sockAddrSize = sizeof(sockAddr);
int slaveSocket = accept(masketSocket, &sockAddr, &sockAddrSize); // slaveSocket is a client socket
// setting a slaveSocket non-blocking
sprintf(buffer, "[%d.%d.%d.%d] has connectedn", 
(sockAddr.sin_addr.s_addr & 0x000000FF), 
(sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8, 
(sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16, 
(sockAddr.sin_addr.s_addr & 0xFF000000) >> 24);
for (const auto &socket : slaveSockets)
send(socket, buffer, strlen(buffer), MSG_NOSIGNAL);
slaveSockets.insert(slaveSocket);
}
for (const auto &socket : slaveSockets)
{
if (FD_ISSET(socket, &set))
continue;
static int recvSize = recv(socket, buffer, bufferSize, MSG_NOSIGNAL);
if (recvSize == 0 && errno != EAGAIN)
{
sockAddrSize = sizeof(sockAddr);
getsockname(socket, (sockaddr *) &sockAddr, &sockAddrSize);
sprintf(buffer, "[%d.%d.%d.%d] has disconnectedn", 
(sockAddr.sin_addr.s_addr & 0x000000FF), 
(sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8, 
(sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16, 
(sockAddr.sin_addr.s_addr & 0xFF000000) >> 24);
shutdown(socket, SHUT_RDWR);
close(socket);
slaveSockets.erase(socket);
for (const auto &socket : slaveSockets)
send(socket, buffer, strlen(buffer), MSG_NOSIGNAL);
}
else if (recvSize > 0) // THE PROBLEM IS HERE
{
static char reply[bufferSize];
sockAddrSize = sizeof(&sockAddr);
getsocklen(socket, (sockaddr *) &sockAddr, &sockAddrSize);
sprintf(reply, "[%d.%d.%d.%d]: %sn",
(sockAddr.sin_addr.s_addr & 0x000000FF),
(sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8,
(sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16,
(sockAddr.sin_addr.s_addr & 0xFF000000) >> 24,
buffer);
int senderSocket = socket;
for (const auto &socket : slaveSockets)
{
if (socket == senderSocket)
continue;
send(socket, reply, strlen(reply), MSG_NOSIGNAL); // even tried the "strlen(reply) + 1"
}
}
}
}

问题是,接收方错误地输出了每条消息:它被完全输出,但最后也有缓冲区旧值的末尾。例如:

客户端A已连接。

客户端B已连接。客户端A已收到"[120.0.1]已连接"。

客户A发送了"你好"。客户端B收到"[120.0.1]:你好\n0.1]已连接\n"。

客户B发送了"怎么了?"。客户端A已收到"[120.0.1]:怎么了?\n已连接\n"。

客户端A已断开连接。客户端B已收到"[120.0.1]已断开连接"。

正如你所看到的,连接/断开信息总是正确输出,但聊天是错误的:它最终包含了连接/断连信息的一部分。

我真诚地相信我正确地使用了缓冲区,不能理解我做错了什么。

recv返回后,buffer不是以null结尾的C字符串。这是合乎逻辑的——如果传输二进制数据怎么办?然后,您将希望recv正好(消息长度(字节,并且不附加任何零字节。

请注意,在send中发送终止的零字节是错误的做法-接收器依赖于发送器来附加这个零字节,但如果发送器是恶意的,那么他可能不会附加零字节,并导致各种错误和漏洞,包括DoS攻击和远程代码执行。

您可能仍然依赖于发送方附加零字节,但您应该将bufferSize-1作为缓冲区长度传递给recv,并在调用recv后设置reply[bufferSize-1]=0。但也许这仍然不是最好的做法:众多其他选项之一是将"消息长度"作为32位无问题整数传递,检查最大长度(例如,没有消息大于1024个字符,如果是,则不接收任何消息,只关闭套接字(,并将传递的"消息长度"字节recv精确地传递到缓冲区。如果您打算将缓冲区用作C样式字符串,则仍然需要附加终止的空字节。

编辑:重要!如果使用TCP(SOCK_STREAM(,请始终使用消息长度:recv可能(将来也会(分段读取消息。你们绝对应该自己把它们连接成整个消息。

为了添加到smitsyn的答案中,您应该使用recvSize(在有问题的分支中,这比0更大,因为您从其中一个客户端收到了一些东西(来设置缓冲区内的"\0"。

您的静态变量被初始化为0(默认值(,因此您的缓冲区确实在收到的最长消息之后立即包含一个"\0"(因为其余消息被覆盖(,并且您很幸运sprintf找到了它,并且没有在程序内存中的其他地方搜索它(甚至超出了范围(。

沿着这些路线的东西应该使它工作(对于一个简单的案例(:

else if (recvSize > 0) // THE PROBLEM IS HERE
{
...
buffer[recvSize] = ''; // of course make sure it fits! could use a std::min(recvSize, bufferSize - 1)
sprintf(reply, "[%d.%d.%d.%d]: %sn",
(sockAddr.sin_addr.s_addr & 0x000000FF),
(sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8,
(sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16,
(sockAddr.sin_addr.s_addr & 0xFF000000) >> 24,
buffer); // now this print should work as expected 

编辑:因为我不能发表评论:(

最新更新