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

**********************************************************************Copyright by 林军鼐文稿内容不得转载于任何商业书刊或做任何商业用途**********************************************************************
    
    简单的 Winsock 应用程序设计(2)
    
    林 军 鼐
    
    在前一期的文章中,笔者为大家介绍了如何在 Winsock 环境下,建立主从架构(Client/Server)的 TCP socket 的连接建立与关闭;今天笔者将继续为大家介绍如何利用 TCP socket 来收送资料,并详细解说 WSAAsyncSelect 函式中的FD_READ 及 FD_WRITE 事件(笔者曾发现有相当多人对这两个事件甚不了解)。
    
    相信读者们已经知道 TCP socket 的连接是在 Client 端呼叫 connect 函式成功,且 Server 端呼叫 accept 函式后,才算完全建立成功;当连接建立成功后,Client 及 Server 也就可以利用这个连接成功的 socket 来传送资料到对方,或是收取对方送过来的资料了。
    
    (图 1. TCP socket 的资料收送)
    
    在介绍资料的收送前,笔者先介绍一下 TCP socket 与 UDP socket 在传送资料时的特性:
    
    Stream (TCP) Socket 提供「双向」、「可靠」、「有次序」、「不重复」之资料传送。
    
    Datagram (UDP) Socket 则提供「双向」之沟通,但没有「可靠」、「有次序」、「不重复」等之保证; 所以使用者可能会收到无次序、重复之资料,甚至资料在传输过程中也可能会遗漏。
    
    由于 UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以我们常用的一些应用程序(如 telnet、mail、ftp、news...等)都是采用 TCPSocket,以保证资料的正确性。(TCP 及 UDP 封包的传送协议不在我们讨论范围,想要了解的读者们,请自行参考相关书籍)
    
    TCP 及 UDP Socket 都是双向的,所以我们是利用同一个 Socket 来做传送及收取资料的动作;一般言 TCP Socket 的资料送、收是呼叫 send() 及 recv() 这两个函式来达成,而 UDP Socket 则是用 sendto() 及 recvfrom() 这两个函式。不过TCP Socket 也可用 sendto() 及 recvfrom() 函式,UDP Socket 同样可用 send() 及recv() 函式;这一点我们稍后再加以解释。
    
    现在我们先看一下 send() 及 recv() 的函式说明,并回到我们的前一期程式。
    
    ◎ send():使用连接式(connected)的 Socket 传送资料。格 式: int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );参 数: s Socket 的识别码buf 存放要传送的资料的暂存区len buf 的长度flags 此函式被呼叫的方式传回值: 成功 - 送出的资料长度失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式适用于连接式的 Datagram 或 Stream Socket 来传送资料。 对Datagram Socket 言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对 Stream Socket 言,Blocking 模式下,若是传送 (transport) 系统内之储存空间(output buffer)不够存放这些要传送的资料,send() 将会被 block住,直到资料送完为止;如果该 Socket 被设定为 Non-Blocking 模式,那么将视目前的 output buffer 空间有多少,就送出多少资料,并不会被 block 住。使用者亦须注意 send()函式执行完成,并不表示资料已经成功地送抵对方了,而是已经放到系统的 output buffer 中,等待被送出。 flags 的值可设为 0 或 MSG_DONTROUTE及 MSG_OOB 的组合。(参见 WINSOCK第1.1版48页)
    
    ◎ recv():自 Socket 接收资料。格 式: int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );参 数: s Socket 的识别码buf 存放接收到的资料的暂存区len buf 的长度flags 此函式被呼叫的方式传回值: 成功 - 接收到的资料长度 (若对方 Socket 已关闭,则为 0)失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来自连接式的 Datagram Socket 或 Stream Socket 接收资料。对 Stream Socket 言,我们可以接收到目前 input buffer 内有效的资料,但其数量不超过 len 的大小。若是此 Socket 设定 SO_OOBINLINE,且有 out-of-band 的资料未被读取,那么只有 out-of-band 的资料被取出。对 Datagram Socket 言,只取出第一个 datagram;若是该 datagram 大 于使用者提供的储存空间,那么只有该空间大小的资料被取出,多余的资料将遗失,且回复错误的讯息。另外如果 Socket为 Blocking 模式,且目前 input buffer 内没有任何资料,则 recv() 将 block 到有任何资料到达为止;如果为 Non-Blocking 模式,且 input buffer 无任何资料,则会马上回复错误。参数 flags 的值可为 0 或 MSG_PEEK、MSG_OOB 的组合;MSG_PEEK 代表将资料拷贝到使用者提供的 buffer,但是资料并不从系统的 inputbuffer 中移走;0 则表示拷贝并移走。(参考 WINSOCK 第1.1版41 页)
    
    【Server 端的资料收送及关闭 Socket】
    
    在前一期中,我们说建立的是一个 Asynchronous 模式的 Server;程序中,我们曾对 listen_sd 这个 Socket 呼叫 WSAAsyncSelect() 函式,并设定FD_ACCEPT 事件,所以当 Client 与我们连接时,系统会传给我们一个ASYNC_EVENT 讯息(请参见前一期文章内容);我们在收到讯息并判断是FD_ACCEPT 事件,于是呼叫 accept() 来建立连接。
    
    my_sd = accept(listen_sd, (struct sockaddr far *)&sa, &sa_len)
    
    我们在呼叫完 accept() 函式,成功地建立了 Server 端与 Client 端的连接后,此时便可利用新建的 Socket(my_sd)来收送资料了。由于我们同样希望用Asynchronous 的方式,因此要再利用 WSAAsyncSelect() 函式来帮新建的Socket 设定一些事件,以便事件发生时 Winsock Stack 能主动通知我们。由于我们的 Server 是被动的接受 Client 的要求,然后再做答复,所以我们设定FD_READ 事件;我们也希望 Winsock Stack 在知道 Client 关闭 Socket 时,能主动通知我们,所以同时也设定 FD_CLOSE 事件。(读者须注意,我们设定事件的 Socket 号码是呼叫 accept 后传回的新 Socket 号码,而不是原先监听状态的Socket 号码)
    
    WSAAsyncSelect(my_sd, hwnd, ASYNC_EVENT, FD_READ|FD_CLOSE)
    
    在这里,我们同样是利用 hwnd 这个窗口及 ASYNC_EVENT 这个讯息;在前文中,笔者曾告诉各位,在收到 ASYNC_EVENT 讯息时,我们可以利用WSAGETSELECTEVENT(lParam) 来判断究竟是哪一事件(FD_READ 或FD_CLOSE)发生了;所以并不会混淆。那我们到底在什么时候会收到FD_READ 或 FD_CLOSE 事件的讯息呢?
    
    【FD_READ 事件】
    
    我们会收到 FD_READ 事件通知我们去读取资料的情况有 :
    
    (1)呼叫 WSAAsyncSelect 函式来对此 Socket 设定 FD_READ 事件时,input buffer 中已有资料。(2)原先系统的 input buffer 是空的,当系统再收到资料时,会通知我们。(3)使用者呼叫 recv 或 recvfrom 函式,从 input buffer 读取资料,但是并没有一次将资料读光,此时会再驱动一个 FD_READ 事件,表示仍有资料在input buffer 中。
    
    读者必须注意:如果我们收到 FD_READ 事件通知的讯息,但是我们故意不呼叫 recv 或 recvfrom 来读取资料的话,尔后系统又收到资料时,并不会再次通知我们,一定要等我们呼叫了 recv 或 recvfrom 后,才有可能再收到FD_READ 的事件通知。
    
    【FD_CLOSE 事件】
    
    当系统知道对方已经将 Socket 关闭了的情况下(收到 FIN 通知,并和对方做关闭动作的 hand-shaking),我们会收到 FD_CLOSE 的事件通知,以便我们也能将这个相对的 Socket 关闭。FD_CLOSE 事件只会发生于 TCP Socket,因为它是 connection-oriented;对于 connectionless 的 UDP Socket,即使设了FD_CLOSE,也不会有作用的。
    
    程序中,当 Client 端送一个要求(request)来时,系统会以ASYNC_EVENT 讯息通知我们的 hwnd 窗口;我们在利用WSAGETSELECTEVENT(lParam) 及 WSAGETSELECTERROR(lParam) 知道是FD_READ 事件及检查无误后,便呼叫 recv() 函式来收取 Client 端送来的资料。
    
    recv(wParam, &data, sizeof(data), 0)
    
    笔者在前一期文章中也曾提到说,FD_XXXX 事件发生,收到讯息时,视窗 handle 被呼叫时的参数 wParam 代表的就是事件发生的 Socket 号码,所以此处 wParam 的值也就是前面提到的 my_sd 这个 Socket 号码。recv() 的第四个参数设为 0,表示我们要将资料从系统的 input buffer 中读取并移走。
    
    收到要求后,我们要答复 Client 端,也就是要送资料给 Client;这时我们就要利用 send() 这个函式了。
    
    我们先将资料放到 data 这个资料暂存区,然后呼叫 send() 将它送出,我们利用的也是 wParam (my_sd) 这个同样的 Socket 来做传送的动作,因为它是双向的。
    
    send(wParam, &data, strlen(data), 0)
    
    Server 与 Client 收送资料一段时间后(资料全部收送完毕),如果 Client 端先呼叫 closesocket() 将它那端的 Socket 关闭,那么系统在知道后,会通知我们一个 FD_CLOSE 事件的讯息,此时我们也可以呼叫 closesocket() 将我们这端的Socket 关闭了;当然我们也可以呼叫 closesocket() 先主动关闭我们这端的Socket。
    
    【Client 端的资料收送及关闭 Socket】
    
    我们例子的 Client 是采 Blocking 模式,所以在呼叫 connect() 函式与 Server连接时,可能会等一下子才成功;connect() 函式返回后,且无错误发生的话,Client 与 Server 端的 TCP socket 连接就算成功了。这时,我们便可利用这个连接成功的 Socket 来送收资料了。由于我们并没有要设定为 Asynchronous 模式,所以也不用呼叫 WSAAsyncSelect() 来设定事件。
    
    Client 端通常是会先主动发出要求到 Server 端,因此我们呼叫 send() 来传送此一资料。我们的资料量很小,所以并不会被 send() 函式 Block 住;不过如果您要送的资料量很大,那么可能会等一段时间才会自 send() 函式返回;也就是说必须等资料都放到系统的 output buffer 后才会返回;这是因为我们 Client 的Socket 是阻拦模式。如果我们用的是非阻拦模式的 Socket,那么 send() 函式会视系统的 output buffer 的空间有多少,只拷贝那么多的资料到 output buffer,然后就返回,并告知使用者送出了多少资料,并不须等所有资料都放到 outputbuffer 才返回。
    
    我们将要求放在 data 资料暂存区,然后呼叫 send() 将要求送出。资料送出后,我们呼叫 recv() 来等待 Server 端的答复。
    
    send(mysd, data, strlen(data), 0)
    
    recv(mysd, &data, sizeof(data), 0)
    
    由于我们 Client 端是 Blocking 模式,所以 recv() 会一直 Block 住,直到下列的情况之一发生,才会返回。
    
    (1)Server 端送来资料。(此时 return 值是读取的资料长度)(2)Server 端将相对的 Socket 关闭了。(此时的 return 值会是 0)(3)Client 端自己呼叫 WSACancelBlockingCall() 来取消 recv() 的呼叫。(此时 return 值是 SOCKET_ERROR 错误,错误码 10004 WSAEINTR)
    
    同样地,资料全部送收完毕后,我们也呼叫 closesocket() 来将 Socket 关闭。
    
    ◎ WSACancelBlockingCall():取消目前正在进行中的 blocking 动作。格 式: int PASCAL FAR WSACancelBlockingCall( void );参 数: 无传回值: 成功 - 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来取消该应用程序正在进行中的 blocking 动作。通常的使用时机有:(a) Blocking 动作正在进行中,该应用程序又收到某一讯息(Mouse、Keyboard、Timer 等),则可在处理该讯息的段落中呼叫此函式。(b)Blocking 动作正在进行中,而 Windows Sockets 又呼叫响应用程序的「blocking hook」函式时,在该函式内可呼叫此函式来取消 blocking 动作。使用者必须注意,在某一 Winsock blocking 函式动作进行时,除了WSAIsBlocking() 及 WSACancelBlockingCall() 外,不可以再呼叫其它任何Windows Sockets DLL 提供的函式,否则会产生错误。另外若取消的blocking 动作不是 accept() 或 select() 的话,那么该 Socket 可能会处于未定状态,使用者最好是呼叫 closesocket() 来关闭该 Socket,而不该再对它做任何动作。
    
    (图 2.)demoserv 与 democlnt 在资策会 WinKing 上收送资料的画面
    
    (图 3.)demoserv 与 democlnt 在资策会 WinKing 上关闭 Socket 后的画面
    
    介绍完了 TCP Socket 的资料收送,笔者接着为读者介绍 sendto() 及recvfrom() 这两个函式,以及许多人可能很容易搞错的 FD_WRITE 事件。
    
    【sendto 及 recvfrom 函式】
    
    一般言,TCP Socket 使用的是 send() 及 recv() 这两个函式;而 UDP Socket用的是 sendto() 及 recvfrom() 函式。这是因为 TCP 是 Connection-oriented,必须做完 Socket 真正的连接程序后,才可以开始收送资料,此时系统已经知道了连接的对方,所以我们不用再指定资料要送到哪里。而 UDP 是 Connectionless,收送资料的双方并没有建立真正的连接,所以我们要利用 sendto() 及 recvfrom()来指定收资料的对方及获知是谁送资料给我们。
    
    TCP Socket 也可以用 sendto() 及 recvfrom() 来送收资料,只是此时这两个函式的最后两个参数没有作用,会被系统所忽略。而 UDP Socket 如果呼叫了connect() 函式来指定对方的地址(这个 connect 并不会真的和对方做连接的动作,而是告知我们本身的系统说我们只想收、送何方的资料),那么也可以利用 send() 及 recv() 来送收资料。
    
    ◎ sendto():将资料送到使用者指定的目的地。格 式: int PASCAL FAR sendto( SOCKET s, const char FAR *buf,int len, int flags, const struct sockaddr FAR *to, inttolen );参 数: s Socket 的识别码buf 存放要传送的资料的暂存区len buf 的长度flags 此函式被呼叫的方式to 资料要送达的地址tolen to 的大小传回值: 成功 - 送出的资料长度失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式适用于 Datagram 或 Stream Socket 来传送资料到指定的地址。 对 Datagram Socket 言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对 Stream Socket 言,其作用与 send() 相同;参数 to 及 tolen 的值将被系统所忽略。 若是传送 (transport) 系统内之储存空间不够存放这些要传送的资料,sendto() 将会被 block 住,直到资料都被送出;除非该 Socket 被设定为 non-blocking 模式。使用者亦须注意 sendto()函式执行完成,并不表示资料已经成功地送抵对方了,而可能仍在系统的 outputbuffer 中。 flags 的值可设为 0、MSG_DONTROUTE 及 MSG_OOB 的组合。(参见 WINSOCK第1.1版51页)
    
    ◎ recvfrom():读取资料,并储存资料来源的地址。格 式: int PASCAL FAR recvfrom( SOCKET s, char FAR *buf, int len, int flags,struct socketaddr FAR *from, int FAR *fromlen );参 数: s Socket 的识别码buf 存放接收到的资料的暂存区len buf 的长度flags 此函式被呼叫的方式from 资料来源的地址fromlen from 的大小传回值: 成功 - 接收到的资料长度 (若对方 Socket 已关闭,则为 0)失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明: 此函式用来读取资料并记录资料来源的地址。对 Datagram Socket(UDP)言,一次读取一个 Datagram;对 Stream Socket (TCP)言,其作用与recv() 相同,参数 from 及 fromlen 的值会被系统忽略。如果 Socket 为 Blocking 模式,且目前 input buffer 内没有任何资料,则 recvftom() 将 block 到有任何资料到达为止;如果为 Non-Blocking 模式,且 input buffer 无任何资料,则会马上回复错误。
    
    【FD_WRITE 事件】
    
    笔者在前面介绍过 FD_READ 事件的发生时机,现在继续介绍 FD_WRITE这个较易使人混淆的事件,因为真的有相当多的人对此一事件的发生不明了。
    
    由字面上看,FD_WRITE 应该是要求系统通知我们某个 Socket 现在是否可以呼叫 send() 或 sendto() 来传送资料?答案可以说「是」,但是它和 FD_READ却又有不同的地方。
    
    在前面我们知道呼叫一次 recv() 后,如果 input buffer 中尚有资料未被取出的话,系统会再通知我们一次 FD_READ。那么如果我们呼叫一次 send() 后,系统的 output buffer 仍有空间可写入的话,它是否会再通知我们一个FD_WRITE,叫我们继续传送资料呢?这个答案就是「否定」的了!系统并不会再通知我们了。
    
    系统会通知我们 FD_WRITE 事件的讯息,只有下列几种情况:
    
    (1)呼叫 WSAAsyncSelect() 来设定 FD_WRITE 事件时,Socket 已经可以传送资料(TCP scoket 已经和对方连接成功了,或 UDP socket 已建立完成),且目前 output buffer 仍有空间可写入资料。(2)呼叫 WSAAsyncSelect() 来设定 FD_WRITE 事件时,Socket 尚不能传送资料,不过一旦 Socket 与对方连接成功,马上就会收到 FD_WRITE 的通知。(3)呼叫 send() 或 sendto() 传送资料时,系统告知错误,且错误码为10035 WSAEWOULDBLOCK (呼叫 WSAGetLastError() 得知这项错误),这时表示 output buffer 已经满了,无法再写入任何资料(此时即令呼叫再多次的send() 也都一定失败);一旦系统将部份资料成功送抵对方,空出 output buffer后,便会送一个 FD_WRITE 给使用者,告知可继续传送资料了。换句话说,读者在呼叫 send() 传送资料时,只要不是返回错误 10035 的话,便可一直继续呼叫 send() 来传送资料;一旦 send() 回返错误 10035,那么便不要再呼叫 send()传送资料,而须等收到 FD_WRITE 后,再继续传送资料。
    
    【结语】
    
    在这一期的文章中,笔者介绍了各位有关 TCP Socket 的资料收、送方式及FD_READ、FD_WRITE 等事件的发生时机;读者们综合前一期的文章,应该已经可以建立出一对主从架构的程序,并利用 TCP Socket 来传送资料了。
    
    下一期,笔者将继续介绍有关如何获取网络信息的函式,如gethostname()、getsockname()、getpeername(),以及同步与异步的网络数据库撷取函式 getXbyY()、WSAAsyncGetXByY()。
    
    本文中所提到的 WinKing 试用版可自 SEEDNET 台北主机 tpts1.seed.net.tw(139.175.1.10)的 UPLOAD/WINKING 目录中取得,文件名为 wkdemo.exe;WinKing 提供 Ethernet 及 PPP 联机功能,适用于一般 Ethernet 网络,亦可用来以电话、调制解调器连上 SEEDNET 的 PPP 伺服主机;范例 demoserv、democlnt,以及一些笔者所写的 Winsock 程序(含原始程序代码)则存放在UPLOAD/WINKING/JNLIN 目录下;有兴趣的读者可自行用 anonymous ftp 方式取得。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页