网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器

1. Linux下的多进程服务器
1.1 进程的概念及应用
并发服务器实现的模型和方法:

  • 多进程服务器(通过创建多个进程提供服务)
  • 多路复用服务器(通过捆绑并统一管理IO对象提供服务)
  • 多线程服务器(通过创建多个线程提供服务)
多进程技术是一种实现并发服务器的手段,在网络通信所占的时间中,数据通信时间比CPU运算时间的占比更大,向多个服务端同时提供服务是一种有效利用CPU资源的方式。
进程定义:占用内存空间的正在运行的程序。例如在电脑上,同时打开文档编辑软件,聊天软件,以及MP3播放器,此时就是创建了三个进程,从操作系统的角度来看,进程是程序流的基本单位,若创建多个进程,操作系统将同时运行,有时一个进程运行的过程中也会产生多个进程。
注:拥有n个运算设备(运算器)的CPU称为n核CPU,核的个数与同时可运行的进程数量相同,若进程数超过核数,进程将分时使用CPU资源,但是由于CPU运算速度足够快,使得用户感觉到所有进程都是同时运行的。
1.2 进程ID
在创建进程的时候,所有进程都会从操作系统分配到对应得分ID,其值为大于2的整数,1是分配给操作系统启动后的首个进程(Linux系统启动后,创建的第一个进程就是init进程),可通过如下命令查看当前Linux下的所有进程:
ps au // 指定au参数可列出进程的所有详细信息ps -ef | grep xxx

创建进程fork
#include pid_t fork(void); // 成功时返回进程ID,失败时返回-1

fork函数创建的是调用它的进程的副本,即它是复制正在运行的,调用fork函数的进程,此外,在fork()函数返回后,两个进程都将执行fork()函数后面的语句。但是因为在fork()的时候,是通过同一个进程,复制相同的内存空间,因此fork()之后的程序需要根据fork()函数的返回值加以区分:
  • 父进程:fork()函数返回子进程的ID
  • 子进程:fork()函数返回0
注:这里的父进程指调用fork()函数的原进程,子进程是指父进程通过调用fork()函数复制出来的进程。
代码实例:
main.cpp
#include #include int gval = 10; int main(int argc, char** argv) { pid_t pid; int localVal = 20; gval++; localVal+=5; pid= fork(); // # fork出一个新的进程if (pid == 0)// 子进程 { /* code */ gval+=2; localVal+=2; printf("The child Process: %d, %d\n", gval, localVal); } else// 父进程 { gval-=2; localVal-=2; printf("The father Process: %d, %d\n", gval, localVal); }return 0; }

输出结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

父进程在调用fork()函数的同事,复制出子进程,并且获取到fork函数的返回值,在复制前,父进程分别对局部变量和全局变量的值进行了修改,在这种状态下进行复制,子进程也将获取到修改后的值。复制完成后,根据fork()函数的返回值,区分父子进程。在父子进程中,修改变变量值不会相互影响,因为调用fork()函数进行复制之后,父子进程具有完全独立的内存结构,二者只是共享相同的代码而已。
1.3 僵尸进程 zombie
文件操作中,文件的打开和关闭同样重要。同样,进程的创建和销毁也同样重要,如果未成功销毁进程,他们将变成僵尸进程。进程在完成工作后(执行完main函数中的程序后)应该被销毁,但是有的进程会变成僵尸进程,占用系统中的重要资源,首先介绍僵尸进程产生的原因,和如何去销毁僵尸进程。
子进程运行结束时,向exit()函数传递的参数值或者return语句的返回值都会传递给操作系统,而操作系统在此时不会将这个值传递给给产生该子进程的父进程,也不会销毁子进程。此时处于这种状态下的进程就是僵尸进程,也就是说,正是操作系统自己,将子进程变成了僵尸进程。
销毁子进程的方法:
将子进程exit()或者return的值传递个产生它的父进程,此时子进程会被销毁
而操作系统不会主动把子进程的返回值传给父进程,只有父进程主动发起请求的时候,操作系统才会传递该值,此时自己成才会被销毁。如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存该值,且让子进程一直处于僵尸状态,即父进程负责回收自己产生的子进程。
通过一个代码实例来演示僵尸进程的产生:
#include #include int main(int argc, char** argv) { pid_t pid = fork(); // 创建一个新的进程if (pid == 0)// 子进程 { printf("This is child process: %d\n", pid); } else { printf("This is Father process: %d\n", pid); sleep(30); // 父进程延时30s }if (pid == 0) printf("Child process finished\n"); else printf("Father process finished\n"); return 0; }

