Unix网络编程主要需要注意socket的阻塞和非阻塞的处理,避免死锁。同时,socket属性的设置也是需要注意的基本问题。

两种类型Socket

Unix下Socket主要有两种阻塞和非阻塞,服务端开发一般使用非阻塞socket+IO复用技术进行网络通讯。如果需要使用阻塞socket,请注意一下两点,防止client和server在数据通讯时发生死锁。

  1. client发送数据给server,server发送数据给client,如果数据都很大时,那么两端一起发送数据时,会阻塞在send操作上,由于每一个连接的接受端缓冲区的大小都是有限的,当接受端的接收缓冲区满时,发送端将一直阻塞在send操作上,这样一来,两端都没有读取接收缓冲区的数据,导致一直阻塞在send操作上。从而出现死锁。
  2. client发送很大的数据给server,server只是echo给client,由于数据比较大,client会阻塞在send上,此时echo给client的数据占满了client的接收缓冲区,那么server的send操作将会阻塞,此时不会读取client发送的数据,这样会导致server的接收缓冲区满了,此时client的send也会一直阻塞,从而出现死锁。 这种原因是因为应用层没有缓冲,如果两端都先接收好完整的数据存在应用层的buffer上,那么就不会出现死锁了。两端先发送需要发送的数据的大小,接下来接收到完整数据后,就可以发送给对端了。

TCP网络编程的基本设置

  1. 设置socket属性,开启SO_REUSEADDR,保证程序在重启的时候可以再一次的监听端口。在TCP下,如果主动关掉连接的话,会有一个TIME_WAITE时间,此时的端口还是处于忙的状态,如果不启用SO_REUSEADDR的话,绑定该端口会发生错误。
  2. 设置socket属性,TCP_NODELAY禁用Nagle算法, 一般情况下都是默认关掉该算法,使得数据可以立刻发送到对端。可以选择设置TCP keepalive定期查看TCP连接是否还存在,当然应用程序一般都有应用层心跳,所以可以不必设置。
  3. 服务端程序忽略信号SIGPIPE。假如服务进程没有及时处理对方断开连接的事件,就可能出现连接断开后继续发送数据的情况。引起SIGPIPE,默认中断进程。

在IO多路复用的时候,服务器监听的端口需要采用非阻塞的方式,因为如果端口可读的情况下,当真正的去accept的时候,如果在可读与真正accept之间,客户端断开了连接,那么在阻塞的情况下accept会出现阻塞。其他的连接socket也应该设置为非阻塞,不然的话,服务器会阻塞在某一个socket上,使得其他的socket被饿死。

在多路复用时,如果buffer没有数据可写或是没有数据想读,那么应该取消掉相应的时间侦听,否则的话会出现busy loop。

对于发布订阅等有多个客户端连接需要发布数据的情况,需要考虑有一些很慢的客户端的情况,极端情况下,客户端有可能不收数据,导致所有的消息都存在服务器上,造成服务器内存被消耗。所有跟多个客户端打交道的场景都需要考虑这个问题,防止个别客户端拖垮整个应用。其实,这个问题是由于接收端和发送端速率不匹配造成的。至于如何解决,一方面可以使用“低水位线”和“高水位线”这种方式进行处理,当应用层发送buffer满了不再进行写操作。在redis中,对于每一个客户端,会有一个buffer来缓冲服务器恢复给client的消息,如果buffer满了,redis会新开一个链表,每一个节点的空间是固定的,用来存放新到来的消息,如果链表尾节点由足够空间可以存放新消息,那么直接存储消息,否则的话需要新增一个节点,用来存放新消息,并将节点连到链表的尾部。每一次在链表存储消息后,redis会检查客户端的消息缓存的总大小,如果超过配置的阈值的话,那么直接关闭客户端。

Unix网络编程注意点

先有TCP,再有UDP,一般情况下使用的是TCP,因为UDP没有拥塞控制,很难很好的使用网络的带宽,发多会丢包,发少有不能合理使用带宽。从编程角度来说,UDP不需要网络库,在服务端,udp只需要一个socket就可以与所有的客户端进行数据交互。而TCP需要一个连接一个socket。TCP是线程不安全的,因为TCP为字节流协议,多个线程操作一个socket会发生数据错乱。而UDP是线程安全的,只需要维护客户端的信息,由于是数据包的格式,所以多个线程写一个UDP socket的话,不会造成数据包内的数据错乱。

在TCP编程的时候,如果没有数据需要发送了,那么应该调用shutdownWrite,关闭写连接,在read返回0的时候,才调用close。如果在没有数据可发送的时候直接的调用close,会导致输入缓冲区的数据直接被清除,使得数据读取不到。

TCP是可靠的意思是协议会帮你把数据安全的发送到对端的接收端口,但是并不负责数据会被对端的程序给读取到,所以为了数据的可靠传输,我们在应用层需要进行数据的接收确认,表示数据已经被程序成功处理。

在linux下使用管道时,如果管道的一端关闭了,那么往里面写数据的话,程序会受到SIGPIPE信号,在默认的情况下,程序会直接退出。在TCP网络编程下,如果对端关闭了socket的读连接,此时往里面写数据的话,也会收到SIGPIPE 信号,所以在server端,需要忽略掉SIGPIPE信号,不然的话,收到SIGPIPE信号的时候,server会退出。

TCP自连接

当在本地进行tcp连接时,有可能会出现自连接。所谓的自连接就是客户端和服务端共用一个port。出现这种情况的原因如下:

  1. client发起连接,连到端口为B的服务器上。
  2. 系统会选择一个port A 作为client的端口。
  3. 如果port B上面没有服务器在侦听,那么client会收到一个rst恢复包,如果有的话就可以连接上。
  4. 如果此时port A 等于 port B, 那么此时实际上是没有服务器在port B 上监听的,不然的话,client不会分配到port B。在这种情况下,client发送的SYN包会来到端口B上,此时由于client已经打开了端口B,系统会以为端口B上有服务器在监听,那么就直接接收SYN包,并发给client。这种情况其实就是client和server同时发起连接,client此时的状态为SYN_RECEIVE,按照TCP的三次握手,client会跟自己建立起了连接。此时就出现了自连接。

自连接是一种难以确定的情况,因为在TCP下,server和client并不是对等的,这种情况下分不清谁是谁了,所以会出现难以确定的问题。为了防止自连接,每当在本地连接后,可以根据client的端口和server的端口来判断是否是自连接。