一个简单的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
源码地址: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
状态。如果网络状况不佳,例如我的服务器程序在腾讯云服务器上,咖啡馆的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
在这个终止过程中,步骤是:
fgets
返回一个空指针,于是str_cli
返回;exit(0)
退出;readline
,readline
返回0,函数str_echo
返回;exit(0)
退出;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
...
信号是发生某事件时对进程的通知,有时称为软中断。它一般是异步的,进程不可能提前知道信号发生的时间。信号可以
SIGCHLD
就是内核在某进程终止时,发送给进程的父进程的信号。我们通过调用函数sigaction
来设置一个信号的处理方法。
提供一个函数,在信号发生随机调用,这个函数称为信号处理函数,此行为则称为捕获信号。信号处理函数原型为
void handler(int signo);//函数名字可自定义
SIG_IGN
来忽略它。SIG_DFL
,使用默认处理方法,包括终止进程、忽略等。SIGKILL
和SIGSTOP
,这两个信号不可被捕获,也不可设置忽略;SIGCHLD
的默认处理方法是忽略。
建立信号的处理方法的Posix方法就是调用函数sigaction
,但是它需要分配并定义结构体作为参数。简单一点的方式是调用signal
,提供信号名和函数指针,或上面提到的常值SIG_IGN
或SIG_DFL
。但是调用signal
时不同的实现提供不同的信号语义。
为了兼容这两个实现,我们定义自己的signal
函数,使用signal
的语义,但是调用Posix函数sigaction
。
源码地址:unpv13e/lib/signal.c
函数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);
如果设置,那么此信号中断的系统调用将由内核自动重启。
调用函数sigaction
,异常时返回SIG_ERR
,成功时,返回oact
的成员sa_handler
作为函数指针。
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
设置僵尸(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存活。
按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
来等待连接完成。
处理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
,它通知内核在没有已终止子进程时不要阻塞。
修改客户端程序,与服务器建立五个连接,在调用函数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
最后,一个健壮的网络程序,需要正确处理下面的情况:
SIGCHLD
,防止出现Zombie进程;SIGCHLD
处理程序要使用waitpid
函数,以免留下僵尸进程。accept
有可能返回一个非致命错误,此时只需再次调用一次accept即可。
三路握手完成,连接建立,然后客户TCP发送一个RST(复位)。在服务器端,连接由TCP排队,等待服务器进程在RST到达后调用accept
。稍后,服务器进程调用accept
。
对于这种情况,有的系统(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。客户程序不应当只阻塞于用户输入,而是应当阻塞于套接口和用户输入任意源的输入。这一点正是select
和epoll
的目的。
真实的情景中,客户和服务器交换的数据格式十分重要,一般客户和服务器会以协议的方式确定好数据格式,分别进行处理。
修改服务器程序,仍然从客户读入一行文本。和客户约定好,期望这行文本包含由空格隔开的两个整数,服务器返回这两个整数的和。
其他保持不变,只修改服务器程序中所调用的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
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。