字符串缓冲区太小怎么解决 | 您所在的位置:网站首页 › 字符串缓冲区大小 › 字符串缓冲区太小怎么解决 |
学习于:https://www.bilibili.com/video/av44660437/?p=9 前文:何柄融:多路复用I/O select poll epoll 何柄融:select poll epoll 再一次的了解 这里就不讲epoll的基础了,如果对epoll没了解的请去前文学习。 当时有: epoll除了提供select/ poll那种IO事件的电平触发(Level Triggered) 外,还提供了边沿触发(Edge Triggered) ,这就使得用户空间程序有可能缓存IO状态,减少epoll_ wait/epoll_ pwait的调用,提高应用程序效率。(这个没怎么看得懂。。) 所以,特地来学习一下epoll的ET和LT。 epoll的三种工作模式: 1.水平触发模式(默认):Level Triggered LT 2.边沿触发模式 : Edge Triggered ET 3.边缘非阻塞触发 先来介绍这个默认使用的水平触发模式(LT) 特点:1.只要fd对应的缓冲区有数据,epoll_wait就返回 2.返回的次数与发送数据的次数没有关系。 3.epoll默认的使用模式 看代码: 核心:关键就在于处理已连接描述符时的缓冲区大小。之前我们都是使用1024个字节,而写的时候也不会太大。 而这里把1024改成了5,也就是说每次处理5个字节。而如果客户端一次发送过来100个字节,那么就会因为客户端的这一次发送,服务器这边要调用20次的epoll_wait(),因为 100个字节发送过来,服务器这边的已连接描述符这里会有个对应的缓冲区缓冲这100个字节,然后因为缓冲区有数据,所以就会出发epoll_wait()的返回,而返回之后服务器又只从缓冲区接收5个字节来处理。那么就会剩下95个字节。那么当服务器处理完这5个字节后,本来应该阻塞在epollwait这里,不断的唤醒沉睡的,可是由于缓冲区还有数据,所以又马上让epollwait返回,然后再一次处理这个文件描述符的事件。所以,这里就会多次调用epollwait。下面是一个比较形象的图: 运行代码的的输出: 注意到,每次都多了69.123.31,这属于乱码,具体解释如下: printf函数:有时候会发现打印不出来。因为printf有一个缓冲区,好像是8k,当你打印字符串的时候,如果没加后面的 "n" ,那么它就有可能因为缓冲区没有满而导致不输出。如果加了后面的“n”,就会强制把字符串从缓冲区中输出。而有时候,n 也不能强制从缓冲区输出。(这个缓冲区其实很像我们在文件io这篇文章中提到的8k的缓冲区,机制都是一样的,缓冲区满/调用flush/进程结束 使数据刷新,关于这里 https://blog.csdn.net/u013790372/article/details/54645860 这篇文章写得不错,大家可以了解一下) 后面改为: 直接向标准输出写就ok了。 正确输出如下: 即多次读取数据,直到把缓冲区的数据读完。 而由前面的知识,我们知道epollwait调用次数越多,系统的开销越大。(总结一下就是:epollwait会进入内核空间检测内核文件描述符的事件,这里就有cpu的在系统调用时产生的上下文切换,估计还有些我不知道的东西。。) 然后,epoll的水平触发模式就到这里了。主要就是缓冲区有数据就epollwait返回。 然后来看边沿触发模式(ET) 特点: 客户端给server发数据,发一次数据,server的epoll_wait返回一次,不在乎客户端的缓冲区的数据是否读完。(其它客户端的连接也会导致返回和数据读取,不要忘了前面的基础) 也就是说,缓冲区里面可能存有大量的客户端之前发来的数据。这样就不太好。 修改后:如下图:也就是设置了结构里的events。或上 epollet ,这样就能变为边沿触发模式。这里是添加新连接的时候的修改,而代码前面一开始把监听集合添加进红黑树的时候,也要修改这个events。(此时代码的buf大小依然是5个字节) 演示: 具体就是:客户端一次性输入比较多的字符,服务端连进一个客户端,就返回一次epollwait,然后读取一次缓冲区的5个字节。后面客户端再次发送,还是读取缓冲区的前5个字节,也就是上次服务器接收了但是没有从缓冲区刷新出来的数据,而此次发送的数据被放在了缓冲区的最后面,所以没显示出来。(这个要理解清楚!) 现在来总结一下两者的区别:LT是我服务端一次只读这么多,我可以多次读你很多数据。消耗系统资源多。 ET是客户端发送一次或者连接一次我就返回一次,然后慢慢读。理解了前面的,其实基本上全部说下来都没关系。 然后我们来看看怎么解决EL读取的总是缓冲区的旧数据的问题: 下面这张图片改为while(recv())就可以了,也就是说,只要缓冲区有数据,我就把它全部读出来。这样就不会说每次都读取很少的字节了。 可是这又产生了一个问题: 这样的话,因为io默认设置是阻塞的啊,也就是说fd默认阻塞属性啊。那么在while(recv())这里,读完数据之后,如果没有数据来的话,就会阻塞在这里。那么就不会继续返回到外面的while(1)的大循环中了。也就不会再epollwait函数那里阻塞了(也就是没有办法委托再去内核检测了),这就完全把整个程序的功能弄错了。 所以,我们要解决阻塞问题,所以,必须设置fd为非阻塞。 所以,我们前面介绍其实是边缘阻塞模式。 接下来我们要介绍边缘非阻塞模式。 我们要解决的是如何设置fd的非阻塞的属性。 让我们先来看看如何设置非阻塞(先脱离前面的问题先) 1.open()函数 它可以设置flags 必须 O_WDRE | O_NONBLOCK 终端文件 : /dev/tty 当前所操作的终端。前面已经讲得很多次了。(也就是说,终端文件就可以使用open来设置非阻塞) 比如 fd=open("/dev/tty" ,O_WDRE | O_NONBLOCK); 那么返回正常的话就是返回的fd就是非阻塞的。这个是针对终端而言的。 2. 由于上面的方法针对的是终端,而我们现在要操作的是内核缓冲区。 fcntl :1.复制文件描述符 2.设置文件描述符的属性为非阻塞。(第二个是我们要用的) 使用流程 int flag=fcntl(fd.F_GETFL); 第一步:获取文件描述符的flags属性 get flag 然后我们要把非阻塞属性给它设置进去 flag |=O_NONBLOCK 修改flag属性 fcntl(fd,F_SETFL.flags) 此处是吧flag属性又设置进去 下面是视频中的文档: 然后我们来到epoll使用代码上看怎么改:下面这里我们只修改已连接的文件描述符,不修改前面的监听描述符了。 然后是接收数据那里: 注意此时的buf还是5个字节! 下面先是len>0的情况,len>0说明此时缓冲区有数据可读,所以不断地循环从缓冲区中接收5个字节,然后打印到终端,并且返回给客户端。 下面是len返回为0和-1的情况。len等于0说明客户端断开了连接,此时把此客户端对应的文件描述符从内核中的红黑树移除,然后关闭掉该文件描述符。 len等于-1。此时会有一个errno的量(好像在线程那里看见过),它如果等于EAGAIN,就说明此时是缓冲区数据读完,那么就跳出这里,回到epollwait中阻塞;如果不等于,那么就是出现异常了,那就退出程序。 成功的返回: 这样的话,我们就很完美地结束了epoll的边缘非阻塞的编程。 这样的话,边缘非阻塞效率最高。 既没有LT的多次重复调用epoll_wait的系统开销大,也没有ET的读取到的都是旧数据或者是ET的阻塞。 客户端一次发送数据,我就一次性把发到缓冲区的数据全部接收并且处理。 简直perfect。 感觉那个视频很不错,最起码epoll的几种模式讲得很不错,推荐一下。 如果大家对java的nio有了解的话,我强烈建议来看看我这篇文章: 何柄融:nio基础和手写一个基于nio的demo , 然后你就会发现epoll和nio的编程居然是一模一样的,没任何差别。 欢迎交流讨论。 |
CopyRight 2018-2019 实验室设备网 版权所有 |