十 多进程服务器端

**

主要讲解多进程的具体实现

**:


10.1 进程概念以及应用


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

有几种具有代表性的并发服务器端的实现模型和方法:
1 多进程服务器端;通过创建多个进程提供服务
2 多路复用服务器:通过捆绑并统一管理I/O对象提供服务
3 通过生成与客户端等量的线程提供服务

本文主要是讲解第一种模型


10.1.2
进程:占用内存空间的正在运行的程序

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.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();
	
	//根据pid的不同执行不同的程序
	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;
}
在这里插入代码片

10.2 进程和僵尸进程

10.2.1 僵尸进程
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源(百度百科)

10.2.2 僵尸进程产生的原因

一个进程结束的标识
1 传递了参数并调用了exit函数
2 main函数中执行了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;
}
在这里插入代码片

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;
}

在这里插入代码片

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;
}
在这里插入代码片

10.3 信号处理
我们已经知道 如何创建进程,以及如何 彻底销毁一个进程。还个问题:
“子进程什么时间结束呢??难道要一直调用waitpid检测么?”
父进程也很忙啊,不能一直等着它啊= - =
这里讨论一下解决方案


10.3.1 操作系统的助力
子进程终止的识别主体是操作系统,如果OS能告诉父进程:你的子进程结束了、这样就可以节省很多时间。
因此OS引入 信号处理机制signal handing: 这里的信号是当特定事件发生时,由OS向进程发送的消息。
另外为了响应该消息,执行与消息相关的自定义操作的过程叫 处理或信号处理


10.3.2 信号与signal函数
进程:“操作系统,如果我之前创建的子进程终止,就帮我调用xxxxxx函数!”
操作系统:“好的! 您把xxxxxx函数编好吧!我绝对完成任务。”
上面是”注册信号“的过程,进程发现自己的子进程结束,请求OS调用特定函数,请求通过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 Timeout后就结束了:
在这里插入图片描述
按照道理来说,怎么会这么快就结束了???
讲道理不应该是
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信号再次降临。 以此类推


10.3.3 利用sigaction函数进行信号处理

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这个函数对这个信号设置相应的信号处理器即可
引入示例:
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 的返回值)


事实上 是这样滴:

nice-feng@nicefeng-Lenovo:~/Desktop$ ./remove 
进入主进程子进程 1 的pid为: 6926
我是子进程1~,我将在10s后退出~

进入主进程子进程 2 的pid为: 6927
主进程 wait.....

我是子进程 2 ,我将在10s后退出

主进程 wait.....

进入SIGCHLD函数处理器中...

已清除 pid 为: 6926 的子进程
子进程的返回值为:12 
主进程 wait.....

进入SIGCHLD函数处理器中...

已清除 pid 为: 6927 的子进程
子进程的返回值为:10 
主进程 wait.....

主进程 wait.....

nice-feng@nicefeng-Lenovo:~/Desktop$ ^C

在这里插入代码片

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.4.2 通过fork函数赋值文件描述符
套接字并非父进程或子进程所拥有的,套接字属于操作系统
进程拥有的是代表相应套接字的文件描述符,因此套接字并没有复制
换言之:如果复制了套接字,同一个端口对应了多个套接字,这是不可能存在的,因此只是复制了文件描述符而已

当调用了fork函数后,主进程和子进程同时拥有相同的一套文件描述符,指向的是同一套套接字
如下图:
在这里插入图片描述由于一个套接字中存在两个文件描述符时,只有两个文件描述符都终止(销毁)后,才能销毁套接字。所以我们不想要上图所示的关系,我们希望由父进程继续指向服务器套接字,以达到当有新的客户端请求访问的时候,能够顺利accept由子进程继续指向连接客户端的套接字,以达到对客户端的数据接收以及数据输入。

因此,在调用fork函数后,我们希望能够实现下图所示的逻辑关系
在这里插入图片描述这也就是和为什么我们在上面的代码中,通过在子进程区域和父进程区域分别通过调用close函数关闭相应的文件描述符的目的


10.5 分割TCP的I/O程序

10.5.1 分割I/O是啥?有什么用??
引入回升客户端的d代码来理解这个概念:

#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的含义:
在客户端开个子进程 主进程用来接收数据 子进程用来发送数据

分割IO后,用俩进程来分割数据的收发过过程(父进程 读 ;子进程 写);分割IO程序后可以数据的收发分离开来,当程序十分复杂的时候,程序的的逻辑结构显得越清晰。
在这里插入图片描述同时分割IO还可以提高频繁交换数据的程序性能。如下图:
在这里插入图片描述左侧是之前的回声客户端数据交换方式,右侧是分割I/O之后的客户端数据传输方式。
服务器端相同,不同的是客户端区域。
分割I/O后的客户端发送数据时不必考虑接收数据的情况,因此可以连续发送数据,由此提高了同一时间内传输的数据量。这种差异在网速较慢时比较明显。


10.5.2 回声客户端的IO程序分割

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));
	}
}
在这里插入代码片
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.liang呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值