运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

结果分析:可以看到在在30S以内查看进程,发现子进程为僵尸进程,当主进程到达30s而退出之后,处于僵尸状态的子进程将同时被销毁。
1.3.1 利用wait函数销毁僵尸进程
为了销毁子进程,父进程需要主动请求获取子进程的返回值,可用wait方法发起请求
#include /* statloc: 包含子进程终止时传递回来的信息 (返回值,返回状态) 需要通过宏进行分离 子进程返回状态:WIFEXITED(statloc) 子进程的返回值:WEXITSTATUS(statloc) 返回值:成功时返回终止的子进程的ID,失败返回-1 */ pid_t wait(int* statloc)

调用wait函数消灭僵尸进程的时候,如果没有已经终止的子进程,程序将阻塞直到有结束的子进程,因此在调用wait函数的时候需要谨慎。
代码示例:
#include #include #include #include int main(int argc, char** argv) { int status; pid_t pid = fork(); if (pid == 0) { // 子进程 return 3; } else { printf("Child pid: %d\n", pid); pid = fork(); if (pid == 0) { // 子进程 exit(5); } else { printf("Child pid %d\n", pid); // 调用wait函数结束子进程 wait(&status); if (WIFEXITED(status)) { printf("Child returned %d\n", WEXITSTATUS(status)); }wait(&status); if (WIFEXITED(status)) { printf("Child returned %d\n", WEXITSTATUS(status)); } sleep(30); } } return 0; }

运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

可以看待在系统下运行的线程中不存在僵尸进程。
1.3.2 使用waitpid函数销毁僵尸进程
wait函数会造成阻塞问题,waitpid()函数也是一种销毁僵尸进程的方法,且能够避免发生阻塞。
#include /* pid: 等待终止的目标子进程ID statloc: 存储进程返回后的状态 options: 传递头文件sys/wait.h中声明的常量WNOHANG, 即使没有终止的进程也不会进入阻塞状态, 而实返回0并退出。 */ pid_t waitpid(pid_t pid, int * statloc, int options);

代码示例:
#include #include #include #include int main(int argc, char** argv) { int status; pid_t pid = fork(); if (pid == 0) { // 子进程 sleep(15); return 1; } else { while(waitpid(pid, &status, WNOHANG) == 0) { sleep(3); printf("The child process %d is running.\n", pid); }if (WIFEXITED(status)) { printf("Child process exited with %d\n", WEXITSTATUS(status)); } } return 0; }

运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

1.4 信号处理
父进程创建子进程之后,子进程何时终止,父进程往往同子进程一样繁忙,因此不能只通过调用waitpid函数等待子进程结束。因此需要寻找其他的解决方案:
子进程终止的识别主体是操作系统,若在子进程结束的时候,操作系统能将结束的信号告诉忙于处理其他业务的主进程,则将大大提高程序的运行效率。此时父进程将暂时放下其他的业务,来专门处理子进程终止的相关事宜,在Linux系统下,可以借助信操作系统的信号机制实现此想法。
Linux中的信号是一种消息处理机制,不同的信号使用不同的值表示,代表不同的含义,虽然信号结构简单,不能携带很大的信息量,但是信号在系统中的优先级很高,在Linux系统下,很多常规的操作,都会产生响应的信号:
  • 键盘操作产生信号 :按下Ctrl+C,键盘输入一个硬件中断,产生一个信号,这个信号会杀死对应的某个进程
  • 通过shell命令产生信号:kill -9 pid ,终止某个进程
  • 函数调用产生信号:如进程中调用sleep()函数,进程收到相关信号,被迫挂起
  • 对硬件进程了非法访问产生了信号:程序访问内存错误,或者段错误,进程退出
