Part 1 start network programming:chapter ten:多进程服务器端

第十章 多进程服务器端

10.1 进程概念及应用

我们在收看视频的时候,无法忍受等待很久很久,因此服务器端的设计必须满足,当有多个客户端同时访问时,不能让大家按照顺序进行排队,这样叫什么服务?

多进程就是解决这个问题的方法。

10.1.1 并发服务器端的实现方法

网络程序中 数据通信时间比CPU运算时间更多,因此,向多个客户端提供服务是一种有效利用CPU的方式。

下面列出集中具有代表性的并发服务器端的实现模型和方法。

  1. 多进程服务器端:通过创建多个进程提供服务。
  2. 多路复用服务器:通过捆绑并统一管理I/O对象提供服务
  3. 通过生成与客户端等量的线程提供服务。

本章主要讲的是第一种方法。

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 僵尸进程产生原因

是什时候是一个进程结束呢?

一般我们认为

  1. 传递参数并调用了 exit函数
  2. 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指向的内存中)中还包含其他信息,因此需要通过下列宏进行分离。

  1. WIFEXITED 子进程正常终止时返回“真”(true);
  2. 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个子进程提供服务。
为了完成这些任务,需要如下的过程,这也是与之前的区别

  1. 第一阶段: 回声服务器端(父进程)通过调用accept函数受理连接请求
  2. 第二阶段: 此时获取的套接字文件描述符,创建并传递给子进程
  3. 第三阶段: 子进程利用传递来的文件描述符提供服务。

实际上过程是非常简单的,因为子进程会复制父进程的资源,因此根本不用另外经过传递文件描述符的过程。

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呢?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值