如何保证数据一定能够被发出去
在网络编程中常常利用write将数据拷贝到内核的发送缓冲区,然后由协议将数据发送到对端,但是如何保证一次write能够将所有的数据都拷贝到内核缓冲区呢?如果还有数据没有被拷贝进去怎么办?如果拷贝的过程中内核缓冲区满了怎么办?如果拷贝的过程中write返回-1错误又怎么办?针对这些问题,muduo库都进行了解决。
- muduo是怎么发送数据的?
muduo库并不是将所有的数据发送操作全部都交给epoll来接管,而是选择了主动发送和epoll发送结合的方式,即当服务器收到请求需要发送数据后,首先采用send函数去主动发送数据,如果send将数据没有全部拷贝到内核缓冲区则让epoll去接管(muduo库利用的水平触发的方式去发送数据,这样不好,因为可能如果内核缓冲区的数据没有被发送,导致epoll被多次无线触发导致空转)。注意,一旦发送缓冲区中的数据发送完毕后,必须立即关闭EPOLLOUT
- 为什么要利用用户发送缓冲区?
利用用户缓冲区的根本目的就是为了解决如果send函数一次性没有将数据全部拷贝到内核缓冲区的问题。如果没有全部拷贝到内核缓冲区则将剩下的数据全部拷贝的用户发送缓冲区,然后交给epoll去接管发送任务。但是需要注意的是当用户主动调用send函数发送数据的时候,此时的用户发送缓冲区一定得是空的(如果不是空的,说明之前还有数据没有发送,这样会造成数据接收方接收数据顺序的问题),同时当前的服务器不能正在利用EPOLLOUT让epoll接管发送(如果出现这种情况,那么说明用户发送缓冲区中还是有数据的,当用户发送缓冲区没有数据的时候会将EPOLLOUT关闭不再监听此事件的)。只有确保了这两个条件才能保证数据顺序的正确性。如果出现了上述两种情况中的一种或者两种,则将需要发送的数据拷贝到用户发送缓冲区,让epoll去接管发送即可。
- send的过程中出现了错误怎么办?
在send数据的过程中,如果write返回了-1等错误则说明此时的拷贝操作是失败的,数据并没有拷贝到内核缓冲区,所以为了避免数据的漏发,需要对问题进行处理,这里需要分错误来进行处理。
首先是错误EPIPE或者ECONNREST错误,这两个错误都是和RST包有关的,也就是说出现这两个错误那么这个连接都是需要进行关闭的,所以此时的数据也就不需要进行发送了,即无需拷贝内核缓冲区了,当然也就不用拷贝到用户发送缓冲区了。剩下来的操作就是epoll触发EPOLLIN+EPOLLHUP+EPOLLERR来处理此错误连接了。同样对于epoll接管的write操作,如果返回了错误则直接向日志输出相应的错误信息就行,下一次再epollwait返回的时候,会触发错误处理的那些事件类型。
- send或者epoll写的时候为什么不循环写?即为什么不等遇到EAGAIN再返回?
muduo没有采取上述的方法,原因是如果在一次写入的时候没有写完,则第二次写入的时候几乎一定会遇到EAGAIN(因为第一次都没有写完,那么说明缓冲区已经写满了,那么第二次写入是毫无意义的)
- shutdown在主动关闭连接的时候如何保证所有要发送缓冲区的数据已经发送完毕?
在主动关闭的时候,如果epoll还在监听EPOLLOUT事件则说明用户发送缓冲区还有数据,则此时不能立即关闭,而是将关闭推迟到用户发送缓冲区的数据全部发送完毕的时候再关闭。怎么推迟?即先将连接的状态设置为disConnecting状态,等handleWrite将用户发送缓冲区的数据全部写完了再shutdown即可。
- 区别内核发送缓冲区和用户发送缓冲区
内核缓冲区是操作系统在连接建立后给每条连接分配的内存缓冲区,每条连接的每一端都包括两个缓冲区。即内核发送缓冲区和内核接收缓冲区。程序将用户层的数据拷贝到内核发送缓冲区中,然后等待协议去将数据进行发送。(协议发送数据会受到接收端滑动窗口大小等因素的影响)数据将会发送到对端的发送缓冲区,对端从内核接收缓冲区拿出数据到应用层进行处理。那么对于用户发送缓冲区而言,里面存放的都是用户想要发送的数据,但是还没有来得及放入内核发送缓冲区(可能是内核缓冲区已满等因素),这些数据一旦内核发送缓冲区可写,一定要及时的数据写入到内核缓冲区。
数据未发送完包括两个方面,一个是内核发送缓冲区中的数据没有发送完毕,一个是用户发送缓冲区中的数据没有发送完毕。对于前者,协议会慢慢的发送数据,如果close和shutdown的时候内核发送缓冲区还有数据,那么协议会等这些数据发送完毕后在关闭连接。但是如果是用户发送缓冲区还有数据,则需要用户在应用层上来确保已经将所有的用户发送缓冲区的数据都已经写入到内核发送缓冲区中。
- 为什么如果epoll还在监听EPOLLOUT状态就说明用户发送缓冲区还有数据?
在最开始的时候,用户先用send发送数据,如果没有发完则写入到用户发送缓冲区中(或者是遇到错误的时候),然后让epoll再监听EPOLLOUT,当用户发送缓冲区的数据全部发送完毕了才关闭掉EPOLLOUT,也就是说打开和关闭EPOLLOUT都只有一个地方,所以如果EPOLLOUT打开了即isWriting则一定是用户发送缓冲区中还有数据。
- muduo为什么采用LT而不是ET
采用LT的目的是为了更好的兼容其他的库,或者其他IO多路复用的写法。因为LT和select和poll的写法是一致的。同时采用ET的目的是当有事件发生时只会相应一次,而对于muduo库而言有了应用层缓冲区的存在,能够保证事件被及时处理,而且也不会导致busyLoop,所以不用ET。
另外实际上的应用层缓冲区采用的是vector来接管,即采取的是堆内存,只要内存足够,要发送的数据都是能够写入到内存的,但是对于文件发送服务器而言,如果全部先将文件都读入内存再发送,则如果并发数高,则会导致内存压力很大。