1。在单线程/进程的TCP通信过程中,服务器启动后,可以同时与多个客户端建立连接,进行网络通信。但是,在引入TCP通信过程时,提供的服务端代码无法满足这样的要求。先简单看一下前面服务器代码的处理思路,再分析一下代码中的不足://server.c#include#include#include#include#includeintmain(){//1.创建一个监听套接字intlfd=socket(AF_INET,SOCK_STREAM,0);//2.将socket()的返回值与本地IP端口绑定structsockaddr_inaddr;地址.sin_family=AF_INET;地址.sin_port=htons(10000);//大端端口//INADDR_ANY代表本地所有IP,假设有三张网卡,就有三个IP地址//这个宏可以代表任意IP地址addr.sin_addr.s_addr=INADDR_ANY;//这个宏的值是0==0.0.0.0intret=bind(lfd,(structsockaddr*)&addr,sizeof(addr));//3.设置监听器ret=listen(lfd,128);//4.阻塞等待并接受客户端连接structsockaddr_incliaddr;intclien=sizeof(cliaddr);intcfd=accept(lfd,(structsockaddr*)&cliaddr,&clien);//5.与客户端通信while(1){//接收数据charbuf[1024];我mset(buf,0,sizeof(buf));intlen=read(cfd,buf,sizeof(buf));if(len>0){printf("客户端说:%s\n",buf);写入(cfd、buf、len);}elseif(len==0){printf("客户端断开连接...\n");休息;}else{错误(“阅读”);休息;}}关闭(差价合约);关闭(lfd);return0;}在上面的代码中,使用了三个会导致程序阻塞的函数,分别是:accept():如果server端没有新的客户端连接,阻塞当前的Process/thread,如果检测到新连接解阻塞,建立连接writebuffer已满,当前进程/线程阻塞(这种情况比较少见)。如果需要与发起新连接请求的客户端建立连接,则必须在服务器端通过循环调用accept()函数。此外,与服务器建立连接的客户端需要与服务器通信。发送数据时的阻塞可以忽略。当收不到数据时,程序也会被阻塞。这时候,就会很矛盾。accept()被阻塞则无法通信,被read()阻塞则无法与客户端建立新的连接。因此得出结论,基于上述处理方式,在单线程/单进程场景??下,服务端无法处理多连接,解决方案有很多种,常用的有四种:使用多线程实现使用多进程实现使用IO多路复用(multiplexing)实现使用IO多路复用+多线程实现2.多进程并发如果要写一个多进程并发服务器的进程版本对于一个程序,首先要考虑创建的多个进程是什么角色,才能在程序中入座。Tcpserver端有两个作用,分别是:监听和通信。监控是一项持续的行动。如果有新连接就建立连接,没有新连接就阻塞。关于通信,需要与多个客户端同时进行,所以需要多个进程,才能达到互不影响的效果。也有两种类型的进程:父进程和子进程。通过分析,我们可以这样分配进程:父进程:负责监听和处理客户端连接请求,即调用父进程中的accept()函数创建子进程:创建新的连接,创建一个新的子进程,并让子进程与相应的客户端进行通信,回收子进程资源:子进程退出,回收其内核PCB资源,防止僵尸进程。连接后得到的文件描述符,对应的客户端完成数据的接收和发送。发送数据:send()/write()接收数据:recv()/read()在服务器端程序的多进程版本中,多个进程是血缘相关的。了解他们拥有哪些资源可以继承,哪些资源是独占的,以及其他一些细节:子进程是父进程的副本。在子进程的内核区PCB中,也可以复制文件描述符,所以在父进程中可以使用的文件描述符在子进程中也有一个副本,可以用它们来做同样的事情父进程。父子进程有自己独立的虚拟地址空间,所以所有资源都是独占的。为了节省系统资源,只能在父进程中使用的资源可以在子进程中释放,父进程也是如此。由于accept()操作需要在父进程中进行,子进程的资源必须释放,如果想更高效,可以使用signals来处理并发TCP服务器的多进程版本。示例代码如下:#include#include#include#include#include#include#include#include//信号处理函数voidcallback(intnum){while(1){pid_tpid=waitpid(-1,NULL,WNOHANG);if(pid<=0){printf("子进程正在运行,或者子进程已经被回收\n");休息;}printf("孩子死了,pid=%d\n",pid);}}intchildWork(intcfd);intmain(){//1.创建一个监视器Socketintlfd=socket(AF_INET,SOCK_STREAM,0);如果(lfd==-1){perror(“套接字”);退出(0);}//2.将socket()返回值与本地IP端口绑定在一起structsockaddr_inaddr;地址.sin_family=AF_INET;地址.sin_port=htons(10000);//bigendianport//INADDR_ANY代表本机的所有IP,假设有三张网卡,就有三个IP地址//这个宏可以代表任意IP地址//这个宏一般用于本地绑定操作addr.sin_addr.s_addr=INADDR_ANY;//这个宏的值为0==0.0.0.0//inet_pton(AF_INET,"192.168.237.131",&addr.sin_addr.s_addr);intret=bind(lfd,(structsockaddr*)&addr,sizeof(addr));如果(ret==-1){perror(“绑定”);退出(0);}//3.设置监听ret=listen(lfd,128);如果(ret==-1){perror(“听”);退出(0);}//捕获注册信号structsigactionact;act.sa_flags=0;act.sa_handler=回调;sigemptyset(&act.sa_mask);sigaction(SIGCHLD,&act,NULL);//接受多个客户端连接,调用acceptwhile(1){//4.阻塞等待并接受客户端连接structsockaddr_incliaddr;intclilen=sizeof(cliaddr);intcfd=accept(lfd,(structsockaddr*)&cliaddr,&clien);if(cfd==-1){if(errno==EINTR){//accept调用被信号中断,解除阻塞,返回-1//再次调用accept继续;}错误(“接受”);退出(0);}//打印客户端的地址信息charip[24]={0};printf("客户端IP地址:%s,端口:%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,sizeof(ip)),ntohs(cliaddr.sin_port));//已经建立了一个新的连接,创建一个子进程,让子进程与这个客户端通信pid_tpid=fork();if(pid==0){//子进程->与客户端通信//通信的文件描述符cfd被复制到子进程中//子进程不负责监听close(lfd);while(1){intret=childWork(cfd);如果(ret<=0){中断;}}//退出子进程close(cfd);退出(0);}elseif(pid>0){//父进程不与客户端通信close(cfd);}}return0;}//5.与客户端通信intchildWork(intcfd){//接收数据charbuf[1024];memset(buf,0,sizeof(buf));intlen=read(cfd,buf,sizeof(buf));如果(len>0){printf("客户说:%s\n",buf);写入(cfd、buf、len);}elseif(len==0){printf("客户端断开连接...\n");}else{错误(“阅读”);}returnlen;}上面示例代码中,父子进程中分别关闭不用的文件描述符(父进程不需要通信,子进程不需要监听)如果客户端主动断开,服务器端负责与客户端通信的子进程也会退出。子进程退出后,会向父进程发送一个名为SIGCHLD的信号,该信号被父进程中的sigaction()函数捕获。该信号通过回调函数callback()中的waitpid()回收退出子进程的资源。还有一个细节需要说明,这是父进程的处理代码:intcfd=accept(lfd,(structsockaddr*)&cliaddr,&clilen);while(1){intcfd=accept(lfd,(structsockaddr*)&cliaddr,&clien);if(cfd==-1){if(errno==EINTR){//accept调用被信号中断,解除阻塞,返回-1//再次调用accept继续;}错误(“接受”);退出(0);如果父进程调用了accept()函数,没有检测到新的客户端连接,父进程就会阻塞在这里。这时,一个子进程退出并向父进程发送信号,父进程捕获信号SIGCHLD。由于信号优先级高,会打断代码的正常执行流程,所以打断父进程的阻塞,转而处理这个信号对应的函数callback(),再次回到accept()位置,不过这次不再阻塞,函数直接返回-1,此时函数调用失败,错误描述为accept:Interruptedsystemcall,对应的错误号为EINTR,因为代码中有信号中断导致的错误,所以在程序中可以判断错误号,让父进程再次调用accept(),继续阻塞或者接受来自客户端的新连接。3.多线程并发写一个多线程版本的并发服务端程序,类似于多进程的思路,只要考虑和理解对勾即可。多线程中有两种线程:主线程(父线程)和子线程,分别处理服务器端的监听和通信过程。按照多进程处理的思想,可以这样设计:主线程:负责监听和处理客户端连接请求,即调用父进程中的accept()函数创建子线程:以建立一个新的连接,创建一个新的子进程,让这个子进程与对应的client进行通信,回收子线程资源:由于回收需要调用一个阻塞函数,这会影响accept(),可以直接做线程分离就可以了。子线程:负责通信,根据主线程建立新连接后得到的文件描述符,与对应的客户端完成数据的接收和发送。发送数据:send()/write()接收数据:recv()/read()在服务器端程序的多线程版本中,多个线程共享同一个地址空间,有的数据共享,有的数据共享exclusive,下面详细分析一下:同一个地址空间的多个线程的栈空间是互斥的,多个线程共享内核区的全局数据区、堆区、文件描述符等资源,所以需要注意数据覆盖问题,多线程访问共享资源时,也需要线程同步。多线程Tcpserver示例代码如下:#include#include#include#include#include#includestructSockInfo{intfd;//通讯pthread_ttid;//线程IDstructsockaddr_inaddr;//地址信息};structSockInfoinfos[128];void*working(void*arg){while(1){structSockInfo*info=(structSockInfo*)arg;//接收数据charbuf[1024];intret=read(info->fd,buf,sizeof(buf));if(ret==0){printf("客户端已经关闭连接...\n");信息->fd=-1;休息;}elseif(ret==-1){printf("接收数据失败...\n");信息->fd=-1;休息;}else{write(info->fd,buf,strlen(buf)+1);}}returnNULL;}intmain(){//1.创建用于监听的套接字socketintfd=socket(AF_INET,SOCK_STREAM,0);如果(fd==-1){错误("套接字");退出(0);}//2.绑定structsockaddr_inaddr;地址.sin_family=AF_INET;//ipv4地址.sin_port=htons(8989);//bytes顺序应该是网络字节顺序addr.sin_addr.s_addr=INADDR_ANY;//==0,获取IP的操作交给内核intret=bind(fd,(structsockaddr*)&addr,sizeof(addr));如果(ret==-1){perror(“绑定”);退出(0);}//3.设置监听ret=listen(fd,100);如果(ret==-1){perror(“听”);退出(0);}//4.等待,接受连接请求intlen=sizeof(structsockaddr);//数据初始化intmax=sizeof(infos)/sizeof(infos[0]);for(inti=0;iaddr,&len);printf("父线程,connfd:%d\n",connfd);如果(connfd==-1){错误(“接受”);退出(0);}pinfo->fd=connfd;pthread_create(&pinfo->tid,NULL,working,pinfo);pthread_detach(pinfo->tid);}//释放资源close(fd);//Listenforreturn0;}在编写多线程并发服务器代码时,需要注意父子线程共享同一个地址空间中的文件描述符,所以每当在主线程中建立新的连接,获取到的文件描述符值需要保存,不能在同一个变量上覆盖,这样会丢失之前的文件描述符值,不知道如何与客户端通信。在上面的示例代码中,将连接成功后获取的用于通信的文件描述符的值保存在一个全局数组中。每个子线程需要与不同的客户端通信。需要的文件描述符值也不一样。只要存储每个有效文件描述符值的变量对应不同的内存地址,就不会在使用过程中出现数据覆盖,造成通信数据混乱的情况。