首页 > 解决方案 > 计算出有多少客户端可以连接到我正在使用的一些 tcp 服务器代码

问题描述

更新

似乎 tcp 服务器最多可以处理 512 个文件描述符。由于第一个连接的客户端获得文件描述符 4,因此可以连接的最大客户端数为 509(即使对于第 509 个文件描述符,我也可以在服务器和客户端之间进行 io) . 我不太确定 512 限制来自哪里?即使将客户端数量限制在 509 以下,但如果同时连接的客户端超过 509 个,不幸的是,并非所有客户端都能够收到一条消息,即连接到服务器的客户端过多。

我仍然有一个问题,当我有MAX_CONNECTIONS = 500CLIENTS_TO_DISCONNECT = 500(或CLIENTS_TO_DISCONNECT = 400)时,test.cc程序不会终止,并且需要手动终止一堆 telnet 进程。有没有人在自己的机器上运行代码?如果他们有,了解人们是否遇到同样的问题会很有用。

我可以使用 epoll 找到的示例对我来说似乎要困难得多。这可能是必要的,但是有人知道使用 epoll 的任何相当简单的多客户端 tcp 服务器吗?

感谢那些花时间阅读这篇文章的人,尤其是那些回复的人。


更新 2

我错了,服务器可以处理大于 512 的文件描述符。如果我启动服务器然后运行两个副本,test.cc那么MAX_CONNECTIONS = 400服务器有 800 个客户端连接到它。服务器最多只能处理 1023 个文件描述符,但可以同时连接 1020 个客户端。

这意味着我之前遇到的 509 个连接的限制是客户端的限制test.cc,这很奇怪,因为我本来预计限制是 512,我猜想不知何故client.cc也在使用类似于文件描述符的数字服务器并撞到了类似的墙。我已经尝试使用超过 512 个 redi::pstream 变量来运行“echo 'hello'”,这似乎没有任何问题,所以我不确定限制来自哪里。

我仍然无法让 redi::pstream 在第 419 次之后连接的客户端上关闭。一个实例test.cc和多个test.cc运行实例都会发生这种情况。

我还设法对另一个使用 poll 而不是 select 的多客户端 tcp 服务器代码进行了一些更正(有关代码,请参见此处)。有趣的是它有完全相同的问题(一个test.cc运行实例最多可以连接 509 个客户端,服务器最多可以有 1020 个客户端,并且我无法让 redi::pstream 在第 419 个之后连接的客户端上关闭)。我认为这表明最多 509 个客户端使用一个实例连接的问题test.cc在于代码test.cc而不是服务器代码,并且可能还因为在第 419 次之后连接的客户端上关闭 redi::pstream 的麻烦。


更新 3

第二个 tcp 服务器在与客户端之间来回发送和接收消息的时间是第一个的两倍,所以我将使用我找到的原始代码(尽管我也可以看看我是否能找到一个可以处理超过连接了 1020 个客户端)。

如果您摆脱关闭 pstreams ( redi::pstream) 的语句,那么测试程序似乎正确结束(并且客户端在测试程序终止之前仍然断开连接)。但是,如果我在没有阅读的情况下让太多输入建立起来redi::pstream,测试程序将无法终止。

我还尝试了 libexecstream而不是 pstream。当我尝试打开超过 337 个流时,libexecstream 中断。所以我最多只能使用一个程序使用 libexecstream 将 337 个客户端连接到服务器。但是,如果我多次运行同一个程序,它可以将更多客户端连接到服务器。

使用 pstreams 我遇到的问题是,在 419 之后连接的客户端无法正确断开和关闭,程序会停止。libexecstreams 没有这个问题,进程/流正确关闭。当我使用 libexecstreams 连接说 300 个客户端时。我可以使用 pstream 连接另外 400 个客户端,但是在关闭 420 之后连接到服务器的客户端的 pstream 时再次遇到问题。虽然这可以通过上面建议的 pstreams 来解决,只需不在 pstream 上调用 close 即可。

