Linux并发服务器的设计模式

循环(单线程且单进程)模式

相当于短链接,当accept之后,就开始数据的接收和数据的发送,一次只能接收一个client,处理完后,处理下一个连接,不存在并发。

listen(serverfd, listen_num);
while(1)
{
	connfd = accept(serverfd);
	read(connfd);
	write(connfd);
	close(connfd);
}

多进程模式

客户端连接到服务器后,每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个子进程来处理。

listen(serverfd, listen_num);
while(1)
{
	connfd = accept(serverfd);
	if ((child_pid = frok()) == 0) {
		close(serverfd);
		read(connfd);
		write(connfd);
		close(connfd);		
	}
	close(connfd);
}

 

多线程模式

多进程服务器是对多进程的服务器的改进,由于多进程服务器在创建进程时要消耗较大的系统资源,所以用线程来取代进程,这样服务处理程序可以较快的创建。

优点:

  • 消耗资源少
  • 不用频繁切换进程

缺点:

  • 资源的互斥随着线程数量增加,变得复杂

如果用户迟迟不进行数据通信,而服务器还是继续等待客户端,这样就浪费了CPU资源,解决办法就是用IO多路复用。

就像下面的情景:

当客人点菜的时候,服务员就可以去招呼其他客人了,等客人点好了菜,直接招呼一声“服务员”,马上就有个服务员过去服务。

参考:使用多线程处理高并发的弊端

listen(serverfd, listen_num);
while(1)
{
	connfd = accept(serverfd);
	pthread_create(&tid, NULL, &doit,  (void *)connfd);
}
void *doit(void *arg)
{
	pthread_detach(pthread_self());
	read(connfd);
	write(connfd);
	close(connfd);
}

I/O多路复用

I/O是为了解决线程/进程阻塞在那个I/O调用中,常用select或者pool

Linux下大规模的TCP并发:

当前并发还有其它的方式。比如线程池。进程池等,每种模式都有他的优缺点,如果大规模的并发,采用epoll会更好。

epoll的时间设置有边缘触发方式和水平触发方式

  1. 水平触发方式:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知。允许在任意时候重复检测IO的状态,没有必要每次描述符就绪后尽可能多的执行IO,select,poll就属于水平触发事件。只要满足要求就触发一个事件。
  2. 边缘触发方式:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知。在收到一个IO事件通知尽可能多的执行IO操作,因为如果再一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取就绪的描述符。信号驱动式IO就属于边缘触发。每当状态改变就触发一个事件。

可以参考:

I/O多路复用总结

select

while(1)
{
	select(fd_max+1, &cpy_reads, 0, 0, &timeout))== -1)

	for(i=0; i<fd_max+1; i++)
	{
		if(FD_ISSET(i, &cpy_reads))
		{
			if(i==serv_sock)// connection request!
			{		
				adr_sz=sizeof(clnt_adr);
				clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				FD_SET(clnt_sock, &reads);
				if(fd_max<clnt_sock)
					fd_max=clnt_sock;
				printf("connected client: %d\n", clnt_sock);
 
			}else{//read message!
				str_len=read(i, buf, BUF_SIZE);
				if(str_len==0) // close request!
				{
					FD_CLR(i, &reads);
					close(i);
					printf("closed client: %d \n", i);
				}else{
					write(i , buf, str_len); // echo!
				}
			}
		}
	}
}

epoll

while(1)
{
	event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
	for(i=0; i<event_cnt; i++ )
	{
		if(ep_events[i].data.fd==serv_sock)
		{
			dr_sz=sizeof(clnt_adr);
			clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
			event.events=EPOLLIN;
			event.data.fd=clnt_sock;
				
			//将连接到服务器的文件描述符加入epoll中
			epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
		}
		else
		{
			str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
			if (str_len==0)//close request!
			{
				epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
				close(ep_events[i].data.fd);
			}
			else{
				buf[str_len] = '\0';
				write(ep_events[i].data.fd, buf, str_len); // echo!
				memset(buf, 0, sizeof(buf));			
			}
		}
	}
}

先看:

再看下面的:

为什么要使用线程池?

操作系统创建线程、切换线程状态、终结线程都要进行CPU调度——这是一个耗费时间和系统资源的事情。

大多数实际场景中是这样的:处理某一次请求的时间是非常短暂的,但是请求数量是巨大的。这种技术背景下,如果我们为每一个请求都单独创建一个线程,那么物理机的所有资源基本上都被操作系统创建线程、切换线程状态、销毁线程这些操作所占用,用于业务请求处理的资源反而减少了。所以最理想的处理方式是,将处理请求的线程数量控制在一个范围,既保证后续的请求不会等待太长时间,又保证物理机将足够的资源用于请求处理本身。

另外,一些操作系统是有最大线程数量限制的。当运行的线程数量逼近这个值的时候,操作系统会变得不稳定。这也是我们要限制线程数量的原因。

线程池

进程池

猜你喜欢

转载自blog.csdn.net/QQ2558030393/article/details/91360750