【问题标题】:Winsock send call is very slowWinsock 发送调用很慢
【发布时间】:2012-07-18 16:20:51
【问题描述】:

我正在制作一个可能需要处理 3000 多个连接的游戏服务器 (TCP)。 目前它有 50 个连接,我已经遇到了延迟。

我发现它的 winsock send() 调用每次返回大约需要 100~300 毫秒,这几乎会减慢整个服务器的速度,因为它是一个单线程服务器。

到目前为止,我已经想到了两种解决方案。

  1. 重新设计我的服务器,为每个客户端创建一个线程(为 3000 个客户端创建 3000+ 个线程真的稳定吗?)。
  2. 想办法让 send() 调用立即返回。

这是我的套接字初始化代码:

int ret = WSAStartup(MAKEWORD(2,2), &wsaData);
if(ret != 0) 
{ 
    printf("Winsock failed to start.\n");
    system("pause");
    return; 
}

server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(52000);

sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == INVALID_SOCKET) 
{ 
    printf("Invalid Socket.\n");
    system("pause");
    return; 
}

if(bind(sock, (sockaddr*)&server, sizeof(server)) != 0)
{ 
    printf("error");
    return; 
}

if(listen(sock, 5) != 0)
{
    printf("error");
    return;
}

在单独的线程中接受代码

sockaddr_in from;
int fromlen = sizeof(from);
SOCKET sTmpSocket = accept(ServerSock, (struct sockaddr*)&from, &fromlen);

我的发送功能

void CClient::SendPacket(BYTE* pPacket, DWORD Len)
{
    DWORD ToSendLen = Len;
    while (ToSendLen)
    {
        int iResult = send(this->ClientSocket, (char*)(pPacket + Len - ToSendLen), ToSendLen, 0);

        if (iResult == SOCKET_ERROR)
            return;

        ToSendLen -= iResult;
    }
}

这是我的服务器线程(不完整,仅相关部分)

while (1)
{
    for (int i = 0; i < MAX_CLIENT; i++)
    {
        if (Clients[i].bConnected == false)
            continue;

        timeval timeout;
        timeout.tv_sec = 0;
        timeout.tv_usec = 1;
        fd_set socketset;
        socketset.fd_count = 1;
        socketset.fd_array[0] = Clients[i].ClientSocket;
        if (select(0, &socketset, 0, 0, &timeout))
        {
            int RecvLen = recv(Clients[i].ClientSocket, (char*)pBuffer, 10000, MSG_PEEK);
            if (RecvLen == SOCKET_ERROR)
            {
                Clients[i].bConnected = false;
                iNumClients--;
            }
            else if (RecvLen == 0)
            {
                Clients[i].bConnected = false;
                iNumClients--;
            }
            else if (RecvLen > 0)
            {
                // Packet handling here
                recv(Clients[i].ClientSocket, (char*)pBuffer, dwDataLen, MSG_WAITALL);

                //...
            }
        }
    }

    Sleep(1);
}

任何帮助将不胜感激。 谢谢。

【问题讨论】:

  • 这就是游戏使用 UDP 的原因——尽管 UDP 不能保证作为数据包被接收。另外,您发送的数据有多大?
  • 你应该明确地找到一种方法让发送调用立即返回。 3000 theads 对服务器来说真的很贵。我不具体了解winsock,所以不会回答这个问题,但在C#中可以通过使用异步调用来完成。
  • 游戏服务器正在使用 TCP 来获取房间列表和重要数据包。这是我遇到瓶颈的地方。我已经将 UDP 用于玩家移动等游戏玩法。
  • 100-300ms 非常可疑。中间的 Bang 是 200 毫秒 - 默认的 nagle 算法延迟。 3000 个线程本身并不是一个特别大的数字。在 32 位系统上,您需要将堆栈大小保持在合理的水平(即没有 1MB 默认值),以免耗尽虚拟地址空间。 IOCP 和类似技术确实可以更好地扩展——阻塞调用是由内核线程池进行的,因此消除了很多堆栈和环周期,并且至少有可能避免数据复制,因为用户线程为要写入的数据提供缓冲区直接由司机。

标签: c++ winsock send


【解决方案1】:

我会选择IOCP (IO completion ports),它可以带来理想的性能并且可以很好地扩展。看看Boost.Asio,它在后台使用IOCP(在Windows 上)。

拥有 3000 或更多线程的想法非常糟糕,而且确实无法扩展(就内存消耗和上下文切换而言)!

【讨论】:

  • 我查看了 IOCP,我发现它可以帮助我检测套接字事件,而不是执行 for 循环。这对 send() 调用延迟有何帮助?
  • IOCP 有助于使发送操作异步。参见例如boost::asio::async_write(..)
【解决方案2】:

马上想到一件事,那就是使用非阻塞套接字并检查套接字是否可写select

作为旁注,您使用来自select 的返回值是错误的。它在超时时返回0,如果任何集合中都有准备好的套接字,则返回一个正数,如果有错误,则返回-1。您不检查错误。此外,虽然在 winsock 版本中不需要,select 的第一个参数实际上应该是最高的套接字号加一。

另外一点,您一次只为一个客户端使用select,将所有个客户端添加到集合中,然后执行select

fd_set readset;
FD_ZERO(&readset);
SOCKET max_socket = 0;

for (int i = 0; i < MAX_CLIENT; i++)
{
    if (Clients[i].bConnected)
    {
        FD_SET(Clients[i].ClientSocket, &readset);
        max_socket = std::max(max_socket, Clients[i].ClientSocket);
    }
}

int res = select(max_socket + 1, &readset, 0, 0, &timeout);
if (res == SOCKET_ERROR)
    std::cout << "Error #" << WSAGetLastError() << '\n';
else if (res > 0)
{
    for (int i = 0; i < MAX_CLIENT; i++)
    {
        if (Clients[i].bConnected && FD_ISSET(Clients[i].ClientSocket, &readset))
        {
            // Read from client
        }
    }
}

【讨论】:

  • 您的想法是为每个客户端创建一个发送缓冲区,每次在该缓冲区上附加要发送的数据,然后在单独的线程中检查套接字是否可写,然后再为每个客户端调用 winsock send()客户?如果套接字是可写的,winsock send() 调用不会花费 100 毫秒以上?
  • 哦,这个想法是错误的。 fdset 最多可以处理 64 个套接字。
  • @user1537941 默认情况下,fd_set 应该能够包含 1024 个套接字。 可能在包含&lt;winsock.h&gt;之前增加#define FD_SETSIZE
  • @user1537941 我不知道时机,但至少做一个大的send会比做小而分散的send更有效。
【解决方案3】:

要考虑的另一种方法是让多个工作线程处理一定比例的活动连接并使其可配置。例如,您可以为每个内核创建 n 个线程,然后对 n 进行试验。

这应该会给您带来很多多线程的好处,而不会达到每个客户端一个线程的极端情况。

【讨论】:

  • 是的,它会带来提升,但由于每次发送调用超过 300 毫秒,它仍然会阻塞。
【解决方案4】:

如果您只发送小数据包,请先使用 setsockopt() 设置 TCP_NODELAY 选项。

【讨论】:

    猜你喜欢
    • 2021-10-27
    • 2017-06-08
    • 1970-01-01
    • 2013-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-17
    相关资源
    最近更新 更多