您还可以将来自客户端的服务器输入“分组”,即。如果在 select/poll 接收到该消息之前到达的不止一条消息,则 read/recv 会将它们全部读入提供的缓冲区数组。如果合并的消息对于缓冲区来说太长,那么缓冲区末尾的消息可以被“切成两半”,并且不容易重新组合在一起。如果缓冲区大小不足以处理将在特定时间段内到达的所有分组消息,我建议这是一个相当大的问题。幸运的是,当我使用非常大的缓冲区大小时,io 的运行时间似乎没有任何重大变化。

但是要注意的一件事是缓冲区大小是否高于 3000。高于该值的某个位置,您不能再将 char 数组视为字符串,输出它并将其设置为等于字符串不起作用。您必须遍历 char 数组并将字符单独添加到字符串中。(请注意,将数据发送回客户端时不需要这样做,但如果您想要包含来自客户端的输入的缓冲区字符数组的字符串版本,则需要这样做)。


很抱歉,这篇文章很长,但这让我很难过。如果人们知道任何可以处理更多客户端而不会出现错误的东西,我愿意为 tcp 服务器使用其他代码(尽管这里的错误可能是我的错,我需要能够在检查输入时设置超时来自客户),如果有人重复我在这篇文章中提到的错误,请发帖说您也遇到了这些错误,即使您无法弄清楚错误发生的原因,这也是有帮助的。

我正在尝试学习如何设置多客户端 tcp 服务器,但是当我尝试测试有多少用户可以连接到我正在使用的 tcp 服务器代码时遇到了麻烦。

我正在使用的 tcp 服务器代码如下,它是此处提供的 tcp 服务器代码的略微修改版本。

注意:修改是在第 36 行输出 FD_SETSIZE(在我的机器上是 1024),将 max_clients 更改为 1500,跟踪连接了多少客户端(no_clients_connected),当 max_clients 已经连接时关闭新客户端的连接并输出连接的数量有新连接和客户端断开连接时的客户端。

您可以使用以下命令编译 tcp 服务器代码(调用时server.cc):

g++ -std=c++11 -Wall -Wextra -pedantic -c -o server.o server.cc
g++ -std=c++11 -Wall -Wextra -pedantic server.cc  -o server 

注意:有谁知道如何处理第 34 行关于从string常量到不推荐的转换的警告char*?(Orbit 中的 Lightness Races 指出了如何解决这个问题)。

如果您编译并运行 tcp 服务器代码,您应该能够通过telnet localhost 8888从终端窗口运行来连接到它。要退出,请输入ctrl+],然后quit在 telnet 提示符处。

//Example code: A simple server side code, which echos back the received message.
//Handle multiple socket connections with select and fd_set on Linux
#include <iostream>
#include <stdio.h>
#include <string.h>   //strlen
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>   //close
#include <arpa/inet.h>    //close
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros

#define TRUE   1
#define FALSE  0
#define PORT 8888

