第十章 多进程服务器端
10.1 进程概念及应用
我们在收看视频的时候,无法忍受等待很久很久,因此服务器端的设计必须满足,当有多个客户端同时访问时,不能让大家按照顺序进行排队,这样叫什么服务?
多进程就是解决这个问题的方法。
10.1.1 并发服务器端的实现方法
网络程序中 数据通信时间比CPU运算时间更多,因此,向多个客户端提供服务是一种有效利用CPU的方式。
下面列出集中具有代表性的并发服务器端的实现模型和方法。
- 多进程服务器端:通过创建多个进程提供服务。
- 多路复用服务器:通过捆绑并统一管理I/O对象提供服务
- 通过生成与客户端等量的线程提供服务。
本章主要讲的是第一种方法。
10.1.2 进程:占用内存空间的正在运行的程序。
相信大家在使用PC的时候都用过任务管理器,里面的子标题就是进程,代表着当前正在运行状态的程序。
CPU核个数与进程数的关系:核的个数与可同时运行的进程数相同。因此当进程数超过核数时,进程将分时使用CPU资源,由于CPU的运转速度极快,我们感到所有的进程都在同时运行。
10.1.3 进程ID:操作系统为进程分配的ID
10.1.4 通过调用fork()函数创建进程
#include <unistd.h>
pid_t fork(void);
->成功时返回进程ID , 失败时返回-1
fork函数创建调用的进程副本。 也就是说,复制正在运行的,调用fork函数的进程。
两个进程都将执行fork函数返回后的语句。
但因为是同一个进程、赋值相同的内存空间,之后的程序流要根据fork函数的返回值加以区分。
下面通过示例来看fork函数得到的结果。
fork.c
#include <unistd.h>
#include <stdio.h>
int gval = 10;
int main(int argc, char* argv[])
{
pid_t pid;
int lval = 20;
gval++ ,lval += 5; // gval = 11 lval = 25
pid = fork();
if(pid == 0){
// 子进程执行
gval += 2 , lval += 2; //gval = 13 lval = 27
}
else{
// 父进程执行
gval -= 2, lval -= 2; //gval = 9 lval = 23
}
if(pid == 0){
// 子进程执行
printf("Child Proc: [%d,%d] \n",gval ,lval);
}
else{
// 父进程执行
printf("Parent Proc: [%d,%d] \n",gval ,lval);
}
return 0;
}
上面的代码中,父进程的pid中存有子进程的id,子进程的pid为0;
第一个if中执行 子进程的代码,因为子进程的pid为0
第一个else中执行父进程的代码。
下面个if 和else 同理
10.2 进程和僵尸进程
如果没有对进程进行认真的销毁,就会变成僵尸进程。
10.2.1 僵尸进程
进程完成工作后(执行完main函数中的程序后)应该被销毁。
但有时这些进程没有关闭(销毁),变成了僵尸进程继续占用系统中有限的资源,给系统带来负担
10.2.2 僵尸进程产生原因
是什时候是一个进程结束呢?
一般我们认为
- 传递参数并调用了 exit函数
- main函数中执行了return语句,并返回值。
也就是说:我们如何真正结束一个子进程呢?就是使用上面的两种方法
向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。
而操作系统不会销毁子进程,直到把这些值传递给产生孩子进程的父进程。处在这种状态下的进程就是僵尸进程。
那么如何
向创建子进程的父进程传递子进程的exit参数值或return语句的返回值呢?
如何传递呢?这个过程不是自动的,需要手动帮助父进程活动的子进程的结束状态值。
请看下面的代码,这将解释僵尸进程到底在哪里。
zombie.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid = fork();
if(pid == 0){
puts("Hi, I am a child process");
}
else{
printf("Child Process ID: %d\n",pid);
sleep(30);
}
if(pid == 0){
puts("End child process");
}
else{
puts("End parent process");
}
return 0;
}
子进程没return,也没有exit 主进程sleep中
可以看到上图中的子进程没有其他的代码了,但是没有return和exit,因此能够查到这个子进程(PID = 4001)
30s后,主进程结束:
这个僵尸进程也结束了。
下面将讲解如何在不结束父进程的情况下,销毁僵尸进程。
10.2.3 销毁僵尸进程 方法1:wait函数
#include <unistd.h>
pid_t wait(int* statloc);
->成功时返回终止的子进程ID,失败时返回-1
调用此函数时,如果已有子进程终止(现在成了僵尸进程,也就是子进程中使用了exit或者return),那么子进程终止时传递的返回值(exit函数的参数值 或者 main函数中的return返回值),将保存到该函数的参数(statloc)所指内存空间中。
如果没有子进程终止,会一直阻塞,等待子进程结束
但函数参数指向的单元(statloc指向的内存中)中还包含其他信息,因此需要通过下列宏进行分离。
- WIFEXITED 子进程正常终止时返回“真”(true);
- WEXITSTATUS 返回子进程的返回值。
怎么用呢? 看下面这块代码
if( WIFEXITED(status)) // 如果是正常终止
{
puts("normal terminaion!");
printf("Child pass num : %d", WEXITSTATUS(status)): // 打印返回值
}
下面是完整的示例,这里展示如何让一个子进程不变成僵尸进程。
wait.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork(); // 创建子进程 1
if(pid == 0){
// 子进程 1
return 3;
}
else{
// 父进程
printf("Child PID : %d \n",pid);
pid = fork();
if(pid == 0){
// 子进程 2
exit(7);
}
else{
// 还是父进程
printf("Child PID: %d \n", pid);
wait(&status); // 得到子进程1的status
if(WIFEXITED(status)){
printf("child send one : %d \n", WEXITSTATUS(status));
}
wait(&status); // 得到子进程2 的status
if(WIFEXITED(status)){
printf("child send two : %d \n", WEXITSTATUS(status));
}
sleep(30); // 让父进程先别结束
printf("baba finished!\n");
}
}
return 0;
}
可以看到,系统进程中并没有上面两个子进程了。
有个小问题: 如果调用wait函数时,没有已终止的子进程,那么程序将阻塞(blocking) 直到有子进程终止。因此需要谨慎调用。
10.2.3 销毁僵尸进程 方法2:waitpid函数
waitpid函数不会引起阻塞
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* statloc, int options);
-> 成功时返回终止的子进程ID(或0),失败时返回-1
pid: 等待终止的目标子进程的ID,若传递-1 则与wait函数相同,可以等待任意子进程终止。
statloc: 与wait函数的statloc函数一样,用来保存返回值
options: 传递头文件 sys/wait.h 中声明的变量 WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。(w no hang)
下面的代码中,程序不会阻塞
waitpid.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
/* code */
int status;
pid_t pid = fork();
if(pid == 0){ // 子进程 delay 15sec 然后return 24
sleep(15);
return 24;
}
else{ // 父进程 当没有子进程结束时(waitpid返回0),打印并delay 3s 等待着。。。。
while(!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status)){ //返回staus是否是正常退出,如果是 打印子进程的退出码
printf("Child send %d \n", WEXITSTATUS(status));
}
}
return 0;
}
从上图结果看出,执行5sleep 终于等到子进程结束(变成僵尸),这证明程序一直在运行,并没有阻塞在waitpid函数上。
10.3 信号处理
我们已经知道 如何创建进程,以及如何 彻底销毁一个进程。还个问题:
“子进程什么时间结束呢??难道要一直调用waitpid检测么?”
父进程也很忙啊,不能一直等着它啊= - =
这里讨论一下解决方案
10.3.1 操作系统的助力
子进程终止的识别主体是操作系统,如果OS能够告诉父进程,你的子进程结束了!
这样就能节省很多父进程的时间了
引入信号处理机制(signal handling),这里的信号是,特定事件发生时,由操作系统向进程发送的消息。
另外为了响应该消息,执行与消息相关的自定义操作的过程 称为 处理或信号处理。
10.3.2 信号与signal函数
进程:“操作系统,如果我之前创建的子进程终止,就帮我调用xxxxxx函数!”
操作系统:“好的! 您把xxxxxx函数编好吧!我绝对完成任务。”
上面的过程是“注册信号”的过程,进程发现自己的子进程结束,请求擦欧洲系统调用特定函数,该请求通过 signal
函数完成
#include <signal.h>
void (*signal(int signo, void (*func)(int))) (int);
-> 为了在产生信号时调用,返回之前注册的函数指针。
-》返回值类型为 函数指针
函数名: signal
参数: int signo, void(*func)(int)
返回类型: 参数为 int型,返回void型函数指针。
调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)
当发生第一个参数代表的情况时(一般为产生了某个信号),调用第二个函数所指向的函数,下面是signal函数中注册的部分特殊情况和对应的参数
- SIGALRM : 已经到了通过调用alarm函数注册的时间,产生本信号
- SINGINT : 输入 CTRL + C 时,产生本信号。
- SIGCHLD: 子进程终止时,产生本信号。
进行下面示例之前,先学习一下alarm函数。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-> 返回0 或以秒为单位的距离SIGALRM 信号发生所剩的时间
传入的参数seconds是一个正整形参数,相应时间后,将产生SIGALRM信号。
若向该函数传递0,则之前对SIGALRM信号的预约将取消。
若未向该信号传递任何处理函数,这(通过调用signal函数)直接终止进程,不做任何处理
(从最后一句话可以看出,我们的alarm函数还是要认真对待的。)
下面给出相关示例:**signal.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig);
void keycontrol(int sig);
int main(int argc, char *argv[])
{
/* code */
int i ;
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
alarm(2);
for(i = 0; i < 3; i++){
puts("wait...");
sleep(100);
}
return 0;
}
void timeout(int sig)
{
if(sig == SIGALRM){
puts("Time out!");
}
alarm(2);
}
void keycontrol(int sig)
{
if(sig == SIGINT){
puts("CTRL + C pressed");
}
}
这里是测试结果。
看到这个我是有疑问的,按照道理来说,怎么会这么快就结束了???
讲道理不应该是
wait…-> Time out! -> wait ->time out ->wait ->time out ->wait ->time out -> wait ->time
直到300s之后
实际上过程是这样的:
调用主函数中alarm后,进入for循环,打印wait 并进入睡眠阻塞状态,2s后产生 SIGALRM 进入到信号处理器 timeout() 中
在信号处理器函数中,打印time out 并调用alarm函数,并退出。
此时由于之前为了调用信号处理器,唤醒了调用sleep函数进入的阻塞状态的进程,所以不会继续sleep,会进入到第二次for循环中!
然鹅只过了 2s 刚刚的alarm函数产生的SIGALRM信号再次降临。 以此类推
上面的过程是没有进行任何输入的结果,如果输入 ctrl + c 可以看下~
比上面运行的还要快~~,一下就结束了
10.3.3 利用sigaction函数进行信号处理
前面所学足以完成防止僵尸进程生成的代码。
(利用wait函数(有阻塞情况),利用waitpid函数(无阻塞情况),利用signal(SIGINT,XXXX))
这里介绍的sigacion
函数,因为sigaction函数在各个unix系列的操作系统中 都可以兼容,而且现实中应用也更为广泛。
#include <signal.h>
int sigcation(int signo, const struct sigaction* act, struct sigaction* oldact);
-> 成功时返回0, 失败时返回-1
signo: 与signal函数相同,传递信号信息
act: 对应于第一个参数的信号处理函数(信号处理器)信息
oldact:通过此参数,获取之前注册的信号处理函数指针,若不需要则传递0
这里看一下上面函数中的结构体信息 声明并初始化 sigaction结构体变量
struct sigaction
{
void(*sa_hadler)(int);
sigset_t sa_mask;
int sa_flags;
}
sa_handler成员保存信号处理函数的指针值(地址值)。
sa_mask和sa_flags的所有位 均初始化为0即可。
这两个成员用于指定信号相关的选项和特性,而我们的目的主要是防止产生僵尸进程,这里先省略,后面再说
这里给出示例
sigaction.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if(sig == SIGALRM){
puts("Time out!");
}
alarm(2);
}
int main(int argc, char *argv[])
{
/* code */
int i;
struct sigaction act; //声明一个sigaction结构体,作为一会sigaction函数的参数
act.sa_handler = timeout; // 设置结构体中的 处理函数的指针
sigemptyset(&act.sa_mask); // 设置第二个成员为0
act.sa_flags = 0; // 设置第三个成员为0
sigaction(SIGALRM, &act, 0); // 注册SIGALRM 信号的处理器
alarm(2);
for(int i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
下面是测试结果
10.3.4 利用信号处理技术消灭僵尸进程
这个感觉很简单啦,因为子进程结束的时候会产生SIGCHLD信号,我们通过sigaction函数对这个信号设置相应的信号处理器就ok啦!
下面看样例
remove_zombie.c
/*
* 文件名 remove_zombie.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
// 利用sigaction函数,消除子进程结束时的僵尸进程。
// 测试当子进程返回 或 退出两种情况下,是否成为僵尸进程
// 当子进程结束时,会产生 SIGCHLD 信号,因此需要完成对应的 信号处理器函数
// 在这个函数中,需要消除僵尸进程,所以需要利用waitpid函数
void read_childporc(int sig){
puts("进入SIGCHLD函数处理器中...\n");
int status; // 存储很多东西,包括子进程的返回值
pid_t pid = waitpid(-1, &status, WNOHANG); // 等待任何结束的子进程 相关信息存入status中 不阻塞
if(WIFEXITED(status)){ // 如果 子进程正常终止
printf("已清除 pid 为: %d 的子进程\n", pid);
printf("子进程的返回值为:%d \n", WEXITSTATUS(status)); // 打印子进程返回值
}
}
int main(int argc, char *argv[])
{
/* code */
pid_t pid;
struct sigaction act;
// 初始化sigaction结构体变量
act.sa_handler = read_childporc;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
// 注册SIGCHLD信号
sigaction(SIGCHLD, &act, 0);
pid = fork();
if(pid == 0){ // 子进程 1 区域
puts("我是子进程1~,我将在10s后退出~\n");
sleep(10);
return 12;
}
else{ // 主进程 区域
printf("进入主进程子进程 1 的pid为: %d\n", pid);
pid = fork();
if(pid == 0){ // 子进程 2 区域
puts("我是子进程 2 ,我将在10s后退出\n");
sleep(10);
exit(10);
}
else{ // 主进程 区域
printf("进入主进程子进程 2 的pid为: %d\n", pid);
// 主进程将进入睡眠~
for(int i = 0; i < 5;i++){
puts("主进程 wait.....\n");
sleep(5);
}
}
}
return 0;
}
我们来预测一下结果~
首先进入子进程 1 和 主进程区域(T1)
打印:*
我是子进程1 我将在10s后退出
子进程 1 的pid为 xxxxxxx
接着进入子进程 2 和主进程区域(T2)
打印 :
我是子进程 2 我将在10s后退出
子进程2 的pid为 xxxx
主进程 wait…(2次,因为for 5s循环一次,在第二次循环时(T3时刻)被打破阻塞状态 进入信号处理器中)
子进程 1 退出 (T3)(距离T1 10s)
打印:
进入SIGCHLD函数处理器中
已清除pid为xxxxxx的子进程
子进程的返回值为 12(这里是子进程1 的返回值)
主进程继续新的一轮sleep,与此同时子进程 2 结束
打印:
主进程wait…
进入SIGCHLD函数处理器中
已清除pid为xxxxxx的子进程
子进程的返回值为 10(这里是子进程1 的返回值)
然鹅,实际上是这个样子的
进入主进程子进程 1 的pid为: 4097(主进程走的飞快,直接进入第一次sleep中)
进入主进程子进程 2 的pid为: 4098
主进程 wait…(T1)
我是子进程 2 ,我将在10s后退出 (竟然是首先进入了子进程2)
我是子进程1,我将在10s后退出~
主进程 wait…(T2 距离 T1 5s)
主进程 wait…(T3 距离T2 5s)
进入SIGCHLD函数处理器中…(刚刚到达10s 进入第三个for 被唤醒了)
已清除 pid 为: 4098 的子进程
子进程的返回值为:10
主进程 wait… (处理了第一信号处理器,回到main中)
进入SIGCHLD函数处理器中… (又被唤醒了)
已清除 pid 为: 4097 的子进程
子进程的返回值为:12
主进程 wait… (最后一个wait 5s后结束main)
10.4 基于多任务的并发服务器
关键是利用好fork函数和信号处理机制
我们在之前几章不止一次实现了各种回声客户端,这里我们想扩展回声服务器端,使其可以同时向多个客户端提供服务。
下面是基于多进程的并发回声服务器端的实现模型。
逻辑过程:每当有客户端请求服务时,回升服务器端都创建子进程来提供服务。
请求服务的客户端若有5个,这将创建5个子进程提供服务。
为了完成这些任务,需要如下的过程,这也是与之前的区别
- 第一阶段: 回声服务器端(父进程)通过调用accept函数受理连接请求
- 第二阶段: 此时获取的套接字文件描述符,创建并传递给子进程
- 第三阶段: 子进程利用传递来的文件描述符提供服务。
实际上过程是非常简单的,因为子进程会复制父进程的资源,因此根本不用另外经过传递文件描述符的过程。
10.4.1 实现并发服务器
下面是修改之后的多进程回声服务器端的代码,可以结合之前第四章的回声客户端进行配合食用
echo_mpserv.c
// echo_mpserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
if(argc != 2){
printf("usage: %s <port>\n", argv[0]);
exit(1);
}
// 声明套接字相关变量
int serv_sock, client_sock;
struct sockaddr_in serv_addr, client_addr;
socklen_t client_addr_sz;
char buf[BUF_SIZE];
int str_len;
// 声明信号相关变量
struct sigaction act; // 注册信号函数 sigaction的输入参数
int sigaction_state; // 接收注册信号函数 sigaction的 返回值(0 成功/ -1失败)
// 初始化信号变量
act.sa_handler = read_childproc; // 设置一下信号处理函数
act.sa_flags = 0; // 设置其他两个暂时用不上的为0
sigemptyset(&act.sa_mask);
// 注册信号(配合处理 僵尸进程的 信号处理函数)
sigaction_state = sigaction(SIGCHLD, &act, 0);
// 初始化套接字、服务器地址啥的
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1){
error_handling("socket() error");
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
// 服务器套接字一条龙
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
error_handling("bind() error");
}
if(listen(serv_sock, 5) == -1){
error_handling("listen error");
}
// 接收到服务器端信息后,开启子进程,在循环中不停的接收子进程的请求
while(1)
{
client_addr_sz = sizeof(client_addr);
client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_sz);
if(client_sock == -1){
continue; // 接收请求不能停
}
puts("new client connected......");
//下面开启子进程,给这个请求干活~
pid_t pid = fork();
if(pid == -1){ // fork失败返回 -1
puts("fork 失败..正在断开客户端连接...\n");
close(client_sock);
}
else if(pid == 0){ // 子进程 区域
// 子进程为客户端提供服务,因此在子进程中多余的服务器端套接字需要断开
close(serv_sock);
// 子进程中提供 回声 服务。
while((str_len = read(client_sock, buf, BUF_SIZE))!= 0){
write(client_sock, buf, str_len);
}
close(client_sock);
puts("client端断开连接\n");
return 0;
}
else{ // 主进程 区域
// 由于主进程要接收其他客户端的服务需求,因此对于主进程来说,客户端的文件描述符是多余的,这里断开主进程中与客户机的连接
close(client_sock);
}
}
// while外,结束提供服务器的服务
close(serv_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig)
{
int status;
puts("进入信号处理器函数.......\n");
if(sig == SIGCHLD){
pid_t pid = waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status)){ // 子进程正常终止
printf("终止pid:%d\t进程号:%d 的子进程", pid,WEXITSTATUS(status));
}
}
}
客户端的参见下方10.5中
下面是测试结果:
10.4.2 通过fork函数赋值文件描述符
上面的代码我在第一次看到的时候很晕,为什么要一会close这个套接字文件描述符,一会又要close另一个
上方示例 echo_mpserv.c中 通过fork开辟子进程时,复制了所有的文件描述符。
父进程将2个套接字(一个服务器端套接字,一个与客户端连接的套接字)文件描述符复制给了子进程。
在这个过程中,是否将套接字也复制了呢?
套接字并非父进程或是子进程所拥有的,套接字属于操作系统
进程拥有的是代表相应套接字的文件描述符,因此套接字并没有复制
换言之:如果复制了套接字,同一个端口对应了多个套接字,这是不可能存在的。
因此只是复制了文件描述符而已。
当调用了fork函数之后,主进程和子进程同时拥有相同的一套文件描述符,都指向同一套接字。
如下图所示:
由于一个套接字中存在两个文件描述符时,只有两个文件描述符都终止(销毁)后,才能销毁套接字。所以我们不想要上图所示的关系,我们希望
由父进程继续指向服务器套接字,以达到当有新的客户端请求访问的时候,能够顺利accept
由子进程继续指向连接客户端的套接字,以达到对客户端的数据接收以及数据输入。
因此,在调用fork函数后,我们希望能够实现下图所示的逻辑关系
这也就是和为什么我们在上面的代码中,通过在子进程区域和父进程区域分别通过调用close函数关闭相应的文件描述符的目的。
10.5 分割TCP的I/O程序
啥是分割IO?????
10.5.1 分割I/O 是啥 有什么用?
我们上面 使用的回声客户端程序是之前第四章的,其数据回声方式如下:
向服务器端传输数据,并等待服务器端回复。无条件等待,直到接收劢服务器端的回声数据后,才能传输下一批数据。
回声客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len,recv_len,recv_count;
struct sockaddr_in serv_addr;
if(argc != 3){
printf("Usage %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_STREAM,0);
if(sock == -1){
error_handling("socket () error ");
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1){
error_handling("connect () error ");
}
else{
puts("Connected......");
}
while(1)
{
fputs("Input message(Q to quit):", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")){
break;
}
str_len = write(sock,message,strlen(message));
recv_len = 0;
// 这里就是修改的部分
while(recv_len < str_len)
{
recv_cnt = read(sock,&message[recv_len],BUF_SIZE - 1);
if(recv_cnt == -1){
error_handling("read() error!");
}
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
说的的就是这部分啦
while(1)
{
fputs("Input message(Q to quit):", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")){
break;
}
str_len = write(sock,message,strlen(message));
recv_len = 0;
// 这里就是修改的部分
while(recv_len < str_len)
{
recv_cnt = read(sock,&message[recv_len],BUF_SIZE - 1);
if(recv_cnt == -1){
error_handling("read() error!");
}
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s", message);
}
那啥是 IO分割呢?
就是在客户端开个子进程,主进程用来接收数据,子进程用来发送数据。
这个有啥用呢
上面的代码中,重复调用read函数和write函数。 因为只有一个进程,但是如果有俩进程呢,就可以分割数据的收发过程。如下图所示:
分割I/O程序可以让收发数据分开,如果程序越复杂,这样清晰的逻辑结构就越有优势
同时,还可以提高频繁交换数据的程序性能。如下图所示:
左侧是之前的回声客户端数据交换方式,右侧是分割I/O之后的客户端数据传输方式。
服务器端相同,不同的是客户端区域。
分割I/O后的客户端发送数据时不必考虑接收数据的情况,因此可以连续发送数据,由此提高了同一时间内传输的数据量。这种差异在网速较慢时比较明显。
ps:实际上回声客户端没有特殊原因没必要进行io程序分割,这里只是为了做个实验,练习一下。
10.5.2 回声客户端的I/O程序分割
下面是进行分割的回声客户端,可以结合之间的echo_mpserv.c 食用
// echo_mpserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
void read_routine(int sock, char* buf);
void write_routine(int sock, char* buf);
int main(int argc, char *argv[])
{
if(argc != 3){
printf("usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
// 声明套接字相关变量
int sock;
struct sockaddr_in serv_addr;
// socklen_t client_addr_sz;
char buf[BUF_SIZE];
// int str_len;
// 声明信号相关变量
pid_t pid;
// 初始化套接字、服务器地址啥的
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1){
error_handling("socket() error");
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 客户端套接字一条龙
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
error_handling("connect() error");
}
//connect之后,下面开启子进程,切割IO
pid = fork();
if(pid == 0){ // 子进程 区域 完成数据写入工作
write_routine(sock, buf);
}
else{ // 主进程 区域 完成数据读取工作
read_routine(sock, buf);
}
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_routine(int sock , char* buf)
{
while(1)
{
int str_len = read(sock, buf, BUF_SIZE);
if(str_len == 0){
break;
}
buf[str_len] = 0;
printf("Message form server : %s", buf);
}
}
void write_routine(int sock, char* buf)
{
while(1)
{
fgets(buf,BUF_SIZE,stdin);
if( !strcmp(buf,"q\n") || !strcmp(buf,"Q\n") ){
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
有一处问题:要仔细思考~为什么用break呢?