Programming 版 (精华区)

发信人: superman (风雨无阻), 信区: Programming
标  题: 简单的 Winsock 应用程式设计(4)(fwd) (转载)
发信站: 紫丁香 (Thu Oct  9 21:55:55 1997)

【 以下文字转载自 Winsock 讨论区 】
【 原文由 topcon 所发表 】
发信站: 白山黑水 (Tue Apr 23 21:33:33 1996)

简单的 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 要被检查是否可读的 Sockets
        writefds        要被检查是否可写的 Sockets
        exceptfds       要被检查是否有错误的 Sockets
        timeout 此函式该等待的时间
传回值:        成功 - 符合条件的 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() 函式便会失败(错误码为 10038
WSAENOTSOCK)。

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, int
optname,
        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    BOOL
        SO_DEBUG        BOOL
        SO_DONTLINGER   BOOL
        SO_DONTROUTE    BOOL
        SO_KEEPALIVE    BOOL
        SO_LINGER       struct linger FAR*
        SO_OOBINLINE    BOOL
        SO_RCVBUF       int
        SO_REUSEADDR    BOOL
        SO_SNDBUF       int
        TCP_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 设定的 level
        optname 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},那麽就会马上 return
0,而不会呼叫到 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 的
话,那麽也就永远被困在这个无穷回圈内了;所以我们在设计自己的 Blocking
Hook 函式时一定也要非常小心这个 return 值。

知道了 Blocking Hook 函式的用途及设计 Blocking Hook 函式该注意的地方
後,我们究竟要如何取代掉系统原有的 Blocking Hook 函式呢?那就要利用
WSASetBlockingHook() 函式了。

◎ WSASetBlockingHook():建立应用程式指定的 blocking hook 函式。
格  式: FARPROC PASCAL FAR WSASetBlockingHook( FARPROC
lpBlockFunc )
参  数:        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 环境上开发网路应用程式的读者有些许的帮
助。谢谢大家。

--
※ 来源:.白山黑水 bbs.neu.edu.cn.[FROM: beaver]

--
※ 来源:·哈尔滨紫丁香站 bbs1.hit.edu.cn·[FROM: bbs@conger.neu.edu.c] 
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:209.535毫秒