一个简单的 TCP 回射客户 - 服务器程序, 应实现下述功能:
客户从标准输入读一行文本, 写到服务器上
服务器从网络输入读此行, 并回射给客户
客户读回射行并写到标准输出
简单的回射客户 - 服务器
TCP 回射服务器程序
源码地址: unpv13e/tcpcliserv/tcpsrv01.c
创建套接口, 捆绑服务器的众所周知端口
创建一个 TCP 套接口, 用通配地址 (INADDR_ANY) 和 unp.h 中定义的众所周知端口(SERV_PORT), 端口号为 9877.
- 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);
等待完成客户连接
服务器阻塞于 accept 调用, 等待客户连接的完成.
- clilen = sizeof(cliaddr);
- connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
并发服务器
对于每个客户, fork 新的子进程, 父进程关闭已连接套接口, 子进程关闭监听套接口.
子进程处理客户请求.
- 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, 调用 readline 从已连接套接口读下一行, 随后调用 writen 回射给客户. 如果客户关闭连接(正常关闭), 那么接收到的客户 FIN 导致子进程的 readline 返回 0, 从而使函数走到控制尾, 正常返回, 子进程退出.(exit(0))
- 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");
- }
源码地址: unpv13e/lib/str_echo.c
TCP 回射客户程序
源码地址: unpv13e/tcpcliserv/tcpcli01.c
创建套接口, 初始化套接口地址结构
创建一个 TCP 套接口, 使用 unp.h 中定义的众所周知套接口 SERV_PORT 作为端口, IP 地址来自命令行参数.
- 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);
与服务器连接
连接服务器, 调用函数 str_cli 完成客户处理的剩余工作.
- Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
- str_cli(stdin, sockfd); /* do it all */
客户处理逻辑
客户调用函数 str_cli, 从标准输入读一行文本, 写到服务器, 读取服务器对该行的回射, 再写到标准输出上.
源码地址: unpv13e/lib/str_cli.c
读一行, 写到服务器
fgets 读一行文本, writen 将此行通过已连接套接口发送到服务器.
- while (Fgets(sendline, MAXLINE, fp) != NULL) {
- Writen(sockfd, sendline, strlen(sendline));
- ...
- }
从服务器读取回射行, 写到标准输出
readline 从服务器读取回射行, fputs 将其写到标准输出.
- if (Readline(sockfd, recvline, MAXLINE) == 0)
- err_quit("str_cli: server terminated prematurely");
- Fputs(recvline, stdout);
正常启动
启动服务器
首先, 编译并启动服务器程序, 可以在本机, 也可以在云服务器上启动. 这里用腾讯云的 CentOS 服务器, 编译执行 tcpsrv01.
- [root@VM_0_6_centos tcpcliserv]# make tcpserv01
- gcc -I../lib -g -O2 -D_REENTRANT -Wall -c -o tcpserv01.o tcpserv01.c
- gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpserv01 tcpserv01.o ../libunp.a -lpthread
- [root@VM_0_6_centos tcpcliserv]# ./tcpserv01
服务器启动过程中, 调用 socket,bind,listen, 最后调用并阻塞于 accept. 启动客户程序之前, 使用 netstat 检查服务器监听套接口的状态
- [root@VM_0_6_centos ~]# netstat -a
- Active Internet connections (servers and established)
- Proto Recv-Q Send-Q Local Address Foreign Address State
- tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
正如我们所期望的, 有一个套接口处于 Listen 状态, 它有通配的本地 IP 地址, 本地端口为 9877.netstat 用通配符 * 来表示一个为 0 的 IP 地址或为 0 的端口号.
启动客户
在本机编译启动客户, 指明服务器的 IP 地址为上述腾讯云服务器的 IP 地址.
- jackieluo@JACKIELUO-MB1 ~/unpv13e/tcpcliserv make tcpcli01
- gcc -I../lib -g -O2 -D_REENTRANT -Wall -c -o tcpcli01.o tcpcli01.c
- gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpcli01 tcpcli01.o ../libunp.a -lresolv -lpthread
- jackieluo@JACKIELUO-MB1 ~/unpv13e/tcpcliserv ./tcpcli01 150.107.102.37
客户创建套接口后, 调用 connect, 引发 TCP 的三路握手过程. 三路握手完成后, connect 返回客户, accept 返回服务器, 连接建立, 由于此时我们还未输入任何文本, 所以此时:
客户调用 str_cli 函数, 阻塞于 fgets 调用, 等待用户输入;
accept 返回服务器, 服务器调用 fork, 由子进程调用 str_echo, 此函数调用 readline, 最终阻塞于 read, 等待客户发送;
服务器父进程, 再次调用 accept, 阻塞等待下一个客户的连接.
在输入之前, 再次在服务器检查套接口状态:
- [root@VM_0_6_centos ~]# netstat -a | grep tcp
- tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
- tcp 0 0 VM_0_6_centos:9877 78.183.35.121.bro:15925 ESTABLISHED
可以看到, 服务器上多出了一个已建立连接的套接口. 此时检查本机 (客户) 的套接口状态:
- jackieluo@JACKIELUO-MB1 ~/unpv13e/tcpcliserv netstat -a | grep tcp
- tcp4 0 0 10.254.166.26.53049 150.107.102.37.9877 ESTABLISHED
可以看到本机 (客户) 也多了一个已建立连接的套接口.
还可以用 ps 命令来检查这些进程的状态和关系.
服务器:
- [root@VM_0_6_centos ~]# ps -la
- F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
- 0 S 0 23304 23185 0 80 0 - 1595 inet_c pts/4 00:00:00 tcpserv01
- 1 S 0 23335 23304 0 80 0 - 1595 sk_wai pts/4 00:00:00 tcpserv01
- 0 R 0 23338 21925 0 80 0 - 38300 - pts/3 00:00:00 ps
可以看到, 第一个 tcpserv01 是父进程服务器, 第二个 tcpserv01 是子进程服务器, 父进程的 PID 即子进程的 PPID.
正常终止
连接已建立, 此时在本机 (客户) 终端, 无论输入什么, 都将得到回射:
- jackieluo@JACKIELUO-MB1 ~/unpv13e/tcpcliserv ./tcpcli01 150.107.102.37
- hello // 客户输入
- hello // 服务器回射
- good bye // 客户输入
- good bye // 服务器回射
此时输入 control+D, 即终端 EOF 字符, 以终止客户. 若立即在本机 (客户) 执行 netstat 命令, 我们将看到以下结果:
- jackieluo@JACKIELUO-MB1 ~/unpv13e/tcpcliserv netstat -a | grep 9877
- tcp4 0 0 10.254.166.26.54297 150.107.102.37.9877 TIME_WAIT
我们知道, 主动关闭连接的一方会进入 TIME_WAIT 状态. 如果网络状况不佳, 例如我的服务器程序在腾讯云服务器上, 咖啡馆的 Wi-Fi 比较卡, 那么客户也会进入这一状态:
- jackieluo@JACKIELUO-MB1 ~/unpv13e/tcpcliserv netstat -a | grep 9877
- tcp4 0 0 10.254.166.26.54297 150.107.102.37.9877 FIN_WAIT_1
在这个终止过程中, 步骤是:
键入 EOF 字符, fgets 返回一个空指针, 于是 str_cli 返回;
客户进程 exit(0)退出;
客户进程终止时, 会关闭所有打开的描述字, 因此该客户已连接套接口关闭, TCP 发送 FIN 给服务器, 开始四次挥手过程.
服务器接收 FIN, 子进程阻塞于 readline,readline 返回 0, 函数 str_echo 返回;
服务器子进程 exit(0)退出;
同样子进程打开的所有描述字也关闭, TCP 发送 FIN 给客户, 客户发送 ACK. 至此连接完全终止, 客户套接口进入 TIME_WAIT 状态;(由于网络卡顿, 迟迟收不到服务器对 FIN 的 ACK, 我的客户套接口进入 FIN_WAIT_1)
服务器子进程终止, 给父进程发送一个信号 SIGCHLD.
本例的代码并未捕获 SIGCHLD, 可使用 ps 命令检查当前进程状态.
- [root@VM_0_6_centos ~]# ps -la
- F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
- 0 S 0 23304 23185 0 80 0 - 1595 inet_c pts/4 00:00:00 tcpserv01
- 1 Z 0 23335 23304 0 80 0 - 0 do_exi pts/4 00:00:00 tcpserv01 <defunct>
- 1 Z 0 23609 23304 0 80 0 - 0 do_exi pts/4 00:00:00 tcpserv01 <defunct>
- 1 Z 0 23699 23304 0 80 0 - 0 do_exi pts/4 00:00:00 tcpserv01 <defunct>
- 1 S 0 23987 23304 0 80 0 - 1595 sk_wai pts/4 00:00:00 tcpserv01
- 1 Z 0 25780 23304 0 80 0 - 0 do_exi pts/4 00:00:00 tcpserv01 <defunct>
- 0 R 0 27102 27062 0 80 0 - 38300 - pts/0 00:00:00 ps
可以看到, 之前结束连接的子进程状态都是 Z(Zombie 僵尸). 要清除这些僵尸进程, 首先 kill 父进程, 然后 kill 每个子进程即可:
- [root@VM_0_6_centos ~]# kill -9 23304
- [root@VM_0_6_centos ~]# kill -9 25780
- ...
来源: https://www.qcloud.com/developer/article/1356202