本文主要为对UNP第五章部分内容的实验和总结。

UNP第五章对一个echo服务器和客户端在各种连接状态下的表现做了详细的分析,包括了:

  1. 正常启动和终止;
  2. accept返回前连接中止;
  3. 服务器进程终止;
  4. 客户进程忽略读错误继续写数据;
  5. 服务器主机崩溃;
  6. 服务器主机崩溃后重启;
  7. 服务器主机关机。

连接模型是最简单的TCP连接模型:

程序代码基本以UNP中提供代码为主。服务器采用图5-12、图5-11和图5-3中的代码;客户端采用图5-4和图5-5中的代码。为简化分析不采用UNP中客户端连续向服务器发起多个连接的模型。

#include	"unp.h"

void
sig_chld(int signo)
{
pid_t pid;
int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
} 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");
} int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int); 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); Signal(SIGCHLD, sig_chld); /* must call waitpid() */ for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
} 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 */
}
}
#include	"unp.h"

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);
}
} 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);
}

本文主要以tcpdump(抓包写文件然后用wireshark查看)、netstat、ps等工具观察上述几种状态。程序运行在Ubuntu 14.04上,有需要时会采用另一台主机(Raspberry Pi)。服务器端口为9877。

一、正常启动和终止

正常TCP连接序列如下图所示:

服务器程序正常启动后,netstat观察到进程在9877端口监听。

随后在同一台主机上启动客户端进程发起连接,ps可以观察到服务端fork出一个子进程2665:

客户端发送数据0123456789,可以观察到服务端程序正常回射了该段数据。

此时netstat观察到连接的两端都已是established状态,客户端中内核分配了端口号50712。

最后客户端进程ctrl-d发送EOF终止连接,完成终止序列后,客户进程和服务端子进程退出,服务端父进程接到SIGCHILD信号,提供waitpid操作,回收了相关资源:

至此一次连接发起、数据传输、连接终止的过程结束。下面是tcpdump抓取的数据包:

对抓包结果的解释:

  1. 包1-3是TCP三次握手的序列,SYN消耗一个序列号;
  2. 包4-7是数据回射的过程,客户端向服务端PUSH数据,服务端ACK确认,然后服务端回射,向客户端PSH,客户端ACK确认。Ack的值是接受到的包的Seq+1,表示Seq及其以前的数据都已接收到,现在要求对方发送Seq+1号报文;
  3. 包8-10是TCP终止序列,可以看到第9个包中,服务端的FIN以及对客户端FIN的确认放在了一个包里。FIN也消耗一个序列号。

在连接终止后短时间内用netstat查询,可以观察到客户端处于time_wait状态。

二、accept返回前连接中止

此种情况相对复杂,暂时略过不表。

三、服务器进程终止

在客户端和服务器完成连接后,杀死服务器进程。这是在模拟服务器进程崩溃的情形。

建立连接和正常回射数据的过程和一中描述一致。

注意到现在服务器进程号为1893和1895,1895是1893的子进程,也是和客户端相连接的进程。

采用kill命令发送SIGINT到1895进程,即可杀死该进程。SIGCHILD信号被发送给父进程,并得到正确的处理。

现在我们来查看一下tcpdump的抓包结果:

服务端已经向客户端发送了FIN,客户端响应以一个ACK。但是此时在应用层上的客户端并没有任何响应,原因是它阻塞在了fgets上,正在等待从终端接受一行文本。

现在来观察一下netstat的输出:

服务器进程在发送FIN并接收ACK后进入了fin_wait2状态,而客户端进程则进入了close_wait状态。TCP终止序列的前半部分已经完成了。

这个时候再在客户端上键入文本,文本并没有正常回射,预设的信息被输出:

出现这个情况的原因是客户端进程在fgets接收到终端的输入后将数据通过writen写入socket,然后立即调用readline,而由于客户端已经收到FIN(在应用层表现为EOF),readline立即返回0,于是错误信息被输出。

而在服务端,接收到数据时,由于之前打开该套接字的进程已经终止,于是响应以一个RST。该RST不会被客户端感知到(UNP中还有对其时序的讨论,一般情况下都应该是先进入readline,而后RST才到达)。

下面是tcpdump的完整输出。

  1. 1-3是连接序列,4-7是正常回射;
  2. 8-9是服务器进程终止时发送的FIN以及客户端的确认;
  3. 10是客户端继续向套接字发送数据;
  4. 11是服务器的RST回应。

UNP中指出问题的原因出在客户端接收到FIN时正阻塞在fgets上,而此时客户端其实在应对终端和套接字两个输入,因此只阻塞在一个源上是错误的。这也是select、poll等多路复用函数的目标之一。

四、客户进程忽略读错误继续写数据

当一个进程向某个已经收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。因此,若客户端忽略readline返回的错误,继续写数据入套接字,因第一次写操作已经引发RST,那么第二次写则会触发SIGPIPE信号。

将str_cli函数改成如下所示:

