上一节我们讲了客户端的编程(现在还没有,之后加上),客户端编程比较简单,fd就只有一个,但是服务器会连接很多个客户端,所以服务器对客户端发送过来的请求(IO操作),做了一个总结,一共有5种,接下来我们就好好分析分析。
2.1 阻塞式IO模型
最流行的IO模型是阻塞式IO模型,基本所有函数默认都是阻塞的,阻塞式编程是最简单的,也是很多人学习网络编程的时候,都使用的方法。 用户进程调用recvfrom函数的时候,kernel就开始装备数据了,一般数据都不是立刻就能获取到,这个时候kernel会等待数据的到来,而应用层就会把这个线程阻塞,当kernel一直等到数据准备好了,就会把数据从内核态拷贝到用户态,然后才返回结果,这时候线程的阻塞才解除,重新开始运行。
2.1.1 服务器模型
用阻塞式IO实现的服务器模型,大部分都是一请求一应答,如果多个客户顿请求,服务器也接收不到数据,所以这个模型比较少用,对于这个模型,有一种改进的方法:就是使用多线程。
多线程的目的就是让每一个客户端的连接的fd都创建一个线程去处理,阻塞的话也是阻塞新创建的线程,并不会影响主线程,主线程还是不断的要接收客户的消息。
缺点: 如果同时相应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。
可以考虑使用线程池,不过所谓的池也是有上限的,当请求大大超过上限时,池构成的系统对外界的响应并不比没有池的时候效果好多少,所以使用池必须考虑其面临的响应规模,并根据响应规模调整池的大小。
线程池可以缓解部分压力,但是不能解决所有问题,总之,多线程模式可以方便高效解决小规模的服务请求,但是面对大规模的服务请求,多线程模型也会遇到瓶颈。
程序:
//fcntl(listenfd, F_SETFL, O_NONBLOCK);
threadPool_t *pool = threadPool_creat(8, 200, threadpool_eventfd_type);
if(pool == NULL) {
printf("pool is null\n");
return -1;
}
while(1) {
struct sockaddr_in client_addr ;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = {0};
int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
if(clientfd <= 0) continue;
// fcntl(clientfd, F_SETFL, O_NONBLOCK);
threadPool_add(pool, callback, (void*)&clientfd);
usleep(100);
}
这个是伪代码,有关线程池的问题,可以关系另一个专题进程/线程/协程
2.2 非阻塞式IO模型
既然有阻塞IO模型,就有非阻塞IO模型,接下来就说说这个非阻塞IO模型。
我们可以通过一个函数把上面的阻塞函数设置成非阻塞,非阻塞的情况是这样的,在请求IO的线程中,不能把当前线程睡眠,而是立即返回一个错误,直到kernel装备好所有数据,并且把数据从内核态拷贝到应用态,才返回读取到的数据。 前三次调用recvfrom时没有数据返回,因此内核立即返回一个EWOULDBLOCK错误。第四次调用redvfrom时已有有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成返回。
设置非阻塞的函数:
fcntl( fd, F_SETFL, O_NONBLOCK );
2.2.1 服务器模型
这种基本都有实用的服务器模型,如果使用的话,一个线程不断的轮询查看recvfrom数据是否准备好,这样做会耗费大量的CPU时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统才会被使用。
2.3 IO复用模型
IO 复用这个词可能都有点陌生,但是提到select/poll/epoll,大概就都能明白了,有些地方也称这种IO方式为事件驱动IO,我们知道,select/poll/epoll的好处就在于单个线程就可以同时处理多个网络连接的IO,它的基本原理就是select/poll/epoll这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户。
我们使用select的时候,其实整个线程也是阻塞的,只不过这次阻塞是select阻塞,不是recvfrom函数阻塞,select阻塞期间,kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回,这个模型的只要优势,在于一个线程可以监控多个fd。
2.3.1 服务器模型
这个IO模型用的地方还是比较多的,一个线程负责监控很多个fd,所以需要好好掌握。
程序:
// select
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
int i = 0;
int max_fd = listenfd;
rset = rfds;
while(1) {
printf("aagag %d\n", max_fd);
int nready = select(max_fd+1, &rset, NULL, NULL, NULL);
if(nready <= 0) continue;
if(FD_ISSET(listenfd, &rset)) {
struct sockaddr_in client_addr ;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len;
int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
if(clientfd <= 0) continue;
FD_SET(clientfd, &rset);
printf("clientfd %d\n", clientfd);
if(clientfd > max_fd) max_fd = clientfd;
if(--nready == 0) continue;
}
for(i = listenfd+1; i<=max_fd; i++) {
if(FD_ISSET(i, &rset))
{
char buffer[1024] = {0};
if( recv(i, buffer, 1024, 0) > 0 ) {
printf("client fd %d %s\n", i, buffer);
} else { // 这个是连接断开了
printf("diconnect %d\n", i);
FD_CLR(i, &rset);
close(i);
}
if(--nready == 0) break;
}
}
}
这段程序是select模型的
int efd = epoll_create(1);
if(efd <= 0) return -1;
struct epoll_event ev, events[1024];
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev);
while(1) {
int nready = epoll_wait(efd, events, 1024, -1);
if(nready <=0 ) continue;
for(int i=0; i<nready; i++) {
if(events[i].data.fd == listenfd) {
struct sockaddr_in client_addr ;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len;
int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_len);
if(clientfd <= 0) continue;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
epoll_ctl(efd, EPOLL_CTL_ADD, clientfd, &ev);
printf("clientfd %d\n", clientfd);
if(--nready == 0) continue;
} else {
int clientfd = events[i].data.fd;
char buffer[1024] = {0};
if( recv(clientfd, buffer, 1024, 0) > 0 ) {
printf("client fd %d %s\n", clientfd, buffer);
} else { // 这个是连接断开了
printf("diconnect %d\n", clientfd);
epoll_ctl(efd, EPOLL_CTL_DEL, clientfd, NULL);
close(clientfd);
}
if(--nready == 0) break;
}
}
}
这段程序是epoll的
2.3.2 select和epoll比较
select: 优点: select模型事件驱动模型只用单线程执行,占用资源少,不消耗太多CPU,同时能够为客户端提供服务 缺点: 当需要探测的句柄值较大时,select()接口本身需要消耗大量事件去轮询各个句柄。 改模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的,很大程度降低了事件探测的及时性
改进: 有很多高效的事件驱动库可以屏蔽上述困难,有libevent、libev
找到了一篇关系这个介绍很详细的文章,大家可以看看: select、poll、epoll之间的区别(搜狗面试)
下面是我总结的思维导图:
2.4 信号驱动IO模型
我们可以直接用信号,让内核处理完成了之后,通过SIGIO信号通知我们,这种模型就是信号驱动IO。
首先开启套接字的信号驱动式IO,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即返回,我们的进程继续工作,也就是我们进程没有被阻塞,也不用去轮询检测,当数据报准备好读取时,内核自己产生一个SIGIO的信号。我们可以在信号处理函数中接收数据,并通知主线程读取数据。
这个模型的好处就是不用等待数据和检测数据,只要等待来自信号处理函数的通知,然后去读取数据即可。 (服务器模型以后再补)
2.5 异步IO模型
linux下的异步IO用在磁盘IO读写操作,不用于网络IO,从内核2.6版本才开始引入: 用户进程发起read操作之后,立刻就可以开始去做其他的事了。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
信号驱动IO和异步IO的区别: 信号驱动IO:内核通知我们可以去操作一个IO操作,需要进程去读取数据。 异步IO:由内核通知我们IO操作何时完成,直接拷贝到用户内存中。
2.6 总结:
同步IO操作:导致请求进程阻塞,直到IO操作完成 异步IO操作:不导致请求进程阻塞
同步IO:阻塞式IO模型、非阻塞式IO模型、IO复用模型和信号驱动IO模型。
异步IO:异步IO模型。