网编(9):多进程服务器端

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/88970594

井发服务器端的实现方法

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

创建进程:

#include <unistd.h>
pid_t fork(void);
//成功时返回进程ID, 失败时返回-1。

销毁僵尸进程1 : 利用wait 函数

#include <sys/wait.h>
pid_t wait(int * statloc);
//成功时返回终止的子进程ID, 失败时返回-1 。

实例代码:

#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();

	if(pid==0)
	{
		return 3;//第一个子进程退出
	}
	else
	{
		printf("Child PID: %d \n", pid);
		pid=fork();
		if(pid==0)
		{
			exit(7);//第二个子进程退出
		}
		else
		{
			printf("Child PID: %d \n", pid);
			wait(&status);
			if(WIFEXITED(status))
				printf("Child send one: %d \n", WEXITSTATUS(status));
			
			wait(&status);
			if(WIFEXITED(status))
				printf("Child send two : %d \n" , WEXITSTATUS(status));
			sleep(5); // Sleep 30 sec.
		}
	}

    return 0;
}

运行结果:

$ ./a.out 
Child PID: 27242 
Child PID: 27243 
Child send one: 3 
Child send two : 7

销毁僵尸进程2: 使用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并退出函数。

实例代码:

#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();

	if(pid==0)
	{
		sleep(9) ;
		return 24;
	}
	else
	{
		while(!waitpid(-1, &status, WNOHANG))
		{
			sleep(3);
			puts("sleep 3sec.");
		}

		if(WIFEXITED(status))
		{
			printf("Child send %d \n", WEXITSTATUS(status));
		}
	}
	return 0;
}

运行结果:

$ ./a.out 
sleep 3sec.
sleep 3sec.
sleep 3sec.
Child send 24 

————————————————————————————————————————————

信号处理

函数原型:signal

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

函数传入值
signum: 指定信号代码
handler:

  1. SIG_IGN: 忽略该信号
  2. SIG_DFL: 采用系统默认方式处理信号
  3. 自定义的信号处理函数指针

函数返回值
成功: 以前的信号处理配置
出错: 1

函数原型:sinaction

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

函数传入值
signum: 信号代码, 可以为除 SIGKILL 及 SIGSTOP 外的任何一个特定有效的信号
act:       指向结构 sigaction 的一个实例的指针, 指定对特定信号的处理
oldact:  保存原来对相应信号的处理

函数返回值
成功: 0
出错: 1

首先给出了 sigaction 的定义, 代码如下:

struct sigaction {
               void     (*sa_handler)(int);//函数指针
               void     (*sa_sigaction)(int, siginfo_t *, void *);//带参数的函数指针
               sigset_t    sa_mask;//信号屏蔽集
               int        sa_flags;//标志位
               void     (*sa_restorer)(void);//现在已经不使用了
           };

利用信号处理技术消灭僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void my_function(int arg)
{
	int status;
	pid_t id=waitpid(-1, &status, WNOHANG);
	if(WIFEXITED(status))
	{
		printf("Removed proc id: %d \n", id);
		printf ("Child send: %d \n", WEXITSTATUS(status));
	}
}
int main(void)
{
	pid_t pid;
	int stat;
	int i;
	struct sigaction new;
	struct sigaction old;
	
	new.sa_handler = my_function;
	new.sa_flags = 0;
	sigemptyset(&new.sa_mask);
	
	sigaction(SIGCHLD, &new, NULL);
	
	pid=fork();
	if(pid == 0)
	{
		sleep(2);
		return 1;
	}
	else
	{
		printf("Child(1) : proc id: %d \n", pid);
		
		pid=fork();
		if(pid == 0)
		{
			sleep(4);
			exit(2);
		}
		else
		{
			printf("Child(2) : proc id: %d \n", pid);
			for(i=0; i<5; i++)
			{
				puts("wait ... ");
				sleep(5);
			}
			/* linux信号会终止休眠,所以要用上面的多次休眠
			sleep(10);
			printf("aaaaaaaa\n");*/
		}
	}

	exit(0);
}

运行结果:

$ ./a.out 
Child(1) : proc id: 28237 
Child(2) : proc id: 28238 
wait ... 
Removed proc id: 28237 
Child send: 1 
wait ... 
Removed proc id: 28238 
Child send: 2 
wait ... 
wait ... 
wait ... 

基于多任务的井发服务器

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

实例代码:

#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[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;

	pid_t pid;
	struct sigaction act;
	socklen_t adr_sz;
	int str_len, state;
	char buf[BUF_SIZE];
	if(argc !=2 ) {
		printf ("Usage : %s <port>\n", argv[0]) ;
		exit(1);
	}
	//定义信号
	act.sa_handler=read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	state=sigaction(SIGCHLD, &act, 0);
	
	//设置sock
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));

	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	while(1)
	{
		adr_sz=sizeof(clnt_adr);
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
		if(clnt_sock==-1 )
			continue;
		else
			puts("new client connected .. . ");
		pid=fork();
		if(pid==-1)
		{
			close(clnt_sock);//子进程关闭服务器描述符
			continue;
		}
		if(pid==0) /*子进程运行区域*/
		{
			close(serv_sock);
			while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)
				write(clnt_sock, buf, str_len);

			close(clnt_sock);
			puts("client disconnected ... ");
			return 0;
		}
		else
			close(clnt_sock);//父进程关闭客户端描述符
	}
	close(serv_sock);
	return 0;
}

void read_childproc(int sig)
{
	pid_t pid;
	int status;
	pid=waitpid(-1, &status, WNOHANG);
	printf("removed proc id : %d \n", pid);
}

void error_handling(char * message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

运行结果:

$ ./a.out 9190
new client connected .. . 
client disconnected ... 
removed proc id : 28460 
new client connected .. . 
client disconnected ... 
removed proc id : 28462 

• 第29~32行为防止产生僵尸进程而编写的代码。
• 第47 、52行第47行调用accept 函数后,在第52行调用fork 函数。因此,父子进程分别带有1 个第47行生成的套接字(受理客户端连接请求时创建的)文件描述符。
• 第58~66行子进程运行的区域。此部分向客户端提供回声服务。第60行关闭第33行创建的服务器套接字,这是因为服务器套接字文件描述符同样也传递到子进程。关于这—点稍后将单独讨论。
• 第69行第47 行中通过accept函数创建的套接字文件描述符己复制给子进程,因此服务器端需要销毁自己拥有的文件描述符。关于这一点稍后将单独说明。

——————————————————————————————————————

通过fork 函数复制文件描述符

示例中给出了通过fork函数复制文件描述符的过程。父进程将2个套接字(一个是服务器端套接字,另一个是与客户端连接的套接字)文件描述符复制给子进程。
“只复制文件描述符吗?是否也复制了套接宇呢?”
文件描述符的实际复制多少有些难以理解。调用fork函数时复制父进程的所有资源, 有些人可能认为也会同时复制套接字。但套接字并非进程所有————从严格意义上说,套接字属于操作系统————只是进程拥有代表相应套接字的文件描述符。也不一定非要这样理解,仅因为如下原因,复制套接字也并不合理。
“复制套接字后,同一端口将对应多个套接宇。”
示例中的fork函数调用过程如图所示。调用fork 函数后, 2个文件描述符指向同一套接字。

如图所示, 1 个套接字中存在2个文件描述符时,只有2个文件描述符都终止(销毁)后,才能销毁套接字。如果维持图中的连接状态,即使子进程销毁了与客户端连接的套接字文件描述符,也无法完全销毁套接字(服务器端套接字同样如此)。因此,调用fork 函数后, 要将无关的套接字文件描述符关掉,如图所示。

————————————————————————————————————————————————

分割客户端的I/O程序

分割I/O程序的优点
我们已经实现的同声客户端的数据回声方式如下:

“向服务器端传输数据,并等持服务器端回复。无条件等待,直到接收完服务器端的回声数据后,才能传输下一批数据。”

传输数据后需要等待服务器端返回的数据,因为程序代码中重复调用了read和write函数。只能这么写的原因之一是, 程序在1 个进程中运行。但现在可以创建多个进程,因此可以分割数据收发过程。默认的分割模型如图所示。

从下图可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样. 无论客户端是否从服务器端接收完数据都可以进行传输。

选择这种实现方式的原因有很多,但最重要的一点是,程序的实现更加简单。也许有人质疑:
既然多产生1个进程,怎么能简化程序实现呢?其实, 按照这种实现方式,父进程中只需编写接收数据的代码, 子进程中只需编写发送数据的代码,所以会简化。实际上,在1个进程内同时实现数据收发逻辑需要考虑更多细节。程序越复杂,这种区别越明显,它也是公认的优点。

分割I/O程序的另一个好处是,可以提高频繁交换数据的程序性能。

上图左侧演示的是之前的回声客户端数据交换方式,右侧演示的是分割VO后的客户端数据传输方式。服务器端相同,不同的是客户端区域。分割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 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[])
{
	int sock;
	pid_t pid;
	char buf[BUF_SIZE];
	struct sockaddr_in serv_adr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));

	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling(" connect() error I");

	pid=fork();
	if(pid==0)
		write_routine(sock, buf);//只负责写
	else
		read_routine(sock, buf) ;//只负责读
	close(sock);
	return 0;
}

void read_routine(int sock, char *buf)
{
	while(1)
	{
		int str_len=read(sock, buf, BUF_SIZE);
		if(str_len==0 )
		return;

		buf[str_len]=0;
		printf("Message from 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));
	}
}
void error_handling(char * message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/88970594