利用信号机制也可以实现进程间通信,但是由于信号的结构简单,不能携带大量信息,且信号的优先级很高,它对应的信号处理函数是通过回调完成,会打乱程序原有的处理流程,因此不适合用信号处理进程间通信。
可通过kill -l 查看系统定义的信号列表:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

Linux中能够产生信号的函数有很多:
(1)kill发送指定的信号到指定的进程:
// 发送指定的信号到指定的进程 int kill(pid_t pid, int sig); kill(getpid(), 9); // 自己杀死自己

(2)raise给当前进程发送指定的信号
// 给自己发送某一个信号 #include int raise(int sig); // 参数就是要给当前进程发送的信号

(3)abort给当前进程发送一个固定信号 (SIGABRT)
// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程 #include void abort(void);

(4)alarm用于单次定时,定时完成向当前进程发出一个信号
#include unsigned int alarm(unsigned int seconds);

(5)setitimer用于周期定时,没触发一次定时器就会发出对应的信号
// 函数可实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号 #include struct itimerval { struct timeval it_interval; /* 时间间隔 */ struct timeval it_value; /* 第一次触发定时器的时长 */ }; // 表示一个时间段: tv_sec + tv_usec struct timeval { time_ttv_sec; /* 秒 */ suseconds_t tv_usec; /* 微妙 */ }; int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); // new_value : 输入值,为定时器设置的参数 // old_value :输出值,上一次为定时器设置的参数,如果不需要知到,传递NULL // which:定时器的计数方式/* which参数可选项: ITIMER_REAL: 自然计时法, 最常用, 发出的信号为SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间(从进程的用户区到内核区切换使用的总时间) ITIMER_VIRTUAL:只计算程序在用户区运行使用的时间,发射的信号为 SIGVTALRM ITIMER_PROF:只计算内核运行使用的时间, 发出的信号为SIGPROF */

信号与signal函数:
进程首先需要告诉操作系统,在它创建的子进程结束之后,请求帮他调用zombie_handler函数,为了完成这一过程,进程首先需要向操作系统注册一个信号才能实现调用这个函数。操作系统调用的这个函数称为信号注册函数。
#include void (*signal(int signo, void(*func)(int)))(int);

函数名:signal
参数:int signo, void (*func)(int)
返回值:返回一个函数指针,这个函数指针指向的函数,具有一个int类型的参数,无返回值。
signal函数原型的理解:signal函数为带有两个参数的函数,一个参数为int类型的整数,另一个参数为函数指针,这个函数指针指向的函数原型没有返回值,具有一个int参数。signal函数执行完毕后,其返回值的也是一个函数指针,这个函数指针指向的是没有返回值,参数为int类型的函数。
在signal函数中可以注册的部分特殊情况和对应的常数值:
  • SIGALARM已到了 通过调用alarm函数注册的时间
  • SIGINT输入CTRL+C 程序中断
  • SIGCHLD子进程终止
在信号注册好之后,当注册的情况发生时,操作系统将调用该信号对应的函数
代码实例:
#include #include #include // 定义信号处理函数,这种函数称为信号处理器 Handler void timeOut(int signo) { if (signo == SIGALRM) { // 到达alram注册的超时时间 printf("Time out\n"); }alarm(2); }// 定义信号处理函数,这种函数称为信号处理器 Handler void keyControl(int signo) { if (signo == SIGINT) { printf("CTRL+C pressed!\n"); } }int main(int argc, char** argv) { // 注册信号 signal(SIGALRM, timeOut); signal(SIGINT, keyControl); alarm(2); for (size_t i = 0; i < 20; i++) { /* code */ printf("Wait...\n"); sleep(30); }return 0; }

运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