void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, 1);
sleep(1);
Writen(sockfd, sendline+1, strlen(sendline)-1); if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout);
}
}

第一次writen将触发RST,第二次writen会触发SIGPIPE。

为了确定我们的确引发了SIGPIPE信号,我们在客户端中尝试捕获该信号,添加如下代码:

// inside echoclient
void sig_pipe(int signo){
printf("SIGPIPE captured!\n");
} // and inside main
Signal(SIGPIPE, sig_pipe);

接下来就是启动服务器,启动客户端发起连接,发送数据验证连接,然后用kill将服务器进程杀死的过程,和前面所述的是基本一样的。

服务器进程被杀死后,在客户端上继续发送数据,此时会出现如下情况:

可以看到,SIGPIPE确实被触发了。

这里有一点需要说明的是,在我进行测试的系统上,如果不设置信号处理函数sig_pipe,那么客户端进程接收到SIGPIPE后的动作是直接退出,并不会输出”written error: Broken pipe”的字样,这是SIGPIPE的默认行为。在设置了处理函数后,该信息被输出,因为SIGPIPE已被处理,不再终止进程,此时终止进程的是writen触发的错误。

tcpdump的结果如下:

  1. 1-3仍然是连接建立;
  2. 4-10是数据传输,由于对一行数据分开了两次发送,因此出现了两次传输的过程。值得注意的是第9个包,该包是服务器进程将对发送过来的数据的确认以及回射的数据同时发送回了客户端。
  3. 客户端进程对回射回来的数据的表现并不是分两次显示的。也就是说,并不会先输出第一个字符,再输出剩下的字符,而是所有字符同时显示的。这应该与系统的缓冲机制有关,标准输出一般是行缓冲。
  4. 11-14是结束的过程,(二)中已经解释过了。

五、服务器主机崩溃

接下来我们恢复正常的代码,来实验服务器主机崩溃的状况。

主机崩溃的与关机不同,因为现代操作系统在关机往往会关闭进程打开的所有文件描述符,导致进程发送一个FIN;与进程崩溃也不同,因为仅进程崩溃而主机仍在正常运行时,客户端发来数据,虽然与客户端连接的进程已经崩溃,但主机仍会回应一个RST给客户端。

主机崩溃与网络连接中断类似,都是客户的数据已无法送至主机,且主机也无法给客户任何响应,因此可以用直接断开以太网连接,也即拔网线的方式模拟主机崩溃的状况。

进行这一实验以及下面两个实验都需要服务器程序和客户端程序运行在不同的主机上,因此除以上实验使用的Ubuntu主机外,加入另一台主机(Raspberry Pi),使用Raspberry Pi作为服务端主机,Ubuntu作为客户端主机。两台主机在同一局域网内,服务端主机IP为172.18.217.188,客户端主机IP为172.18.217.80。

此外,由于ARP会定时更新地址解析表,而网线的断开将导致服务器无法对ARP查询进行响应,因此我们提前将服务器IP和MAC地址的对应关系以静态的方式写入地址解析表中,否则客户端会由于无法确定服务器MAC地址而终止重传。

我们分别在两个主机中启动服务器程序和客户端程序,客户端向服务器发起连接,并发送数据验证连接正常建立。

接下来,拔掉网线断开服务器主机和以太网的连接。此时客户端程序对其没有任何感知。使客户端继续向服务器发送数据,并不会得到回应。在本次实验中,客户端在过了大约15分钟后才有所相应,客户端输出connection timed out,然后终止退出。

观察tcpdump的结果(去掉了一些不相关的包):

  1. 1-9是正常的连接建立和数据发送的过程,如前所述;
  2. 在第10个包发送前断开了连接,第10个包无法送达服务端;
  3. 从11-32可以看到在800多秒的时间内,客户端将同样的数据重传了16次,仔细观察重传的时间间隔,可以发现tcp执行了指数退避的拥塞控制算法。

六、服务器主机崩溃后重启

主机崩溃后重启,将丢失原有的TCP连接信息,那么当客户端再向服务器发送数据时,服务器会响应以一个RST,而此时客户端正阻塞在readline上,那么readline会返回错误,客户端程序退出。

我们采用先断开连接然后重启服务器的方式来模拟这种状况。客户端的输入如下:

tcpdump的结果如下(去掉不相关的包):

  1. 前7个包不再解释;
  2. 在服务器完成重启后,第36个包客户端发送数据至服务器,服务器由于以丢失连接信息,直接响应以RST。

七、服务器主机关机

Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号(该信号可被捕获),等待一段固定的时间(往往在5~20秒),然后给所有仍在运行的进程发送SIGKILL信号(该信号不能被捕获)。因此接下来的结果和(三)中讨论的是基本一致的。区别在于因为服务器关机,不会再接到客户端发来的最后的数据,当然也不会响应以RST。

总结

在Unix系统中应用层的程序的表现和是直接与TCP/IP协议挂钩的,系统函数的调用会根据网络不同而产生不同的返回值或结果。了解在应用层怎样的操作将导致网络层中怎样的通信动作,以及可能产生的后果,对于网络编程来说是最基础的内容。

