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

Posted

技术标签:

【中文标题】计算出有多少客户端可以连接到我正在使用的一些 tcp 服务器代码【英文标题】:Working out how many clients can connect to some tcp server code I am using 【发布时间】:2018-10-11 08:51:01 【问题描述】:

更新

似乎 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.ccMAX_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 服务器代码进行了一些更正(有关代码,请参见 here)。有趣的是它有完全相同的问题(test.cc running 的一个实例最多可以连接 509 个客户端,服务器最多可以有 1020 个客户端,我无法让 redi::pstream 在第 419 个之后连接的客户端上关闭)。我认为这表明最多 509 个客户端使用 test.cc 的一个实例进行连接的问题在于 test.cc 的代码而不是服务器代码,并且可能还在于让 redi::pstream 在客户端上关闭的问题在 419 号之后连接。


更新 3

第二个 tcp 服务器在与客户端之间来回发送和接收消息的时间是第一个的两倍,所以我将使用我找到的原始代码(尽管我也可以看看我是否能找到一个 epoll 解决方案,可以处理超过 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 服务器代码 here 的略微修改版本。

注意:修改在第 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 来连接到它。要退出,请在 telnet 提示符处输入 ctrl+],然后输入 quit

//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 上,您可以使用 sudo apt-get install libpstreams-dev 获取 pstream,或者您可以通过 here 下载它。

您可以使用以下代码编译以下代码(当调用 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 部分下的 this 页面上,它说:

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

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

【问题讨论】:

const char* ptr = "literal"; const 自 C++11 以来不是可选的。在此之前这是非常好的做法。 “尽管 select 或 poll 都与实际连接到服务器的客户端无关,但它们与监视连接客户端的文件描述符上的活动有关”这包括检测是否准备好接受侦听套接字上的连接,但您所说的并不完全正确 干杯,我在第 34 行更新了帖子以使用 const char*。我还在第 141 行的比较中添加了 (size_t),这也给出了警告。 关于 select 用于检测是否准备好接受侦听套接字上的连接的有效点。我想知道这是否是我的问题出现的地方。我会玩一玩(尽管如果其他人同时设法找到问题,那也很棒)。如果存在连接 500 多个客户端的问题,我不会感到惊讶,尽管我仍然不确定为什么一旦连接 500 个客户端就无法断开连接,除非文件描述符高于某个值存在问题. 很抱歉没有直接回答您的问题,但您是否有理由要使用原始 TCP 而不是 HTTP?到那时,它就成为一个已解决的问题,您可以使用许多可扩展的高性能 HTTP 服务器中的任何一个。 【参考方案1】:

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


代替:

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

做:

send(sd, buffer, valread, 0);

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

【讨论】:

no_clients_connected &gt;= max_clientstrue 时,您对错误的看法是正确的,我已修复此错误的服务器代码。尽管此错误不会出现在帖子中给出的示例问题中。 我已更改为send(sd, buffer, valread, 0);,尽管这对于不幸出现的主要问题应该没有太大影响。 我会看看我是否可以使用epoll 获得一个示例,不过如果能弄清楚这个示例的问题所在也很好。

以上是关于计算出有多少客户端可以连接到我正在使用的一些 tcp 服务器代码的主要内容,如果未能解决你的问题,请参考以下文章

通过文件客户端连接到远程服务器

客户端只能在同一台计算机上连接到服务器

连接到 redis 时,Unix 套接字比 tcp 慢

连接到 sql server 数据库 mdf 文件而不在客户端计算机上安装 sql server?

Windows phone:无法使用套接字连接到计算机

客户端将无法连接到服务器