确定有多少客户端可以连接到我正在使用的某些tcp服务器代码

时间:2018-10-11 08:51:01

标签: c++ linux c++11

更新

看来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个,我猜想是{{ 1}}还在使用类似于服务器上文件描述符的数字,并且碰到了类似的问题。我已经尝试使用超过512个redi :: pstream变量来仅运行“ echo'hello'”,但这似乎没有任何问题,因此我不确定该限制来自何处。

在419号之后连接的客户端上,关闭redi :: pstream仍然很麻烦。在运行client.cc的一个实例和运行test.cc的多个实例时都会发生这种情况。

我还设法对使用轮询而不是选择的另一个多客户端tcp服务器代码进行了更正(有关代码,请参见here)。有趣的是,它具有完全相同的问题(运行的test.cc的一个实例最多可以连接509个客户端,服务器最多可以连接1020个客户端,并且在重新连接后在连接的客户端上关闭redi :: pstream时,我遇到了麻烦419)。我认为这表明使用一个test.cc实例最多连接509个客户端的问题在于test.cc的代码,而不是服务器代码,可能还在于获取redi :: pstream的麻烦在第419号之后连接的客户端上关闭。


更新3

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

如果您放弃了关闭pstreams(test.cc)的语句,则测试程序似乎正常结束(并且客户端仍然会在测试程序终止之前断开连接)。但是,如果我在不读redi::pstream的情况下在redi::pstream上建立了太多输入,则测试程序将无法终止。

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

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

您还从客户端到服务器的输入已被“分组”,即。如果在选择/轮询接收到消息之前有多个消息到达,则读/接收将全部读取到提供的缓冲区阵列中。如果合并后的消息对于缓冲区而言太长,则可以将缓冲区末尾的消息“切成两半”,并且不容易放回原处。我建议如果您的缓冲区大小不足以处理在特定时间段内到达的所有分组消息,那么这将是一个很大的问题。幸运的是,当我使用非常大的缓冲区大小时,io的运行时似乎没有任何重大变化。

但是要注意的一件事是,如果缓冲区大小大于3000。在该值以上的某个位置,您将无法再将char数组视为字符串,将其输出并将其设置为等于字符串的方法无效。您必须遍历char数组并将字符分别添加到字符串中。 (请注意,在将数据发送回客户端时不需要这样做,但是如果要包含客户端输入的缓冲区char数组的字符串版本,则需要这样做。)


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

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

我正在使用的tcp服务器代码如下,并且是here可用的tcp服务器代码的略微修改版本。

注意:修改是在第36行(在我的计算机上为1024)上输出FD_SETSIZE,将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*的弃用转换的警告吗? (“轨道中的竞速种族”已指出如何解决此问题)。

如果编译并运行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客户端的连接,并且(在我的机器上)程序不会出现任何问题。

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

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

如果我将MAX_CONNECTIONS更改为550,那么我什至无法将550个客户端连接到服务器。但是,在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;
}

1 个答案:

答案 0 :(得分:2)

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


代替:

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

要做:

send(sd, buffer, valread, 0);

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