1. TCP回射示例
服务器代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define SRV_PORT 8888 #define MAXLINE 4096 void str_echo(int fd); int main(int argc, char **argv) { int listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd < 0) { perror("create socket error."); } struct sockaddr_in srvaddr; bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); srvaddr.sin_port = htons(SRV_PORT); if(bind(listenfd, (struct sockaddr*)&srvaddr, sizeof(srvaddr)) < 0) { perror("bind error."); } if(listen(listenfd, 1023) < 0) { perror("listen error."); } struct sockaddr_in cliaddr; for(; ;) { socklen_t clilen = sizeof(cliaddr); int connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen); if(connfd < 0) { perror("accept error."); } pid_t childpid; if( (childpid = fork()) == 0 ) { close(listenfd); str_echo(connfd); exit(0); } close(connfd); } return 0; } void str_echo(int sockfd) { char line[MAXLINE]; while(read(sockfd, line, MAXLINE) != 0) { if(write(sockfd, line, strlen(line)) != strlen(line)) { perror("write error"); } } }
客户端代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define SRV_PORT 8888 #define MAXLINE 4096 void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { if(argc != 2) { printf("usage:tcpcli <ip address>\n"); exit(0); } int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("create socket error."); } struct sockaddr_in srvaddr; bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(SRV_PORT); if(inet_pton(AF_INET, argv[1], &srvaddr.sin_addr) <= 0) { printf("address error %s\n", argv[1]); exit(0); } if(connect(sockfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0 ) { perror("connect error"); } str_cli(stdin, sockfd); exit(0); } void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE]; char readline[MAXLINE]; while(fgets(sendline, MAXLINE, fp)) { if( write(sockfd, sendline, strlen(sendline)) != strlen(sendline) ) { perror("send data error"); } if( read(sockfd, readline, MAXLINE) == 0) { perror("recv data error"); } fputs(readline,stdout); } }
2. 示例启动过程
客户端服务器建立连接后发生的动作:
- 客户端调用str_cli函数,阻塞于fgets,等待输入;
- 服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo,子进程阻塞于read等待客户端发送的数据;
- 另一方面服务器的父进程再次调用accept等待下一个客户连接。
所以至此有三个阻塞进程:客户进程,服务器父进程,服务器子进程。
3. 示例正常终止过程
- 客户端键入EOF,str_cli函数返回main函数, main函数调用exit终止进程;
- 由于客户端程序没有关闭其描述符,所以其描述符由内核关闭,此时开始发送FIN与服务器进行TCP四次握手关闭连接;
- 服务器子进程收到FIN,子进程从str_echo返回子进程的main函数,通过调用exit终止子进程,子进程所有描述符关闭;
- 最终服务器发FIN给客户端,客户端返回ACK,进入TIME_WAIT状态。连接完全终止。
- 服务器子进程终止时会给父进程发送一个SIGCHLD信号,我们没有捕捉此信号,信号默认被忽略,这导致子进程进入僵死状态,僵死进程占用系统资源,所以我们还需要处理僵死的进程。
4. POSIX信号处理(处理3产生的僵死进程)
信号由一个进程发给另一个进程(或自身),也可由内核发给某个进程。
1)调用函数sigaction设定一个信号的处理有以下三种选择:
- 提供一个信号处理函数(signal handler),捕获到特定信号(SIGKILL与SIGSTOP不能被捕获)发生它就被调用,其原型是 void handler(int signo);
- 可以设置信号为SIG_IGN来忽略它(SIGKILL与SIGSTOP不能被忽略);
- 可以设置信号为SIG_DFL来启用信号的默认处理。
2)signal函数(使用系统提供的)
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
3)处理SIGCHLD信号(处理僵死进程)
- 设置僵死进程的目的是为了维护子进程的信息,以便父进程在某个时候获取;
- 为了防止僵死进程产生无论何时调用fork创建子进程,父进程都得wait它们,为此我们可以建立一个捕获SIGCHLD的信号处理函数
4)例子
- 在创建子进程之前调用如下函数:
signal(SIGCHLD, sig_child);
- 创建信号处理函数sig_child
void sig_chld(int signo) { int stat; pid_t pid = wait(&stat); printf("child %d terminated\n", pid); return; }
5)处理被中断的系统调用
- 慢系统调用:用于描述永远阻塞的系统调用如accept
- 适用慢系统调用的规则:当阻塞于某个慢系统调用的一个进程捕获某个信号,且相应信号处理函数返回时该系统调用可能返回一个EINTR错误。编写捕获信号的程序时,应该对慢系统调用返回EINTR有所准备。
- 如:处理被中断的accept,以下是用于处理被中断的accept调用的代码
for( ; ; ) { clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0) { if(errno == EINTR) { continue; } else { perror("accept error"); } } }
5. wait和waitpid
#include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
status为进程的终止状态:正常终止,信号杀死或由作业控制停止。
waitpid的pid参数允许我们指定想等待的进程,pid值为-1时表示等待第一个终止的进程。
- wait不足以防止僵死进程的出现,在调用wait之前可能有几个SIGCHLD信号产生,但是wait的调用次数不确定,可能只调用一次。
- 防止僵死进程正确的解决方法是用waitpid,waitpid必须指定WNOHANG选项,以告知waitpid在有尚未终止的子进程在运行时不要阻塞。可以改造上面sig_chld函数如下:
void sig_chld(int signo) { int stat; pid_t pid; while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) { printf("child %d terminated\n", pid); } return; }
小结:网络编程中可能会遇到的三种情况
- fork子进程时,必须捕获SIGCHLD信号;
- 捕获信号时,必须处理被中断的系统调用(EINTR);
- SIGCHLD的信号处理函数必须编写正确,用waitpid而不是wait,以免留下僵死进程。
6. accept返回前连接终止:服务器在调用accept之前收到RST(此种情况在16章再详细给出)
7.服务器进程终止
示例程序中,如果服务器终止,当服务器发出的FIN到达客户端时,客户端正阻塞在fgets上等待用户输入。客户端此时要应对两个描述符(套接字和用户输入),它不能单纯的阻塞在其中任何一个源的输入上,这个涉及到select和poll,第六章继续讨论。
8.SIGPIPE信号
- 场景:例子中服务器终止后,客户端在读数据之前,向服务器执行两次的写操作。
- 客户端第一次写操作引起服务器的RST回复,而当进程向收到RST的套接字就行写操作时,内核会向该进程发送一个SIGPIPE信号,该信号的默认行为是终止进程。因此进程应该捕获SIGPIPE信号以免它被动的被终止。不论进程忽略SIGPIPE信号还是捕获处理了,写操作都将返回EPIPE错误。
- 处理SIGPIPE的建议方法:取决于其发生时进程想要做什么,如果没有什么特殊的事情做,一般直接将此信号设置为SIG_IGN,并假设后续的输出操作将捕获EPIPE错误。注意,如果有多个套接字,该信号无法区分哪个套接字出错了,要知道哪个write出差,要么忽略该信号,要么信号处理完后再处理来自write的EPIPE信号。
9.服务器主机崩溃
主机崩溃可能导致客户端长时间(9分钟左右)对服务器进行重连,这使得我们有时候需要在更短的时间发现服务器已经挂掉,此时可以把read设置一个超时(后面再详说),或者用SO_KEEPALIVE套接字选项和一些心跳技术实现(第七章)。
10. 服务器主机崩溃后重启
服务器崩溃后,不向客户端发送任何信息,客户端继续向服务器发送数据。服务器重启后由于失去了崩溃前所有连接信息,所有服务器TCP对所有来自客户的数据分节响应RST,客户收到RST后,阻塞的read调用返回ECONNRESET错误。
11. 服务器主机关机:会先发SIGTERM信号,再发SIGKILL信号。进程终止后与7描述的情况一样。
12. 关于数据格式
- 在客户和服务器之间传递文本:文本传输不论客户机与服务器的字节序如何,都可以对数据进行正常的传递,不发生错误。
- 在客户和服务器之间传递二进制结构:传递二进制结构如果客户机和服务器字节序不同将发生错误。
注意三个潜在的问题:
- 不同的实现以不同格式存储二进制数(如字节序大端小端);
- 不同实现在存储相同的C数据类型可能存在差异(对于short,int或long等整数类型大小在不同系统间可能不同);
- 不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数及机器对齐限制。
解决以上问题的常用方法:
- 把所用数据用文本串传递;
- 显式定义所支持数据类型的二进制格式(位数、大端、小端),并以这样的格式在客户机与服务器之间传递。