请不要将此问题标记为重复。我已经看过SO上有关使用套接字编程处理多个客户端的大量文章。 大多数人建议只使用多线程,但是我试图避免使用该路径,因为我已阅读它并存在一些问题:
我读过的所有专门谈论使用单个线程的帖子要么回答不好/没有答案,要么解释不清楚,就像人们说“只使用select()
!”一样。
我正在为服务器编写代码以处理多个(〜1000)客户端,但是我在弄清楚如何创建有效的解决方案时遇到了麻烦。现在,我已经有了服务器的代码,该代码能够一次处理1个客户端。两者都是用C编写的;服务器在Windows上使用WinSock,而客户端在Linux上。
服务器和客户端使用send()
并阻止recv()
调用来回发送多个通信。编写这段代码非常简单,我不会在这里发布它,因为它很长,而且我怀疑有人会真正阅读所有这些代码。同样,确切的实现也不重要,我只想谈论高级伪代码。真正的困难在于更改服务器以处理多个客户端。
我找到了一个不错的PDF教程,该教程介绍了如何创建一个可处理多个客户端的WinSock服务器,可以在以下网址找到WinSock Multiple Client Support。它在C ++中,但是很容易转移到C中。
据我了解,服务器的运行方式如下:
while (running) {
Sleep(1000);
/* Accept all incoming clients and add to clientArray. */
for (client in clientArray) {
/* Interact with client */
if (recv(...) == "disconnect") {
/* Disconnect from client */
}
}
}
/* Close all connections. */
我看到的使用这种方法的问题是,您基本上一次只能处理一个客户端(这很明显,因为您不是多线程的),但是如果与每个客户端进行交互会怎样?只需要发生一次?意思是,如果我只想来回发送一些数据并关闭连接该怎么办? 此操作可能需要5秒钟到5分钟的时间,具体取决于客户端连接的速度,因此在服务器处理客户端5分钟的同时,其他客户端将在对服务器的connect()
调用中被阻止。 似乎效率不高,但是最好的方法可能是实现等待队列,在该队列中客户端已连接并被告知等待一段时间?我不确定,但是这使我感到好奇的是,大型服务器会同时向数千个客户端发送更新下载,以及我是否应该以相同的方式进行操作。
此外,如果服务器和客户端之间的Sleep(1000)
和send()
花了一段时间(〜1分钟),是否有理由在主服务器循环中添加recv()
调用?
我想要的是一种在单个线程服务器上处理多个客户端的解决方案,该解决方案足以满足约1000个客户端的需求。如果您告诉我PDF中的解决方案很好,那对我来说已经足够了(也许我太关注效率了。)
请提供答案,包括对实现的口头解释,服务器/客户端伪代码,甚至是服务器的一小段示例代码(如果您感到很悲伤)。
谢谢。
答案 0 :(得分:0)
我已经编写了单线程套接字池处理。我使用非阻塞套接字并选择调用来处理所有发送,接收和错误。 我的课将所有套接字保持在数组中,并为select调用构建3个fd集。当发生某种情况时,它会检查读取或写入或错误列表并处理这些事件。 例如,在连接期间非阻塞客户端套接字会触发写入或错误事件。如果发生错误事件,则连接失败。如果发生写入,则建立连接。 所有套接字均处于读取fd集中。如果创建服务器套接字(具有绑定和监听),则新连接将触发读取事件。然后检查套接字是否是服务器套接字,然后调用accept以建立新连接。如果读取操作是由常规套接字触发的,则有一些字节需要读取..只需调用带有缓冲区Arge的recv就足以从该套接字中吸收所有数据。
SOCKET maxset=0;
fd_set rset, wset, eset;
FD_ZERO(&rset);
FD_ZERO(&wset);
FD_ZERO(&eset);
for (size_t i=0; i<readsockets.size(); i++)
{
SOCKET s = readsockets[i]->s->GetSocket();
FD_SET(s, &rset);
if (s > maxset) maxset = s;
}
for (size_t i=0; i<writesockets.size(); i++)
{
SOCKET s = writesockets[i]->s->GetSocket();
FD_SET(s, &wset);
if (s > maxset) maxset = s;
}
for (size_t i=0; i<errorsockets.size(); i++)
{
SOCKET s = errorsockets[i]->s->GetSocket();
FD_SET(s, &eset);
if (s > maxset) maxset = s;
}
int ret = 0;
if (bBlocking)
ret = select(maxset + 1, &rset, &wset, &eset, NULL/*&tv*/);
else
{
timeval tv= {0, timeout*1000};
ret = select(maxset + 1, &rset, &wset, &eset, &tv);
}
if (ret < 0)
{
//int err = errno;
NetworkCheckError();
return false;
}
if (ret > 0)
{
// loop through eset and check each with FD_ISSET. if you find some socket it means connect failed
// loop through wset and check each with FD_ISSET. If you find some socket check is there any pending connectin on that socket. If there is pending connection then that socket just got connected. Otherwise select just reported that some data has been sent and you can send more.
// finally, loop through rset and check each with FD_ISSET. If you find some socket then check is this socket your server socket (bind and listen). If its server socket then this is signal new client want to connect.. just call accept and new connection is established. If this is not server socket, then just do recv on that socket to collect new data.
}
几乎没有什么要处理的了。所有套接字都必须处于非阻塞模式。每个send或recv调用都将返回-1(错误),但错误代码为EWOULDBLOCK。多数民众赞成在正常情况下,忽略错误。如果recv返回0,则此连接断开。如果发送返回0字节已发送,则内部缓冲区已满。 您需要编写其他代码来序列化和解析数据。例如,recv后,消息可能不完整(取决于消息大小),因此可能需要多个recv调用才能接收完整消息。有时,如果消息很短,则recv调用可以在缓冲区中传递多个消息。因此,您需要编写良好的解析器或设计良好的协议,以使其易于解析。
答案 1 :(得分:0)
首先,关于单线程方法:我认为这是个坏主意,因为您的服务器处理能力受到单处理器内核性能的限制。但除此之外,它会在某种程度上起作用。
现在关于多客户端问题。我建议将 WSASend
和 WSARecv
与它们的编译例程一起使用。如有必要,它还可以扩展到多个线程。
服务器核心看起来像这样:
struct SocketData {
::SOCKET socket;
::WSAOVERLAPPED overlapped;
::WSABUF bufferRef;
char buf [1024];
// other client-related data
SocketData (void) {
overlapped->hEvent = (HANDLE) this;
bufferRef->buf = buf;
bufferRef->len = sizeof (buf);
// ...
}
};
void OnRecv (
DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags) {
auto data = (SocketData*) lpOverlapped->hEvent;
if (dwError || !cbTransferred) {
::closesocket (data->socket);
delete data;
return;
}
// process received data
// ...
}
// same for OnSend
void main (void) {
// init and start async listener
::SOCKET serverSocket = ::socket (...);
HANDLE hAccept = ::CreateEvent (nullptr, 0, 0, nullptr);
::WSAEventSelect (serverSocket, FD_ACCEPT, hAccept);
::bind (serverSocket, ...);
::listen (serverSocket, ...);
// main loop
for (;;) {
int r = ::WaitForSingleObjectEx (hAccept, INFINITE, 1);
if (r == WAIT_IO_COMPLETION)
continue;
// accept processing
auto data = new SocketData ();
data->socket = ::accept (serverSocket, ...);
// detach new socket from hAccept event
::WSAEventSelect (data->socket, 0, nullptr);
// recv first data from client
::WSARecv (
data->socket,
&data->bufferRef,
1,
nullptr,
0,
&data->overlapped,
&OnRecv);
}
}
要点:
WaitForSingleObjectEx
、WaitForMultipleObjectsEx
等)必须是可提醒的;OnSend/OnRecv
中完成;OnSend/OnRecv
中的 API 的情况下完成所有处理;OnRecv
将为每个处理的传入数据包调用。 OnSend
将为每个处理的传出数据包调用。请记住:您要求发送/接收的数据量与数据包中实际处理的数据量不同。