UNP总结 Chapter 5 TCP客户/服务器程序实例
1.概述
这章的TCP客户/服务器模型
2.TCP回射服务器程序
1).main函数
#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) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit (0); } Close(connfd); /* parent closes connected socket */ } }
2).str_echo函数
#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"); }
3.TCP回射客户程序
1).main函数
#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); /* do it all */ exit(0); }
2).str_cli函数
#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); } }
4.POSIX 信号处理
每个信号都有一个处理办法(disposition),也称为与此信号关联的行为(action)。我们通过调用函数sigaction来设置一个信号的处理办法。
1).可以提供一个函数,在信号发生时随即调用。这个函数称为信号处理函数(signal handler),而此行为便称为捕获(catching)信号,有两个信号不能捕获SIGKILL和SIGSTOP,函数由信号值这一单一参数来调用且无返回值,函数原型为
void handler(int signo);
信号SIGIO,SIGPOLL,SIGURG还要求捕获它的进程有其它动作。
2).可以通过设置信号的处理办法为SIG_IGN来忽略它,但是SIGKILL和SIGSTOP不能忽略。
3).可以设置信号的处理办法为SIG_DFL来为它设置缺省处理办法
函数signal的函数原型层次复杂
void ( * signal (int signo, void ( * func)(int) ) )(int);
用typedef简化函数原型
typedef void Sigfunc(int); // 它说明信号处理程序是带有一个整形参数且无返回值的函数
这样signal的函数原型就变为
Sigfunc * signal (int signo, Sigfunc * func); // 此函数的第二个参数和返回值都是指向信号处理函数的指针
5.处理SIGCHLD信号
设置僵尸(Zombie)状态的目的就是维护子进程的信息,以便父进程在稍后的某个时候取回。如果一个进程终止,且该进程有子程序处于僵尸状态,则所有僵尸子进程的父进程ID均置为1(init进程),init进程将作为这些子进程的继父,并负责清除他们(也就是说,init进程将wait它们,从而去除僵尸进程),有些Unix系统给僵尸进程输出的COMMAND列为<defunct>(ps命令输出)。
另外:
- 如果fork子进程,那么就要wait它们,以防止它们变成僵死进程。
- 捕获SIGCHLD信号,并在信号处理函数中wait子进程,我们始终应该调用waitpid而非wait来处理子进程。
- 我们始终应该检查慢系统调用是否返回EINTR错误。并决定是否重启这些系统调用。(一些系统会自动重启被中断的系统调用)。
- connect不能被重启,当connect函数被信号中断且不自动重启时,我们必须调用select来等待连接完成
6.wait和waitpid函数
可以调用如下两个函数处理已终止的子进程
#include <sys/wait.h> pid_t wait (int *statloc); pid_t waitpid (pid_t pid, int *statloc, int options); //返回值:成功返回进程ID,出错返回返回0或-1;
对于参数pid 想等待的进程ID号。-1表示等待第一个结束的子进程,options附加选项,常用的是WNOHANG,告知内核在没有以终止子进程时不要阻塞
函数wait和waitpid均返回两个值: 函数的返回值是终止子进程的进程ID号,子进程的终止状态(一个整数)则是通过指针statloc返回的。
wait和waitpid的区别: wait 等待第一个结束的子进程,如果没有结束的子进程,wait将阻塞。waitpid 通过参数设置,可以在没有子进程结束时waitpid不阻塞。
7.accept返回前连接终止
Berkeley 的实现在内核中处理终止的连接。POSIX 规定返回一个ECONNABORTED 的 errno(详见UNP3)
8.服务进程终止
如果向一个服务进程已终止的服务器发起连接,服务器将返回一个RST 信号
PS RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求
9.SIGPIPE信号
- 当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号,
- SIGPIPE信号的默认行为是终止进程,
10.服务器主机崩溃
如果,客户端和服务器已经建立了连接的时候,此时服务器崩溃(达到这一标准可以把服务器的网线拔掉,这个时候,服务器就不能发送FIN数据报了,和关机不一样的)
- 如果这时客户端向服务器发送数据的时候,因为服务器已经不存在了,那么客户端就不能接受到服务器给客户端的ack信息,这个时候,客户端建立的是TCP连接,就会重发数据报,而服务器对客户的数据分节根本没有响应,那么所返回的错误就是ETIMEDOUT。
- 如果中间某个路由判断目的主机不可到达,从而响应一个"destination ETINEDOUT"(目的地不可达)ICMP消息,所返回的错误是EHOSTUNREACH或者ENETUNREACH
11.服务器主机崩溃后重启
当客户端和服务器已经建立连接的时候,服务器发生崩溃,重新启动的时候,丢失了原来和客户端的连接信息,这个时候,当客户端向服务器发送数据的时候(客户端并不知道,服务器已经忘记三次握手了),此时服务器发送RST数据报,就结束了客户端的发送
12.服务器主机关机
Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号。等待5-20秒后给所有仍然在运行的进程发送SIGKILL信号,这么做的目的是给进程一小段时间来清除和终止。
13.TCP程序例子小结
需要通信的客户/服务器程序在通信之前都要指定套接字对:本地IP地址,本地端口号,外地IP地址,外地端口。
客户程序的本地IP地址和本地端口号通常是内核分配。服务程序的本地IP地址和端口号有bind函数指定。
14.数据格式
网络传递数据存在三个潜在问题:
(1)不同的实现以不同的格式存储二进制数,最常见的是大端字节序和小端字节序。
(2)不同的实现在存储相同的C数据类型上可能存在差异,例如32位系统中的long 为32位,64位系统中的long为64位。
(3)不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数以及机器的对齐限制,因此,穿越套接字传送二进制结构绝不明智。
解决上述问题的两个常用方法:
(1)把所有的数值数据作为文本串来传递,前提是客户和服务器机器具有相同的字符集。
(2)显式定义所支持数据类型的二进制格式(位数,大端或小端字节序),并以这样的格式在客户与服务器之间传递所有数据。