深入 CSocket 编程之阻塞和非阻塞模式

有时,花上几个小时阅读、调试、跟踪优秀的源码程序,能够更快地掌握某些技术关键点和精髓。当然,前提是对这些技术大致上有一个了解。

  我通过几个采用
CSocket 类编写并基于 Client/Server (客户端 / 服务端)的网络聊天和传输文件的程序 ( 详见: 源代码参考 )
,在调试这些程序的过程中,追踪深入至 CSocket 类核心源码 Sockcore.cpp , 对于CSocket
类的运行机制可谓是一览无遗,并且对于阻塞和非阻塞方式下的 socket 程序的编写也是稍有体会。

阅读本文请先注意:

  这里的阻塞和非阻塞的概念仅适用于 Server 端 socket 程序。socket 意为套接字,它与 Socket
不同,请注意首字母的大小写。

  客户端与服务端的通信简单来讲:服务端 socket 负责监听,应答,接收和发送消息,而客户端 socket
只是连接,应答,接收,发送消息。此外,如果你对于采用 CSocket 类编写 Client/Server 网络程序的原理不是很了解,请先查询一下(
详见:参考书籍和在线帮助 )。 
在此之前,有必要先讲述一下: 网络传输服务提供者, ws2_32.dll , socket 事件 和
socket window 。

1、网络传输服务提供者(网络传输服务进程), Socket 事件, Socket
Window

  网络传输服务提供者 ( transport service provider )是以 DLL 的形式存在的,在
windows 操作系统启动时由服务进程 svchost.exe 加载。当 socket 被创建时,调用 API 函数 Socket (在 ws2_32.dll
中), Socket 函数会传递三个参数 : 地址族,套接字类型 ( 注 2 ) 和协议,这三个参数决定了是由哪一个类型的 网络传输服务提供者
来启动网络传输服务功能。所有的网络通信正是由网络传输服务提供者完成 , 这里将 网络传输服务提供者 称为 网络传输服务进程 更有助于理解,因为前文已提到
网络传输服务提供者 是由 svchost.exe 服务进程所加载的。 
  下图描述了网络应用程序、 CSocket ( WSock32.dll
)、 Socket API(ws2_32.dll) 和 网络传输服务进程 之间的接口层次关系:

  当 Client 端 socket 与 Server 端 socket 相互通信时,两端均会触发 socket
事件。这里仅简要说明两个 socket 事件:

  FD_CONNECT: 连接事件 , 通常 Client 端 socket 调用 socket API 函数 Connect
时所触发,这个事件发生在 Client 端。 

  FD_ACCEPT :正在引入的连接事件,通常 Server 端 socket
正在接收来自 Client 端 socket 连接时触发,这个事件发生在 Server 端。 

  网络传输服务进程 将 socket
事件 保存至 socket 的事件队列中。此外, 网络传输服务进程 还会向 socket window 发送消息 WM_SOCKET_NOTIFY , 通知有
socket 事件 产生,见下文对 socket window 的详细说明。

  调用 CSocket::Create 函数后,socket
被创建。 socket 创建过程中调用 CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket*
pSocket, BOOL bDead) 。该函数的作用是: 将 socket 实例句柄和 socket 指针添加至 当前模块状态 ( 注 1
)的一个映射表变量 m_pmapSocketHandle 中。 

  在 AttachHandle 过程中,会 new 一个
CSocketWnd 实例 ( 基于 CWnd 派生 ) ,这里将这个实例称之为 socket window ,进一步理解为它是存放所有 sockets
的消息池 ( window 消息),请仔细查看,这里 socket 后多加了一个 s ,表示创建的多个 socket 将共享一个 消息池
。 

  当 Client 端 socket 与 Server 端相互通信时 , 此时 网络传输服务进程 向 socket window
发送消息 WM_SOCKET_NOTIFY ,需要说明的是 CSocketWnd 窗口句柄保存在 当前模块状态 的 m_hSocketWindow
变量中。 

2、阻塞模式

  阻塞模式下 Server 端与 Client 端之间的通信处于同步状态下。在 Server 端直接实例化 CSocket
类,调用 Create 方法创建 socket ,然后调用方法 Listen 开始侦听,最后用一个 while 循环阻塞调用 Accept 函数用于等待来自
Client 端的连接,如果这个 socket 在主线程(主程序)中运行,这将导致主线程的阻塞。因此,需要创建一个新的线程以运行 socket
服务。 
调试跟踪至 CSocket::Accept 函数源码:

while(!Accept(...))

     // The socket is marked
as nonblocking and no connections are present to be accepted. 
if
(GetLastError() == WSAEWOULDBLOCK)
 PumpMessage(FD_ACCEPT); 
else 
return FALSE; 
}

  它不断调用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 类)判断
