SO_REUSEADDR选项
在上一篇文章的最后我们贴出了一个简单的C/S通信的例程。在该例程序中,使用"Ctrl+c"结束通信后,服务器是无法立即重启的,如果尝试重启服务器,将被告知:
bind: Address already in use
原因在于服务器重新启动时需要绑定地址:
bind (listenfd , (struct sockaddr*)&servaddr, sizeof(servaddr));
而这个时候网络正处于TIME_WAIT的状态,只有在TIME_WAIT状态退出后,套接字被删除,该地址才能被重新绑定。TIME_WAIT的时间是两个MSL,大约是1~4分钟。若每次服务器重启都需要等待TIME_WAIT结束那就太不合理了,好在选项SO_REUSEADDR能够解决这个问题。
服务器端尽可能使用REUSEADD,在bind()之前调用setsockopt来设置SO_REUSEADDR套接字选项,使用SO_REUSEADDR选项可以使不必等待TIME_WAIT状态消失就可以重启服务器。
/*设置地址重复使用*/int on = 1; //on为1表示开启if(setsockopt(listenfp ,SOL_SOCKET,SO_REUSEADDR,&on,sieof(on))<0)ERR_EXIT("setsockopt error");
处理多客户的服务器
在上一篇文章例程中,服务器端只能够连接一个客户端,并不能处理多个客户端的连接。原因在于服务器使用accept从已连接队列中获取一个连接后,便进入了对该连接的服务中,处于while循环状态。当一个新的客户端连接已经放入已连接队列时,服务器并不能执行到accpet的代码去获取队列中的连接。
为了解决这个问题,我们可以fork()一个子进程,让子进程来处理一个客户端的连接,而父进程循环执行accept的代码,获取新的连接:
int conn ;while(1){conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);if(conn <0)ERR_EXIT("accept error");elseprintf("连接到服务器的客户端的IP地址是:%s,端口号是:%d\n",inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));pid_t pid ;pid = fork();//创建一个新进程if (pid ==0) //子进程{close(listenfd); //子进程不需要监听套接字,将其关闭/*循环获取数据、发送数据*/char recvbuf[1024];while(1){memset(recvbuf,0,sizeof(recvbuf));int ret = read(conn,recvbuf ,sizeof(recvbuf));fputs(recvbuf,stdout);write(conn,recvbuf,sizeof(recvbuf));}exit(EXIT_SUCCESS);}if(pid >0) //父进程{close(conn);//父进程无需该连接套接字,它的任务是执行accept获取连接}else {close(conn);}}
启动服务器端,使用多个客户端进行连接,可以看到服务器能够同时处理多个连接:
实现一个P2P简单聊天程序
为了实现聊天的功能,客户端与服务器端都需要有一个进程来读取连接,另一个进程来处理键盘输入。使用fork()来完成这个简单的聊天程序。
客户端程序:
//p2pcli.c#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<signal.h>#include<arpa/inet.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#define ERR_EXIT(m)\do \{\perror(m);\exit(EXIT_FAILURE);\}while(0)void handler(){exit(EXIT_SUCCESS);}int main(){/*创建一个套接字*/int sock = socket(AF_INET,SOCK_STREAM,0);if(sock == -1){ERR_EXIT("socket");}/*定义一个地址结构*/struct sockaddr_in servaddr;memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(5888);servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*进行连接*/if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0){ERR_EXIT("connect");}else{printf("连接成功\n");}pid_t pid ;pid = fork();if(pid == -1)ERR_EXIT("fork");if(pid == 0) //子进程复制接收数据并显示出来{char recvbuf[1024]={0};while(1){memset(recvbuf,0,sizeof(recvbuf));int ret = read(sock ,recvbuf,sizeof(recvbuf));if(ret == -1){ERR_EXIT("read");}if(ret == 0) //连接关闭{printf("连接关闭\n");kill(getppid(),SIGUSR1);break;}else{printf("接收到信息:");fputs(recvbuf,stdout);}}}else //父进程负责从键盘接收输入并发送{signal(SIGUSR1,handler);char sendbuf[1024]={0} ;while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL){write(sock,sendbuf,strlen(sendbuf));memset(&sendbuf,0,sizeof(sendbuf));}}close(sock);return 0;}
服务器端程序:
// p2pser.c#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<signal.h>#include<arpa/inet.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#define ERR_EXIT(m)\do \{\perror(m);\exit(EXIT_FAILURE);\}while(0)/*信号处理函数*/void handler(int sig){exit(EXIT_SUCCESS);}int main(){/* 创建一个套接字*/int listenfd= socket(AF_INET ,SOCK_STREAM,0);if(listenfd==-1)ERR_EXIT("socket");/*定义一个地址结构并填充*/struct sockaddr_in addr;addr.sin_family = AF_INET; //协议族为ipv4addr.sin_port = htons(5888); //绑定端口号addr.sin_addr.s_addr = htonl(INADDR_ANY);//主机字节序转为网络字节序/*重复使用地址*/int on = 1;if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0){ERR_EXIT("setsockopt");}/*将套接字绑定到地址上*/if(bind(listenfd,(const struct sockaddr *)&addr ,sizeof(addr))==-1){ERR_EXIT("bind");}/*监听套接字,成为被动套接字*/if(listen(listenfd,SOMAXCONN)<0){ERR_EXIT("Listen");}struct sockaddr_in peeraddr;socklen_t peerlen = sizeof(peeraddr);int conn ;conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);if(conn <0)ERR_EXIT("accept error");elseprintf("连接到服务器的客户端的IP地址是:%s,端口号是:%d\n",inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));pid_t pid ;pid = fork();//创建一个新进程if(pid == -1){ERR_EXIT("fork");}if(pid == 0)//子进程{signal(SIGUSR1,handler);char sendbuf[1024] = {0};while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL){write(conn,sendbuf,sizeof(sendbuf));memset(sendbuf,0,sizeof(sendbuf));}exit(EXIT_SUCCESS);}else //父进程 用来获取数据{char recvbuf [1024]={0};while(1){memset(recvbuf,0,sizeof(recvbuf));int ret = read(conn ,recvbuf,sizeof(recvbuf));if(ret == -1){ERR_EXIT("read");}if(ret == 0) //对方已关闭 {printf("对方关闭\n");break;}fputs(recvbuf,stdout);}kill(pid,SIGUSR1);exit(EXIT_SUCCESS);}/*关闭套接字*/close(listenfd);close(conn);return 0;