网络编程常用基础知识总结
1. socket阻塞和非阻塞
关于阻塞还是非阻塞,socket一般需要考虑的情况有以下3种:
<1> connect
<2> read
<3> write
当socket进行connect的时候,会有一个三次握手的机制。connect函数也是只有接收到自己发送的SYN的ACK之后才会返回。这时候如果网络环境发生拥堵,RTT时间就会比较长,connect被阻塞住的时间也就会很长(TCP默认超时时间的典型值是75s)。而且,平时业务代码中,往往不会只建立一个socket连接,每个client会和多个server建立多个连接。如果每次调用connect都阻塞这么长时间,那么业务代码会堵到让人难以接受。这时候给connect调用设置一个自己可以接受的超时时间就很有必要了。比如connect函数如果3秒之内还没有连接成功,我就终止connect操作,根据业务逻辑来进行下一步操作。connect调用没有像SO_RCVTIMEO和SO_SNDTIMEO这样的参数可以设置超时时间,所以一般的解决办法是使用select来解决。具体怎么实现,网上的例子有很多,这里就不再细讲。主要的操作就是将socket设置为非阻塞,并注册到select监控的可写文件描述符列表。调用select时传入可以接受的TIMEOUT。如果select最后的返回值为0,则说明超过TIMEOUT,socket还是没有建立好连接。如果返回显示socket可写,则说明连接建立完成。但是一般还需要做一个二次检查,调用getsockopt取得套接字的待处理错误,如果建立成功,该值将为0。select是阻塞操作,同样也会阻塞住线程,但是可以设置timeout。这样就绕过了直接调用connect无法设置TIMEOUT而长时间阻塞线程的情况。而且这个技术经常被用到并行建立socket连接上,因为socket设置非阻塞后,connect会立即返回。这时接连调用多次connect与多个server发起连接,同时把这些socket都由select进行TIMEOUT监控。这样就做到了并行发起连接的效果。
read 和 write 因为有SO_RCVTIMEO和SO_SNDTIMEO,在socket为阻塞的情况下可以直接设置TIMEEOUT,所以比较简单。这里主要需要注意的地方是,在socket为阻塞和非阻塞的情况下,read和write的返回值的差异。
(1)socket为阻塞,且没有设置SO_RCVTIME和SO_SNDTIMEO。对于read,如果请求的数据长度为1024,但是socket缓冲区中只有1000的数据,那么read会立即返回,并且返回值为1000。如果socket缓冲区中没有数据,那么read会一直等待下去,除非对端关闭socket,read会立即返回,并且返回值为0。read还有一个参数是MSG_WAITALL,在阻塞模式下不等到指定数目的数据不会返回,除非超时时间到,或者对端关闭socket。对于write,如果请求写入的数据长度为1024,但是socket缓冲区只有1000的剩余,则write会一直等待,直到数据全部写入socket缓冲区,并返回写入文件的长度。如果出错,则返回-1。
(2)socket为阻塞,但是设置了SO_RCVTIMEO和SO_SNDTIMEO。对于read和write,如果在超时时间内socket缓冲区中没有数据到来,或者缓冲区中一直没有腾出来可以让write写完数据的空间,那么read和write都会返回-1,并且将errno设置为EAGAIN | EWOULDBLOCK 。如果在超时时间内完成了read或者write,则返回值和阻塞时无区别。
(3) socket为非阻塞。 对于read,如果socket缓冲区中没有数据到来,那么read立即返回-1,并且将errno设置为EAGAIN | EWOULDBLOCK。对于write,如果请求写入数据为1024,而socket缓冲区只有1000的空间,那么write会立即返回1000。
2. 使用TCP和UDP发送0字节数据
TCP和UDP都可以发送0字节数据并正常返回。区别在于TCP发送0字节数据后,对端接收不到。而UDP的对端可以接收。
原因是,TCP本身是面向连接和面向流的,如果recv函数返回0,则代表对端已经将socket关闭,应该是这种机制导致设计recv函数时,不能接收0字节数据。而且TCP协议send的时候,是把数据放入TCP缓存中。如果发送的数据是0字节,那么压根就不会有数据放在缓存,也不会有数据发送出去,所以对端接收不到。
3. send函数使用注意事项
当给断开的socket写数据的时候,写第一次,对端会返回一个rst报文。当写第二次时,系统还会给进程发一个SIGPIPE。这个信号的默认处理方式就是结束进程。我们可以在send函数的最后一个参数加上MSG_NOSIGNAL。这样系统就不会发送这个信号给进程了。
4. 同步和异步, 阻塞和非阻塞概念区别
关于这两组概念之间的区别,感觉是因为描述的东西行为上比较相像,而且这几个名词在不同的场景下会代表不同的意思,所以让人特别容易混淆。
知乎上这位大神总结的可以说是非常透彻了 https://www.zhihu.com/question/19732473。
同步和异步关注的点是消息传递机制,而阻塞和非阻塞关注的点是程序在等待结果时的状态。
阻塞和非阻塞在平常调用一些接口的时候,这些接口本身就有实现。比如read和write,会根据socket是否为阻塞的来进行相应的操作。
而同步和异步与业务逻辑结合的比较紧密,没有固定的实现方式。
同样一个业务,比如现在有client A 和 server B, B上有一些数据,A需要定时拖下来,并存储到redis中。
如果使用同步的方式实现,那么就是,A的主线程先请求B,从B上把数据拉下来。然后主线程再请求redis,把刚刚拉下来的数据存储到redis中。
但是如果这个拖数据下来需要的时间很长,比如1个小时。但是A的主线程不想等待,想要立即返回,怎么办?这时后就必须使用异步的方式实现这个业务了。
<1> 使用线程池实现
把A从B上拖数据这个操作另起线程,让新的线程阻塞住拉B的数据,然后主线程返回干其他的事。
<2> 使用事件驱动
假设给A中维护一个任务队列,主线程把A从B上拖数据作为一个task推入任务队列中之后就立即返回。同时存储redis作为这个task的回调。线程池中的线程会从任务队列中取task去执行,把数据从B拉下来之后,触发事件,调用存储redis的回调。
这两个实现是我个人比较常见的形式。区别就是,当A需要并行从B拉互不相干的很多份数据时,会有很多个线程同时阻塞在拉数据的操作上。开着线程对操作系统来说是有开销的。第二种方法虽然也是线程池,但是线程池中线程的个数可以定制,可以少开一些线程。对A的主线程来说,把很多个任务放入任务队列中就可以返回了,而不是像方法一一样来多少个任务,就必须开多少个线程之后才可以返回。但是方法二相对方法一来说,编程会比较复杂,如果并发量不是太大,方法一也是一个很好的选择。
以上的东西是个人理解和总结,难免有理解错误和疏忽之处,欢迎大家改正。如果喜欢本文章,转载请注明原文出处,感谢配合~