TCP回射客户-服务器程序

TCP回射客户-服务器程序

一个简单的TCP回射客户-服务器程序,应实现下述功能:

  1. 客户从标准输入读一行文本,写到服务器上
  2. 服务器从网络输入读此行,并回射给客户
  3. 客户读回射行并写到标准输出
简单的回射客户-服务器

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

服务器启动过程中,调用socketbindlisten,最后调用并阻塞于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返回服务器,连接建立,由于此时我们还未输入任何文本,所以此时:

  1. 客户调用str_cli函数,阻塞于fgets调用,等待用户输入;
  2. accept返回服务器,服务器调用fork,由子进程调用str_echo,此函数调用readline,最终阻塞于read,等待客户发送;
  3. 服务器父进程,再次调用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状态。如果网络状况不佳,例如我的服务器程序在腾讯云服务器上,咖啡馆的wifi比较卡,那么客户也会进入这一状态:

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

在这个终止过程中,步骤是:

  1. 键入EOF字符,fgets返回一个空指针,于是str_cli返回;
  2. 客户进程exit(0)退出;
  3. 客户进程终止时,会关闭所有打开的描述字,因此该客户已连接套接口关闭,TCP发送FIN给服务器,开始四次挥手过程。
  4. 服务器接收FIN,子进程阻塞于readlinereadline返回0,函数str_echo返回;
  5. 服务器子进程exit(0)退出;
  6. 同样子进程打开的所有描述字也关闭,TCP发送FIN给客户,客户发送ACK。至此连接完全终止,客户套接口进入TIME_WAIT状态;(由于网络卡顿,迟迟收不到服务器对FIN的ACK,我的客户套接口进入FIN_WAIT_1
  7. 服务器子进程终止,给父进程发送一个信号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
...

Posix信号处理

信号是发生某事件时对进程的通知,有时称为软中断。它一般是异步的,进程不可能提前知道信号发生的时间。信号可以

  1. 由一个进程发往自身或另一个进程
  2. 由内核发往某进程

SIGCHLD就是内核在某进程终止时,发送给进程的父进程的信号。我们通过调用函数sigaction来设置一个信号的处理方法。

提供一个函数,在信号发生随机调用,这个函数称为信号处理函数,此行为则称为捕获信号。信号处理函数原型为

void handler(int signo);//函数名字可自定义
  1. 将信号的处理方法设为SIG_IGN来忽略它。
  2. 将信号的处理方法设为SIG_DFL,使用默认处理方法,包括终止进程、忽略等。

SIGKILLSIGSTOP,这两个信号不可被捕获,也不可设置忽略;SIGCHLD的默认处理方法是忽略。

signal函数

建立信号的处理方法的Posix方法就是调用函数sigaction,但是它需要分配并定义结构体作为参数。简单一点的方式是调用signal,提供信号名和函数指针,或上面提到的常值SIG_IGNSIG_DFL。但是调用signal时不同的实现提供不同的信号语义。

为了兼容这两个实现,我们定义自己的signal函数,使用signal的语义,但是调用Posix函数sigaction

源码地址:unpv13e/lib/signal.c

用typedef简化函数原型

函数signal的正常函数原型因层次太多而变得很复杂:

void (*signal(int signo, void (*func)(int)))(int);

为了简化它,定义一个类型Sigfunc

typedef void Sigfunc(int);	/* for signal handlers */

这样,signal的函数原型就简化为

Sigfunc *signal(int signo, Sigfunc *func);

设置处理程序

将传入的func参数作为sigaction结构的元素sa_handler

设置处理程序的信号掩码

设置sigaction结构的元素sa_mask为空集,确保程序运行时没有别的信号阻塞。

sigemptyset(&act.sa_mask);

设置标志SA_RESTART

如果设置,那么此信号中断的系统调用将由内核自动重启。

调用函数sigaction

调用函数sigaction,异常时返回SIG_ERR,成功时,返回oact的成员sa_handler作为函数指针。

if (sigaction(signo, &act, &oact) < 0)
	return(SIG_ERR);
return(oact.sa_handler);

处理SIGCHLD信号

设置僵尸(Zombie)状态的目的就是维护子进程的信息,包括子进程的PID,终止状态及资源利用信息(CPU时间、内存等)。如果父进程终止,且该进程有子进程处于僵尸状态,则所有僵尸子进程的PPID均为1(init进程)。init进程将作为这些子进程的继父并负责清除它们(将wait它们,从而去除僵尸进程)。有些Unix系统(如Mac OSX)给僵尸进程输出的CMD是<defunct>

处理僵尸进程

僵尸进程占用内核空间,最终导致系统无法正常工作。我们建立一个信号处理程序来捕获信号SIGCHLD,修改服务器程序,在调用listen之后,增加信号处理程序调用:

Signal(SIGCHLD, sig_chld);

来建立信号处理程序(必须在创建第一个子进程之前完成,且只做一次)。然后定义函数sig_chld

#include	"unp.h"

void
sig_chld(int signo)
{
	pid_t	pid;
	int		stat;

	pid = wait(&stat);
	printf("child %d terminated\n", pid);
	return;
}

修改后的服务器程序位于unpv13e/tcpcliserv/tcpsrv02.c,编译后启动,同样使用tcpcli01客户端程序,测试回射后,使用Ctrl+D键入EOF字符,观察之前说的SIGCHLD处理情况。

启动服务器

[root@VM_0_6_centos tcpcliserv]# make tcpserv02
gcc -I../lib -g -O2 -D_REENTRANT -Wall   -c -o tcpserv02.o tcpserv02.c
gcc -I../lib -g -O2 -D_REENTRANT -Wall   -c -o sigchldwait.o sigchldwait.c
gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpserv02 tcpserv02.o sigchldwait.o ../libunp.a -lpthread
[root@VM_0_6_centos tcpcliserv]# ./tcpserv02

启动客户端

jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 150.107.102.37
hi there
hi there

此时在服务器终端查看服务器进程状态,

[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0 24965 24771  0  80   0 -  1595 inet_c pts/0    00:00:00 tcpserv02
1 S     0 25120 24965  0  80   0 -  1595 sk_wai pts/0    00:00:00 tcpserv02
0 R     0 25152 24977  0  80   0 - 38300 -      pts/1    00:00:00 ps

此时父进程24965和子进程25120存活。

客户端输入EOF

Control+D输入EOF字符,观察服务器终端输出和进程状态,

[root@VM_0_6_centos tcpcliserv]# ./tcpserv02
child 25120 terminated

可以看到子进程25120终止的文本打印。观察进程状态,

[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0 24965 24771  0  80   0 -  1596 inet_c pts/0    00:00:00 tcpserv02
0 R     0 25701 24977  0  80   0 - 38300 -      pts/1    00:00:00 ps

此时子进程25120已经没有了,并且没有以Zombie的状态存在。

处理被中断的系统调用

在处理信号的时候,服务器程序正好阻塞于accept,此时信号处理程序返回,系统可能返回EINTR错误,accept函数必须处理这个异常,否则进程会直接退出。因此我们的服务器程序里一般都有如下处理,

if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
	if (errno == EINTR)
		continue;		/* back to for() */
	else
		err_sys("accept error");

上面的处理实际上是我们自己重启被中断的系统调用,但是有一个函数我们是不能自己重启的,那就是connect函数。当connect因为捕获信号被系统中断时,必须调用select来等待连接完成。

wait和waitpid函数

处理SIGCHLD的时候,我们调用了函数wait来处理被终止的子进程。

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

这两个函数均返回两个值,函数的返回值是终止子进程的进程ID号,statloc指针传递回来子进程的终止状态。如果没有子进程终止,但是有子进程正在运行,那么函数wait将阻塞直到第一个子进程的终止。

waitpid函数多了两个参数,pid参数可以指定等待哪个进程,比如值为-1时表示等待第一个终止的子进程。options参数指定附加选项,例如WNOHANG,它通知内核在没有已终止子进程时不要阻塞。

wait和waitpid的区别

修改客户端程序,与服务器建立五个连接,在调用函数str_cli时仅用第一个连接。源码地址:unpv13e/tcpcliserv/tcpcli04.c

编译并运行服务器程序tcpserv03

[root@VM_0_6_centos tcpcliserv]# make tcpserv03
gcc -I../lib -g -O2 -D_REENTRANT -Wall   -c -o tcpserv03.o tcpserv03.c
gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpserv03 tcpserv03.o sigchldwait.o ../libunp.a -lpthread
[root@VM_0_6_centos tcpcliserv]# ./tcpserv03

编译并运行客户端程序tcpcli04

jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv make tcpcli04
gcc -I../lib -g -O2 -D_REENTRANT -Wall   -c -o tcpcli04.o tcpcli04.c
gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpcli04 tcpcli04.o ../libunp.a -lresolv -lpthread
jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli04 150.107.102.37

输入几行文本,观察客户端和服务端输出,和服务端进程状态,可以看到有一个服务器父进程1132,和它的五个子进程。

[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0  1132  1069  0  80   0 -  1595 inet_c pts/0    00:00:00 tcpserv03
1 S     0  1305  1132  0  80   0 -  1595 sk_wai pts/0    00:00:00 tcpserv03
1 S     0  1307  1132  0  80   0 -  1595 sk_wai pts/0    00:00:00 tcpserv03
1 S     0  1310  1132  0  80   0 -  1595 sk_wai pts/0    00:00:00 tcpserv03
1 S     0  1311  1132  0  80   0 -  1595 sk_wai pts/0    00:00:00 tcpserv03
1 S     0  1312  1132  0  80   0 -  1595 sk_wai pts/0    00:00:00 tcpserv03
0 R     0  1569  1544  0  80   0 - 38300 -      pts/1    00:00:00 ps

在客户端终端键入EOF终止符,服务端此时输出,

[root@VM_0_6_centos tcpcliserv]# ./tcpserv03
child 2892 terminated
child 2891 terminated
child 2888 terminated
child 2889 terminated

按道理五个客户端终止时,所有打开的描述字由内核自动关闭,引发五个FIN,也就是说此时服务端五个子进程也几乎同时终止。也就导致几乎同时有五个SIGCHLD信号递交给父进程。但是观察输出发现,子进程终止的打印,没有五行,看起来似乎不是所有子进程终止信号都被正确处理。

ps查看服务器进程状态,发现有一个处于Zombie状态的子进程2851:

[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0  2851  2772  0  80   0 -  1596 inet_c pts/0    00:00:00 tcpserv03
1 Z     0  2890  2851  0  80   0 -     0 do_exi pts/0    00:00:00 tcpserv03 <defunct>
0 R     0  2970  2946  0  80   0 - 38300 -      pts/1    00:00:00 ps

我们改用函数waitpid,循环处理子进程终止信号,并且设置选项WNOHANG,告诉waitpid在有未终止子进程运行时不要阻塞。修改后的服务器程序位于unpv13e/tcpcliserv/tcpserv04.c

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

编译运行tcpserv04后,重复之前的步骤,最后观察打印,是否还会遗留Zombie子进程:

[root@VM_0_6_centos tcpcliserv]# ./tcpserv04
child 3872 terminated
child 3874 terminated
child 3867 terminated
child 3871 terminated
child 3869 terminated
[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0  3855  2772  0  80   0 -  1596 inet_c pts/0    00:00:00 tcpserv04
0 R     0  3935  2946  0  80   0 - 38300 -      pts/1    00:00:00 ps

最后,一个健壮的网络程序,需要正确处理下面的情况:

  1. 派生了子进程后,必须捕获信号SIGCHLD,防止出现Zombie进程;
  2. 当捕获信号时,必须处理被中断的系统调用;
  3. SIGCHLD处理程序要使用waitpid函数,以免留下僵尸进程。

accept返回前夭折

accept有可能返回一个非致命错误,此时只需再次调用一次accept即可。

三路握手完成,连接建立,然后客户TCP发送一个RST(复位)。在服务器端,连接由TCP排队,等待服务器进程在RST到达后调用accept。稍后,服务器进程调用accept

ESTABLISHED状态的连接在调用accept之前收到RST

对于这种情况,有的系统(Berkeley)是在内核中完成对这种连接的处理,服务器进程并无感知。而其他大多数实现返回一个错误(EPROTO)给进程作为accept()的返回,此错误与实现方式有关。

#define EPROTO 100 /* Protocol error */

Posix.1g则返回ECONNABORTED,明确告诉进程这是一个accept夭折错误,服务器可以忽略错误,再次调用一次accept

#define ECONNABORTED 53 /* Software caused connection abort */

服务器进程终止

服务器进程崩溃是现实中存在的一种服务端异常,我们可以模拟这一过程:

1.在本机启动客户端程序,在腾讯云主机上启动服务器程序,此时在客户端输入文本,服务器正常回射。

[root@VM_0_6_centos tcpcliserv]# ./tcpserv04 //服务器
jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 150.107.102.37
hello!
hello!

2.在腾讯云主机上找到回射服务器的子进程ID号,杀死该进程。按照正常的进程终止处理流程,子进程中打开的描述字都关闭,发送FIN给客户,客户TCP相应地回复ACK响应。

3.信号SIGCHLD被发往服务器父进程并被正确处理。

[root@VM_0_6_centos ~]# kill -9 5754
[root@VM_0_6_centos tcpcliserv]# ./tcpserv04
child 5754 terminated
[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0  5750  2772  0  80   0 -  1596 inet_c pts/0    00:00:00 tcpserv04
0 R     0  6172  2946  0  80   0 - 38300 -      pts/1    00:00:00 ps

4.客户端毫无动静,阻塞于fgets等待用户输入。

5.用netstat观察此时客户端和服务器的套接口状态:

jackieluo@JACKIELUO-MB1 ~ netstat -a //客户端
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  10.254.166.25.58686    150.107.102.37.9877    CLOSE_WAIT

[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
tcp        0      0 VM_0_6_centos:9877      119.123.199.113:8796    FIN_WAIT2

6.在客户端键入一行,观察输出:

jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 150.107.102.37
hello!!
hello!!
are you alive?
str_cli: server terminated prematurely

当服务器收到客户的数据时,由于此套接口对于的进程已终止,所以返回RST响应,可以用tcpdump观察分组:

[root@VM_0_6_centos ~]# tcpdump -ni any port 9877 and 'tcp[13] & 4 != 0 ' -s0  -w rst.cap -vvv
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
Got 2

客户端调用write后就阻塞于readline,此时第2步的那个FIN导致readline返回0,客户端进程终止。由于我们的客户端和服务器程序在不同主机上,因此较早就收到的FIN优先被客户处理,而客户接收到最后服务器发来的RST需要几毫秒的时间,因此没等到RST,客户进程就终止了。

if (Readline(sockfd, recvline, MAXLINE) == 0)
	err_quit("str_cli: server terminated prematurely");

这个例子可以看出来我们的客户端程序有个问题,当服务器发来FIN时,仅仅因为客户此时阻塞于用户输入fgets,就无法及时处理FIN。客户程序不应当只阻塞于用户输入,而是应当阻塞于套接口和用户输入任意源的输入。这一点正是selectepoll的目的。

客户和服务器交换的数据格式

真实的情景中,客户和服务器交换的数据格式十分重要,一般客户和服务器会以协议的方式确定好数据格式,分别进行处理。

传递字符串

修改服务器程序,仍然从客户读入一行文本。和客户约定好,期望这行文本包含由空格隔开的两个整数,服务器返回这两个整数的和。

其他保持不变,只修改服务器程序中所调用的str_echo函数。

void str_echo_inner(int sockfd) {
  long l1, l2;
  ssize_t n;
  char buf[MAXLINE];

  for (;;) {
    n = Readline(sockfd, buf, MAXLINE);
    if (n == 0) {
      printf("connection closed by other end");
      return;
    }
    if (sscanf(buf, "%ld%ld", &l1, &l2) == 2) {
      snprintf(buf, sizeof(buf), "%ld\n", l1 + l2);
    } else {
      snprintf(buf, sizeof(buf), "input error\n");
    }
    n = strlen(buf);
    Writen(sockfd, buf, n);
  }
}

尝试运行,可以看到,输入两个长整型数,服务器回射回来两个数的和,其他输入回复输入异常。

jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 127.0.0.1
12 24
36
111111
input error
1.29 2.00
input error

不论客户和服务器主机的字节序如何,这个新的客户和服务器都能工作的很好。

传递二进制结构

实际中,服务器和客户端不会约定字符串这样简单的协议,而多以传递二进制结构为主。但是这样做,客户和服务器在不同字节序的主机上运行或是在不支持相同大小长整型的主机上运行时,客户和服务器便无法工作。

我们约定一个入参结构体和出参结构体。

// tcpcliserv/sum.h

struct args {
  long	arg1;
  long	arg2;
};

struct result {
  long	sum;
};

修改str_cli函数,客户端将读入的字符串解析到args中,发送给服务器,

#include "sum.h"

void str_cli_inner(FILE *fp, int sockfd) {
  char sendline[MAXLINE];
  struct args req;
  struct result rsp;

  while (Fgets(sendline, MAXLINE, fp) != NULL) {
    if (sscanf(sendline, "%ld%ld", &req.arg1, &req.arg2) != 2) {
      printf("invalid input:%s", sendline);
      continue;
    }
	printf("req.arg1:%ld\n", req.arg1);
	printf("req.arg2:%ld\n", req.arg2);
    Writen(sockfd, &req, sizeof(req));

    if (Readn(sockfd, &rsp, sizeof(rsp)) == 0)
      err_quit("str_cli: server terminated prematurely");
    printf("rsp.sum:%ld\n", rsp.sum);
  }
}

修改str_echo函数,计算两个参数之和,存储到二进制结构体中,回射给客户端。

#include "sum.h"

void str_echo_inner(int sockfd) {
  struct args req;
  struct result rsp;

  for (;;) {
    if (Readn(sockfd, &req, sizeof(req)) == 0) {
      printf("connection closed by other end");
      return;
    }
    rsp.sum = req.arg1 + req.arg2;
    Writen(sockfd, &rsp, sizeof(rsp));
  }
}

在同一台主机(我的是MacOSX)上运行客户端和服务器,结果是OK的,

jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 127.0.0.1
123 11
req.arg1:123
req.arg2:11
rsp.sum:134

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏应用案例

VBA创建Access数据库的4种方法

Excel由于本身的局限性,存储数据量过大的时候,往往会导致工作簿假死无反应,电脑卡顿等情况。那么,将数据存取到Access数据库中就是一种好的解决方法。今天,...

72510
来自专栏MasiMaro 的技术博文

WinSock2 API

title: WinSock2 API tags: [WinSock, 网络编程, WinSock2.0 API, 动态加载, WinSock 异步函数] ...

1561
来自专栏DannyHoo的专栏

iOS开发中内存泄漏检测工具--MLeaksFinder

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

3652
来自专栏Jackson0714

C#多线程之旅(1)——介绍和基本概念

2989
来自专栏美团技术团队

不可不说的Java“锁”事

Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例,为...

1412
来自专栏码洞

深入Python多进程编程基础——图文版

多进程编程知识是Python程序员进阶高级的必备知识点,我们平时习惯了使用multiprocessing库来操纵多进程,但是并不知道它的具体实现原理。下面我对多...

1231
来自专栏大学生计算机视觉学习DeepLearning

c++ 网络编程(九)TCP/IP LINUX/windows--使用IOCP模型 多线程超详细教程 以及 多线程实现服务端

原文链接:https://www.cnblogs.com/DOMLX/p/9661012.html

4422
来自专栏xingoo, 一个梦想做发明家的程序员

Spark源码分析之Spark Shell(下)

继上次的Spark-shell脚本源码分析,还剩下后面半段。由于上次涉及了不少shell的基本内容,因此就把trap和stty放在这篇来讲述。 上篇回顾:S...

32910
来自专栏极客日常

kubernetes源码阅读笔记:理清 kube-apiserver 的源码主线

我最近开始研究 kubernetes 源码,希望将阅读笔记记录下来,分享阅读思路和心得,更好的理解 kubernetes,这是第一篇,从 kube-apiser...

4323
来自专栏逸鹏说道

Python3 与 C# 并发编程之~ 进程篇下

看看 connection.Pipe方法的定义部分,是不是双向通信就看你是否设置 duplex=True

1973

扫码关注云+社区

领取腾讯云代金券