UNP学习总结(一)的更多相关文章

  1. UNP学习笔记(第十五章 UNIX域协议)

    UNIX域协议是在单个主机上执行客户/服务器通信的一种方法 使用UNIX域套接字有以下3个理由: 1.UNIX域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍 2.UNIX域套接字可用于在同 ...

  2. UNP学习笔记(第十四章 高级I/O函数)

    本章讨论我们笼统地归为“高级I/O”的各个函数和技术 套接字超时 有3种方法在涉及套接字的I/O操作上设置超时 1.调用alarm,它在指定超时时期满时产生SIGALRM信号 2.在select中阻塞 ...

  3. UNP学习笔记(第六章 I/O复用)

    I/O模型 首先我们将查看UNIX下可用的5种I/O模型的基本区别: 1.阻塞式I/O 2.非阻塞式I/O 3.I/O复用(select和poll) 4.信号驱动式I/O(SIGIO) 5.异步I/O ...

  4. UNP学习笔记(第五章 TCP客户/服务程序实例)

    我们将在本章使用前一章中介绍的基本函数编写一个完整的TCP客户/服务器程序实例 这个简单得例子是执行如下步骤的一个回射服务器: TCP回射服务器程序 #include "unp.h" ...

  5. unp学习笔记——Chapter1

    1.发现网络拓扑的几个重要的命令 (1).netstat -i 提供网络接口的信息.我们还指定-n 标志以输出数值地址,而不是试图把它们反向解析成名字.netstat -r 展示路由表. dzhwen ...

  6. UNP学习总结(二)

    本文是UNP复习系列的第二篇,主要包括了以下几个内容 UNIX系统下5种I/O模型 阻塞.非阻塞,同步.异步 epoll函数用例 一.Unix下的五种可用I/O模型 阻塞式I/O模型 阻塞式I/O是最 ...

  7. UNP学习笔记(第三十章 客户/服务器程序设计范式)

    TCP测试用客户程序 #include "unp.h" #define MAXN 16384 /* max # bytes to request from server */ in ...

  8. UNP学习笔记(第二十六章 线程)

    线程有时称为轻权进程(lightweight process) 同一进程内的所有线程共享相同的全局内存.这使得线程之间易于共享信息,然后这样也会带来同步的问题 同一进程内的所有线程处理共享全局变量外还 ...

  9. UNP学习笔记(第二十五章 信号驱动式I/O)

    信号驱动式I/O是指进程预先告知内核,使得当某个描述符发生某事时,内核使用信号通知相关进程. 套接字的信号驱动式I/O 针对一个套接字使用信号驱动式I/O(SIGIO)要求进程执行以下3个步骤: 1. ...

随机推荐

  1. HTML字体的设置

    CSS字体设置 box-sizing:border #content-box   box-shadow:设置盒子边框的阴影.     字体动作:   font-family:设置字体.比如:‘微软雅黑 ...

  2. js 语法简写积累

    if (!a || !b || !c || !d){//} 简写为:if([a, b, c, d].map(Boolean).includes(false)){//};

  3. 【NOIP题解】NOIP2017 TG D2T3 列队

    列队,NOIP2017 TG D2T3. 树状数组经典题. 题目链接:洛谷. 题意: Sylvia 是一个热爱学习的女孩子. 前段时间,Sylvia 参加了学校的军训.众所周知,军训的时候需要站方阵. ...

  4. JDk1.8源码StringBuffer

    一.概念 StringBuffer A thread-safe, mutable sequence of characters. A string buffer is like a {@link St ...

  5. 2018ICPC南京网络赛

    2018ICPC南京网络赛 A. An Olympian Math Problem 题目描述:求\(\sum_{i=1}^{n} i\times i! \%n\) solution \[(n-1) \ ...

  6. Linux压缩打包方法连载之三:bzip2, bzcat 命令

    Linux压缩打包方法有多种,本文集中讲解了bzip2, bzcat 命令的使用.案例说明,例如# 与 gzip 同样的,都是在计算压缩比的参数,-9 最佳,-1 最快. AD: 我们遇见Linux压 ...

  7. go语言 documentation

    Documentation文档   The Go programming language is an open source project to make programmers more pro ...

  8. angular 如何使用第三方组件ng-bootstrap

    1.在你的项目中以下指令    npm install --save @ng-bootstrap/ng-bootstrap 安装完成会显示 + @ng-bootstrap/ng-bootstrap@1 ...

  9. maven package exec 及 maven 配置文件详解

    maven package test包下执行test 的配置文件 生成target目录,编译.测试代码,生成测试报告,生成jar/war文件 maven 配置文件详解 http://blog.csdn ...

  10. 【前端vue开发】vue子调父 $emit (把子组件的数据传给父组件)

    ps:App.vue 父组件 Hello.vue 子组件 <!--App.vue :--> <template> <div id="app"> ...