int main()
{
    int no_clients_connected = 0;
    int opt = TRUE;
    int master_socket , addrlen , new_socket , client_socket[1500] ,
          max_clients = 1500 , activity, i , valread , sd;
    int max_sd;
    struct sockaddr_in address;

    char buffer[1025];  //data buffer of 1K

    //set of socket descriptors
    fd_set readfds;

    //a message
    const char *message = "ECHO Daemon v1.0 \r\n";

    std::cout << "FD_SETSIZE " << FD_SETSIZE << std::endl;

    //initialise all client_socket[] to 0 so not checked
    for (i = 0; i < max_clients; i++)
    {
        client_socket[i] = 0;
    }

    //create a master socket
    if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0)
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    //set master socket to allow multiple connections ,
    //this is just a good habit, it will work without this
    if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt,
          sizeof(opt)) < 0 )
    {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    //type of socket created
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons( PORT );

    //bind the socket to localhost port 8888
    if (bind(master_socket, (struct sockaddr *)&address, sizeof(address))<0)
    {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Listener on port %d \n", PORT);

    //try to specify maximum of 3 pending connections for the master socket
    if (listen(master_socket, 3) < 0)
    {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    //accept the incoming connection
    addrlen = sizeof(address);
    puts("Waiting for connections ...");

    while(TRUE)
    {
        //clear the socket set
        FD_ZERO(&readfds);

        //add master socket to set
        FD_SET(master_socket, &readfds);
        max_sd = master_socket;

        //add child sockets to set
        for ( i = 0 ; i < max_clients ; i++)
        {
            //socket descriptor
            sd = client_socket[i];

            //if valid socket descriptor then add to read list
            if(sd > 0)
                FD_SET( sd , &readfds);

            //highest file descriptor number, need it for the select function
            if(sd > max_sd)
                max_sd = sd;
        }

        //wait for an activity on one of the sockets , timeout is NULL ,
        //so wait indefinitely
        activity = select( max_sd + 1 , &readfds , NULL , NULL , NULL);

        if ((activity < 0) && (errno!=EINTR))
        {
            printf("select error");
        }

        //If something happened on the master socket ,
        //then its an incoming connection
        if (FD_ISSET(master_socket, &readfds))
        {
            if ((new_socket = accept(master_socket,
                    (struct sockaddr *)&address, (socklen_t*)&addrlen))<0)
            {
                perror("accept");
                exit(EXIT_FAILURE);
            }

            //inform user of socket number - used in send and receive commands
            printf("New connection , socket fd is %d , ip is : %s , port : %d\n" , new_socket , inet_ntoa(address.sin_addr) , ntohs(address.sin_port));

            if(no_clients_connected >= max_clients)
            {
                close(new_socket);
                std::cout << "kicked them because too many clients connected" << std::endl;
            }
            else
            {
                no_clients_connected++;

                //send new connection greeting message
                if( (size_t) send(new_socket, message, strlen(message), 0) != strlen(message) )
                {
                    perror("send");
                }

                puts("Welcome message sent successfully");

                //add new socket to array of sockets
                for (i = 0; i < max_clients; i++)
                {
                    //if position is empty
                    if( client_socket[i] == 0 )
                    {
                        client_socket[i] = new_socket;
                        printf("Adding to list of sockets as %d\n" , i);

                        break;
                    }
                }
            }
            std::cout << "number of clients connected is " << no_clients_connected << std::endl;
        }

        //else its some IO operation on some other socket
        for (i = 0; i < max_clients; i++)
        {
            sd = client_socket[i];

            if (FD_ISSET( sd , &readfds))
            {
                //Check if it was for closing , and also read the
                //incoming message
                if ((valread = read( sd , buffer, 1024)) == 0)
                {
                    //Somebody disconnected , get his details and print
                    getpeername(sd , (struct sockaddr*)&address , \
                        (socklen_t*)&addrlen);
                    printf("Host disconnected , ip %s , port %d \n" ,
                          inet_ntoa(address.sin_addr) , ntohs(address.sin_port));

                    no_clients_connected--;
                    std::cout << "number of clients connected is " << no_clients_connected << std::endl;

                    //Close the socket and mark as 0 in list for reuse
                    close( sd );
                    client_socket[i] = 0;
                }

                //Echo back the message that came in
                else
                {
                    //set the string terminating NULL byte on the end
                    //of the data read
                    send(sd, buffer, valread, 0);
                    //buffer[valread] = '\0';
                    //send(sd , buffer , strlen(buffer) , 0 );
                }
            }
        }
    }

    return 0;
}

我用来测试我可以连接的客户端数量的代码如下并使用pstreams。在 Ubuntu 上,您可以使用 获得 pstream sudo apt-get install libpstreams-dev,或者您可以在此处下载。

您可以使用以下方法编译以下代码(调用时test.cc):

g++ -std=c++11 -pthread -c test.cc -o test.o
g++ -o test test.o -pthread

如果您在服务器已经运行的情况下运行测试代码,它应该与服务器建立 MAX_CONNECTIONS=400 个连接。如果您返回并检查服务器的运行位置,它现在应该有 400 个客户端连接。如果您然后返回到运行测试代码的位置并输入一个字符串(它读取一整行),它应该通过并断开 CLIENTS_TO_DISCONNECT=400 个客户端,并且(在我的机器上)程序结束没有问题。

在我的机器(2012 11" macbook air running ubuntu)上,如果我将 CLIENTS_TO_DISCONNECT 更改为 350 并再次执行相同操作,则 400 个客户端可以正常连接到服务器,并且(在我输入一行后)350 个客户端可以正常断开连接,我从客户端输出一大堆“连接被外国主机关闭”字符串我没有断开连接,尽管测试程序最后仍然退出没问题。

如果我将 MAX_CONNECTIONS 更改为 500 并将 CLIENTS_TO_DISCONNECT 更改为 400。 500 个客户端连接到服务器,当我输入一个 400 个客户端断开连接的字符串时,400 个客户端确实断开了连接,但测试程序没有结束,并且没有多少剩余连接被关闭外国主机,所以服务器仍然认为它连接了一堆客户端,并且需要强制结束测试程序(有时也需要手动终止 telnet 进程)。

如果我将 MAX_CONNECTIONS 更改为 550,那么我什至无法让 550 个客户端连接到服务器。但是,在页面上的 BUGS 部分下,它说:

POSIX 允许实现定义一个上限,通过常量 FD_SETSIZE 通告,可以在文件描述符集中指定的文件描述符范围。Linux 内核没有施加固定限制,但 glibc 实现使 fd_set 成为固定大小的类型,其中 FD_SETSIZE 定义为 1024,并且 FD_*() 宏根据该限制运行。要监视大于 1023 的文件描述符,请改用 poll(2)。

所以我希望能够有至少 1024 个使用 select() 的客户端,如果我改用 poll(2) 可能会更多?尽管 select 或 poll 都与实际连接到服务器的客户端无关,但它们与监视连接的客户端的文件描述符上的活动有关。(Orbit 中的 Lightness Races 指出,前一句是不正确的,因为 select 用于监视传入的连接)。

如果有人能弄清楚为什么会发生任何奇怪的行为,那将非常有帮助和感激。

#include <cstdio>
#include <iostream>
#include <pstreams/pstream.h>

const char ESCAPE_CHAR = 0x1d; //this is 'ctrl+]'
const int MAX_CONNECTIONS = 400;
const int CLIENTS_TO_DISCONNECT = 400;

int main()
{
    redi::pstream servers[MAX_CONNECTIONS];

    for(int i=0; i<MAX_CONNECTIONS; i++)
        servers[i].open("telnet localhost 8888");

    std::cout << "'connected'" << std::endl;

    std::string s;
    getline(std::cin, s);

    for(int i=0; i<CLIENTS_TO_DISCONNECT; i++)
    {
        //std::cout << i << std::endl;
        servers[i] << ESCAPE_CHAR << " quit" << std::endl;

        servers[i].close();
    }

    std::cout << "made it to here" << std::endl;

    return 0;
}

标签: c++linuxc++11

解决方案


您的代码中的一个错误是,当条件成立no_clients_connected >= max_clientstrue,它在断开连接后继续使用该套接字。


代替:

buffer[valread] = '\0';
send(sd, buffer, strlen(buffer), 0);

做:

send(sd, buffer, valread, 0);

对于必须处理许多客户端的服务器,最好使用epoll通知机制。select它的扩展性比and好得多poll(请参阅https://libevent.org/上的基准部分)。


推荐阅读