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 <unistd.h>
pid_t fork(void); // 成功时返回进程ID,失败时返回-1
fork函数创建的是调用它的进程的副本,即它是复制正在运行的,调用fork函数的进程,此外,在fork()函数返回后,两个进程都将执行fork()函数后面的语句。但是因为在fork()的时候,是通过同一个进程,复制相同的内存空间,因此fork()之后的程序需要根据fork()函数的返回值加以区分:
- 父进程:fork()函数返回子进程的ID
- 子进程:fork()函数返回0
注:这里的父进程指调用fork()函数的原进程,子进程是指父进程通过调用fork()函数复制出来的进程。
代码实例:
main.cpp
#include <stdio.h>
#include <unistd.h>
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;
}
输出结果:
父进程在调用fork()函数的同事,复制出子进程,并且获取到fork函数的返回值,在复制前,父进程分别对局部变量和全局变量的值进行了修改,在这种状态下进行复制,子进程也将获取到修改后的值。复制完成后,根据fork()函数的返回值,区分父子进程。在父子进程中,修改变变量值不会相互影响,因为调用fork()函数进行复制之后,父子进程具有完全独立的内存结构,二者只是共享相同的代码而已。
1.3 僵尸进程 zombie
文件操作中,文件的打开和关闭同样重要。同样,进程的创建和销毁也同样重要,如果未成功销毁进程,他们将变成僵尸进程。进程在完成工作后(执行完main函数中的程序后)应该被销毁,但是有的进程会变成僵尸进程,占用系统中的重要资源,首先介绍僵尸进程产生的原因,和如何去销毁僵尸进程。
子进程运行结束时,向exit()函数传递的参数值或者return语句的返回值都会传递给操作系统,而操作系统在此时不会将这个值传递给给产生该子进程的父进程,也不会销毁子进程。此时处于这种状态下的进程就是僵尸进程,也就是说,正是操作系统自己,将子进程变成了僵尸进程。
销毁子进程的方法:
“将子进程exit()或者return的值传递个产生它的父进程,此时子进程会被销毁”
而操作系统不会主动把子进程的返回值传给父进程,只有父进程主动发起请求的时候,操作系统才会传递该值,此时自己成才会被销毁。如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存该值,且让子进程一直处于僵尸状态,即父进程负责回收自己产生的子进程。
通过一个代码实例来演示僵尸进程的产生:
#include <stdio.h>
#include <unistd.h>
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;
}
运行结果:
结果分析:可以看到在在30S以内查看进程,发现子进程为僵尸进程,当主进程到达30s而退出之后,处于僵尸状态的子进程将同时被销毁。
1.3.1 利用wait函数销毁僵尸进程
为了销毁子进程,父进程需要主动请求获取子进程的返回值,可用wait方法发起请求
#include <sys/wait.h>
/*
statloc: 包含子进程终止时传递回来的信息 (返回值,返回状态)
需要通过宏进行分离
子进程返回状态:WIFEXITED(statloc)
子进程的返回值:WEXITSTATUS(statloc)
返回值:成功时返回终止的子进程的ID,失败返回-1
*/
pid_t wait(int* statloc)
调用wait函数消灭僵尸进程的时候,如果没有已经终止的子进程,程序将阻塞直到有结束的子进程,因此在调用wait函数的时候需要谨慎。
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
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;
}
运行结果:
可以看待在系统下运行的线程中不存在僵尸进程。
1.3.2 使用waitpid函数销毁僵尸进程
wait函数会造成阻塞问题,waitpid()函数也是一种销毁僵尸进程的方法,且能够避免发生阻塞。
#include <sys/wait.h>
/*
pid: 等待终止的目标子进程ID
statloc: 存储进程返回后的状态
options: 传递头文件sys/wait.h中声明的常量WNOHANG, 即使没有终止的进程也不会进入阻塞状态,
而实返回0并退出。
*/
pid_t waitpid(pid_t pid, int * statloc, int options);
代码示例:
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
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;
}
运行结果:
1.4 信号处理
父进程创建子进程之后,子进程何时终止,父进程往往同子进程一样繁忙,因此不能只通过调用waitpid函数等待子进程结束。因此需要寻找其他的解决方案:
子进程终止的识别主体是操作系统,若在子进程结束的时候,操作系统能将结束的信号告诉忙于处理其他业务的主进程,则将大大提高程序的运行效率。此时父进程将暂时放下其他的业务,来专门处理子进程终止的相关事宜,在Linux系统下,可以借助信操作系统的信号机制实现此想法。
Linux中的信号是一种消息处理机制,不同的信号使用不同的值表示,代表不同的含义,虽然信号结构简单,不能携带很大的信息量,但是信号在系统中的优先级很高,在Linux系统下,很多常规的操作,都会产生响应的信号:
- 键盘操作产生信号 :按下Ctrl+C,键盘输入一个硬件中断,产生一个信号,这个信号会杀死对应的某个进程
- 通过shell命令产生信号:kill -9 pid ,终止某个进程
- 函数调用产生信号:如进程中调用sleep()函数,进程收到相关信号,被迫挂起
- 对硬件进程了非法访问产生了信号:程序访问内存错误,或者段错误,进程退出
利用信号机制也可以实现进程间通信,但是由于信号的结构简单,不能携带大量信息,且信号的优先级很高,它对应的信号处理函数是通过回调完成,会打乱程序原有的处理流程,因此不适合用信号处理进程间通信。
可通过kill -l 查看系统定义的信号列表:
Linux中能够产生信号的函数有很多:
(1)kill 发送指定的信号到指定的进程:
// 发送指定的信号到指定的进程
int kill(pid_t pid, int sig);
kill(getpid(), 9); // 自己杀死自己
(2)raise 给当前进程发送指定的信号
// 给自己发送某一个信号
#include <signal.h>
int raise(int sig); // 参数就是要给当前进程发送的信号
(3)abort 给当前进程发送一个固定信号 (SIGABRT)
// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
#include <stdlib.h>
void abort(void);
(4)alarm 用于单次定时,定时完成向当前进程发出一个信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
(5)setitimer 用于周期定时,没触发一次定时器就会发出对应的信号
// 函数可实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include <sys/time.h>
struct itimerval {
struct timeval it_interval; /* 时间间隔 */
struct timeval it_value; /* 第一次触发定时器的时长 */
};
// 表示一个时间段: tv_sec + tv_usec
struct timeval {
time_t tv_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 <signal.h>
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 <stdio.h>
#include <unistd.h>
#include <signal.h>
// 定义信号处理函数,这种函数称为信号处理器 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;
}
运行结果:
发生信号时将会唤醒由于调用sleep而进入休眠状态的进程(即上述代码中,调用sleep之后,程序进入阻塞状态,当alarm(2)超时之后,会唤醒进程,直接的体现就是打印输出了"Wait")。调用函数的主体的确是操作系统,但是进程处于睡眠状态时无法调用函数,因此,产生信号的时候,为了调用信号处理函数,将唤醒由于调用sleep而处于阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入休眠状态,即使还未到sleep中规定的睡眠时间。所以上述实例会很快运行结束。
利用sigaction函数进行信号处理
相比于signal函数,sigaction更加稳定,且具有通用性,因为signal在Unix系列的不同系统下,可能存在区别,但是sigaction函数完全相同。建议使用sigaction函数编写程序,以增强代码的可移植性。
#include <signal.h>
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 <stdio.h>
#include <unistd.h>
#include <signal.h>
// 定义信号注册函数
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;
}
运行结果:
利用信号处理技术消灭僵尸进程:
/*
利用信号机制处理僵尸进程
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
// 定义信号处理函数
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);
}
}
}
}
运行结果:
通过上述信号机制处理进程,可以避免创建的子进程变成僵尸进程。
1.5 基于多进程的并发服务器
对回声服务器的例子进行扩展,使其可以向多个客户端同时提供服务。每当有客户端请求服务的时候,回升服务器端都会创建一个子进程以提供服务,此时的服务器端运行主流程如下:
- 回声服务器端(父进程)通过调用accept函数受理连接请求
- 此时获取的套接字文件描述符创建并传递给子进程
- 子进程利用传递来的文件描述符为客户端提供服务
代码示例:
服务端:
/*
多进程服务器
create_date: 2022-7-27
*/
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFF_SIZE 30 // 缓冲区大小
#define PORT 13100 // 端口号
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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUFF_SIZE 30
#define ADDRESS "127.0.0.1"
#define PORT 13100
int 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;
}
运行结果:
服务端运行结果:
客户端1运行结果:
客户端2运行结果:
查看运行的线程:
问题:
在上述多进程服务器代码中,在调用fork函数创建子进程的过程中,父进程将两个套接字(一个服务端套接字,一个客户端套接字) 的文件描述符复制给了子进程,在这一过程中,是仅仅复制了文件描述符吗?是否对套接字也进行了复制?
调用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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUFF_SIZE 30
#define ADDRESS "127.0.0.1"
#define PORT 13100
void 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));
}
}
运行结果:
服务端:
客户端1:
客户端2:
通过客户端数据结果可观察到,在提示输入之后,马上又会出现提示输入的语句,然后才出现服务端回复的内容,反映了在子进程中发送数据后,不用等待服务端回复,而又能马上发送数据,而父进程接收服务端回复数据会稍微慢于子进程发送数据,但是却不会对子进程的发送流程造成影响,适合客户端需要频繁发送数据的应用场景。
注:
在子进程中的发送流程中,有如下的代码:
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结束了。此时服务端未收到任何内容,也就不会向客户端回复内容,此时客户端父进程将一致处于等待接收的状态而无法结束,且出现了僵尸进程。
---------------------------------The end---------------------------------------