发生信号时将会唤醒由于调用sleep而进入休眠状态的进程(即上述代码中,调用sleep之后,程序进入阻塞状态,当alarm(2)超时之后,会唤醒进程,直接的体现就是打印输出了"Wait")。调用函数的主体的确是操作系统,但是进程处于睡眠状态时无法调用函数,因此,产生信号的时候,为了调用信号处理函数,将唤醒由于调用sleep而处于阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入休眠状态,即使还未到sleep中规定的睡眠时间。所以上述实例会很快运行结束。
利用sigaction函数进行信号处理
相比于signal函数,sigaction更加稳定,且具有通用性,因为signal在Unix系列的不同系统下,可能存在区别,但是sigaction函数完全相同。建议使用sigaction函数编写程序,以增强代码的可移植性。
#include int sigaction(int signo, const struct sigaction* act, const struct sigaction* oldact);

  • signo与signal函数相同,用于传递信号信息
  • act对应于第一个参数的信号处理函数信息
  • oldact通过此参数获取之前注册的信号处理函数的函数指针,若不需要则传递0
结构体sigaction定义如下:
struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; }

  • sa_handler用于保存信号处理函数的指针值
  • sa_mask 所有位均初始化0即可(用于指定信号相关的选项和特性)
  • sa_flags所有位均初始化位0即可(用于指定信号相关的选项和特性)
代码实例:
#include #include #include // 定义信号注册函数 void timeOut(int signo) { if (signo == SIGALRM) { // 到达alram注册的超时时间 printf("Time out\n"); }alarm(2); }int main(int argc, char** argv) { // 注册信号 struct sigaction act; act.sa_handler = timeOut; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGALRM, &act, 0); alarm(2); for (size_t i = 0; i < 5; i++) { /* code */ printf("Wait...\n"); sleep(50); }return 0; }

运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

利用信号处理技术消灭僵尸进程:
/* 利用信号机制处理僵尸进程 */ #include #include #include #include #include // 定义信号处理函数 void readChildProc(int signo) { int status; pid_t pid = waitpid(-1, &status, WNOHANG); if (WIFEXITED(status)) { printf("Terminated process %d\n", pid); printf("Process return %d at termination\n", WEXITSTATUS(status)); } }int main(int argc, char** argv) { struct sigaction sigact; sigact.sa_handler = readChildProc; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; // 注册信号 sigaction(SIGCHLD, &sigact, 0); pid_t pid = fork(); // 创建进程if (pid == 0) { // 子进程 printf("I am child process.\n"); sleep(10); return 5; } else { // 父进程 printf("Create child process %d\n", pid); // 再创建一个子进程 pid = fork(); if (pid == 0) { // 子进程 printf("I am child process.\n"); sleep(20); return 3; } else { // 父进程 printf("Create child process %d\n", pid); for (size_t i = 0; i < 5; i++) { /* code */ printf("Wait.....\n"); sleep(15); }} }}

运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

通过上述信号机制处理进程,可以避免创建的子进程变成僵尸进程。
1.5 基于多进程的并发服务器
对回声服务器的例子进行扩展,使其可以向多个客户端同时提供服务。每当有客户端请求服务的时候,回升服务器端都会创建一个子进程以提供服务,此时的服务器端运行主流程如下:
  1. 回声服务器端(父进程)通过调用accept函数受理连接请求
  2. 此时获取的套接字文件描述符创建并传递给子进程
  3. 子进程利用传递来的文件描述符为客户端提供服务
