[转帖]简单的 Winsock 应用程序设计(4)

来源:互联网 发布:尚学堂java视频百度云 编辑:程序博客网 时间:2024/06/09 14:08
********************************************************************Copyright by 林军鼐 (Lin Jyun-Naih)本文稿非经同意, 不得转载于任何刊物或做任何商业用途*********************************************************************/
    
    简单的 Winsock 应用程序设计(4)
    
    林 军 鼐
    
    笔者在前几期的文章中已经介绍了大部份 Winsock 1.1 所提供的应用程序发展接口;笔者也相信有读者已经开始利用这些 API 来开发自己的网络应用程序了。但是可能仍有部份读者还是不清楚自己该先有哪些发展工具才能开发Winsock 1.1 的应用程序?
    
    基本上,读者当然一定要有 Microsoft C 或 Borland C 之类的编译程序(Compiler)才能编译您的程序;至于和 Winsock 有关的档案只有两个,一个是『winsock.h』,另一个是『winsock.lib』。这两个档案,读者们可以利用anonymous ftp 的方式从 SEEDNET 台北主机「tpts1.seed.net.tw」的『UPLOAD/WINKING/Winsock_Documents』目录下取得。
    
    接着笔者要再为各位介绍剩下的几个函式,包括 select()、setsockopt()、getsockopt(),以及变更系统的 Blocking Hook 函式时,所要用到的WSASetBlockingHook() 和 WSAUnhookBlockingHook()。
    
    【特殊的 select 函式】
    
    如果写过 UNIX BSD socket 程序的读者,一定都知道这个 select() 函式是很好用的。因为它可以帮您检查一整组(set)的 sockets 是否可以读、写资料,也可以用来检查 socket 是否已和对方连接成功,或者是对方是否已将相对的socket 关闭了等等。
    
    但是在 Winsock 1.1 及 MS Windows 3.X 「非强制性多任务」的环境下,它是否仍是那么好用呢?我们在使用它时,是否要注意些什么呢?现在就让笔者来告诉您吧。
    
    ◎ select():检查一或多个 Sockets 是否处于可读、可写或错误的状态。格 式: int PASCAL FAR select( int nfds, fd_set FAR *readfds,fd_set FAR *writefds, fd_set FAR *exceptfds, const struct timeval FAR*timeout )参 数: nfds 此参数在此并无作用readfds 要被检查是否可读的 Socketswritefds 要被检查是否可写的 Socketsexceptfds 要被检查是否有错误的 Socketstimeout 此函式该等待的时间传回值: 成功 - 符合条件的 Sockets 总数 (若 Timeout 发生,则为 0)失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 使用者可利用此函式来检查 Sockets 是否有资料可被读取,或是有空间可以写入,或是有错误发生。
    
    Winsock 1.1 所提供的 select() 函式与 UNIX BSD 的 select() 函式,在参数的个数及资料型态上是一样,都有 nfds、readfds、writefds、exceptfds、及 timeout五个参数;但是 Winsock 的 nfds 是没有作用的,有这个参数的目的,只是为了与 UNIX BSD 的 select() 函式一致。
    
    至于 readfds、writefds、exceptfds 同样是一组 sockets 的集合,所以您可以同时设定许多 sockets 的号码在这三个参数里面;当然这些 sockets 必须是属于您的这个应用程序所建立的。如果您设定的 socket 号码中有任一个不是属于您的这个程序的话,呼叫 select() 函式便会失败(错误码为 10038WSAENOTSOCK)。
    
    Winsock 同样也提供了一些 macros 来让您设定或检查 readfds、writefds、exceptfds 的值,包括有:(其中 s 代表的是某一个 socket 的号码,set 代表的就是 readfds、writefds 或 exceptfds)
    
    FD_ZERO(*set) -- 将 set 的值清干净FD_SET(s, *set) -- 将 s 加到 set 中FD_CLR(s, *set) -- 将 s 从 set 中删除FD_ISSET(s, *set) -- 检查 s 是否存在于 set 中
    
    读者们要知道参数 readfds、writefds、及 exceptfds 都是 「called by value-result」;而「called by value-result」的意思就是说,我们在将参数传给系统时,要先设启始值,并将这些参数的地址(address)告诉系统;而系统则会利用到这些值来做些运算或其它用途,最后并将结果再写回这些参数的地址中。因此这些参数的值在传入前和函式回返后,可能会不同;所以读者们每次呼叫select() 前,对这些参数一定要重新设定它们的值。
    
    假设我们要检查 socket 1 和 2 目前是否可以用来传送资料,以及 socket 3 是否有资料可读;我们不打算检查 sockets 是否有错误发生,所以 exceptfds 设为NULL。步骤大致如下:
    
    FD_ZERO( &writefds ); /* 清除 writefds */FD_ZERO( &readfds ); /* 清除 readfds */FD_SET( 1, &writefds ); /* 将 socket 1 加到 writefds */FD_SET( 2, &writefds ); /* 将 socket 2 加到 writefds */FD_SET( 3, &readfds ); /* 将 socket 3 加到 readfds */select( ..., &readfds, &writefds, NULL, ...) /* 呼叫 select() 来检查事件 */if (FD_ISSET( 1, &writefds )) /* 检查 socket 1 是否可写 */send( 1, data ); /* 呼叫 send() 一定成功 */if (FD_ISSET( 2, &writefds )) /* 检查 socket 2 是否可写 */send( 2, data ); /* 呼叫 send() 一定成功 */if (FD_ISSET( 3, &readfds )) /* 检查 socket 2 是否可读 */recv( 3, data ); /* 呼叫 recv() 一定成功 */
    
    select() 函式的第五个参数「timeout」,是让我们用来设定 select 函式要等待(block)多久。兹述说如下:
    
    (1)如果 timeout 设为「NULL」,那么 select() 就会一直等到「至少」某一个 socket 的事件成立了才会 return,这和其它的 blocking 函式一样。
    
    select( ..., NULL ) /* blocking */
    
    (2)如果 timeout 的值设为 {0, 0} (秒, 微秒),那么 select() 在检查后,不管有没有 socket 的事件成立,都会马上 return,而不会停留。
    
    timeout.tv_sec = timeout.tv_usec = 0;select( ..., &timeout ) /* non-blocking */
    
    (3)如果 timout 设为 {m, n},那么就会等到至少某一个 socket 的事件发生,或是时间到了(m 秒 n 微秒),才会 return。
    
    timeout.tv_sec = m;timeout.tv_usec = n;select( ..., &timeout ) /* wait m secconds n microseconds */
    
    在 UNIX 系统上,我们通常会利用 select() 来做「polling」的动作,检查事件是否发生;但是在 MS Windows 3.X 的环境下一直做 polling 的动作一定要非常小心,不然可能会造成整个 Windows 系统停住(因为 CPU 都被您的程序占用了);所以使用时一定要注意「控制权释放」,不然就是「不要将 timeout 设为 {0,0}」(因为 timeout 设为 {0,0} 的话, Winsock 系统内部可能不会呼叫到Blocking Hook 函式来释放控制权)。UNIX 系统由于是「Time Sharing」的方式,所以并不会有类似的问题。(所谓 polling 的动作是指,您在程序中有一个循环,而在循环内一直呼叫像 select 这样的函式做检查的动作)
    
    select() 除了可以用来检查 socket 是否可擦写外;对于 non-blocking 的socket 在呼叫 connect() 后,也可利用 select() 的 writefds 来检查连接是否已经成功了(当这个 non-blocking 的 socket 被设定在 writefds,且被 select 成功时);此外,我们亦可利用 readfds 来检查 TCP socket 连接的对方是否已经关闭了(当此 socket 被设定在 readfds,且被 select 成功,但呼叫 recv 去收资料却 return 0时)。
    
    (图 1.) select 函式的几种不同用途
    
    UNIX 系统上因为没有提供 WSAAsyncSelect() 函式,所以我们要用 select()函式来做 polling 的动作;但是 Winsock 系统上已经有了可以设定异步事件的WSAAsyncSelect() 函式,为了让 MS Windows 「讯息驱动」(message driven)的环境更有效率,读者们应该尽量使用 WSAAsyncSelect(),而少用 select() 的方式;这也是当初为什么要定义一个 WSAAsyncSelect() 函式的最大目的。
    
    【变更 socket 的 options 的函式】
    
    Winsock 1.1 也提供了一个变更 socket options 的 setsockopt() 函式;由于options 的项目很多,笔者仅就数个较会用到的项目来解说,其余的项目请读者们自行研究。
    
    ◎ setsockopt():设定 Socket 的 options。格 式: int PASCAL FAR setsockopt( SOCKET s, int level, intoptname,const char FAR *optval, int optlen )参 数: s Socket 的识别码level option 设定的 level (SOL_SOCKET 或IPPROTO_TCP)optname option 名称optval option 的设定值optlen option 设定值的长度传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来设定 Socket 的一些 options,藉以更改其动作。可更改的options 有:(详见 Winsock Spec. 54 页)
    
    Option Type-----------------------------------------------------SO_BROADCAST BOOLSO_DEBUG BOOLSO_DONTLINGER BOOLSO_DONTROUTE BOOLSO_KEEPALIVE BOOLSO_LINGER struct linger FAR*SO_OOBINLINE BOOLSO_RCVBUF intSO_REUSEADDR BOOLSO_SNDBUF intTCP_NODELAY BOOL
    
    (1)SO_BROADCAST -- 适用于 UDP socket。其意义是允许 UDP socket「广播」(broadcast)讯息到网络上。(2)SO_DONTLINGER -- 适用于 TCP socket。其意义是让 socket 在呼叫closesocket() 关闭时,能马上 return,而不用等到资料都送完后才从函式呼叫return;closesocket() 函式 return 后,系统仍会继续将资料全部送完后,才真正地将这个 socket 关闭。一个 TCP socket 在开启时的默认值即是 Don't Linger。(3)SO_LINGER -- 适用于 TCP socket 来设定 linger 值之用。如果 linger 的值设为 0,那么在呼叫 closesocket() 关闭 socket 时,如果该 socket 的 output buffer中还有资料的话,将会被系统所忽略,而不会被送出,此时 closesocket() 也会马上 return;如果 linger 值设为 n 秒,那么系统就会在这个时间内,尝试去送出output buffer 中的资料,时间到了或是资料送完了,才会从 closesocket() 呼叫return。(4)SO_REUSEADDR -- 允许 socket 呼叫 bind() 去设定一个已经用过的地址(含 port number)。
    
    我们就以设定某个 socket 的 linger 值为例,看看程序中该如何呼叫 setsockopt()这个函式:
    
    struct linger Linger;Linger.l_onoff = 1; /* 开启 linger 设定*/Linger.l_linger = n; /* 设定 linger 时间为 n 秒 */setsockopt( s, SOL_SOCKET, SO_LINGER, &Linger, sizeof(struct linger) )
    
    相对地,如果我们想要知道目前的某个 option 的设定值,那么就可以利用getsockopt() 函式来取得。
    
    ◎ getsockopt():取得某一 Socket 目前某个 option 的设定值。格式: int PASCAL FAR getsockopt( SOCKET s, int level, int optname,char FAR *optval, int FAR *optlen )参 数: s Socket 的识别码level option 设定的 leveloptname option 名称optval option 的设定值optlen option 设定值的长度传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来获取目前 Socket的某些 options 设定值。
    
    同样地,我们仍以取得某个 socket 的 linger 值为例,看一下程序中应该如何呼叫 getsockopt():
    
    struct linger Linger;int opt_len = sizeof(struct linger);getsockopt( s, SOL_SOCKET, SO_LINGER, &Linger, &opt_len)
    
    【什么是 Blocking Hook 函式及如何设定自己的 Blocking Hook 函式】
    
    什么是「Blocking Hook」函式呢?在解释之前,我们要先来剖析一下Winsock 1.1 提供的 Blocking 函式(如 accept、connect 等)的内部究竟做了哪些事?
    
    在 Winsock Stack 的 Blocking 函式内部,除了会检查一些条件外(比如该应用程序是否已呼叫过 WSAStartup()?传入的参数是否正确?等等),便会进入一个类似下面的循环:
    
    for (;;) {/* 执行 Blocking Hook 函式 */while (BlockingHook());/* 检查使用者是否已经呼叫了 WSACancelBlockingCall()? */if (operation_cancelled())break;/* 检查动作是否完成了? */if (operation_complete())break;}
    
    现在我们可以很清楚地知道 Blocking 函式的循环中,有三件重要的事:(1)执行 Blocking Hook 函式(2)检查使用者是否呼叫了 WSACancelBlockingCall()来取消此 Blocking 函式的呼叫?(3)检查此 Blocking 函式的动作是否已经完成了?
    
    读者们必须注意,不同的 Winsock Stack 在执行这三件事时的顺序可能会不相同;有的 Winsock Stack 可能会先检查 Blocking 函式的动作是否已经完成了,然后再执行 Blocking Hook 函式;所以 Blocking Hook 函式有可能不会被呼叫到。待会解释完 Blocking Hook 函式的重点后,读者们就可以知道笔者为什么在前面告诉各位在使用 polling 方式时一定要非常小心了。
    
    由上面的循环,我们现在可以知道 Blocking Hook 函式的使用时机是让系统在等待 Blocking 函式完成前所呼叫的,它并不是给我们自己的应用程序所使用的。Winsock 系统本身内部就有一个预设的 Blocking Hook 函式;现在我们就来看一下这个预设的 Blocking Hook 函式会做些什么事?
    
    BOOL DefaultBlockingHook(void) {MSG msg;BOOL ret;/* 取得下一个讯息;如果有,就处理它;如果没有,就释出控制权 */ret = (BOOL) PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);if (ret) {TranslateMessage(&msg);DispatchMessage(&msg);}return ret;}
    
    哦!原来 Blocking Hook 函式中很重要的地方就是:让 Blocking 函式在等待动作完成前能够处理其它讯息,或是释出 CPU 控制权,以便让其它的应用程序也有执行的机会。
    
    现在回到前面一点的地方,大家仔细想一想:如果在一个 Winsock Stack 的Blocking 函式的循环内,先检查 Blocking 函式的动作是否已经完成了,然后再执行 Blocking Hook 函式的话;那么是否就有可能不会释出 CPU 控制权来让其它的程序有执行的机会呢?如果我们的程序中再有类似下面的一个循环,那么整个Windows 环境可能就会因我们的程序而 hang 住了。
    
    for (;;) {FD_ZERO(&writefds);FD_SET( s, &writefds );timeout.tv_sec = timeout.tv_usec = 0;n = select( 64, NULL, &writefds, NULL, &timeout );if ( n > 0 )break;if ( n == 0) /* timeout */continue;...}send( s, data ... );
    
    在这个循环例子中,我们原是希望利用 select() 及 polling 的方式来检查 socket的 output buffer 中是否尚有空间可写入资料?如果此时 output buffer 恰好满了,select() 函式中一检查到如此的情况,且 timeout 又是 {0,0},那么就会马上 return0,而不会呼叫到 Blocking Hook 函式来释放 CPU 控制权给 Windows 环境中的其他程序(包括 Winsock 收送的 Protocol Stack );由于没有分配到 CPU 时间,所以 Winsock Kernel 便无法将 output buffer 中任何资料送出;循环中由 select() 回返后,又回到循环的最前面,然后又呼叫 select(),马上又 timeout......;Windows 系统因此就 hang 住了 !
    
    Blocking Hook 函式中除了 CPU 控制权释放的问题外,还需注意什么呢?大家再看一看前面 Blocking 函式的循环;循环内呼叫 Blocking Hook 函式是包在另一个无穷的 while 循环内。如果一个 Blocking Hook 函式的 return 值永远不为 0 的话,那么也就永远被困在这个无穷循环内了;所以我们在设计自己的 BlockingHook 函式时一定也要非常小心这个 return 值。
    
    知道了 Blocking Hook 函式的用途及设计 Blocking Hook 函式该注意的地方后,我们究竟要如何取代掉系统原有的 Blocking Hook 函式呢?那就要利用WSASetBlockingHook() 函式了。
    
    ◎ WSASetBlockingHook():建立应用程序指定的 blocking hook 函式。格 式: FARPROC PASCAL FAR WSASetBlockingHook( FARPROClpBlockFunc )参 数: lpBlockfunc 指向要装设的 blocking hook 函式的地址的指针传回值: 指向前一个 blocking hook 函式的地址的指针说明: 此函式让使用者可以设定他自己的 Blocking Hook 函式,以取代原先系统预设的函式。被设定的函式将会在应用程序呼叫到「blocking」动作时执行。唯一可在使用者指定的 blocking hook 函式中呼叫的 Winsock 接口函式只有WSACancelBlockingCall()。
    
    假设我们自己设计了一个 Blocking Hook 函式叫 myblockinghook(),那么在程式中向 Winsock 系统注册的方法如下:(其中 _hInst 代表此 task 的 Instance)
    
    FARPROC lpmybkhook = NULL;lpmybkhook = MakeProcInstance( (FARPROC)myblockinghook, _hInst) );WSASetBlockingHook( (FARPROC)lpmybkhook );
    
    (图 2.)设定自己的 Blocking Hook 函式
    
    我们在设定自己的 Blocking Hook 程序后,仍可以利用WSAUnhookBlockingHook() 函式,来取消我们设定的 Blocking Hook 函式,而变更回原先系统内定的 Blocking Hook 函式。
    
    ◎ WSAUnhookBlockingHook():复原系统预设的 blocking hook 函式。格 式: int PASCAL FAR WSAUnhookBlockingHook( void )参 数: 无传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式取消使用者设定的 blocking hook 函式,而回复系统原先预设的 blocking hook 函式。
    
    最后笔者要再说明一点,一个应用程序所设定的 Blocking Hook 函式,只会被这个应用程序所使用;其它的应用程序并不会执行到您设定的 Blocking Hook 函式的。另外,若非极有必要,最好是不要任意变更系统的 Blocking Hook 函式;因为一旦您没有设计好的话,整个 Windows 环境可能就完蛋了。
    
    (图 3.)使用自己的 Blocking Hook 函式时该注意事项
    
    【结语】
    
    四期的「Winsock 应用程序设计篇」在此结束了;笔者除了介绍 Winsock API外,也将自己亲身设计 winsock.dll 的经验与各位读者分享了;希望这几期的文章,对于国内想要在 Winsock 1.1 环境上开发网络应用程序的读者有些许的帮助。谢谢大家。