TCP服务器与多个客户端互发消息

3
我有一个TCP聊天程序:server.cclient.c
服务器在一个while(1)循环中运行,并使用select检测想要连接到其套接字的客户端。然后为已接受的客户端创建一个新线程,并将其套接字描述符作为线程参数传递:pthread_create(&thread,NULL, do_something,(void *) &socket_descriptor); 当从客户端接收到消息时,服务器应将此消息发送到所有连接的客户端(尚未实现)。
现在我想知道如何做到这一点。我绝对需要每个已接受的连接都在一个线程中。
我考虑在do_something中也使用select;是否会检测到套接字描述符上有数据进来?或者你会用其他方式做吗?
编辑:添加代码 我的代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include "tcp_comm.h"
#include <sys/time.h>
#include <sys/types.h>

#define BUFSIZE 1024
#define PORT 1234

void *do_something(void *a);

int main (void){
    Socket server = tcp_passive_open( PORT );
    MySocket *s = (MySocket *)server;
    printf("Server socked_id (main): %i", s->sd);

    pthread_t thread;

    fd_set active_socketDescriptors,read_socketDescriptors;

    FD_ZERO(&active_socketDescriptors);         
    FD_SET(s->sd,&active_socketDescriptors);    

    while (1){
        read_socketDescriptors = active_socketDescriptors;
        if (select (FD_SETSIZE, &read_socketDescriptors, NULL, NULL, NULL) < 0){
            perror ("select"); 
            exit (EXIT_FAILURE);
        }

        int i;
        for (i = 0; i < FD_SETSIZE; ++i){
            if (FD_ISSET (i, &read_socketDescriptors)){
                if (i == s->sd){
                    Socket client = tcp_wait_for_connection( server ); 
                    pthread_create (&thread,NULL, do_something, (void *)client); 
                    FD_SET (s->sd, &active_socketDescriptors);          
                } else {
                }
            }
        }
    }

    tcp_close( server );
    return 0;

}
void *do_something(void *client){
    unsigned char input[BUFFER_SIZE]; 
    pthread_detach(pthread_self());

    MySocket *s = (MySocket *)client;
    printf("Client socked_id (thread): %i", s->sd);
    int j;
    while (1){
        int nbytes = tcp_receive(client, input, BUFSIZE );
        if (nbytes <= 0){
                if (nbytes ==0){
                    /* connection closed by client*/    
                    printf("Client closed connection");         
                } else {
                    /* other error*/
                    perror("tcp_receive");              
                }
                tcp_close(&client);
                /*remove the socket descriptor from set in the main BRAINSTORM ABOUT THIS */
        } else {
            /*data incoming */
            printf("\nMessage from client: %s",input);
        }
    }
    return 0;
}

编辑2:问题的重新表述 我必须使用线程(不是因为系统;Linux),但因为在任务中每个客户端都必须有一个线程,所以必须这样做。

我的具体问题是,只有主线程才能将每个客户端线程接收到的数据发送给所有客户端,因为只有主线程可以访问包含套接字描述符的集合。

编辑3:我需要在每个线程中添加什么,但由于s.thread和s.main位于不同的地方并且线程不知道主线程的集合,所以我无法这样做。

for (j=0; j<=FD_SETSIZE;j++){
    if(FD_ISSET(j,&active_socketDescriptors)){
        if (j != s.thead && j!=s.main){
            tcp_send(j, (void*)input,nbytes);
        }       
    }   
}

编辑4:我是这样解决的: 我有一个动态数组列表,其中放置了一组已连接客户端和它们的套接字描述符。在服务器线程(做某些事情)中,我有一个接收阻塞,直到它获得输入,然后使用从列表循环遍历的套接字描述符将此输入发送到所有已连接的客户端。在客户端中,存在一个正在侦听的线程和一个正在发送的线程。

3个回答

3
如果客户端连接套接字是非阻塞的,那么可以使用例如select等方法等待套接字接收数据。然而,由于您已经在线程中有了连接的套接字,您可以使它们保持阻塞状态,并在其上执行read调用。调用read将会一直阻塞直到接收到数据,这些数据随后可分发到其他线程。 编辑 更好地理解您的要求后,您应该让套接字处于非阻塞状态,并使用带有短超时的循环进行select操作。当select超时(即返回0)时,请检查是否有数据需要发送。如果有,则发送数据并返回select呼叫。

