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 前面讨论过预先派生一个子进程池快于为每个客户线程派生一个子进程.在支持线程的系统上,我们有理由预期在服务器启动阶段预先创建一个线程池以取 ...
随机推荐
- 数据库的事务、ACID及隔离级别
事务 所谓事务是用户定义的一个数据库操作序列,这些操作要么全做,要么不做,是一个不可分割的工作单位.例如,在关系数据库中,一条或一组SQL语句.整个程序都可以是一个事务. 事务和程序是两个概念,一个程 ...
- VS2012代码对齐快捷键
1.选中想要对齐的代码 2.全选代码后按住Ctrl+K,Ctrl+F键,就可以了
- python 程序中调用go
虽然python优点很多,但是有一个致命的缺点就是运行速度太慢,那么python程序需要一些计算量比较大的模块时一般会调用c或者c++的代码来重写,但是c/c++编写代码代价太高,耗费太多的人力.那么 ...
- python函数调用之自我调用与C++比较
C++下的函数自我自我调用 第一种方法 #include <iostream> using namespace std; int rel_do(){ int a; cout<< ...
- java 反射机制 观点
反射,当时经常听他们说,自己也看过一些资料,也可能在设计模式中使用过,但是感觉对它没有一个较深入的了解,这次重新学习了一下,感觉还行吧! 一,先看一下反射的概念: 主要是指程序可以访问,检测和修改它本 ...
- Spring--AOP 例子
先用代码讲一下什么是传统的AOP(面向切面编程)编程 需求:实现一个简单的计算器,在每一步的运算前添加日志.最传统的方式如下: Calculator.Java package cn.limbo.spr ...
- CSS缩写的样式
熟悉和了解CSS的朋友都知道,CSS样式表有很多缩写方式.比如,定义字体.定义背景等,都可以把CSS代码缩写到一行.为了能更好的搞清楚CSS缩写方法,我收集整理了一些有关CSS简写的参考资料,也是对自 ...
- Android 学习笔记一 自定义按钮背景图
入门学到的一些组件都是比较规矩的,但在实际应用中,我们需要更多特色的组件,例如一个简单的Button,所以我们必须要自定义它的属性. 遇到的问题:用两张图片来代替按钮,分别表示点击前后 解决方法:用I ...
- linux网络编程之二-----多播(组播)编程
多播编程实例 服务器端 下面是一个多播服务器的例子.多播服务器的程序设计很简单,建立一个数据包套接字,选定多播的IP地址和端口,直接向此多播地址发送数据就可以了.多播服务器的程序设计,不需要服务器加入 ...
- Python 3 智能发音
真是十分神奇.. import win32com.client import time s = win32com.client.Dispatch("SAPI.SpVoice") s ...