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

Working out how many clients can connect to some tcp server code I am using

本文关键字:代码 服务器 tcp 多少 客户端 连接 计算      更新时间:2023-10-16

更新

看来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'",这似乎没有任何问题,所以我不确定限制来自哪里。

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

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


更新3

第二个tcp服务器的时间是第一个服务器的两倍,用于与客户端来回发送和接收消息,所以我将使用我找到的原始代码(不过也可以看看我是否能找到一个可以处理1020多个连接客户端的epoll解决方案)。

如果去掉了关闭pstreams(redi::pstream)的语句,那么测试程序似乎可以正常结束(并且在测试程序终止之前客户端仍然断开连接)。但是,如果我在redi::pstream上积累了太多的输入而没有读取它,那么测试程序就无法终止。

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

对于pstreams,我有一个问题,419之后连接的客户端没有正确断开和关闭,程序停滞。我对libexecstreams没有这个问题,进程/流可以正常关闭。当我连接时,比如说使用libexecstreams的300个客户端。我可以使用pstream连接另外400个客户端,但是在关闭420之后连接到服务器的客户端的pstream时再次出现问题。尽管这可以通过上面建议的pstream来解决,只需不在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_client已经连接时关闭新客户端的连接,并在有新连接和客户端断开连接时输出连接的客户端数。

您可以使用编译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*的不推荐转换的警告吗?(Lightness Races in Orbit已经指出了如何解决这个问题)。

如果您编译并运行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 rn";
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 : %dn" , 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 %dn" , 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] = '';
//send(sd , buffer , strlen(buffer) , 0 );
}
}
}
}
return 0;
}

下面是我用来测试可以连接的客户端数量的代码,它使用pstreams。在Ubuntu上,你可以用sudo apt-get install libpstreams-dev获得pstreams,也可以在这里下载。

您可以使用编译以下代码(当调用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运行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的文件描述符,请改用轮询(2)。

所以我希望至少有1024个客户端使用select(),如果我改用poll(2),可能会更多?尽管选择或轮询都与实际连接到服务器的客户端无关,但它们与监视连接的客户端的文件描述符上的活动有关。(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;
}

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


代替:

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

Do:

send(sd, buffer, valread, 0);

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

相关文章: