博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网络编程之 Socket的模式(一) --- “阻塞/非阻塞” 与 “同步/异步”
阅读量:2176 次
发布时间:2019-05-01

本文共 4385 字,大约阅读时间需要 14 分钟。

1.  阻塞/非阻塞

        对于网络编程而言,Socket模式是开发者必须明确的一个问题。对于Socket的操作,可以分为阻塞模式和非阻塞模式两种。在两种不同模式下,同一个Socket函数的表现可能完全不同,所以必须引起开发者的注意。
        在解释阻塞(blocking)和非阻塞(non-blocking)之前,先看另外一对相对的词语,"同步"(synchronous)和"异步"(asynchronous)。同步和异步指的是两个模块间的消息交互方式。在程序中,模块之间的所有关系都是通过不同的函数去体现的。我们知道所有的函数都可以存在返回值,如果模块A调用模块B的f()函数时,得到的参数返回值能够代表业务上所需要的结果,则可以称此函数f()为同步函数,模块A和模块B就此消息流程的过程是同步的。否则则称为异步。
        上面的解释太过于绕口令了,举个简单的例子:
        模块A为应用界面,模块B为网络通讯模块,A通过B同远端的模块C进行实时通讯。模块B存在接口函数f(),函数f()能够保证网络通讯的数据完整性和正确性,并通过返回值bool量来表示成功失败(成功表示C端接收到完整的模块B所发送的数据,而失败则相反)。如果模块A调用模块B的f()函数的目的,只是为了把数据发送到C端,则在此情况下,我们可以把f()函数所代表的动作看作是一个同步动作。而如果模块A通过模块B的f()函数发送的数据具有某种实际意义,模块A期望的是得到所发送数据的真实响应结果,如A发送的数据是用来操作C促发某个动作,如操作一个数据库,或者前端的某个机械部件,A希望得到此动作的一个结果。则模块B提供的f()函数就不能被看作是一个同步的接口,因为f()函数的返回值只表示网络数据发送的成功与失败。如果A需要得到真实的操作结果,必须对B进行其他调用。A发送消息并得到其想要的对应结果的过程被分割成了两个部分,因此可以被看作是一个异步过程。
        抛开上面的同步和异步,跑的更远一些。我们都知道编程当中,在对现实世界的业务组织上,模块的思想是非常重要的,每一模块都只专注于自己需要实现的事情,彼此之间互不影响,模块之间通过有限的接口来实现互动。通过对模块的合理规划,可以使代码具有更好的组织性和维护性。
        同时我们也知道,在计算机所提供实现编程方式里,能够使用的技术只有函数调用,线程,进程。当一个函数的调用结果能够满足其调用者的调用目的时,这个函数可以被认为是一个同步函数。反之则可认为是一个异步函数。当函数f()为异步时,调用者想要知道f()函数的真实结果,有两种选择: 第一,调用另外一个函数去查询结果,这种方式可以称为查询,定期的查询则变成轮询。第二,存在一个函数能够主动的通知调用者,真实的调用结果,这种方式可以被称为回调函数方式(或者类似的称呼“通知”、“事件”)。
        存在被调方对调用方的主动通知的模块,我们可以认为这是一个主动对象模块。主动对象模块在实现上有两个特点,缺一不可。第一,必须依赖于多线程,或者多进程。第二,存在回调函数接口。要注意的是,存在多线程的模块未必是可以成为是主动对象模块,因为可能不满足第二个条件。同主动对象模块相对应的,就是被动对象模块了,被动对象模块只存在被调用的接口。

        有些扯远了,让我们回到Socket编程上。如果把操作系统看成一个模块,而把所有的应用程序看成是另外一个模块的话,Socket函数可以被看成是模块操作系统对其他应用模块提供的一组接口函数,而这组函数有阻塞和非阻塞两种模式。在Linux设计中,目录、文件、Socket、设备等一切都可以看成是I/O,对这些设备、文件等的读写可以被认为是I/O操作。阻塞和非阻塞就是针对I/O操作而言的,在阻塞模式下,在I/O操作完成前,执行的操作函数一直等候而不会立即返回,该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,操作函数会立即返回,而不管I/O是否完成,该函数所在的线程会继续运行。这就是阻塞和非阻塞的区别,仅此而已。非阻塞式模式可能在I/O操作没有完成的情况下返回,太奇怪了,要它干么? 其实不然,非阻塞式I/O的这种方式,实际上就是上面讲述的结果查询或轮询方法。

2. Socket函数

        让我们对照Socket函数来具体讨论阻塞和非阻塞。讨论前,需要注意是,只有涉及I/O操作的函数才存在阻塞和非阻塞的区别。
        不涉及I/O操作的函数包括:
socket();bind();listen();accept();close();shutdown();gethostbyname();gethostbyaddr();htons()、htonl()、ntohs()、ntohl();getsockopt()/setsockopt();inet_addr()/inet_ntoa();
        涉及I/O操作的函数包括:
connect();send()/recv();recvfrom()/sendto();select()/poll();

2.1 connect()

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
        在阻塞模式下, 函数返回0表示远程成功, -1表示失败。

        在非阻塞模式下,函数会立刻返回-1, 但是-1并不表示失败, 通常情况下会返回EINPROGRESS(linux)/WSAEWOULDBLOCK(windows), 此时需要调用者使用select函数和getsockopt函数去查询Socket的状态。

2.2 send()

int send(int sockfd, const void *msg, int len, int flags);
        阻塞模式下,send()函数成功时返回发送数据的字节数,失败时-1,并设置errno。
        非阻塞模式下,send()函数成功时,同样返回发送数据的字节数,失败时-1,并设置error。通常情况下,error值可能为WSAEWOULDBLOCK/EWOULDBLOCK, 这并不表示Socket本身出现问题,而只是表示Socket为非阻塞式,暂时无法写入数据,用户可以稍后尝试重新写入。

