UNIX网络编程——并发服务器(TCP)
在迭代服务器中,服务器只能处理一个客户端的请求,如何同时服务多个客户端呢?在未讲到select/poll/epoll等高级IO之前,比较老土的办法是使用fork来实现。
网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程,最简单的办法就是直接忽略SIGCHLD信号。
当一个连接建立时,accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接套接字connfd),父进程则等待另一个连接(通过监听套接字listenfd)。既然新的客户由子进程提供服务,父进程就关闭已连接套接字。
首先下图给出了在服务器阻塞于accept调用且来自客户的连接请求到达时客户和服务器的状态。
从accept返回后,我们立即就有下面的状态。连接被内核接受,新的套接字connfd被创建。这是一个已连接套接字,可由此跨连接读写数据。
并发服务器的下一步是调用fork,下面是从fork返回后的状态。
注意,此时listenfd和connfd这两个描述符都在父进程和子进程之间共享(被复制),再下一步是由父进程关闭已连接套接字,由子进程关闭监听套接字。如下图:
在编写TCP并发服务器的时可能会遇到三种情况:
- 当fork子进程时,必须捕获SIGCHLD信号;
- 当捕获信号时,必须处理被中断的慢系统调用;
- SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数,以免留下僵死进程。
我们用术语慢系统调用描述accept,该术语也适用于那些可能永远阻塞的系统调用。
适用于慢系统调用的基本规则是:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。有些内核自动重启某些被中断的系统调用。不过为了便于移植,当我们编写捕获信号的程序时(多数并发服务器捕获SIGCHLD),我们必须对慢系统调用返回EINTR有所准备。
服务器程序serv.c:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h> #define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0) void do_service(int); int main(void)
{
signal(SIGCHLD, SIG_IGN);
int listenfd; //被动套接字(文件描述符),即只可以accept, 监听套接字
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// listenfd = socket(AF_INET, SOCK_STREAM, 0)
ERR_EXIT("socket error"); struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */ int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt error"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind error"); if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
ERR_EXIT("listen error"); struct sockaddr_in peeraddr; //传出参数
socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
int conn; // 已连接套接字(变为主动套接字,即可以主动connect) pid_t pid; while (1)
{
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完成的序列
{
if( errno == EINTR ) ///////////////////////////////////////////////////////////////////必须处理被中断的系统调用
continue;
else
ERR_EXIT("accept error");
}
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port)); pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{
// 子进程
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
}
else
close(conn); //父进程
} return 0;
} void do_service(int conn)
{
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
if (ret == 0) //客户端关闭了
{
printf("client close\n");
break;
}
else if (ret == -1)
ERR_EXIT("read error");
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
}
上述程序利用了一点,就是父子进程共享打开的文件描述符,因为在子进程已经用不到监听描述符,故将其关闭,而连接描述符对父进程也没价值,将其关闭。当某个客户端关闭,则read 返回0,退出循环,子进程顺便exit,但如果没有设置对SIGCHLD信号的忽略,则因为父进程还没退出,故子进程会变成僵尸进程。
现在先运行server,再打开另外两个终端,运行client(直接用<<UNIX网络编程——TCP回射服务器/客户端程序>>中的客户端程序),可以看到server输出如下:
huangcheng@ubuntu:~$ ./serv
recv connect ip=127.0.0.1 port=42114
recv connect ip=127.0.0.1 port=42115
在另一个终端ps一下:
huangcheng@ubuntu:~$ ps -aux | grep serv
/usr/lib/system-service/system-service-d
1000 3813 0.0 0.0 1640 404 pts/1 S+ 11:27 0:00 ./serv
1000 3815 0.0 0.0 1640 168 pts/1 S+ 11:27 0:00 ./serv
1000 3817 0.0 0.0 1640 156 pts/1 S+ 11:27 0:00 ./serv
1000 3824 0.0 0.0 3572 904 pts/3 S+ 11:28 0:00 grep --color=auto serv
发现共有3个进程,其中一个是父进程处于监听中,另外两个是子进程处于对客户端服务中,现在ctrl+c 掉其中一个client,由上面的分析可知对应服务的子进程也会退出,而因为我们设置了父进程对SIGCHLD信号进行忽略,故不会产生僵尸进程,输出如下:
huangcheng@ubuntu:~$ ps -aux | grep serv
1000 3813 0.0 0.0 1640 404 pts/1 S+ 11:27 0:00 ./serv
1000 3815 0.0 0.0 1640 168 pts/1 S+ 11:27 0:00 ./serv
1000 3831 0.0 0.0 3572 904 pts/3 S+ 11:29 0:00 grep --color=auto serv
如果把第22行代码注释掉,上述的情景输出为:
1000 3876 0.0 0.0 1640 408 pts/1 S+ 11:32 0:00 ./serv
1000 3878 0.0 0.0 1640 172 pts/1 S+ 11:32 0:00 ./serv
1000 3880 0.0 0.0 0 0 pts/1 Z+ 11:32 0:00 [serv] <defunct>
1000 3885 0.0 0.0 3572 900 pts/3 S+ 11:33 0:00 grep --color=auto serv
即子进程退出后变成了僵尸进程。
如果不想忽略SIGCHLD信号,则必须在信号处理函数中调用wait处理,但这里需要注意的是wait只能等待第一个退出的子进程,所以这里需要使用waitpid函数,如下所示:
signal(SIGCHLD, handler);
..................... void handler(int sig)
{
pid_t pid;
int stat;
/* wait(NULL); //只能等待第一个退出的子进程 */
/* 即使因为几个连接同时断开,信号因不能排队而父进程只收到一个信号
* 直到已经waitpid到所有子进程,返回0,才退出循环 */
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
1. 必须编写SIGCHLD信号的信号处理函数,原因为:防止出现僵死进程
2. 当捕获信号时,必须处理被中断的系统调用,原因为:
(1)我们键入EOF字符来终止客户。客户TCP发送一个FIN给服务器,服务器响应以一个ACK。
(2)收到客户的FIN导致服务器TCP递送一个EOF给子进程阻塞中的read,从而子进程终止。
(3)当SIGCHLD信号递交时,父进程阻塞于accept调用。handler函数(信号处理函数)执行,其wait调用取到子进程的PID和终止状态,随后是printf调用,最后返回。
(4)既然该信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核就会使accept返回一个EINTR错误(被中断的系统调用)。父进程不处理该错误,于是终止。
3. SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数,以免留下僵死进程,原因为:
客户建立于服务器5个连接
修改过后的客户端程序如下:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h> #define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0) void do_echocli(int sock)
{ char sendbuf[1024] = {0};
char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{ write(sock, sendbuf, strlen(sendbuf)); int ret = read(sock, recvbuf, sizeof(recvbuf));
if (ret == -1)
ERR_EXIT("read error");
else if (ret == 0) //服务器关闭
{
printf("server close\n");
break;
} fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf)); } close(sock);
} int main(void)
{
int sock[5];
int i;
for (i = 0; i < 5; i++)
{
if ((sock[i] = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// listenfd = socket(AF_INET, SOCK_STREAM, 0)
ERR_EXIT("socket error"); struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */ if (connect(sock[i], (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect error"); struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if (getsockname(sock[i], (struct sockaddr *)&localaddr, &addrlen) < 0)
ERR_EXIT("getsockname error");
/* getpeername()获取对等方的地址 */
printf("local ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr),
ntohs(localaddr.sin_port));
}
/* 一个进程也可以发起多个socket连接,因为每次的端口号都不同 */
do_echocli(sock[0]); //发起5个套接字连接,但只借助第一个套接口通信 return 0;
}
在上述程序中,我们发起5个sock连接,但只是使用sock0通信,且利用getsockname 打印5个连接的信息。
先运行服务器程序,再运行客户端,客户端输出如下:
huangcheng@ubuntu:~$ ./cli
local ip=127.0.0.1 port=33867
local ip=127.0.0.1 port=33868
local ip=127.0.0.1 port=33869
local ip=127.0.0.1 port=33870
local ip=127.0.0.1 port=33871
huangcheng
huangcheng
即每个连接的ip地址是一样的,但端口号不同,服务器方面通过accept返回的信息也打印出连接信息,如下:
huangcheng@ubuntu:~$ ./serv
recv connect ip=127.0.0.1 port=33867
recv connect ip=127.0.0.1 port=33868
recv connect ip=127.0.0.1 port=33869
recv connect ip=127.0.0.1 port=33870
recv connect ip=127.0.0.1 port=33871
huangcheng
当客户终止时,所有打开的描述符由内核自动关闭(我们不调用close,仅调用exit),且所有的5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个,他们反过来使服务器的5个子进程基本在同一时刻终止。这又导致差不多在同一时刻有5个SIGCHLD信号递交给父进程:
我们预期所有的5个子进程都终止了。但是运行PS,我们发现其他4个子进程仍然作为僵死进程存在着。
正确的解决办法是调用waitpid而不是wait。我们必须指定WNOHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。我们不能再循环内调用wait,因为没有办法防止wait在运行的子进程尚未终止时阻塞。
注意前面的代码:
while (1)
{
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完成的序列
{
if( errno == EINTR ) ///////////////////////////////////////////////////////////////////必须处理被中断的系统调用
continue;
else
ERR_EXIT("accept error");
}
这段代码所做的事情就是自己重启被中断的系统调用。对于accept以及诸如read、write、select和open之类函数来说,这是合适的。不过有一个函数我们不能重启:connect。如果该函数返回EINTR,我们就不能再次调用它,否则将立即返回一个错误。当connect被一个捕获的信号中断而且不能重启时,我们必须调用select来等待连接完成。
注意:关于此说明在后面的博客<<UNIX网络编程——非阻塞connect:时间获取客户程序>>里面的被中断的connect有说明。
UNIX网络编程——并发服务器(TCP)的更多相关文章
- UNIX网络编程——客户/服务器程序设计示范(总结)
(1)当系统负载较轻是,每来一个客户请求现场派生一个子进程为之服务的传统并发服务器程序模型就足够了.这个模型甚至可以与inetd结合使用,也就是inetd处理每个连接的接收.我们的其他意见是就重负荷运 ...
- UNIX网络编程——客户/服务器心搏函数
阅读此博客时,可以参考以前的博客<<UNIX网络编程--socket的keep-alive>>和<<UNIX网络编程--套接字选项(心跳检测.绑定地址复用)> ...
- UNIX网络编程——客户/服务器程序设计示范(六)
TCP并发服务器程序,每个客户一个线程 前面讲述了,每个客户一个进程的服务器,或为每个客户现场fork一个子进程,或者预先派生一定数目的子进程.如果服务器主机支持线程,我们就可以改用线程以取代子进程. ...
- UNIX网络编程——客户/服务器程序设计示范(三)
TCP预先派生子进程服务器程序,accept无上锁保护 我们的第一个"增强"型服务器程序使用称为预先派生子进程的技术.使用该技术的服务器不像传统意义的并发服务器那样为每个客户现场派 ...
- UNIX网络编程——客户/服务器程序设计示范(二)
TCP并发服务器程序,每个客户一个子进程 传统上并发服务器调用fork派生一个子进程来处理每个客户.这使得服务器能够同时为多个客户服务,每个进程一个客户.客户数目的唯一限制是操作系统对以其名义 ...
- 【Unix网络编程】 chapter5 TCP客户,服务器程序实例
chapter5 5.1 概述 5.2 TCP回射服务器程序:main函数 int main(int argc, char **argv) { int listenfd,connfd; pid_t c ...
- UNIX网络编程——客户/服务器程序设计示范(一)
下面给出的是客户程序用于测试我们的服务器程序的各个变体. #include "unp.h" #define MAXN 16384 /* max # bytes to request ...
- UNIX网络编程——客户/服务器程序设计示范(八)
TCP预先创建线程服务器程序,主线程统一accept 最后一个使用线程的服务器程序设计示范是在程序启动阶段创建一个线程池之后只让主线程调用accept并把每个客户连接传递给池中某个可用线程. ...
- UNIX网络编程——客户/服务器程序设计示范(七)
TCP预先创建线程服务器程序,每个线程各自accept 前面讨论过预先派生一个子进程池快于为每个客户线程派生一个子进程.在支持线程的系统上,我们有理由预期在服务器启动阶段预先创建一个线程池以取 ...
随机推荐
- day4 liaoxuefeng---面向对象编程、IO编程
一.面向对象编程 二.面向对象高级编程 三.IO编程
- PowerBI 第九篇:修改查询
在PowerBI的查询编辑器中,用户可以使用M语言修改Query,或修改Query字段的类型,或向Query中添加数据列(Column),对Query进行修改会导致PowerBI相应地更新数据模型(D ...
- 干货满满,腾讯云+社区技术沙龙 Kafka Meetup 深圳站圆满结束
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 云+导语:4月22日,由腾讯云和 Kafka 社区主办.开源中国协办的腾讯云+社区技术沙龙 Kafka Meetup 深圳站在腾讯大厦举行, ...
- 指尖大冒险H5小游戏
前些天看了一篇很赞的文章,又因为想学习phaser,所以有了这个案例,在线预览可以点下方链接. 本案例中,核心原理是按文章中所提到的内容制作,整体遵循"大道至简"的原则开发,其实是 ...
- Python小代码_3_购物车
product_list = [ ('MacBook', 9000), ('kindle', 500), ('tesla', 900000), ('book', 100), ('bike', 2000 ...
- Windows环境下,从零开始搭建Nodejs+Express+Ejs框架(一)---安装nodejs
第一步,安装nodejs https://nodejs.org/en/download/ 这个是nodejs的官网,由于操作系统是win7 64位的,所以,我下载的是node-v8.11.1-x64的 ...
- 深入理解DirectByteBuffer
介绍 最近在工作中使用到了DirectBuffer来进行临时数据的存放,由于使用的是堆外内存,省去了数据到内核的拷贝,因此效率比用ByteBuffer要高不少.之前看过许多介绍DirectBuffer ...
- Dynamics CRM2016 The value of field on record of type entity is outside the valid range问题的解决方法
今天在用web api创建一条记录时报了个标题里的错,咋看这错说的很明白了,属性字段的值超范围了,但咱们看下具体的问题 请求url是这样的http://xx/api/data/v8.0/new_rec ...
- java开源即时通讯软件服务端openfire源码构建
java开源即时通讯软件服务端openfire源码构建 本文使用最新的openfire主干代码为例,讲解了如何搭建一个openfire开源开发环境,正在实现自己写java聊天软件: 编译环境搭建 调试 ...
- Leetcode难度表及解题汇总
Leetcode难度表及解题汇总 参考网上一份题目难度表,以及本人的解题. Id Question Difficulty Frequency Data Structures Algorithms Bl ...