这一章主要是完成一个完整的tcp客户/服务器程序.通过一很简单的例子.弄清客户和服务器如何启动,如何终止,发生了某些错误会发生什么.这些事很重要的
客户端代码
#include "unp.h"
//static void str_cli1(FILE*fp,int sockfd);
int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
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);//逻辑处理
exit(0);
}
str_cli函数代码
#include "unp.h"
void str_cli(FILE*fp,int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
while(Fgets(sendline,MAXLINE,fp)!=NULL){//从键盘接收保存到sendline
Writen(sockfd,sendline,strlen(sendline)); //写给服务端
if(Readline(sockfd,recvline,MAXLINE)==0)//接收从服务端返回的数据
err_quit("str_cli:服务端提前关闭");
Fputs(recvline,stdout);//打印到屏幕
}
}
(1)三次握手状态
当客户端调用connect(阻塞状态)的时候.将发送一个syn.此时状态为syn_sent.
服务端此刻在监听.创建了一个syn未连接队列.一个已连接队列.接收到客户端的syn包,将放在syn未连接队列.调用accept(阻塞)将发送一个syn+ack.服务端状态为syn_recv
客户端接收到服务端的syn+ack后.connect返回.将发送一个ack给服务端.服务端接收到ack后.syn未连接将会被转移到已连接队列.然后Accept从中拿一个已连接socket返回...双方状态为ESTABLISH
此时客户端阻塞于str_cli函数中的fget调用.因为我们木有从键盘输入.
当服务端的accept返回.服务端调用fork.再有子进程调用str_echo.该函数调用了readline,readline调用了read.而read在等待客户数据的到来而阻塞
另外.服务端的父进程再次调用accept并且阻塞(未完成连接队列跟已完成的链接队列都为空).此时监听套接字为Listen状态
以上.我们将有3个进程都在睡眠.客户进程,服务器父进程,服务器子进程
#include "unp.h"
int main(int argc,char *argv[])
{
int listenfd=Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr;
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_addr.s_addr=0;
Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
Listen(listenfd,LISTENQ);
for(;;){
pid_t childpid;
int connect=Accept(listenfd,NULL,NULL);
if((childpid=Fork())==0){
Close(listenfd);
str_echo(connect);
exit(0);
}
close(connect);
- }
}
str_echo套接字回射数据
#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");
}
客户端关闭状态
关闭客户端键入cltr+d使得客户端终止
过程分析
(1)我们键入eof.fgets返回一个空指针.str_cli返回
(2)str_cli返回到客户端main函数后.main通过exit终止
(3)进程一旦终止将关闭所有打开的描述符,内核将关闭客户打开的套接字.这将会使得客户端的tcp发送一个FIN(进入FIN_WAIT_1).
服务端TCP受到FIN.将返回ACK响应(服务端进入CLOSE_WAIT)状态.客户端收到服务端的ACK.将进入FIN_WAIT_2
(4)服务端受到由客户端发送的FIN.服务器子进程还阻塞到readline调用.接收了fin.将返回0.使得Str_echo函数返回到服务器子进程的main函数中
(5)服务器子进程通过调用exit来终止.这将导致所有描述符都关闭..子进程关闭已连接套接字回使得服务端发送FIN(LAST_ACK).
客户端一旦接收到服务端发来的FIN将进入time_wait状态.并发送一个ACK给服务端.服务端接收到ACK后.将处于close.
以上是tcp 4次断手的过程
(7)这里有一个bug.服务器子进程终止时.给父进程发送一个SIGCHLD信号.但以上代码没捕捉这个信号,默认忽略.
这样会使得子进程进入僵尸状态.占用系统资源.
POSIX信号处理
信号也称为软件中断.信号通常异步发生的
信号来源:一个进程发给其他进程,由内核发给某个进程.
信号发生对应的动作:
(1)我们可以提供一个函数,用来执行对应的动作,这样的函数表示信号处理函数,这种行为表示捕获信号.但有2个信号无法捕捉(SIGKILL跟SIGSTOP)
(2)把信号的处置设定为SIG_IGN来忽略它.
(3)把信号的处置设置为SIG_DFL启动默认处置(绝大多数是终止进程)
这是实现一个signal函数
Sigfunc * signal(int signo,Sigfunc *func)
{
struct sigaction act,oact;
act.sa_handler=func;
sigemptyset(&act.sa_mask);// empty the block setmask
act.sa_flags=0;
if(sigaction(signo,&act,&oact)<0)
return (SIG_ERR);
return oact.sa_handler;
}
1.当信号处理函数运行时,突然发生一个新的信号.这个信号是会被阻塞的,而且安装处理函数时在sa_mask信号集任何额外的信号也会被阻塞
2.一个信号被阻塞期间产生一次或多次,该信号被解除阻塞后,只会提交一次,可以用sigprocmask函数选择性的阻塞跟解除阻塞
处理僵尸进程
设置僵尸状态是为了维护子进程的信息,父进程可能在某个时刻需要获取子进程相关信息,这些信息包括子进程的进程ID,终止原因,资源利用信息,
如果父进程挂了,子进程处于僵尸状态.init 进程将成为这些子进程的父进程.一般会清理她们,去除僵尸状态.
用以下函数来捕捉信号
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid=wait(&stat);
printf("child %d stoped\n",pid);
}
(书上用的是solaris9特定的例子.以下是该环境说明.被信号打断的系统调用不会重启)
当子进程挂了.发送一个SIG_CHLD信号给父进程,此时父进程阻塞于accept,因为信号原因,accept被打断,将返回一个EInTR错误(被中断的系统调用),父进程不处理该错误,父进程将终止
我用的是centos.这个环境下,被信号打断的系统调用将会自动重启不会出现书上的错误
[root@localhost five]#
child 13099 stoped
处理被中断的系统调用
慢系统调用:表示可能永远阻塞的系统调用
适用于慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且处理函数返回时,该系统调用可能返回一个EINtR错误,有些内核会自动重启某些被中断的系统调用,不过为了方便移植,我们编写捕获信号函数时,必须被返回EINTR有准备,一般处理规则如下
if((connfd=accept(listenfd,NULL,NULL)<0){
if(errno==EINTR)
continue;
else
err_sys("accept error");
}
注意:这段代码表示的就是自己重启被中断的系统调用,不过有一个函数我们不能重启:connect.该函数返回EINTR.不能再次重启,否则立刻返回一个该套接字已被使用的错误,因为connect发送syn.一直没收到服务端的ack,一旦被打断,重启可能又发送syn.这样会又用该套接字会被认为是不对的,比如打断后,服务端恰好已发送了ack只是没接受到而已.
wait跟waitpid函数
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
服务器进程终止
服务器子进程杀死.将发送一个fin给客户端,客户端回一个ack给服务端.SIGCHLD信号.然而客户端此时依旧阻塞在fgets调用等待从中断接收一行文本
如果我们依旧客户端write发送数据给服务端,服务端接收数据时.发现之前打开那个套接字进程已经挂了,于是响应一个RST.
这里要分情况 1.如果客户端调用readline在收到RST包之前.那客户端readline返回0表示服务端已经关闭,否则如果客户端在收到RST后,才调用readline.那readline将会返回一个ECONNRESET(connection reset by peer)
这种情况一般发生在服务进程较客户进程提前终止。当服务进程终止时会向客户 TCP 发送 FIN 分节,客户 TCP
回应 ACK,服务 TCP 将转入 FIN_WAIT2 状态。此时如果客户进程没有处理该 FIN (如阻塞在其它调用上而没有关闭 Socket
时),则客户 TCP 将处于 CLOSE_WAIT 状态。当客户进程再次向 FIN_WAIT2 状态的服务 TCP 发送数据时,则服务 TCP
将立刻响应 RST。一般来说,这种情况还可以会引发另外的应用程序异常,客户进程在发送完数据后,往往会等待从网络IO接收数据,很典型的如
read 或 readline 调用,此时由于执行时序的原因,如果该调用发生在 RST 分节收到前执行的话,那么结果是客户进程会得到一个非预期的
EOF 错误。此时一般会输出“server terminated prematurely”-“服务器过早终止”错误
回应 ACK,服务 TCP 将转入 FIN_WAIT2 状态。此时如果客户进程没有处理该 FIN (如阻塞在其它调用上而没有关闭 Socket
时),则客户 TCP 将处于 CLOSE_WAIT 状态。当客户进程再次向 FIN_WAIT2 状态的服务 TCP 发送数据时,则服务 TCP
将立刻响应 RST。一般来说,这种情况还可以会引发另外的应用程序异常,客户进程在发送完数据后,往往会等待从网络IO接收数据,很典型的如
read 或 readline 调用,此时由于执行时序的原因,如果该调用发生在 RST 分节收到前执行的话,那么结果是客户进程会得到一个非预期的
EOF 错误。此时一般会输出“server terminated prematurely”-“服务器过早终止”错误
这里的问题在于:当fin到达套接字时,客户阻塞在fgets.但客户实际在对应2个描述符--套接字跟用户输入,它也不能单纯阻塞到这个2个之中一个.而应该阻塞在其中任何一个.(select poll可以解决)
SIGPIPE信号
如果一个进程向已收到的RST的套接字继续执行写操作的话,内核将向该进程发送一个SIGPIPE信号,该信号默认行为是终止.进程必须捕获该信号以免不情愿的终止
服务器主机奔溃
(1)当服务器主机奔溃时,已有的网络连接发不出任何东西(不是正常关机)
(2)当客户write发送数据候,阻塞到readline中,等待回射的应答.
(3)用tcpdump观察,可以看出客户TCP一直在持续重传数据分节,想要重服务端接收一个Ack,一共等待9分钟放弃重传,一旦客户TCP放弃重传.将给客户端返回一个ETIMEDOUT错误..如果一个路由已经判断服务器主机不可达,将响应一个 "destination unreachable (目的不可达)ICMP消息",返回的错误将是 EHOSTUNREACH
服务器主机奔溃后重启
假设客户端已跟服务器建立连接,服务器突然断网挂了,然后又重启了(客户端不知道)
当客户端发送数据时,客户TCP将收到RST,客户正阻塞readline.这将导致readline返回ECONNRESET错误
服务器正常关机.
init进程通常先给所有进程发送SIGTERM信号(这样给所有运行的进程一小段时间来清除和终止).然后再给仍然在运行的进程发送SIGKILL,服务器进程终止过程跟之前一样,