已有的事,后必再有;已行的事,后必再行。
UNIX网络编程,一往无前!冲啊!
进入正题…
首先来了解一下正常的迭代服务器
该服务器正常处理一个客户,如果多个客户连接差不多同时达到,系统内核最大数目的限制下把他们排入队列(相当于一个管道,有先后次序),然后每个返回一个accept函数。
// 迭代服务器,对于每个客户他都迭代执行一次
while(1) {} || for ( ; ; )
如果程序运行速度需要较多时间服务每个客户,那么我们必须以某种方式重叠对各个用户的服务,即并发服务器。
并发服务器开始辽!
同时能处理多个客户的并发服务器,有多重编写技术,在此只介绍Unix的fork函数,为每个客户创建一个子进程。
#include <unistd.h>
pid_t fork(void);
我们来看他的返回值
// 返回值:在子进程中为0,在父进程中为子进程的ID,若出错则为-1
RETURN VALUE:On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
直接来看代码吧
server.cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <iostream>
#include <sys/wait.h>
#define MAXSIZE 100
#define SERV_PORT 5678
#define SERV_IP "127.0.0.1"
#define LIST_NUM 8
using std::cout;
using std::endl;
void str_echo(int);
void sig_chld(int);
int main(void) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
// 创建一个TCP套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
// INADDR_ANY为通配IP地址
servaddr.sin_addr.s_addr = inet_addr(SERV_IP);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
// 将TCP套接字转换成监听套接字
listen(listenfd, LIST_NUM);
for( ; ; ) {
clilen = sizeof(cliaddr);
// 服务器阻塞于accept调用,等待客户连接的完成
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
// 创建一个子进程
if ((childpid = fork()) == 0) {
// 子进程关闭监听套接字
close(listenfd);
// 打印IP地址
cout << "[" << inet_ntoa(cliaddr.sin_addr) << ":" << ntohs(cliaddr.sin_port) << "]" << endl;
// 子进程调用str_echo函数
str_echo(connfd);
// 关闭子进程
exit(0);
}
// 父进程关闭已连接套接字
close(connfd);
// 当子进程关闭后,会返回父进程一个SIGCHLD信号,父进程一般是默认忽略的,我们应该定义一个信号,让父进程杀死这个已经结束的子(僵尸)进程
signal(SIGCHLD, sig_chld);
}
return 0;
}
void str_echo(int sockfd) {
ssize_t n;
char buf[MAXSIZE];
while((n = read(sockfd, buf, MAXSIZE)) > 0) {
write(sockfd, buf, n);
}
if (n < 0) {
perror("str_echo: read error");
}
}
void sig_chld(int signo) {
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminnated\n", pid);
return;
}
client.cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define MAXSIZE 100
#define SERV_PORT 5678
#define SERV_IP "127.0.0.1"
#define LIST_NUM 8
void str_cli(int);
void str_cli(int sockfd) {
char readbuf[MAXSIZE], recvbuf[MAXSIZE];
while(fgets(readbuf, MAXSIZE, stdin) != NULL) {
write(sockfd, readbuf, strlen(readbuf));
if (read(sockfd, recvbuf, MAXSIZE) == 0) {
perror("read error");
}
fputs(recvbuf, stdout);
}
}
int main(void) {
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_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = inet_addr(SERV_IP);
connect(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
str_cli(listenfd);
exit(0);
}
其中终止一个子进程的函数中,即
void sig_chld(int signo) {
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminnated\n", pid);
return;
}
此时要注意wait和waitpid的区别
// pid_t wait(int *status);
/* 非空:子进程退出状态放在它所指向的地址中。
* 空:不关心退出状态
* 返回值:如果执行成功则返回子进程ID,如果有错误发生则返回-1
* 注意事项:
* 1. 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回
* 2. 如果没有任何子进程,则立即出错返回
* 3. 如果其所有子进程都还在运行,将阻塞到现有子进程第一个终止为止
*/
// pid_t waitpid(pid_t pid, int *status, int options);
/* pid == -1 等待任一子进程。就这一方面而言,waitpid与wait等效。
* pid > 0 等待其进程ID与pid相等的子进程。
* pid ==0 等待其组ID等于调用进程组ID的任一子进程。
* pid < -1 等待其组ID等于pid绝对值的任一子进程。
* status:是一个整型数指针
* 非空:子进程退出状态放在它所指向的地址中。
* 空:不关心退出状态
* options:介绍一个WNOHAND,告知内核没有已终止子进程时不要阻塞
*/
我们以五个客户端进行建立五个子进程
int main(void) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd[i] = socket(AF_INET, SOCK_STREAM, 0);
for (int i = 0; i < 5; i++) {
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = inet_addr(SERV_IP);
connect(listenfd[i], (struct sockaddr*)&servaddr, sizeof(servaddr));
}
str_cli(listenfd); /* do it all */
exit(0);
}
当同一时刻终止,这就引发了5个FIN,在同一时刻有5个SIGCHLD信号递交给父进程,在shell运行ps -aux |grep Z可以看到4个子进程仍然作为僵尸进程,建立一个信号处理函数不足以防止出现僵尸进程(有时候FIN到达时间不同,可能能杀死3到4个),因此可用waitpid
# 查看僵尸进程
ps -aux |grep Z
void sig_chld(int signo) {
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHAND)) > 0) {
printf("child %d terminated\n", pid);
}
return;
}
下面来写一个额外的服务器四大问题
服务器进程终止
- 启动服务器,启动客户端,开辟一个子进程,客户端正常输入输出
- 人为的在shell中找到子进程的并且kill它
- SIGCHLD信号返回给父进程,并得到正确处理
- 然而!客户端上没有发生任何事,客户端接收到来自服务器的FIN并响应一个ACK,然而问题是客户端阻塞在fgets调用上,等待从终端接收一行文本 ,在等待从终端接收一行文本此时,服务器是CLOSE_WAIT状态,客户端是FIN_WAIT2状态
- 当我们输入文本的时候,子进程函数调用write,客户TCP接着把数据发送给服务器(TCP允许这么做,因为此时TCP客户端接收到FIN只是表示服务器进程已关闭了连接的服务器端,从而不往其中发送数据而已。FIN的接收并没有告知客户TCP服务器进程已经终止)
- 当服务器TCP接受来自客户的数据时,既然先前打开那个套接字的进程已经终止,于是响应一个RST
- 然后客户端看不到这个RST,于是客户端会收到一个“server terminated prematurely”(服务器过早终止)
- 当客户终止时,他所有打开着的描述符都被关闭
SIGPIPE信号
如果客户端不理会readline函数返回的错误,反而向服务端写入更多的数据,这是可能的。因为RST返回是有时间的,客户很可能在读回任何数据之前执行两次针对服务器的写操作,而RST是由其中第一次写操作引发的。
因此,当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号,该信号的默认行为是终止进程,因此进程必须捕获到他以免不情愿地被终止。不论该进程是捕获了该信号并从其信号处理函数返回,还是简单的忽略该信号,写操作都将返回EPIPE错误
服务器主机崩溃
情景模拟:
- 打开一个服务端,打开一个客户端,关闭服务端
- 当服务器主机崩溃时,已有的网络连接上不发出任何东西(此时我们假设是主机崩溃,而不是操作员执行命令关机)
- 客户输入文本,他由write写入内核,再有客户TCP作为一个数据分节送出,客户随后阻塞于read调用,等待回射的应答
- 在此要提一个典型模式。客户TCP持续重传数据 分节,试图从服务器上接收一个ACK。其中共分节12次,共等待约9分组才放弃重传。当客户TCP最后终于放弃时,给客户进程返回一个错误
- 此时回返回一个ETIMEDOUT错误。然而如果中间某个路由器判定服务器主机不可达,从而响应一个“destination unreachable”(目的地不可达)的ICMP消息,那么所返回的错误是EHOSTUNREACH或ENETUNREACH
服务器主机崩溃后重启
这种情况注意讨论在客户TCP不主动向服务器发送数据也想检测出服务器主机的崩溃
- 启动服务器和客户,并在客户写操作确认已经建立
- 服务器主机崩溃并重启
- 当客户写操作,他将作为一个TCP数据分节发送到服务器主机
- 当服务器主机崩溃后重启,他的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的所有客户的数据分节响应一个RST
- 当客户TCP收到一个RST时,客户正阻塞于read调用。导致该调用返回ECONNRESET错误
服务器主机关机
前面是服务器主机崩溃的情况,最后讨论一下服务器正在运行时,服务器主机被操作员关机将会发生什么。
Unix系统关机时,init进程通常给所有进程发送STGTERM信号(该信号可被捕获),等待一段固定的时间(5~20s),然后给仍然运行的进程发送SIGKILL信号(该信号不可被捕获),这么做给所有运行的进程一小段时间来清楚和终止。如果我们不捕获SIGTERM信号,我们的服务器将由SIGKILL信号终止,当服务器子进程终止时,他的所有打开着的描述符都被关闭。(当使用select或poll函数,使得服务器端进程的终止一经发生,客户就能就按测到)