代码示例:
服务端:
/* 多进程服务器 create_date: 2022-7-27 */ #include #include #include #include #include #include #include #include #define BUFF_SIZE 30// 缓冲区大小 #define PORT13100// 端口号void error_handler(char* msg) { printf("%s\n", msg); exit(1); }void read_child_proc(int signo) { int status; pid_t pid = waitpid(-1, &status, 0); printf("Removed process %d\n", pid); }int main(int argc, char** argv) { int serverSocket; int clientSocket; struct sockaddr_in serverAddr; struct sockaddr_in clientAddr; char buffer[BUFF_SIZE]; socklen_t addrSize; // 注册信号处理函数 struct sigaction sigact; sigact.sa_handler = read_child_proc; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigaction(SIGCHLD, &sigact, 0); // 初始化服务端地址 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(PORT); serverSocket = socket(PF_INET, SOCK_STREAM, 0); // TCP socket// 为服务端socket绑定法地址 if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { close(serverSocket); error_handler("Failed to bind server address"); }// 开始监听客户端 if (listen(serverSocket, 5) == -1) { close(serverSocket); error_handler("Failed to listen client"); }while (true) { addrSize = sizeof(clientAddr); printf("Successfully init server and wait for connect......\n"); clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &addrSize); if (clientSocket == -1) continue; printf("Receive connection from %s : %d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); // 创建新的进程 pid_t pid = fork(); if (pid == -1)// 创建进程失败 { close(clientSocket); continue; }if (pid != 0) { printf("Created new process for client.\n"); // 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去重要! close(clientSocket); memset(&clientAddr, 0, sizeof(clientAddr)); continue; } else { // 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭重要! close(serverSocket); int str_len = 0; memset(buffer, 0, BUFF_SIZE); while ((str_len = read(clientSocket, buffer, BUFF_SIZE)) != 0) { /* code */ write(clientSocket, buffer, str_len); memset(buffer, 0, BUFF_SIZE); // 清一下缓冲区 }close(clientSocket); printf("Disconnect %s: %d client from server.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); return 0; } }close(serverSocket); return 0; }

客户端:
/* 客户端 create_date: 2022-7-29 */#include #include #include #include #include #include #define BUFF_SIZE30 #define ADDRESS"127.0.0.1" #define PORT13100int main(int argc, char** argv) { int socket; char buffer[BUFF_SIZE]; struct sockaddr_in serverAddr; // 服务端地址 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(ADDRESS); serverAddr.sin_port = htons(PORT); memset(buffer, 0, BUFF_SIZE); socket = ::socket(PF_INET, SOCK_STREAM, 0); if (socket == -1) { printf("Failed to init socket.\n"); return -1; }if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { printf("Failed to connect to server.\n"); return -2; }printf("Successfully connect to the server.\n"); while (true) { fputs("Input message(Type Q(q) to quit): ", stdout); fgets(buffer, BUFF_SIZE, stdin); if (strncmp(buffer, "Q\n", 2) == 0 || strncmp(buffer, "q\n", 2) == 0) break; int write_len = write(socket, buffer, strlen(buffer)); int recv_len = 0; while (recv_len < write_len) { int recv_count = read(socket, &buffer[recv_len], BUFF_SIZE-1); recv_len += recv_count; // printf("Received %d bytes data from sever.\n"); }printf("Receive data: %s", buffer); memset(buffer, 0, BUFF_SIZE); }close(socket); return 0; return 0; }


运行结果:
服务端运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

客户端1运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

客户端2运行结果:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

查看运行的线程:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