2.3 recv()

int recv(int sockfd, void *buf, int len, unsigned int flags);
        阻塞模式下,recv()函数成功时返回实际读入缓冲的数据的字节数。错误的时候返回-1, 同时设置 errno。
        非阻塞模式下,recv()函数成功时,同样返回发送数据的字节数,失败时-1,并设置error。通常情况下,error值可能为WSAEWOULDBLOCK/EWOULDBLOCK, 这并不表示Socket本身出现问题,而只是表示Socket为非阻塞式,暂时无法读取数据,用户可以稍后尝试重新读取。

2.4 select()/poll()

        select()/poll()本身并不对I/O进行操作,这两个函数的目的是用来监视I/O的状态,其目的是在I/O可用时,通知用户进行I/O操作。

2.5 recvfrom()/sendto()

        讨论见send()/recvfrom()

3. 阻塞/非阻塞以及协议

        我们知道计算机操作系统实现了网络的TCP和IP层协议栈,Socket函数是操作系统提供给上层应用开发者的一个接口。阻塞非阻塞通常情况下只和使用TCP协议的Socket相关,这是出于协议的不同要求。TCP同UDP协议相比,是可靠传输的数据协议, TCP协议通过移动窗口来保证数据的可靠性()。本机是否可以继续发送数据,也就是是否可以滑动窗口位置,取决于对端是否对已发送数据进行确认。在对端没有对本端已发数据确认前,如果本端缓存已经存满数据,数据将无法写入。
        当用户调用send函数时,操作系统首先会将send要发送的内容从用户空间复制一份,放入内核空间,如果内核空间用于发送数据系统缓冲已满,对于阻塞式的Socket, send函数的行为,就是等待,直到发送端收到对端确认信息后,从发送缓冲中移除已发送数据,使发送缓冲有多余的空间,接受用户新发送的数据后,才会返回。对于非阻塞式的Socket,如果发现系统缓冲已满,send函数会立刻返回-1,并设置error为WSAEWOULDBLOCK/EWOULDBLOCK,表示接受缓冲已满,用户层无法对继续向内核的发送缓冲写入数据。用户可以在接下去的一段时间内,重新调用send函数再次发送数据。上面的这段话,同时也解释了为什么调用send函数发送n个字节的数据,返回值会小于n。
        当用户调用recv函数时,上面过程是相反的。如果接受缓冲中存在数据,操作系统会直接把内核空间中的数据复制一份放入用户空间,recv函数返回。如果接受缓冲中没有数据,在阻塞模式下,recv函数会等待,直到接受缓冲中收到数据,并被拷到用户空间。而在非阻塞模式下,如果接受缓冲中没有数据,recv函数会直接返回-1,并设置error为WSAEWOULDBLOCK/EWOULDBLOCK,表示接受缓冲中并不存在数据,用户可以在接下去的一段时间内,重新调用recv函数再次尝试接收数据。
        上面的内容讲的非常的粗糙,事实上操作系统实现的TCP协议栈,要远远复杂许多。TCP协议栈的一个特点是复用,不同TCP连接会复用一个协议栈,这在实现层次上可能表现为系统底层维护一个数据缓冲,用于和网卡交互,所有的TCP连接都共同使用此块缓冲。此块缓冲也有可能是一个cache,在设备驱动层次,也就是网卡。而对于每一个连接,操作系统会单独为每个连接各再维护一块发送和接受缓存(事实上Linux操作系统下,为每个文件的读写都会在内核里维护两块单独的缓冲区),其中发送缓冲用于实现TCP协议所要求的滑动窗口,而接受缓冲用于平滑用户应用层的接受变化。不同的操作系统虽然都实现了TCP协议,但是方法并不相同,所以在某些细节上,Socket函数的特性也略有不同。

        上面讲了TCP协议的情况。再来看看UDP协议下的情况,我们都知道UDP协议是不可靠的数据传输协议,发送端并不关心接受端是否能够正确的接受到所发送的数据,而只是尽可能的尽快的发送数据。因此在UDP协议下,操作系统理论上是可以不维护缓冲的,发送时可以只是简单的把用户层的数据复制一份,直接交给网卡去发送。而接收时,把网卡数据拷贝进入系统缓冲,如果用户没有从系统缓冲中及时拿走数据,后来的数据可以覆盖先前的数据。所有对于UDP协议,应用层在发送时,应该考虑的是尽量发送,而在接收时,应该考虑的是及时接受。因此阻塞式和非阻塞并非设计时需要考虑的要点。

(版权所有,转载时请注明作者和出处)

你可能感兴趣的文章
用 LSTM 做时间序列预测的一个小例子
查看>>
用 LSTM 来做一个分类小问题
查看>>
详解 LSTM
查看>>
按时间轴简述九大卷积神经网络
查看>>
详解循环神经网络(Recurrent Neural Network)
查看>>
为什么要用交叉验证
查看>>
用学习曲线 learning curve 来判别过拟合问题
查看>>
用验证曲线 validation curve 选择超参数
查看>>
用 Grid Search 对 SVM 进行调参
查看>>
用 Pipeline 将训练集参数重复应用到测试集
查看>>
PCA 的数学原理和可视化效果
查看>>
机器学习中常用评估指标汇总
查看>>
什么是 ROC AUC
查看>>
Bagging 简述
查看>>
详解 Stacking 的 python 实现
查看>>
简述极大似然估计
查看>>
用线性判别分析 LDA 降维
查看>>
用 Doc2Vec 得到文档/段落/句子的向量表达
查看>>
使聊天机器人具有个性
查看>>
使聊天机器人的对话更有营养
查看>>