循环(单线程且单进程)模式
相当于短链接,当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的时间设置有边缘触发方式和水平触发方式
- 水平触发方式:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知。允许在任意时候重复检测IO的状态,没有必要每次描述符就绪后尽可能多的执行IO,select,poll就属于水平触发事件。只要满足要求就触发一个事件。
- 边缘触发方式:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知。在收到一个IO事件通知尽可能多的执行IO操作,因为如果再一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取就绪的描述符。信号驱动式IO就属于边缘触发。每当状态改变就触发一个事件。
可以参考:
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调度——这是一个耗费时间和系统资源的事情。
大多数实际场景中是这样的:处理某一次请求的时间是非常短暂的,但是请求数量是巨大的。这种技术背景下,如果我们为每一个请求都单独创建一个线程,那么物理机的所有资源基本上都被操作系统创建线程、切换线程状态、销毁线程这些操作所占用,用于业务请求处理的资源反而减少了。所以最理想的处理方式是,将处理请求的线程数量控制在一个范围,既保证后续的请求不会等待太长时间,又保证物理机将足够的资源用于请求处理本身。
另外,一些操作系统是有最大线程数量限制的。当运行的线程数量逼近这个值的时候,操作系统会变得不稳定。这也是我们要限制线程数量的原因。
线程池
进程池