这篇文章主要是用于复习UNP的第5章,这一章主要通过一个回射服务的实例来进一步讲解socket编程。这一章同时引入了多进程的内容,因此除了socket编程外,还需要懂一些Unix下的多进程编程。当然,Unix下的I/O操作也要熟悉,因为socket的读写以及从stdin
获取数据需要使用Unix的I/O操作。
什么是回射服务?
回射服务就是客户端发送信息给服务器端,服务器端读取数据后又马上发送相同数据给客户端。图1是该服务的示意图:
图1:回射服务示意图
这个例子很简单,一般用于socket编程的入门,但其中还是有很多细节需要注意。下面将按照书的编排一步一步地搭建完整的回射服务。
第一版回射服务
服务器端代码
我们先来看看第一版的回射服务代码,下面是服务器端的主函数代码,代码中已经添加了注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; 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); for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); if ((childpid = Fork()) == 0) { Close(listenfd); str_echo(connfd); exit(0); } Close(connfd); } }
|
下面是str_echo()
函数的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include "unp.h" 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"); }
|
客户端代码
下面是客户端的主函数代码,代码中已经添加了注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include "unp.h" 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); exit(0); }
|
下面是str_cli
的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #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); } }
|
正常启动的流程
正常启动后,会有3个进程被阻塞:
客户端进程被str_cli()
中的Fgets()
阻塞,等待stdin
的输入;
服务器端子进程被str_echo()
中的read()
阻塞;
服务器端父进程进入第二次循环,被accept()
函数阻塞。
直到我们从stdin
输入数据,这3个进程都将一直阻塞下去。
正常终止的流程
如果客户端输入EOF
,那么整个连接将终止。下面是这种情况下的终止流程:
当用户输入EOF
时,str_cli()
中的Fgets()
跳出循环;
客户端主程序因为Fgets()
的返回而结束,进程关闭,所有socket连接断开,进入FIN_WAIT_2
状态;
客户端socket断开时,发送FIN
给服务器端;
收到FIN
后,服务器端str_echo()
中的read()
函数返回0,从而导致str_echo()
的返回;
str_echo()
返回后,服务器端的子进程调用exit()
进行关闭;
服务器端的子进程发送FIN
给客户端,客户端返回ACK
,至此客户端socket进入TIME_WAIT
状态,并等待变成CLOSED
状态;服务器端子进程socket从LAST_ACK
状态变成CLOSED
状态;
服务器端子进程结束时,会给父进程发送SIGCHLD
信号,这里父进程并没有处理这个信号。
第二版回射服务
正如上面第7点说的,子进程结束时会发送SIGCHLD
信号给父进程,但我们没有对这个信号进行处理。这么做的结果就是子进程会变成僵尸进程!为了解决这个问题,我们必须要让父进程捕获这个信号,因此第二版服务器端代码增加了相应代码,客户端不变。这里只是简单地介绍下多进程,多进程的内容会有其他文章详细介绍。
服务器端代码
主要改进是主函数,下面看看修改了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include "unp.h" 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); for ( ; ; ) { clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) { if (errno == EINTR) continue; else err_sys("accept error"); } if ((childpid = Fork()) == 0) { Close(listenfd); str_echo(connfd); exit(0); } Close(connfd); } }
|
下面看看Signal()
函数。这个函数主要是用于注册信号,注册后的信号就可以被进程所捕获。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
#include "unp.h" Sigfunc * signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (signo == SIGALRM) { #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; #endif } if (sigaction(signo, &act, &oact) < 0) return(SIG_ERR); return(oact.sa_handler); }
Sigfunc * Signal(int signo, Sigfunc *func) { Sigfunc *sigfunc; if ( (sigfunc = signal(signo, func)) == SIG_ERR) err_sys("signal error"); return(sigfunc); }
|
接下来我们看看处理SIGCHLD
的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #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; }
|
这个函数用于释放子进程的资源,我们不用wait()
而是用waitpid()
是因为wait()
只能用于第一个关掉的进程。如果有多个进程被关掉,那么会产生多个SIGCHLD
信号,但Unix一般不支持信号的排队(信号也不能抢占正在执行的信号处理程序),因此用wait()
只会释放一个进程的资源,其他进程还是僵尸进程!用waitpid()
并使用-1
作为参数,那么信号处理函数将处理该进程所有被关闭的子进程。
服务器正常/非正常关闭
前面我们讨论的都是客户端的正常关闭,这里我们讨论下服务器端的正常或者非正常关闭。
服务器正常关闭
服务器正常关闭就是通过Linux命令关闭服务器端的子进程,我们分析下发生了什么:
- 服务器端子进程发送
FIN
给客户端,子进程发送SIGCHLD
给父进程;
- 客户端收到
FIN
后发送ACK
给服务器端,但客户端现在被fgets()
阻塞;
- 如果我们通过
stdin
给客户端输入数据,客户端还是会还是会发送给服务器端;
- 服务器端收到后返回
RST
,但客户端却很大可能收不到这个RST
,因为它阻塞在Readline()
,并且会先收到2中的FIN
,因此返回0;
- 客户端随后返回错误退出。
上面这个流程是不正确的,原因在于客户端要么只能阻塞在了stdin
,要么只能阻塞在Readline
,这将导致程序错误的退出。正确的做法应该是让客户端可以阻塞于stdin
或者Readline
,这将是下章节改进的地方。
如果我们在Readline
返回0
后不理会,继续写入,那么第二次写入时会触发SIGPIPE
。根据实际情况,我们可以忽视这个信号或者对其进行相应的处理。
服务器非正常关闭
服务器崩溃后,一般情况下,只要客户端不发送数据给服务器端,客户端是不知道服务器端崩溃了的。如果服务器崩溃后,客户端发送数据给服务器端,服务器端将返回RST
,客户端收到后将返回错误。如果想要不发送数据也知道服务器是否崩溃了,我们需要使用其他技术。
处理数据格式
一般而言,我们收到数据都是需要特定格式的,我们可以用sscanf()
和snprintf()
来进行格式化,这里简单介绍下这两个函数。
sscanf()
sscanf()
函数原型如下:
1
| int sscanf ( const char * s, const char * format, ...);
|
这个函数主要是将s
指向的字符串,根据format
来存储到相应的format
中。比如:
1 2 3 4
| sscanf (sentence,"%s %*s %d",str,&i);
|
sscanf()
的格式很多,比较复杂,和正则表达式不一样。如果成功,函数返回有多少个format
参数被赋值。
snprintf()
snprintf()
函数原型如下:
1
| int snprintf ( char * s, size_t n, const char * format, ... );
|
这个函数不是将字符串输出到stdout
而是写入s
中。n
是写入的字符数,最多写n - 1
个字符,最后一个字节会自动添加\0
。