Server 端 socket 的事件队列中是否存在正在引入的连接事件 - FD_ACCEPT (见 1 ),换句话说,就是判断是否有来自 Client 端
socket 的连接请求。 

  如果当前 Server 端 socket 的事件队列中存在正在引入的连接事件, Accept
返回一个非 0 值。否则, Accept 返回 0,此时调用 GetLastError 将返回错误代码 WSAEWOULDBLOCK
,表示队列中无任何连接请求。注意到在循环体内有一句代码: PumpMessage(FD_ACCEPT); 

  PumpMessage
作为一个消息泵使得 socket window 中的消息能够维持在活动状态。实际跟踪进入 PumpMessage 中,发现这个消息泵与 Accept
函数的调用并不相关,它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重绘消息)处于活动状态,而绝大部分的 socket
window 消息被阻塞,被阻塞的消息中含有 WM_SOCKET_NOTIFY。

  很显然,如果没有来自 Client 端 socket
的连接请求, CSocket 就会不断调用 Accept 产生循环阻塞,直到有来自 Client 端 socket
的连接请求而解除阻塞。 

  阻塞解除后,表示 Server 端 socket 和 Client 端 socket 已成功连接,
Server 端与 Client 端彼此相互调用 Send 和 Receive 方法开始通信。

3、非阻塞模式

  在非阻塞模式下 利用 socket 事件 的消息机制, Server 端与 Client
端之间的通信处于异步状态下。

  通常需要从 CSocket 类派生一个新类,派生新类的目的是重载 socket 事件 的消息函数,然后在
socket 事件 的消息函数中添入合适的代码以完成 Client 端与 Server
端之间的通信,与阻塞模式相比,非阻塞模式无需创建一个新线程。 

  这里将讨论当 Server 端 socket 事件 -
FD_ACCEPT 被触发后,该事件的处理函数 OnAccept 是如何进一步被触发的。其它事件的处理函数如 OnConnect, OnReceive
等的触发方式与此类似。 

  在 1 中已提到 Client/Server 端通信时, Server 端 socket 正在接收来自
Client 端 socket 连接请求,这将会触发 FD_ACCEPT 事件,同时 Server 端的 网络传输服务进程 向 Server 端的 socket
window (CSocketWnd )发送事件通知消息 WM_SOCKET_NOTIFY , 通知有 FD_ACCEPT 事件产生 , CsocketWnd
在收到事件通知消息后,调用消息处理函数 OnSocketNotify:

LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM
lParam) 

CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam,
lParam); 
CSocket::ProcessAuxQueue(); 
return 0L

}

  消息参数 wParam 是 socket 的句柄, lParam 是 socket 事件
。这里稍作解释一下,CSocketWnd 类是作为 CSocket 类的 友元类 ,这意味着它可以访问 CSocket 类中的保护和私有成员函数和变量,
AuxQueueAdd 和 ProcessAuxQueue 是 CSocket 类的静态成员函数,如果你对友元不熟悉,请迅速找本有关 C++
书看一下友元的使用方法吧! 

  ProcessAuxQueue 是实质处理 socket 事件的函数,在该函数中有这样一句代码:
CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam,
TRUE); 

  其实也就是由 socket 句柄得到发送事件通知消息的 socket 指针 pSocket:从
m_pmapSocketHandle 中查找! 

  最后, WSAGETSELECTEVENT(lParam)
会取出事件类型,在一个简单的 switch 语句中判断事件类型并调用事件处理函数。在这里,事件类型是 FD_ACCEPT ,当然就调用
pSocket->OnAccept !

结束语 

  Server 端 socket
处于阻塞调用模式下,它必须在一个新创建的线程中工作,防止主线程被阻塞。 

  当有多个 Client 端 socket 与 Server
端 socket 连接及通信时, Server 端采用阻塞模式就显得不适合了,应该采用非阻塞模式 , 利用 socket 事件 的消息机制来接受多个
Client 端 socket 的连接请求并进行通信。 

  在非阻塞模式下,利用 CSocketWnd 作为所有 sockets
的消息池,是实现 socket 事件 的消息机制的关键技术。文中存在用词不妥和可能存在的技术问题,请大家原谅,也请批评指正,谢谢!

注:

  当前模块状态——用于保存当前线程和模块状态的一个结构,可以通过 AfxGetThreadModule()
获得。AFX_MODULE_THREAD_STATE 在 CSocket 重新定义为 _AFX_SOCK_THREAD_STATE
。 

  socket 类型——在 TCP/IP 协议中, Client/Server 网络程序采用 TCP 协议:即 socket
类型为 SOCK_STREAM ,它是可靠的连接方式。在这里不采用 UDP 协议:即 socket 类型为 SOCK_DGRAM
,它是不可靠的连接方式。

深入 CSocket 编程之阻塞和非阻塞模式,布布扣,bubuko.com

时间: 2024-12-06 07:08:28

