正常启动
我们一起来学习使用netstat调试网络程序。
启动服务器程序:./tcpserv01 &
(&
表示后台启动)
服务器程序启动后,调用socket bind listen 和 accept,并阻塞于accept 调用。运行netstat程序来检查服务器监听套接字的状态,如下:LISTEN
。有没有注意到-a
选项可以查看所有监听套接字。我们的监听端口是9877,监听地址是通配地址0。
![644d386ac8df91aa0597d40d1a1be973.png](https://img-blog.csdnimg.cn/img_convert/644d386ac8df91aa0597d40d1a1be973.png)
启动客户程序:./tcpcli01
我修改了代码中IP地址为环回地址(127.0.0.1
)
客户端调用socket
和 connect
,引起TCP三次握手,这是服务器的accept
开始忙活了,当三次握手完成,客户端的connect
和服务端的accept
均返回,连接建立。
- 客户端:调用
str_cli
函数,该函数阻塞于fgets
调用。 - 服务端:调用fork,由子进程调用
str_echo
函数,该函数阻塞于read
调用。 - 服务端:父进程再次阻塞于
accept
函数并阻塞,等待下一个客户连接。
提示:客户端connect
在接收到三次握手的第二个握手报文返回,服务端accept
在接收到三次握手的第三个握手报文返回。
可以看出我们的子进程和客户端建立了TCP连接,服务端端口是9877,客户端端口是42120。
![c482bd386d6fdbfb7e66822a55b42810.png](https://img-blog.csdnimg.cn/img_convert/c482bd386d6fdbfb7e66822a55b42810.png)
通过ps
来看看进程间的关系:ps -t pts/0 -o pid,ppid,tty,stat,args,wchan
提示:我在这里重启了客户端和服务端的程序,所以进程ID、端口可能会发生变化。
- pts/0:0号伪终端,服务端跑的终端
- pts/1:1号伪终端,客户端跑的终端
注意:STAT
列的S
表示进程在为等待某些资源而睡眠,有+
号表示光标在该进程,可以输入数据。 当进程阻塞于accept
或connect
时,输出inet_csk_accept
。当进程阻塞于套接字输入或输出时,输出sk_wait_data
。当进程阻塞于终端IO时,输出wait_woken
。
![8a785199fa097007b7d07c877e8aacb7.png](https://img-blog.csdnimg.cn/img_convert/8a785199fa097007b7d07c877e8aacb7.png)
正常关闭
提示:在分析正常关闭网络程序的时候,我们再次重启了程序,而这是不必要的,如果你时间充裕,能一次性把这些东西都研究懂,当下是正常启动后。
![f2d54f50ec53e046ddf4859f43707220.png](https://img-blog.csdnimg.cn/img_convert/f2d54f50ec53e046ddf4859f43707220.png)
现在们试图关闭客户端连接,我们输入EOF
(Control+D)结束客户端连接,再看看连接状态,发现刚刚连接着的服务端没了,客户端状态变成TIME_WAIT
,监听服务器不变,依然在等待。
![0760f404f56940edcefaac9d22a8a655.png](https://img-blog.csdnimg.cn/img_convert/0760f404f56940edcefaac9d22a8a655.png)
- 当输入
EOF
字符,fgets
返回一个空指针,于是str_cli
返回。 str_cli
返回到客户端的main
函数时,调用exit
结束进程。- 结束进程会做很多事,其中包括关闭所有打开的描述符。所以,客户打开的套接字描述符由内核关闭,这将触发客户TCP发一个FIN报文给服务端,服务端收到FIN报文将回送一个ACK报文。此时,服务器套接字处于
CLOSE_WAIT
状态,客户套接字处于FIN_WAIT_2
状态。 - 当服务端TCP上层接收到FIN报文时,处于阻塞状态的
readline
函数返回0,str_echo返回。 str_echo
返回到服务端的main
函数时,调用exit
结束进程。- 服务端子进程中打开的所有描述符随即关闭,关闭服务端套接字将会触发TCP四次挥手的最后2个阶段,服务端内核将发送一个FIN报文给客户端,客户端收到FIN报文,发送一个ACK报文。至此,连接完全终止,客户套接字进入
TIME_WAIT
状态。(上图) - 服务端还需要注意一点,进程结束还需要做一件事:服务端子进程结束时,给父进程发送一个
SIGCHLD
信号,由于我们没有在代码里捕获这个信号,并且这个信号的默认行为是被忽略。所以,就是下图我们看到的,父进程没有处理,子进程变成僵尸进程。
![ab507721bb0bf653f0915e134df3a4ad.png](https://img-blog.csdnimg.cn/img_convert/ab507721bb0bf653f0915e134df3a4ad.png)
我们在运行一个客户端,然后发送EOF
字符,再次观察,如下图所示,我们发现又多了一个僵尸进程。(Z
表示僵尸进程)
![bbbd1f5148fd8d142cc898712a17a46a.png](https://img-blog.csdnimg.cn/img_convert/bbbd1f5148fd8d142cc898712a17a46a.png)
接下来,咱们一起学习一下如何徒手打僵尸,很简单擒贼先擒王,手动kill 掉僵尸的爸爸。
![cc80737573fefa95e7c00f4cf7f84b80.png](https://img-blog.csdnimg.cn/img_convert/cc80737573fefa95e7c00f4cf7f84b80.png)
参考文献:《UNIX网络编程 卷1:套接字联网API》