UNIX网络编程笔记(3):简单的并发服务器
2015-12-02 09:46
441 查看
上一讲中的简单时间获取服务器是一个迭代服务器,对于获取时间来说够用了。迭代服务器有这样的特点:同一时间只能给一个客户服务。也就是说,如果某一时刻服务器与某个客户正在连接,其它客户必须等到上一个客户与服务器断开连接后才能连接成功。这对于需要花长时间处理客户请求的服务器来说并不适用。
解决办法是将服务器改为并发服务器。这样,即使有一个客户正在和服务器连接,其它的客户也能与服务器建立连接获得服务。最简单的办法就是fork函数。
1、fork函数
fork函数是UNIX中派生进程的唯一方法,定义在<unistd.h>头文件中:
fork函数在子进程中返回0而不是父进程ID的原因是:任何子进程只有一个父进程,而这个父进程的ID可以通过函数getppid获得;但是对于父进程,可以有多个子进程,所以父进程无法通过函数获得子进程ID,如果父进程想跟踪所有子进程的ID,那么父进程必须在每次调用fork时记住子进程ID。
2、并发服务器
上面介绍了fork函数,可以通过调用fork函数达到并发的效果。下面是典型的并发服务器框架:
(1)服务器调用socket、bind和listen函数后,处于监听状态,等待客户发送连接请求,这个时候服务器还没有调用accept:
(2)这时服务器只有一个套接字描述符listenfd,即监听描述符。当服务器中accept函数返回时,就有了两个套接字描述符listenfd和connfd,套接字描述符connfd是一个连接描述符,可以进行读写操作:
(3)这个时候客户与服务器建立了连接。随后服务器调用fork函数,自己变成父进程,复制自己形成一个子进程。由于两个进程相同,所以客户与两个进程的两个connfd都处于连接状态:
(4)然后,父进程中关闭连接套接字connfd,子进程关闭监听套接字listenfd:
这就达到了我们希望的状态,由子进程处理客户请求,父进程继续监听。
需要注意的是,close函数会导致发送一个FIN,随后TCP连接应该终止。那为什么父进程中close(connfd)后没有终止与客户的连接呢?每个文件描述符都有一个引用计数,是当前打开着的引用该文件的描述符的个数,只有引用计数为0时文件才会关闭。而fork函数执行后,对connfd套接字的引用计数是2,父进程关闭connfd后引用计数变为1,所以不会关闭连接。然而在子进程中,关闭connfd后引用计数变为0,会关闭与客户的连接。
3、时间获取程序的改进
这里我们只需要改进服务器程序,将它变成一个并发服务器。代码如下:
(1)打开服务器:
(2)客户1连接(进程ID是3594):
(3)客户2连接(进程ID是3596):
(4)下图是服务器处理情况:
可以看到,在客户1(进程ID是3594)仍在sleep的时候,服务器接受了客户2(进程ID是3596)的连接。
解决办法是将服务器改为并发服务器。这样,即使有一个客户正在和服务器连接,其它的客户也能与服务器建立连接获得服务。最简单的办法就是fork函数。
1、fork函数
fork函数是UNIX中派生进程的唯一方法,定义在<unistd.h>头文件中:
pid_t fork(void);这个函数的奇特之处在于一次调用,两次返回。在父进程中返回子进程的ID,在子进程中返回0。所以,我们可以通过返回值判断当前进程是父进程还是子进程。
fork函数在子进程中返回0而不是父进程ID的原因是:任何子进程只有一个父进程,而这个父进程的ID可以通过函数getppid获得;但是对于父进程,可以有多个子进程,所以父进程无法通过函数获得子进程ID,如果父进程想跟踪所有子进程的ID,那么父进程必须在每次调用fork时记住子进程ID。
2、并发服务器
上面介绍了fork函数,可以通过调用fork函数达到并发的效果。下面是典型的并发服务器框架:
pid_t pid; int listenfd,connfd; listenfd=socket(...); bind(listenfd,...); listen(listenfd,...); for(;;) { connfd=accept(listenfd,...); if((pid=fork)==0) { close(listenfd);//关闭listenfd doit(connfd);//处理请求 close(connfd);//关闭子进程connfd exit(0);//子进程退出 } close(onnfd);//关闭父进程connfd }这个框架中究竟发生了什么?可以用下面的图示展示出来。
(1)服务器调用socket、bind和listen函数后,处于监听状态,等待客户发送连接请求,这个时候服务器还没有调用accept:
(2)这时服务器只有一个套接字描述符listenfd,即监听描述符。当服务器中accept函数返回时,就有了两个套接字描述符listenfd和connfd,套接字描述符connfd是一个连接描述符,可以进行读写操作:
(3)这个时候客户与服务器建立了连接。随后服务器调用fork函数,自己变成父进程,复制自己形成一个子进程。由于两个进程相同,所以客户与两个进程的两个connfd都处于连接状态:
(4)然后,父进程中关闭连接套接字connfd,子进程关闭监听套接字listenfd:
这就达到了我们希望的状态,由子进程处理客户请求,父进程继续监听。
需要注意的是,close函数会导致发送一个FIN,随后TCP连接应该终止。那为什么父进程中close(connfd)后没有终止与客户的连接呢?每个文件描述符都有一个引用计数,是当前打开着的引用该文件的描述符的个数,只有引用计数为0时文件才会关闭。而fork函数执行后,对connfd套接字的引用计数是2,父进程关闭connfd后引用计数变为1,所以不会关闭连接。然而在子进程中,关闭connfd后引用计数变为0,会关闭与客户的连接。
3、时间获取程序的改进
这里我们只需要改进服务器程序,将它变成一个并发服务器。代码如下:
#include <sys/socket.h> #include <string.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <time.h> #include <unistd.h> #define MAXLINE 1024 int main(int argc,char *argv[]) { int listenfd; struct sockaddr_in servaddr; char buff[MAXLINE]; time_t ticks; if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0) { printf("socket error\n"); return 0; } bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(5000); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) { printf("bind error\n"); return 0; } if(listen(listenfd,5)<0) { printf("listen error\n"); return 0; } int connfd; socklen_t len; struct sockaddr_in cliaddr; pid_t pid; for(;;) { len=sizeof(cliaddr); if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0) { printf("accept error\n"); return 0; } if((pid=fork())==0) { if(close(listenfd)<0) { printf("close listenfd error\n"); return 0; } printf("[PID]%ld Receive a connection from:%s.%d\n",(long)getpid(),inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port)); ticks=time(NULL); snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks)); if(write(connfd,buff,strlen(buff))<0) { printf("write error\n"); return 0; } printf("[PID]%ld sleep 5s.\n",(long)getpid()); sleep(5); printf("[PID]%ld sleep done.\n",(long)getpid()); if(close(connfd)<0) { printf("close child connfd error\n"); return 0; } return 0; } if(close(connfd)<0) { printf("close parent connfd error\n"); return 0; } } }这里我们为了增加服务器处理每个请求的时间,加了sleep(5),会看到,服务器在处理一个客户的请求时,也会与另一个客户建立连接处理请求。运行结果如下:
(1)打开服务器:
(2)客户1连接(进程ID是3594):
(3)客户2连接(进程ID是3596):
(4)下图是服务器处理情况:
可以看到,在客户1(进程ID是3594)仍在sleep的时候,服务器接受了客户2(进程ID是3596)的连接。
相关文章推荐
- CAS实现http验证的单点登陆
- tomcat配置Https
- 网络编程的几个知识点
- HTTP状态码查询
- [连载]《C#通讯(串口和网络)框架的设计与实现》- 9.插件引擎设计
- WireShark简单使用以及TCP三次握手
- iOS-pthread && NSThread && iOS9网络适配
- jsp 无法加载“http://jsptags.com/tags/navigation/pager”解决办法
- iOS-网络处理框架AFN
- PIMSM
- jsp 无法加载“http://java.sun.com/jsp/jstl/core”解决办法
- java 通过网络 ntp 获取网络时间
- 《TCP/IP详解 卷1:协议》 读书笔记 第六章 ICMP:Internet控制报文协议
- Android网络优化6--写一个网络请求模板2--基于Volley
- android studio中观看json&http视频疑问
- HttpResponse cannot be resolved to a type android
- Android 判断手机是否连接网络
- 厦门巨游网络科技有限公司(HOTPOWER)承接游戏UI外包
- HTTP解析
- TCP/IP详解之:UDP协议