我对使用C ++进行网络编程和多线程还比较陌生。当前,我的recv()调用返回未知错误。我现在不确定错误从何而来,不胜感激。
我用腻子在本地连接到服务器
class Threaded_TCPListener{
int Threaded_TCPListener::Init()
{
// Initializing WinSock
WSADATA wsData;
WORD ver = MAKEWORD(2,2);
int winSock = WSAStartup(ver, &wsData);
if(winSock != 0)
return winSock;
// Creating listening socket
this->socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(this->socket == INVALID_SOCKET)
return WSAGetLastError();
// Fill sockaddr with ip addr and port
sockaddr_in hint;
hint.sin_family = AF_INET;
hint.sin_port = htons(this->port);
inet_pton(AF_INET, this->ipAddress, &hint.sin_addr);
// Bind hint to socket
if(bind(this->socket, (sockaddr*)&hint, sizeof(hint)) == SOCKET_ERROR)
return WSAGetLastError();
// Start listening on socket
if(listen(this->socket, SOMAXCONN) == SOCKET_ERROR)
return WSAGetLastError();
// Accept first client
this->createAcceptThread();
return 0;
}
int Threaded_TCPListener::Run()
{
bool isRunning = true;
// Read from all clients
std::vector<std::thread> threads;
threads.reserve(this->clients.size());
// Recv from client sockets
for (int i=0; i < this->clients.size(); ++i)
{
threads.emplace_back(std::thread(&Threaded_TCPListener::receiveFromSocket, this, socket));
}
// Wait for all threads to finish
for(std::thread& t : threads)
{
t.detach();
}
return 0;
}
void Threaded_TCPListener::onMessageReceived(int clientSocket, const char* msg, int length)
{
Threaded_TCPListener::broadcastToClients(clientSocket, msg, length);
std::thread t(&Threaded_TCPListener::receiveFromSocket, this, clientSocket);
t.detach();
return;
}
void Threaded_TCPListener::sendMessageToClient(int clientSocket, const char * msg, int length)
{
send(clientSocket, msg, length, 0);
return;
}
void Threaded_TCPListener::broadcastToClients(int senderSocket, const char * msg, int length)
{
std::vector<std::thread> threads;
threads.reserve(clients.size());
// Iterate over all clients
for (int sendSock : this->clients)
{
if(sendSock != senderSocket)
threads.emplace_back(std::thread(&Threaded_TCPListener::sendMessageToClient, this,sendSock, msg, length));
}
// Wait for all threads to finish
for(std::thread& t : threads)
t.join();
return;
}
void Threaded_TCPListener::createAcceptThread()
{
// Start accepting clients on a new thread
this->listeningThread = std::thread(&Threaded_TCPListener::acceptClient, this);
this->listeningThread.detach();
return;
}
void Threaded_TCPListener::acceptClient()
{
int client = accept(this->socket, nullptr, nullptr);
// Error
if(client == INVALID_SOCKET)
{
std::printf("Accept Err: %d\n", WSAGetLastError());
}
// Add client to clients queue
else
{
// Add client to queue
this->clients.emplace(client);
// Client Connect Confirmation
onClientConnected(client); // Prints msg on server
// Create another thread to accept more clients
this->createAcceptThread();
}
return;
}
void Threaded_TCPListener::receiveFromSocket(int receivingSocket)
{
// Byte storage
char buff[MAX_BUFF_SIZE];
// Clear buff
memset(buff, 0, sizeof(buff));
// Receive msg
int bytesRecvd = recv(receivingSocket, buff, MAX_BUFF_SIZE, 0);
if(bytesRecvd <= 0)
{
char err_buff[1024];
strerror_s(err_buff, bytesRecvd);
std::cerr << err_buff;
// Close client
this->clients.erase(receivingSocket);
closesocket(receivingSocket);
onClientDisconnected(receivingSocket); // Prints msg on server
}
else
{
onMessageReceived(receivingSocket, buff, bytesRecvd);
}
}
}
我正在尝试创建一个多线程TCP'服务器',该服务器将通过不断运行一个接受线程(侦听新连接)和一个等待带有recv块的线程来处理传入的客户端,每个连接到服务器的客户端。 / p>
答案 0 :(得分:0)
代码中对于如何实现TCP服务器存在误解:
您似乎假设您可以拥有一个可以处理所有通信的服务器套接字文件描述符。不是这种情况。您必须有一个专用的套接字文件描述符,该描述符仅用于侦听和接受传入的连接,然后对于每个现有的连接都有一个附加的文件描述符。
在您的代码中,我看到您总是使用侦听套接字调用receiveFromSocket()
。错了同样为所有客户循环调用receiveFromSocket()
是错误的。
您宁愿要做的是: -有一个专用线程在循环中调用accept()。从多个线程调用accept()没有性能优势。 -一个accept()返回一个新连接,您产生了一个新线程,该线程在循环中调用recv()。然后,它将按您的疑问阻止并等待新数据。
您还需要放弃从新线程调用单个函数的习惯。这不是多线程编程。线程通常包含一个循环。其他一切通常都是设计缺陷。
还请注意,多线程编程在2019年仍是火箭科学,尤其是在C ++中。如果您不是绝对的专家,您将无法做到。另请注意,多线程编程的绝对专家将尽可能避免使用多线程编程。通过单线程基于事件的系统,可以更好地处理许多看似并发的受I / O约束的任务。
答案 1 :(得分:0)
您的Init
看起来不错:
在接受线程的acceptClient
中看起来还可以:
clients
队列中您的Run
毫无意义:
clients
中为每个元素创建一个线程,以从正在监听的socket
中接收似乎您为每个套接字操作都生成了一个新线程。那是一个非常浪费的设计。线程完成后,它可以返回其他位置。
因此,在acceptClient
中创建一个新的接受线程是一种浪费,您可以循环回到开头::accept
到下一个客户端。像这样:
acceptClient() {
while (alive) {
int client = accept(socket, ...);
createClientHandler(client);
}
}
似乎缺少的是产生一个新的客户端线程来服务客户端套接字。您当前在Run
中执行此操作,但是那是在任何客户端被实际接受之前。然后您为错误套接字进行操作!相反,您应该在receiveFromSocket
中生成acceptClient
线程,并 并将其传递给 client 套接字。这是一个错误。
在您的receiveFromSocket
中,您也无需再次创建另一个线程来receiveFromSocket
,而只需循环回到开头即可。
此“每次操作线程”设计最大的担忧是,您正在每条传入消息上生成发件人线程。这意味着您实际上可能有多个发送方线程尝试在同一TCP套接字上进行::send
。那不是很安全。
对WSASend的调用顺序也是将缓冲区传输到传输层的顺序。不应同时从不同的线程在同一面向流的套接字上同时调用WSASend,因为某些Winsock提供程序可能会将大型发送请求拆分为多个传输,这可能导致意外的数据从同一面向流的多个并发发送请求中交错套接字。
https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsasend
类似地,我建议您不要在broadcastToClients
中为每个客户端套接字产生一个持久性发送者线程(与在某些{{1}中的acceptClient
线程一起) }。
要与发送方线程通信,应使用线程安全的阻塞队列。每个发送方线程如下所示:
receiveFromSocket
然后收到您刚收到的消息:
createClientHandler
总而言之,要处理每个客户,您需要一个像这样的结构:
while (alive) {
msg = queue.next_message();
send(client_socket, msg);
}
安全清理是另外一回事。
最后,在您的代码中对此注释进行评论:
for (client : clients) {
client.queue.put_message(msg);
}
这几乎与struct Client {
int client_socket;
BlockingQueue queue;
// optionally if you want to keep track of your threads
// to be able to safely clean up
std::thread recv_thread, send_thread;
};
的相反:
https://en.cppreference.com/w/cpp/thread/thread/detach
它使您可以销毁线程对象,而不必等待线程完成执行。