UNP卷1:第五章(TCP客户/服务器程序示例)
最后更新于:2022-04-01 14:48:58
## 1. 经典的回射程序
### 1) 服务器程序srv.c
~~~
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <errno.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
int buff[MAXLINE];
pid_t pid;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t cliLen;
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(9877);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
for ( ; ; ){
cliLen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &cliLen);
if ((pid = fork()) == 0){
close(listenfd);
str_echo(connfd);
_exit(0);
}
if (waitpid(pid, NULL, 0) != pid){
printf("waitpid error\n");
exit(1);
}
close(connfd);
}
return 0;
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ((n = read(sockfd, buf, MAXLINE)) > 0){
buf[n] = '\0';
write(sockfd, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
printf("str_echo:read error\n");
}维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状
~~~
### 2) 客户端程序cli.c
~~~
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd, n;
struct sockaddr_in servaddr;
char buff[MAXLINE + 1];
struct sockaddr_in cliaddr;
socklen_t cliLen;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
str_cli(stdin, sockfd);
return 0;
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL){
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli:server terminated prematurely\n");
return;
}
fputs(recvline, stdout);
}
}
~~~
### 3)程序运行
### (1)服务器后台启动
~~~
leichaojian@ThinkPad-T430i:~$ ./srv &
[1] 3932
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
~~~
### (2)启动客户端,并且键入一行文本
客户端:
~~~
leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1
hello world
hello world
~~~
服务端:
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:9877 localhost:43399 ESTABLISHED
tcp 0 0 localhost:43399 localhost:9877 ESTABLISHED
~~~
### (3)客户端终止
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:43399 localhost:9877 TIME_WAIT
~~~
## 2. 处理信号
### 1) POSIX信号处理
信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。
信号可以:
(1)由一个进程发给另一个进程。
(2)由内核发给某个进程。
每个信号都有一个与之关联的处置,也称为行为:
(1)我们可以提供一个函数,只要有特定信号发生它就会被调用。这样的函数称为信号处理函数,这种行为称为捕获信号。有两个信号不能被捕获,它们是SIGKILL和SIGSTOP。信号处理函数由信号值这个单一的整数参数来调用,且没有返回值,其函数原型如下:
~~~
void handler( int signo );
~~~
(2)我们可以把某个信号的处置设定为SIG_IGN来忽略它。SIGKILL和SIGSTOP这两个信号不能被忽略。
(3)我们可以把某个信号的处置设定为SIG_DFL来启用它的默认处置。
### 2) 处理SIGCHLD信号
设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状态以及资源利用信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们。
而僵尸进程出现时间是在子进程终止后,但是父进程尚未读取这些数据之前。所有解决之道就是保证父进程处理这些数据,我们可以通过wait或者waitpid函数来达到这个要求。
由于子进程的终止必然会产生信号SIGCHLD信号,所以重写TCP服务器程序最终版本:
### (1)服务器程序srv.c
~~~
#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <signal.h>
#include <errno.h>
#define MAXLINE 1024
#define SA struct sockaddr
void sig_chld(int signo);
typedef void Sigfunc(int);
Sigfunc *Signal(int signo, Sigfunc *func);
void str_echo(int sockfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in servaddr, cliaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
Signal(SIGCHLD, sig_chld);
for ( ; ; ){
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0){
if (errno == EINTR)
continue;
else{
printf("accept error\n");
exit(-1);
}
}
if ((childpid = fork()) == 0){
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
return 0;
}
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
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);
}
void str_echo(int sockfd)
{
char buff[MAXLINE];
int n;
for ( ; ; ){
if ((n = read(sockfd, buff, MAXLINE)) > 0){
buff[n] = '\0';
write(sockfd, buff, n);
}
else if (n < 0 && errno == EINTR)
continue;
else if (n < 0){
printf("str_echo:read error\n");
return;
}
else if (n == 0){
break;
}
}
}
~~~
### (2)客户端测试程序cli.c
~~~
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd[5], n;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
int i;
for (i = 0; i < 5; i++){
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]);
return 0;
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL){
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0){
printf("str_cli:server terminated prematurely\n");
return;
}
fputs(recvline, stdout);
}
}
~~~
程序输出如下:
客户端:
~~~
leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1
hello world
hello world
^C
~~~
服务端:
~~~
leichaojian@ThinkPad-T430i:~$ ./srv
child 9831 terminated
child 9835 terminated
child 9832 terminated
child 9833 terminated
child 9834 terminated
^C
~~~
### 3) 测试僵尸进程的产生
test.c:
~~~
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
int main( void )
{
pid_t pid;
if ( ( pid = fork() ) == 0 ){
printf("child:%d\n", getpid());
exit(0);
}
sleep( 20 );
if ( pid > 0 ){
printf("parent:%d\n", getpid() );
}
return 0;
}
~~~
程序运行:
~~~
leichaojian@ThinkPad-T430i:~$ ./a.out
child:14447
parent:14446
~~~
在显示child:14447而尚未显示parent:14446(即20秒的睡眠时间),我们执行如下命令:
~~~
leichaojian@ThinkPad-T430i:~$ ps -eo state,pid,cmd | grep '^Z'
Z 14447 [a.out] <defunct>
~~~
发现子进程14447果真称为僵尸进程。但是过了20秒后,再次执行时候,则没有任何数据,说明僵尸进程已经被父进程杀死了(就是父进程读取了子进程的数据)
### 4) 服务器进程终止
具体操作如下:
### 1)运行服务器程序,运行客户端程序:
服务端:
~~~
leichaojian@ThinkPad-T430i:~$ ./tcpserv
~~~
客户端:
~~~
leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1
hello
hello
~~~
监视端:
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:37935 localhost:9877 ESTABLISHED
tcp 0 0 localhost:9877 localhost:37935 ESTABLISHED
~~~
### 2) 终止服务器程序(先终止服务器程序,然后执行监视端,再执行客户端,再执行监视端)
监视端:
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
tcp 0 0 localhost:9877 localhost:37953 FIN_WAIT2
tcp 1 0 localhost:37953 localhost:9877 CLOSE_WAIT
~~~
客户端:
~~~
leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1
hello
hello
world
str_cli error
~~~
监视端:(无任何输出,说明客户端进程已经终止,这里终止是产生了信号,强行终止)
来自UNP上的解释是:当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿的被终止。
~~~
leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877
~~~
### 3) 问题出在哪里?
当服务端的FIN到达套接字时,客户正阻塞与fgets调用上。客户实际上在应对两个描述符--套接字和用户输入,它不能单纯阻塞在这两个源中的某个特定源的输入上(正如目前编写的str_cli函数所为),而是应该阻塞在其中任何一个源的输入上,这正是select和poll这两个函数的目的之一。