问题:
在上述多进程服务器代码中,在调用fork函数创建子进程的过程中,父进程将两个套接字(一个服务端套接字,一个客户端套接字) 的文件描述符复制给了子进程,在这一过程中,是仅仅复制了文件描述符吗?是否对套接字也进行了复制?
调用fork()函数的时候,会复制父进程的所有资源,同理文件客户端,服务端的描述符也属于父进程资源同样会被复制,但是不会复制套接字。因为套接字属于操作系统的资源,,而文件描述符属于父进程的资源(假设套接字被复制了,那么将会出现同一端口对应多个套接字的情况)。
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片
调用fork函数复制文件描述符 如上图所示,如果一个套接字存在两个文件描述符的时候,只有当两个文件描述符都关闭之后,才能销毁套接字。如上图中所示,即使子进程销毁了与客户端连接的套接字的文件描述符,也无法完全销毁套接字。服务端套接字也是同样如此。因此在调用fork函数之后,需要将无关的套接字进行关闭。
if (pid != 0) { printf("Created new process for client.\n"); // 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去重要! close(clientSocket); memset(&clientAddr, 0, sizeof(clientAddr)); continue; } else { // 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭重要! close(serverSocket); int str_len = 0; ....

1.6 分割TCP的IO程序
对上述的回声服务器程序进行改进,将客户端进行IO分割。在之前的客户端实现中,客户端首先向服务端发送数据,发送完成之后,无条件等待服务端回复(客户端中调用read),只有服务端回复之后,客户端才能进行下一次数据的发送。现在可利用多进程方法对客户端的程序进行改进,将接收与发送的逻辑放在两个不同的进程中进行。
设计方案:
  • 客户端父进程负责接收数据
  • 客户端子进程负责发送数据
这样设计之后,无论客户端是否从服务端接收到数据,都可以进行数据发送,IO分割之后,可以提高频繁交换数据的程序性能,可以提高同一时间数据的传输量。
客户端代码示例:
/* 客户端 create_date: 2022-7-29 */#include #include #include #include #include #include #define BUFF_SIZE30 #define ADDRESS"127.0.0.1" #define PORT13100void readRoutine(int sock, char* buf); void writeRoutine(int sock, char* buf); int main(int argc, char** argv) { char buffer[BUFF_SIZE]; struct sockaddr_in serverAddr; // 服务端地址 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(ADDRESS); serverAddr.sin_port = htons(PORT); memset(buffer, 0, BUFF_SIZE); int socket = ::socket(PF_INET, SOCK_STREAM, 0); if (socket == -1) { printf("Failed to init socket.\n"); return -1; }if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) { printf("Failed to connect to server.\n"); return -2; }printf("Successfully connect to the server.\n"); pid_t pid = fork(); if (pid == 0) { // 子进程负责发送 writeRoutine(socket, buffer); } else { // 父进程负责接收 readRoutine(socket, buffer); }close(socket); return 0; }void readRoutine(int sock, char* buf) { while (true) { memset(buf, 0, BUFF_SIZE); int str_len = read(sock, buf, BUFF_SIZE); if (str_len == 0)// EOF { return; }printf("\nReceive data from server: %s\n", buf); }}void writeRoutine(int sock, char* buf) { while (true) { fputs("Input message(Type Q(q) to quit): ", stdout); fgets(buf, BUFF_SIZE, stdin); if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0) { shutdown(sock, SHUT_WR); return; }write(sock, buf, strlen(buf)); }}

运行结果:
服务端:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

客户端1:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

客户端2:
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

通过客户端数据结果可观察到,在提示输入之后,马上又会出现提示输入的语句,然后才出现服务端回复的内容,反映了在子进程中发送数据后,不用等待服务端回复,而又能马上发送数据,而父进程接收服务端回复数据会稍微慢于子进程发送数据,但是却不会对子进程的发送流程造成影响,适合客户端需要频繁发送数据的应用场景。
注:
在子进程中的发送流程中,有如下的代码:
fputs("Input message(Type Q(q) to quit): ", stdout); fgets(buf, BUFF_SIZE, stdin); if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0) { shutdown(sock, SHUT_WR); return; }

在用户输入Q/q结束发送流程时,客户端子进程会通过shutdown关闭客户端socket的写功能,此时调用shutdown,相当于向服务端传输EOF。在客户端调用完shutdown之后,继续调用后面的代码,也就是main的close和return
close(socket); return 0;

此时只是将子进程中的客户端进行一次关闭。
接着服务端接收到客户端发送来的的EOF,并将其返回给客户端(此时客户端的接收功能并未关闭,还能正常接收服务端的EOF),客户端在判断接收到服务端发送来的EOF之后,结束接收流程,也同样调用main中后续的代码,执行close和return,此时客户端的socket又被关闭了一次,至此,客户端中的socket被成功关闭,子进程和父进程都结束。(可查看并没有出现僵尸进程,因为在子进程结束后,父进程马上结束)。
另一种情况:
如果没有在客户端发送流程中调用shutdown,而是直接return,此时客户端子进程不会发送任何内容便关闭socket结束了。此时服务端未收到任何内容,也就不会向客户端回复内容,此时客户端父进程将一致处于等待接收的状态而无法结束,且出现了僵尸进程。
网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器
文章图片

【网络编程|TCP/IP网络编程(8) 基于Linux的多进程服务器】 ---------------------------------The end---------------------------------------

    推荐阅读