深入 CSocket 编程之阻塞和非阻塞模式的相关文章

阻塞与非阻塞IO -- 网络编程随想

阻塞和非阻塞IO 阻塞IO指当进行IO操作时, 如果IO操作无法立即完成,当前线程进入阻塞状态,直到IO操作完成,IO函数返回. 非阻塞IO指当进行IO操作时,如果IO操作无法立即完成,IO函数立即返回,线程不会阻塞. 写与读操作对阻塞与非阻塞IO的语义 写操作,只有完成所有指定数据的写入时,写操作才算完成. 读操作,只要能读取到数据,读操作就算完成. 阻塞IO 写操作.len 为指定写入的数据量. 如果只写入部分数据,IO函数会阻塞直至写入数据或发生错误才返回. 以soket的send()为例

网络编程释疑之:同步,异步,阻塞,非阻塞

一讲到网络编程的I/O模型,总会涉及到这几个概念.问了很多人,没几个能清晰地讲出他们之间的区别联系,甚至在网络上也有很多不同的观点,也不知是中国文字释义的博大精深,还是本来这几个概念就是绕人不倦.今天我也来给大家讲解一下我对这几个概念的理解. 既然网络上众说纷纭,不如找个权威参考一下,这个权威就是<UNIX网络编程:卷一>第六章——I/O复用.书中向我们提及了5种类UNIX下可用的I/O模型: 阻塞式I/O: 非阻塞式I/O: I/O复用(select,poll,epoll...): 信号驱动

(转载)网络编程释疑之:同步,异步,阻塞,非阻塞

一讲到网络编程的I/O模型,总会涉及到这几个概念.问了很多人,没几个能清晰地讲出他们之间的区别联系,甚至在网络上也有很多不同的观点,也不知是中国文字释义的博大精深,还是本来这几个概念就是绕人不倦.今天我也来给大家讲解一下我对这几个概念的理解. 既然网络上众说纷纭,不如找个权威参考一下,这个权威就是<UNIX网络编程:卷一>第六章——I/O复用.书中向我们提及了5种类UNIX下可用的I/O模型: 阻塞式I/O: 非阻塞式I/O: I/O复用(select,poll,epoll...): 信号驱动

socket编程的同步、异步与阻塞、非阻塞示例详解

socket编程的同步.异步与阻塞.非阻塞示例详解之一 分类: 架构设计与优化 简介图 1. 基本 Linux I/O 模型的简单矩阵 每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点.本节将简要对其一一进行介绍. 一.同步阻塞模式在这个模式中,用户空间的应用程序执行一个系统调用,并阻塞,直到系统调用完成为止(数据传输完成或发生错误). /* * \brief * tcp client */ #include <stdio.h> #include <stdlib

Node.js学习笔记【1】入门(服务器JS、函数式编程、阻塞与非阻塞、回调、事件、内部和外部模块)

笔记来自<Node入门>@2011 Manuel Kiessling JavaScript与Node.js Node.js事实上既是一个运行时环境,同时又是一个库. 使用Node.js时,我们不仅仅在实现一个应用,同时还实现了整个HTTP服务器. 一个基础的HTTP服务器 server.js:一个可以工作的HTTP服务器 var http = require("http"); http.createServer(function(request, response) { r

Socket编程中,阻塞与非阻塞的区别

阻塞:一般的I/O操作可以在新建的流中运用.在服务器回应前它等待客户端发送一个空白的行.当会话结束时,服务器关闭流和客户端socket.如果在队列中没有请示将会出现什么情况呢?那个方法将会等待一个的到来.这个行为叫阻塞.accept()方法将会阻塞服务器线程直到一个呼叫到来.当5个连接处理完闭之后,服务器退出.任何的在队列中的呼叫将会被取消. 非阻塞:非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回.比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,

阻塞和非阻塞

对于阻塞和非阻塞主要是消息的处理机制,并不是说非阻塞模式下完全没有阻塞的东西.简单点说: 阻塞就是干不完不准回来,   非阻塞就是你先干,我现看看有其他事没有,完了告诉我一声 我们拿最常用的send和recv两个函数来说吧... 比 如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不 代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话...这时候就体现

同步、异步、阻塞和非阻塞区别

简单点说: 阻塞就是干不完不准回来,一直处于等待中,直到事情处理完成才返回: 非阻塞就是你先干,我先看看有其他事没有,一发现事情被卡住,马上报告领导. 我们拿最常用的send和recv两个函数来说吧... 比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话...这时候就体现出阻塞和非阻塞的不同之处了:

同步与异步、阻塞与非阻塞

"阻塞"与"非阻塞"与"同步"与"异步"不能简单的从字面理解,提供一个从分布式系统角度的回答.1.同步与异步同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回.但是一旦调用返回,就得到返回值了.换句话说,就是由*调用者*主动等待这个*调用*的结果. 而异步则是相反