文章目录
- 前言
- 一、什么是socket缓冲区
- 二、阻塞与非阻塞内核缓冲区
- 1、如果发送缓冲区满了会怎么样
- 阻塞
- 非阻塞
- 2、如果接受缓冲区为空会怎么样
- 阻塞
- 非阻塞
- 三、epoll与缓冲区的恩恩怨怨
- 水平触发
- 边缘触发
- 非阻塞
- 阻塞
- 结论
前言
本文深挖网络编程中的缓冲区,从什么是缓冲区出发,然后分析epoll 两个模式使用阻塞与非阻塞缓冲区的区别,
一、什么是socket缓冲区
TCP三次握手成功,TCP连接成功建立后,操作系统内核会为每个连接创建配套的基础设施,包括 发送缓冲区,其大小可通过套接字选项改变。程序调用write函数时,实际所做的事是把数据从应用程序中拷贝到操作系统内核中。既然是写给操作系统,那操作系统就需要提供一个地方给用户写。同理,接收消息也是一样。这个地方就是 socket 缓冲区。
也就是说一个socket ,会带有两个缓冲区,一个用于发送,一个用于接收。因为这是个先进先出的结构,有时候也叫它们发送、接收队列。
二、阻塞与非阻塞内核缓冲区
使用TCP建立连接之后,一般会使用 send 发送数据。执行 send 之后,数据只是拷贝到了socket 缓冲区。至 什么时候会发数据,发多少数据,全听操作系统安排。根据实际情况(比如拥塞窗口等)判断是否要发数据。如果不发送数据,那么此时直接返回。
阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。当你去写一个阻塞的文件描述符时,如果在该文件描述符上没有空间(通常是缓冲区)可写,那么它会一直阻塞,直到有空间可写。以上的读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据,还包括接收连接accept(),发起连接connect()等操作…
非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样,卡在那里不动!!
1、如果发送缓冲区满了会怎么样
发送缓冲区有足够的空间,可以用于拷贝待发送数据。如果发送缓冲区空间不足,或者满了,执行发送,会怎么样?这里分两种情况。
阻塞
如果此时 socket 是阻塞的,那么程序会在那干等、死等,直到释放出新的缓存空间,就继续把数据拷进去,然后返回。
非阻塞
如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息,意思是 Try again , 现在缓冲区满了,你也别等了,待会再试一次。
2、如果接受缓冲区为空会怎么样
阻塞
如果此时 socket 是阻塞的,那么程序会在那干等,直到接收缓冲区有数据,就会把数据从接收缓冲区拷贝到用户缓冲区,然后返回。
非阻塞
如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息。
三、epoll与缓冲区的恩恩怨怨
我们都知道epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!
首先明确read函数以及recv函数分别在阻塞和非阻塞状态的时候他们对缓冲区有没有数据的反应
ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
在非阻塞状态下:
1、如果没有数据可读,并且没有发生错误,recv() 返回值为 -1,同时 errno 被设置为 EAGAIN 或 EWOULDBLOCK。这表示当前没有可读数据,并且需要稍后再次尝试读取。
2、如果连接已被关闭(对方已关闭连接),recv() 返回值为 0,表示读取到的数据长度为 0,这意味着连接已经结束。
3、如果发生其他错误,recv() 返回值为 -1,并且 errno 被设置为相应的错误代码,例如 ECONNRESET 表示连接被重置,ETIMEDOUT 表示连接超时等。
在阻塞的状态下
如果是阻塞模式,read() 函数或 recv() 函数在没有数据可读时会一直阻塞,直到有数据可读或者出现错误。在此期间,程序会一直等待,并且不会执行其他操作。如果连接被关闭或者发生错误,read() 或 recv() 函数会立即返回相应的错误代码
水平触发
在水平触发的情况下,设置非阻塞与阻塞他们的效果都是一样的。不过为了防止特殊情况,还是建议设置非阻塞。
个人认为,因为在水平触发的情况下,缓冲区有数据就会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写。那么水平触发就保证每次缓冲区都是有数据的,就不会出现阻塞connfd遇到缓冲区为空的情况了。
/* LT处理流程 */
void epoll_lt(int sockfd){
char buffer[MAX_BUFFER_SIZE];
int ret;
memset(buffer, 0, MAX_BUFFER_SIZE);
printf("开始recv()...\n");
ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
printf("ret = %d\n", ret);
if (ret > 0)
printf("收到消息:%s, 共%d个字节\n", buffer, ret);
else{
if (ret == 0)
printf("客户端主动关闭!!!\n");
close(sockfd);
}
printf("LT处理结束!!!\n");
}
实验
设置应用缓冲区大小为5,编译运行后,用一个客户端连接,并发送1-9这几个数:
再看服务器的反映,可以看到水平触发触发了2次。因为我们代码里面设置的缓冲区是5字节,处理代码一次接收不完,水平触发一直触发,直到数据全部读取完毕:
边缘触发
由于边缘触发只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知。所以要用循环来处理。
/* 带循环的ET处理流程 */
void epoll_et_loop(int sockfd){
char buffer[MAX_BUFFER_SIZE];
int ret;
printf("带循环的ET读取数据开始...\n");
while (1){
memset(buffer, 0, MAX_BUFFER_SIZE);
ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
if (ret == -1){
// 非阻塞的时候 没有东西读了 就会返回-1
if (errno == EAGAIN || errno == EWOULDBLOCK){
printf("循环读完所有数据!!!\n");
break;
}
close(sockfd);
break;
}
else if (ret == 0){
printf("客户端主动关闭请求!!!\n");
close(sockfd);
break;
}
else
printf("收到消息:%s, 共%d个字节\n", buffer, ret);
}
printf("带循环的ET处理结束!!!\n");
}
非阻塞
在非阻塞的情况下,epoll_wait()一次提醒进入循环之后,会不断地读取一定长度的数据,知道套接字上没有新的数据到达时,recv() 函数会立即返回 -1,并且退出。
用一个客户端连接,并发送1-9。再观测服务器的反映,可以看到数据全部读取完毕,处理函数也退出了,因为非阻塞IO如果没有数据可读时,会立即返回,并设置error,这里我们根据EAGAIN和EWOULDBLOCK来判断数据全部读取完毕了,可以退出循环了:
阻塞
用一个客户端连接,发送1-9。看看服务器,可以看到数据全部读取完毕:
程序没有输出"带循环的ET处理结束",是因为程序一直卡在了recv()函数上,因为是阻塞IO,如果没数据可读,它会一直等在那里,直到有数据可读。如果这个时候,用另一个客户端去连接,服务器不能受理这个新的客户端!!
结论
所以ET模式下必须设为非阻塞模式。循环读取的时候,如果缓冲区没有数据或者低于水位线,recv/read就会阻塞等待读事件就绪,这会影响到epoll模型中其他文件描述符的操作。