是的,但这样我就无法从其他客户端接收消息,因为所有发送到服务器的消息都必须发送到所有客户端。 - Thomas
不错,你是对的;第一次我错了。但是我如何将消息从一个客户端发送到所有连接的客户端?(是否可以将其集合发送到客户端或在线程和主线程之间共享此信息?) - Thomas
@ThomasVerbeke 首先,您必须跟踪所有线程,例如在列表中。然后,每个线程都有一个消息队列,当一个线程接收到消息时,将在每个其他线程的消息队列中放置一份副本。然后,每个线程检查自己的队列以查找要发送的消息。请记得使用类似于互斥锁的东西来保护队列。 - Some programmer dude
谢谢,你的答案最终帮了我很多:它让我以以下方式使其工作;我有一个动态数组列表,其中我放置了一组连接客户端及其套接字描述符。在服务器线程(执行某些操作)中,我使用接收阻塞直到获取输入,然后使用列表中的套接字描述符将此输入发送到所有连接的客户端。在客户端内部,有一个正在侦听和发送的线程;完美地工作!再次感谢 - Thomas

1
根据您的描述,重新思考应用程序的架构可能是值得的(除非这是由于系统限制所导致)。让我再解释一下...
根据您的描述,如果我理解正确,在客户端连接到服务器后,它(客户端)发送的任何消息都将被中继(由服务器)到所有其他客户端。因此,为什么不将新连接的套接字简单地添加到选择的FDSET中,而不是创建一个新线程呢?然后当消息进来时,您可以简单地中继给其他人。
如果您预计单个服务器会有大量客户端,则应查看您的系统是否支持poll系统调用(它与select类似,但支持监视更多客户端)。良好的poll/select版本应该比您的线程版本表现更好。
如果您真的想继续使用线程版本,这里有一种实现您要尝试的方法。当您为每个已接受的客户端创建线程时,还要创建一个管道返回到服务器线程(并将其添加到服务器选择/轮询集合中),并将其传递给客户端线程。因此,您的服务器线程现在不仅接收新连接,还中继消息。

虽然你说必须在单独的线程中处理每个客户端,但除非你使用实时操作系统,否则你很快就会发现需要进行的线程上下文切换/同步将很快占据我建议的第一种解决方案的复用开销。 (但由于你没有提到操作系统,我只是猜测!)


我必须使用线程(不是因为系统;Linux),但因为在任务中为每个客户端都有一个线程是强制性的。我遇到的问题是只有主线程才能将每个客户端的每个线程接收到的数据发送到所有客户端,因为只有主线程可以访问包含套接字描述符的集合。你说使用管道传递消息,也许可以直接在主线程和子线程之间通信集合? - Thomas
嗨,Thomas,你所做的是让服务器侦听(添加到选择FDSET)每个管道的读取端口,从客户端读取数据的线程将其接收到的数据写入其各自管道的写入端口。当客户端线程接收到数据时,它只需将其写入其管道即可。当服务器在其中一个客户端管道上接收到数据时,它会将其发送给所有其他客户端。希望这可以帮助! - idz

0

这与你的设计有关。

如果每个连接的客户端只需要做一两个功能,那么建议您仅使用一个线程来实现您的服务器。

如果每个连接的客户端需要执行许多功能,则可以使用多线程设计。但是,你问的问题应该是如何将数据从接收线程传递给其他所有线程。我建议的答案是:

a)使用消息队列传递线程间数据:每个线程都有一个消息队列,并且每个线程都会监听它自己的套接字和消息队列。当从套接字接收数据时,线程将数据发送到所有其他消息队列。

b)使用单个全局缓冲区:如果有任何来自套接字的输入数据,请将此数据放入此全局缓冲区,并为此数据添加标记以指示此数据来自哪里。

我的看法。


使用消息队列可能可以实现,但是传递整个集合不是更好吗? - Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接