高性能服务器编程----多进程与多线程

看过我之前的TCP编程的那个博客大家可以看到,我一个客户端没有和服务器断开时,其他的客户端不能与客户端进行交互(相当于串行的交互模式,效率很低),因为服务器获取其他连接的accept函数被前一个客户端的交互(recv)阻塞住了,那么有什么办法可以使得我们服务器在同一时刻可以连接多个客户端呢?

给大家介绍两种方法,但是原理是一样的。我以线程举例,我们需要用一个线程来获取客户端的连接,一旦有一个客户端成功连接上,我们就需要给他分配一个线程使他和服务器进行交互(每一个客户端分配一个“保姆”),一旦你这个线程被分给某个客户端进行交互,无论有没有事件发生,这个线程都得一直为其服务,直到这个客户端主动断开,那么我们这个线程也就可以释放了。这样就实现了同一时刻一个服务器可以和多个客户端进行交互(并发交互)

多线程实现代码示例:

服务器

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
#include<pthread.h>
void* fun(void *p)
{
	int c=(int)p;
	while(1)
	{
		char buff[128]={0};
		int n=recv(c,buff,127,0);
		if(n<=0)
		{
			close(c);    //一旦close就断了服务器与客户端的连接,因为文件描述父是在进程的PCB中,线程共享,所以主线程不能关闭
			printf("%d over\n",c);
			break;
		}
		printf("%d:%s\n",c,buff);
	}
}
int main()
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd!=-1);

	struct sockaddr_in ser,cli;
	ser.sin_family=AF_INET;
	ser.sin_port=htons(6000);
	ser.sin_addr.s_addr=inet_addr("127.0.0.1");

	int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
	assert(res!=-1);

	listen(sockfd,5);

	while(1) //主线程不退出
	{
		int len=sizeof(cli);
		int c=accept(sockfd,(struct sockaddr*)&cli,&len); 
		if(c<0)  //c=0是可以的
		{
			printf("link error\n");
			continue;
		}
		pthread_t t;
		int n=pthread_create(&t,NULL,fun,(void*)c);  //不能使用地址传递,有可能本次地址值被主线程更改
		assert(n==0);
		//close(c);  主线程不能close否则就断开了服务器与客户端连接
		//产生一个问题就是连接一个少一个文件描述符,不能大量连接	
	}
	close(sockfd);
}

运行结果:

可以看出文件描述符sockfd是递增的(一个进程)

多线程有什么需要注意的?

1:主线程负责接收客户连接,函数线程负责处理客户连接

2:主线程通过创建线程的时候将客户连接的文件描述符sockfd通过参数传递给函数线程,但是传递文件描述符的时候只能以值传递,而不能使用地址传递,因为是多线程环境,没有做同步控制,有可能函数线程刚拿到传给他的文件描述符地址,然后主线程再次accept获取同样变量的sockfd,将前一次的sockfd覆盖掉,那么上一次函数线程拿到的sockfd就无效了,也就不能通讯了。

3:主线程中不需要关闭文件描述符,因为在同一个进程中(文件描述符是在PCB中的struct file*数组的下标,是独一份的),所以任何一个线程关闭文件描述符就直接断开服务器与客户端交互的通道了(同一进程线程共享进程资源,除了独立的栈区)

多进程实现代码示例:

编程思想:父进程完成于客户端的连接工作,完成后创建子进程,子进程与客户端具体交互。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
//不能通过文件描述符来判断是否是同一个客户端连接,需要用客户端的ip和端口(地址对)来标识唯一的一个客户端,用netstat -natp查看
void CommClient(int c)   //与客户端交互的函数
{
	while(1)
	{
		char buff[128]={0};
		int n=recv(c,buff,127,0);
		if(n<=0)
		{
			close(c);  //只是关闭了子进程文件描述符的服务器连接
			printf("%d over\n",c);
			break;
		}
		printf("%d:%s\n",c,buff);
	}
}
void Zombie(int sign)  //处理僵尸进程
{
	wait(NULL);
}
int main()
{
	signal(SIGCHLD,Zombie);
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd!=-1);

	struct sockaddr_in ser,cli;
	ser.sin_family=AF_INET;
	ser.sin_port=htons(6000);
	ser.sin_addr.s_addr=inet_addr("127.0.0.1");

	int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
	assert(res!=-1);

	listen(sockfd,5);

	while(1)
	{
		int len=sizeof(cli);
		int c=accept(sockfd,(struct sockaddr*)&cli,&len); //c作为文件描述符传给子进程的,所以父子进程连接的客户端是一样的
		if(c<0)  //c=0是可以的
		{
			printf("link error\n");
			continue;
		}
		pid_t n=fork();
		assert(n!=-1);
		if(n==0)
		{
			CommClient(c);
			exit(0);  //结束子进程,避免子进程出去while循环进行accept获取,但是PCB依旧没释放及避免子进程生成孙子进程
		}
		else  //父进程不结束
		{
			close(c);   //仅仅关闭父进程关闭文件描述符,并未关闭连接,只是计数器减1(计数器为0才会断开与客户端的连接)
		}
	}
	close(sockfd);
}

运行结果: 

可以看出多进程下,我们主线程获取连接就一直用的是4下标的文件描述符(多进程用各自PCB的,所以就可能相同)

多进程有什么需要注意的?

1:主进程负责接收客户连接,子进程负责处理客户连接

2:各个进程之间是独立的文件描述符,所以主进程在fork之前的accept获取到文件描述符后,在子进程中关闭这个获取到的文件描述符,因为子进程已经获取到相同的文件描述符了,两个文件描述符所指向的是同一个struct file。为了不占用连接资源(主进程的文件描述符),我们需要在子进程获取到sockfd后就在父进程中关闭相应的sockfd(对于这种后期不适用的资源一定要及时释放),而且不像多线程那样,任何一个线程关闭sockfd就会断开与客户端的连接,在多进程中即便在父进程中关闭sockfd,这个与客户端的连接不会断开,只是struct file中的引用计数减1,直到子进程将其减为0才会真实的断开。

3:多进程环境需要处理僵尸进程(修改信号的相应方式)

多线程与多进程比较?

对于选择多线程还是多进程编程,我们需要根据不同场景不同的业务需求来选择。

<1>从编程角度:多线程代码实现简单一些,控制简单一些

<2>从占据资源:多进程开销要比多线程大

<3>切换角度:线程切换要比进程快

<4>资源共享:线程比进程间共享资源多(堆,全局,文件等),因为共享资源多,线程就存在不安全性,需要进行同步控制

<5>从能够创建的数量:多进程要比多线程多很多,因为线程与栈区有关,一个进程中能分配的栈是有限的。一个进程打开线程大约300来个,但是一个系统上打开进程数量1024个,而且这个值可以修改。

猜你喜欢

转载自blog.csdn.net/Eunice_fan1207/article/details/84499809