文章目录
版本1
服务端——多进程
- 服务端父进程负责监听并接收客户连接请求;
- 每当accept函数收到一个客户端连接请求,父进程就fork一个子进程来为该客户服务;
- 子进程采用str_echo()(版本1)函数进行回射服务;
服务端程序:
//tcpcliserv/tcpserv01.c
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
str_echo函数:
//lib/str_echo.c
#include "unp.h"
//完成的工作:读取套接字的内容并写回
void
str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0)
Writen(sockfd, buf, n);
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
err_sys("str_echo: read error");
}
客户端
- 发出连接请求,与服务器建立连接
- 调用str_cli函数从键盘读取一行数据并向服务器发送,并输出服务器的回射响应;
客户端程序
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
str_cli函数:
#include "unp.h"
//循环进行工作:1.从键盘读取一行数据,2.然后发给服务端,3.再从服务端读取数据
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, strlen(sendline));
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
知识点
1.正常终止:
- 当客户端键盘输入EOF时,fgets函数返回NULL,导致str_cli函数返回;
- str_cli函数返回之后,main函数随之结束,关闭其打开的所有文件描述符;
- 套接字的关闭过程为:客户TCP会向服务器发送一个FIN,此时TCP连接处于半关闭状态
- 服务器(正阻塞于read)收到FIN报文后,n = read(…)返回0,这会使str_echo函数结束并返回到(对应的服务器子进程的)main函数,接着服务器子进程调用exit(0)结束;
- 服务器子进程所打开的描述符随之关闭,服务器TCP向客户端发送FIN字段(四次握手中的第三个包),最终TCP连接被正确关闭;
2.存在的问题
- 当服务器子进程被杀死后,会给父进程(listen进程)发送一个SIGCHLD信号,而这个信号并未被捕获并处理,导致出现僵死进程;
- 因此会引入信号处理函数以及wait、waitpid函数。
版本2——处理僵死进程
主要内容:
- 改进之处:处理僵死进程;
- wait和waitpid对比;
- 为了更好地展示处理僵死进程的方法,修改了服务器主程序和客户端子程序;
客户端(tcpcli04)
为了更加直观的区分wait和waitpid,客户端进程与服务器建立5个连接,并在几乎同一时刻断开连接,这会导致服务器父进程几乎在同一时间收到5个SIGCHLD信号:
#include "unp.h"
int
main(int argc, char **argv)
{
int i, sockfd[5];
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
//建立5个连接
for (i = 0; i < 5; i++) {
sockfd[i] = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]); /* do it all */
exit(0);
}
服务器(tcpserv04)
修改的地方:在main()函数中加入以下一行,以处理SIGCHLD信号:
signal(SIGCHLD,sig_chld);
这行代码的意思是,当进程收到SIGCHLD信号时,会触发中断,而sig_chld函数会处理这个中断,因此我们需要在sig_chld函数内处理已终止的子进程,避免其成为僵死进程。
wait与waitpid
1.介绍
#include<sys/wait.h>
pid_t wait(int * statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
相同点:
- 返回值:成功则返回被杀死的子进程ID,失败则返回0或-1;
- 通过指针statloc返回子进程的终止状态;
- 每次可以处理一个已终止的子进程
区别:
- 调用wait时,若没有已经终止的子进程,但是有一个或多个子进程在运行,那么wait会阻塞到现有子进程的第一个终止为止;
- waitpid给了我们更多的选择,参数pid指定我们想等待的进程ID,值-1表示等待(处理)第一个终止的子进程,options参数指定附加选项,最常用的是WNOHANG,它告诉内核在没有已终止子进程时不要阻塞,例如:
//表示处理第一个已终止的子进程
//WNOHANG告知内核如果没有已终止的子进程时不要阻塞
waitpid(-1,&stat,WNOHANG);
2.应用
在书中,模拟了服务端listen父进程的五个子进程几乎同时终止的情况,也就是listen父进程会在几乎同一时间收到五个SIGCHLD信号:
- 由于UNIX的信号是不可排队的,因此某些信号可能会丢失,也就是说listen父进程可能只执行一次(执行次数不定,看信号到来时间)信号处理函数。PS:如果信号是隔一段时间发一个,那么信号就不会丢失,也就是会执行五次信号处理函数。
- 因此,我们需要在执行一次信号处理函数的情况下处理五个已终止的子进程;
- 如果在循环利用wait,而listen父进程除了那5个已终止子进程之外还有其他正在运行的子进程,那么listen父进程就会被阻塞在wait函数,无法进行监听;
- 而我们可以循环利用waitpid来处理,只需要设置一下WNOHANG参数:
void sig_chld(int signo)
{
pid_t pid;
int stat;
while((pid=waitpid(-1,&stat,WNOHANG))>0)
printf("child %d terminated",pid);
return;
}
待改进
服务器子意外进程终止
- 将一个客户进程与一个服务器子进程正常连接;
- kill杀死该服务器子进程,父进程收到子进程的SIGCHLD信号并正确处理;
- 服务器子进程的所有文件描述符(包括套接字)都被关闭,因此向客户发送一个FIN包;
- 客户TCP接受到FIN包并自动回发ACK包,此时连接处于半关闭状态,但是客户端不知道服务器子进程已经死了。
- 客户端此时正阻塞在fgets上,等待一行文本的输入;
- 输入一行文本之后,客户进程调用write函数将文本发送到服务器子进程;
- 由于服务器子进程已经终止,因此TCP会回发一个RST;
- 然而客户进程看不到这个RST,因为它调用write函数后立即进入readline函数,并由于3中接收到的FIN包,readline读到这个FIN包会返回0,然后打印出错信息并终止;
- 存在的问题是:客户端进程没有及时在第3步时就察觉到FIN字段的到来,原因是它正阻塞在fget(标准输入)上,我们可以在后面的版本中进行改进,使得客户端进程及时察觉到FIN包的到来;
- 此外,针对客户未能捕捉到RST,我们可以引入SIGPIPE信号。
SIGPIPE信号
如果一个进程不理会readline函数返回的错误,反而继续向服务器写入数据,会发生什么呢,这里先引入一个规则:
- 当一个进程向某个已收到RST的套接字执行写操作时,内核会向该进程发送一个SIGPIPE信号,该信号默认行为是终止进程,因此进程需要捕获该信号以避免被不情愿的终止。
下面来模拟一下上面的情形看看会发生什么:
- 修改上述的第8步,使得readline函数返回0时不终止客户进程,反而向写入(第二次写入)更多数据;
- 这样就会触发上面的规则,也就是说在第二次写入时,客户进程会收到SIGPIPE信号,这时由于客户进程不对SIGPIPE信号进行捕获,因此它被终止了,而bash可能也会提示相关内容。
版本3——I/O复用
为什么要引入I/O复用呢,原因之一是在版本2中服务器子意外进程终止的情况下存在如下问题:
str_cli改进版1
使用select改进上述问题,不再阻塞在fgets上面,这样就能及时收到FIN包了:
//select/tcpservselect01.c
#include"unp.h"
void str_cli(FILE*fp,int sockfd)
{
int maxfdpl;//最大描述符,select的第一个参数
fd_set rset;
char sendline[MAXLINE],recvline[MAXLINE];
FD_ZERO(&rset);
while(1)
{
//每次都要重置
FD_SET(fileno(fp),&rset);
FD_SET(sockfd,&rset);
maxfdpl = max(fileno(fp),sockfd);
Select(maxfdpl,&rset,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rset))
{
if(Readline(sockfd,recvline,MAXLINE)==0)
//及时收到FIN包并报错退出
err_quit("str_cli: server terminated peraturely");
Fputs(recvline,stdout);
}
if(FD_ISSET(fileno(fp),&rset))
{
//存在问题1:返回太早:接收到了EOF时就是NULL,这时str_cli函数返回,客户
//端进程也很快被终止,但是这时候可能还有些请求在去服务器的路上,或者有些
//响应在回来的路上,导致一些响应来不及显示在客户端上;
//存在问题2:此外Fgets存在缓冲区问题,应该避免select与stdio的混合使用
if(Fgets(sendline,MAXLINE,fp)==NULL)
return;
Writen(sockfd,sendline,strlen(sendline));
}
}
}
str_cli改进版2
上面的版本中仍然存在一些问题(见上面的代码注释),因此引入改进版2,解决了以下问题:
- 返回太早问题:引入shundown函数来解决:用shutdown关闭客户端向服务端写这一半,但不影响客户端继续读;
- 避免了select与stdio的混合使用;
#include "unp.h"
void str_cli(FILE*fp,int sockfd)
{
int maxfdpl,stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
//若该值为0,则在循环中select标准输入的可读性
//若已经收到了标准输入的EOF,则置为1,即不再检查标准输入
stdineof = 0;
FD_ZERO(&rset);
while(1)
{
if(stdineof == 0)
FD_SET(fileno(fp),&rset);
FD_SET(sockfd,&rset);
maxfdpl = max(fileno(fp),sockfd);
Select(maxfdpl,&rset,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rset))
{
if((n = Read(sockfd,buf,MAXLINE))==0)
{
if(stdineof==1)//说明用户输入完毕,这是正常的
return;
else
err_quit("str_cli: server terminated prematurely");
}
Writen(fileno(stdout),buf,n);
}
if(FD_ISSET(fileno(fp),&rset))
{
//判断是否结束输入
if((n=Read(fileno(fp),buf,MAXLINE))==0)
{
stdineof = 1;
Shutdown(sockfd,SHUT_WR);//发送FIN,关闭一边
FD_CLR(fileno(fp),&rset);//不再监视标准输入
continue;
}
Writen(sockfd,buf,n);
}
}
}
服务器的SELECT版本
既然已经学了select,那就尝试用select写一个单进程的服务器来处理多个客户端的请求:
#include "unp.h"
//第一个应用select的服务端
int main(int argc,char**argv)
{
int sockfd,i,maxi,maxfd,listenfd,connfd;
//FD_SETSIZE是内核允许本进程所能处理的最大客户数目
int nready, client[FD_SETSIZE];
ssize_t n;
//allset可以看做rset的副本
fd_set rset,allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in servaddr,cliaddr;
listenfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
Listen(listenfd,LISTENQ);//监听套接字
maxfd = listenfd;
maxi = -1;
for(i =0;i < FD_SETSIZE;i++)//初始化客户数组
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd,&allset);
while(1)
{
rset = allset;
nready = Select(maxfd+1,&rset,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&rset))
{
clilen = sizeof(cliaddr);
connfd = Accept(listenfd,(SA*)&cliaddr,&clilen);
//把accept到的客户connfd放到数组中保存
for(i = 0; i < FD_SETSIZE;i++)
{
if(client[i] < 0)
{
client[i] = connfd;
break;
}
if(i==FD_SETSIZE)
err_quit("too many clients");
//在allset新增元素,这里体现了同时声明allset
//和rset的用意,因为如果这里直接在rset中新增元素
//会影响到后面的FD_ISSET的判断
FD_SET(connfd,&allset);
if(connfd > maxfd)
maxfd = connfd;
if(i > maxi)
maxi = i;//maxi使得遍历client数组时更快
if(--nready <=0 )
continue;
}
for(i =0 ;i <= maxi;i++)
{
//sockfd是用来回射客户的信息,connfd是连接客户
if((sockfd = client[i]) < 0)
continue;
if(FD_ISSET(sockfd,&rset))
{
if((n=Read(sockfd,buf,MAXLINE))==0)
//客户端终止连接
{
Close(sockfd);
FD_CLR(sockfd,&allset);
client[i] = -1;
}
else
//正常回射
Writen(sockfd,buf,n);
}
if(--nready<=0)
break;
}
}
}
}
服务器的POLL版本
下面来用一下POLL来写服务器:
#include "unp.h"
#include <limits.h>
int main(int argc,char**argv)
{
int i,maxi,listenfd,connfd,sockfd;
int nready;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in servaddr,cliaddr;
//poll
struct pollfd client[OPEN_MAX];//头文件limits中引入
listenfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
Listen(listenfd,LISTENQ);
//需要监听的fd全部都要放到client数组中
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for(i=1;i < OPEN_MAX;i++)
client[i].fd = -1;
maxi=0;
while(1)
{
nready = Poll(client,maxi+1,INFTIM);
if(client[0].revents&POLLRDNORM)
{
clilen = sizeof(cliaddr);
connfd = Accept(listenfd,(SA*)&cliaddr,&clilen);
//监听新增的connfd
for(i = 1;i < OPEN_MAX;i++)
{
if(client[i].fd<0)
{
client[i].fd = connfd;
client[i].events = POLLRDNORM;
break;
}
}
if(i==OPEN_MAX)
err_quit("too many client");
if(i > maxi)
maxi = i;
if(--nready<=0)
continue;
}
for(i = 1;i<=maxi;i++)
{
if((sockfd=client[i].fd)<0)
continue;
if(client[i].revents&(POLLRDNORM|POLLERR))
{
if((n=read(sockfd,buf,MAXLINE))<0)
{
//连接被客户重置
if(errno == ECONNRESET)
{
Close(sockfd);
client[i].fd = -1;
}
else
err_sys("read error");
}
else if(n==0)
{
//连接被客户中断
Close(sockfd);
client[i].fd = -1;
}
else
Writen(sockfd,buf,n);
if(--nready <=0)
break;
}
}
}
}