winsock编程宝典之收送消息

转载:http://blog.csdn.net/msgsnd/article/details/2153768

收送资料

在前一期的文章中,笔者为大家介绍了如何在 Winsock 环境下建立主从架构(Client/Server)的 TCP socket 的连接建立与关闭;今天笔者将继续为大家介绍如何利用 TCP socket 来收送资料,并详细解说 WSAAsyncSelect 函式中的FD_READ 及 FD_WRITE事件。相信读者们已经知道 TCP socket 的连接是在 Client 端呼叫 connect 函式成功,且 Server 端呼叫 accept 函式後,才算完全建立成功;当连接建立成功後, Client 及 Server 也就可以利用这个连接成功的 socket 来传送资料到对方,或是收取对方送过来的资料了。在介绍资料的收送前,笔者先介绍一下 TCP socket 与 UDP socket 在传送资料时的特性:

 Stream (TCP) Socket 提供「双向」、「可靠」、「有次序」、「不重覆」之资料传送。

Datagram (UDP) Socket 则提供「双向」之沟通,但没有「可靠」、「有次序」、「不重覆」等之保证;

所以使用者可能会收到无次序、重覆之资料,甚至资料在传输过程中也可能会遗漏。由於 UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以我们常用的一些应用程式(如 telnet、mail、ftp、news...等)都是采用 TCP Socket,以保证资料的正确性。

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,但是资料并不从系统的 input buffer 中移走;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,然後就返回,并告知使用者送出了多少资料,并不须等所有资料都放到 output buffer 才返回。我们将要求放在 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,而不该再对它做任何动作。

介绍完了 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, int tolen );

参数:

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()函式执行完成,并不表示资料已经成功地送抵对方了,而可能仍在系统的 output buffer 中。 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 方式取得。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值