简单的 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...等)都是采用 TCP

Socket,以保证资料的正确性。(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,但是资料并不从系统的 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,而不该再对它做任

何动作。





(图 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, 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 方式

取得。

--

! Origin: ● ZUCAI BBS ● From: 202.121.132.131








像一只青蛙在井底跳啊跳,什么时候才能跳出去呢…………………………

2002-12-10 02:26 #33



风清高
中校

会员



UID 186
精华 6
积分 2088
帖子 990
阅读权限 40
注册 2002-11-4
第二十八篇


简单的 Winsock 应用程式设计(3)



林 军 鼐



在前两期的文章中,笔者介绍了如何在 Winsock 环境下建立主从架构的

TCP Socket,以及如何利用 Socket 来收送资料;今天,我们接著来看一看如何

利用 Winsock 所提供的函式来取得一些基本的网路资料,包括我们本身主机的

名称是什麽、系统主动指定给我们的 Socket 的 IP 位址及 port number、我们的

Socket 所连接的对方是谁、如何查得某些主机的 IP 位址或名称、以及某些

well-known 服务(如 ftp、telnet 等)所用的 port number 是哪一个等等。



今天我们使用的展示程式是笔者以前所撰写的一个针对 Winsock 1.1 的 46

个函式做测试或教学用的程式,有兴趣了解 46 个函式该如何呼叫的读者,可用

anonymous ftp 方式到 「tpts1.seed.net.tw」 的 「UPLOAD/WINKING/JNLIN」

目录下取得此程式的执行档及原始程式码,档名为 hello.*。读者们也可利用

hello 程式来模拟 Server 或 Client 程式,以验证我们所做的动作。



【如何知道我们所使用的 local 主机名称】



通常我们都会帮我们自己所使用的这台主机设定一个名称;在程式中,我

们也可以透过 Winsock 所提供的一个称为 gethostname() 的函式来取得这一个主

机名称。



◎ gethostname():获取目前使用者使用的 local host 的名称。

格 式: int PASCAL FAR gethostname( char FAR *name, int namelen );

参 数: name 用来存放 local host 名称的暂存区

namelen name 的大小

传回值: 成功 - 0

失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式用来获取 local host 的名称。



在程式中我们呼叫的方法如下:



gethostname( (char FAR *) hname, sizeof(hname) )



读者们如果使用过 Trumpet Winsock 的话,可能知道 Trumpet 的环境设定中

并没有让我们设定 local host 名称的栏位,所以在执行一些 Public Domain 的

Winsock 应用程式(如 ws_ping、wintalk)时,在呼叫 gethostname() 时会产生错

误;解决的方法是在 Trumpet 的 「hosts」 档中加上您的主机 IP 位址及名称,

那麽呼叫这个函式时就不会再产生错误了。



【如何得知系统主动指定给我们的 IP 位址及 port number】



以前的文章中,笔者曾提到 Client 端的 TCP Socket 在呼叫 connect() 函式去

连接 Server 端之前,可以呼叫 bind() 函式来指定 Client 端 Socket 所用的 IP 位址

及 port number;但是一般而言,我们 Client 端并不需要呼叫 bind() 来指定特定

的 IP 位址及 port number 的,而是交由系统主动帮我们的 Socket 设定 IP 位址及

port number (呼叫 connect() 函式时)。但是我们如何得知系统指定了什麽 IP

位址及 port number 给我们呢?这就要借助 getsockname() 这个函式了。



◎ getsockname():获取 Socket 的 Local 位址及 port number 资料。

格式: int PASCAL FAR getsockname( SOCKET s,

struct sockaddr FAR *name, int FAR *namelen );

参 数: s Socket 的识别码

name 存放此 Socket 的 Local 位址的暂存区

namelen name 的长度

传回值: 成功 - 0

失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是用来取得已设定位址或已连接之 Socket 的本端位址资料。若

是此 Socket 被设定为 INADDR_ANY,则需等真正建立连接成功後才会传回正确

的位址。



在程式中呼叫的方法为:



struct sockaddr_in sa;

int salen = sizeof(sa);

getsockname( sd, (struct sockaddr FAR *)&sa, &salen )



【如何知道和我们的 Socket 连接的对方是谁】



连接的 Socket 是有两端的,所以相对於 getsockname() 函式,Winsock 也提

供了一个 getpeername() 函式,来让我们获得与我们连接的对方的 IP 位址与 port

number。



◎ getpeername():获取连接成功之 Socket 的对方 IP 位址及 port number。

格 式: int PASCAL FAR getpeername( SOCKET s,

struct sockaddr FAR *name, int FAR *namelen );

参 数: s Socket 的识别码

name 储存与此 Socket 连接的对方 IP 位址的暂存区

namelen name 的长度

传回值: 成功 - 0

失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式可用来取得已连接成功的 Socket 的彼端之位址资料。



呼叫的方式如下:



struct sockaddr_in sa;

int salen = sizeof(sa);

getpeername( sd, (struct sockaddr FAR *)&sa, &salen )



现在我们仍然利用 WinKing 来当我们的 Winsock Stack,并利用它所提供的

工具来观察 Sockets 的连结及资料是否正确。



由图 1,我们可以由 WinKing 的视窗看到我们设定这台主机的名称是

「vincent」,IP 位址是 「140.92.61.24」。我们并利用两个 hello 程式,一个当

成 Client (画面右边打开者),一个当成 Server (画面左边最小化者)。Server

所用的 port number 是 「7016」; Client 并没有呼叫 bind() 来指定 port

number,而是呼叫 connect() 时由系统指定。



我们呼叫 gethostname(),得到的答案是 「vincent」;而 Client 呼叫

getsockname() 得到自己的 IP 位址是 「140.92.61.24」,port number 是 「2110」

(笔者以前曾提过,由系统主动指定的 port number 会介於 1024 到 5000 间);

再呼叫 getpeername() 得到与 Client 连接的 Server 端 IP 位址是 「140.92.61.24」

(因为我们的 Client 和 Server 都在同一台主机),port number 是 「7016」。果

然没错!(由 WinKing 的 Sockets' Status 视窗亦可观察到相互连接的 Sockets 资

料,与我们呼叫函式所得结果相同)





(图 1)利用 hello 程式来模拟 Client 和 Server



读者必须注意一点,getsockname() 及 getpeername() 所取得的 IP 位址及 port

number 都是 network byte order,而不是 host byte order;如果您想转成 host byte

order,就必须借助 ntohl() 及 ntohs() 两个函式。而我们能看到 IP 位址以「字

串」方式表达出来,则又是利用了 inet_ntoa() 函式;相对地,我们也可利用

inet_addr() 函式将字串方式的 IP 位址转换成 in_addr 格式(network byte order 的

unsigned long)。



◎ inet_ntoa():将一网路位址转换成「点格式」字串。

格 式: char FAR * PASCAL FAR inet_ntoa( struct in_addr in );

参 数: in 一个代表 Internet host 位址的结构

传回值: 成功 - 一个代表位址的「点格式」(dotted) 字串

失败 - NULL

说明: 此函式将一 Internet 位址转换成「a.b.c.d」字串格式。



◎ inet_addr():将字串格式的位址转换成 32 位元 in_addr 的格式。

格 式: unsigned long PASCAL FAR inet_addr( const char FAR *cp );

参 数: cp 一个代表 IP 位址的「点格式」(dotted) 字串

传回值: 成功 - 一个代表 Internet 位址的 unsigned long

失败 - INADDR_NONE

说明: 此函式将一「点格式」的位址字串转换成适用之 Intenet 位址。

「点格式」字串可为以下四种方式之任一:

(i) a.b.c.d (ii) a.b.c (iii) a.b (iv) a



图 1 的 hello 程式中,我们将 Local 资料写到 dispmsg 中,再显示出来;其

用法如下:



wsprintf((LPSTR)dispmsg, "OK! local ip=%s, local port=%d",

inet_ntoa(sa.sin_addr), ntohs(sa.sin_port));



【Winsock 提供的资料库函式】



Winsock 也提供了同步与非同步的网路资料库函式;不过读者们要知道,此

处的资料库指的并非如 Informix, Oracle 等商业用途的资料库系统,而是指主机

IP 位址及名称、well-known 服务的名称及 Socket 型态及所用的 port number、

以及协定(protocol)名称及代码等。



【同步资料库函式】



首先我们来看一下第一组:gethostbyname() 及 gethostbyaddr() 函式



这两个函式的用途是让我们可以由某个主机名称求得它的 IP 位址,或是由

它的 IP 位址求得它的名称。一般我们经常会用到的是由名称求得 IP 位址;因

为很少人会去记某台机器的 IP 位址的,另外 TCP/IP 封包的 IP header 上也必须

记载送、收主机的 IP 位址,而不是主机名称。



◎ gethostbyname():利用某一 host 的名称来获取该 host 的资料。

格 式: struct hostent FAR * PASCAL FAR

gethostbyname( const char FAR *name );

参 数: name host 的名称

传回值: 成功 - 指向一个 hostent 结构的指标

失败 - NULL (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是利用 host 名称来获取该主机的其他资料,如 host 的位址、

别名,位址的型态、长度等。



◎ gethostbyaddr():利用某一 host 的 IP 位址来获取该 host 的资料。

格 式: struct hostent FAR * PASCAL FAR

gethostbyaddr( const char FAR *addr, int len, int type );

参 数: addr network 排列方式的位址

len addr 的长度

type PF_INET(AF_INET)

传回值: 成功 - 指向一个 hostent 结构的指标

失败 - NULL (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是利用 IP 位址来获取该主机的其他资料,如 host 的名称、别

名,位址的型态、长度等。



程式中呼叫的方式分别如下:



char host_name[30];

struct hostent far *htptr;

/* 假设 host_name 的值已先设定为我们要求得资料的主机名称 */

htptr = (struct hostent FAR *) gethostbyname( (char far *) host_name )



struct in_addr host_addr;

struct hostent far *htptr;

/* 假设 host_addr 的值已先设定为我们要求得资料的主机的network byte

order 方式的 IP 位址*/

htptr = (struct hostent FAR *) gethostbyaddr((char far *)&host_addr, 4,

PF_INET)



一般言,程式中呼叫到 gethostbyname() 及 gethostbyaddr() 时,Winsock

Stack 会先在 local 的 「hosts」档中找看看是否有这个主机的资料;如果没有,

则可能再透过「领域名称服务」(Domain Name Service)的功能,向「名称伺

服器」(Name Server)查询;所以呼叫这两个函式时,有时会等一下子才获得

答覆。如果您想让程式执行快一些的话,可将常用主机的资料放在 hosts 档中,

这样就不必透过 DNS 去查询了。



接下来我们来看 getservbyname() 及 getservbyport() 这两个函式。



大部份的读者应该都用过 telnet、mail、ftp、news 等服务应用程式;这些应

用程式的协定,比如服务名称、伺服器端所用的 port number、以及 Socket 的型

态,都是固定的;这些资料,我们就可以利用 getservbyname() 或 getservbyport()

来取得,而不必刻意去记颂它们。



◎ getservbyname():依照服务 (service) 名称及通讯协定(tcp/udp)来获取该

服务的其他资料。

格 式: struct servent * PASCAL FAR

getservbyname( const char FAR *name, const char FAR *proto );

参 数: name 服务名称

proto 通讯协定名称

传回值: 成功 - 一指向 servent 结构的指标

失败 - NULL (呼叫 WSAGetLastError() 可得知原因)

说明: 利用服务名称及通讯协定来获得该服务的别名、使用的 port 号码

等。



◎ getservbyport():依照服务 (service) 的 port 号码及通讯协定(tcp/udp)来

获取该服务的其他资料。

格 式: struct servent * PASCAL FAR

getservbyport( int port, const char FAR *proto );

参 数: port 服务的 port 编号

proto 通讯协定名称

传回值: 成功 - 一指向 servent 结构的指标

失败 - NULL (呼叫 WSAGetLastError() 可得知原因)

说明: 利用 port 编号及通讯协定来获得该服务的名称、别名等。



程式中的使用方法分别为:



char serv_name[20];

char proto[10];

struct servent far *svptr;

/* 假设 serv_name 及 proto 已先设好服务名称及通讯协定 */

svptr = (struct servent FAR *)getservbyname( (char far *)serv_name, (char far

*)proto )



int serv_port;

char proto[10];

struct servent far *svptr;

/* 假设 serv_port 及 proto 已先设好服务所用的 port number 及通讯协定 */

svptr = (struct servent FAR *)getservbyport( htons(serv_port), (char far

*)proto) )



Winsock 环境下,我们能够查询到的服务资料都是存放在 local 的

「services」档中;这个档所存放的都是 well-known 的服务,基本上我们是不需

去更改它的。读者也可以将自己提供的服务加到这个档中,不过您所用的服务

资料要公诸於世,不然别人的 services 档中可是没有您的服务的资料哟。



最後的这组 getprotobyname() 及 getprotobynumber() 函式是用来取得一些

「协定」的资料,比如 tcp、udp、igmp 等。一般而言,我们是不太会用到的。



◎ getprotobyname():依照通讯协定 (protocol) 的名称来获取该通讯协定的其

他资料。

格 式: struct protoent FAR * PASCAL FAR

getprotobyname( const char FAR *name );

参 数: name 通讯协定名称

传回值: 成功 - 一指向 protoent 结构的指标

失败 - NULL (呼叫 WSAGetLastError() 可得知原因)

说明: 利用通讯协定的名称来得知该通讯协定的别名、编号等资料。



◎ getprotobynumber():依照通讯协定的编号来获取该通讯协定的其他资料。

格 式: struct protoent FAR * PASCAL FAR

getprotobynumber( int number );

参 数: number 以 host order 排列方式的通讯协定编号

传回值: 成功 - 一指向 protoent 结构的指标

失败 - NULL (呼叫 WSAGetLastError() 可得知原因)

说明: 利用通讯协定的编号来得知该通讯协定的名称、别名等资料。



程式中呼叫方式分别如下:



struct protoent far *ptptr;

char proto_name[20];

/* 假设 proto_name 已先设好协定名称 */

ptptr = (struct protoent FAR *)getprotobyname( (char far *)proto_name )



struct protoent far *ptptr;

int proto_num;

/* 假设 proto_num 已先设好协定编号 */

ptptr = (struct protoent FAR *)getprotobynumber( proto_num )



Winsock Stack 对於应用程式呼叫 getprotobyname() 及 getprotobynumber() 的

资料,是取自於 local 的「protocol」档;如无需要,我们也不用去变更这个档

案的内容。





(图 2)hello 程式呼叫同步资料库函式



【非同步资料库函式】



Winsock 1.1 针对前面笔者所描述的 6 个同步资料库函式,也提供了相对的

6 个非同步资料库函式,它们分别是 WSAAsyncGetHostByName()、

WSAAsyncGetHostByAddr()、WSAAsyncGetServByName()、

WSAAsyncGetServByPort()、WSAAsyncGetProtoByName()、

WSAAsyncGetProtoByNumber()。



由於它们取得的资料与同步资料库函式相同,所以笔者仅以

WSAAsyncGetHostByName() 为例,说明这些非同步函式,并告诉各位读者,同

步和非同步资料库函式不同的地方。



由字面来看,「非同步」的意思就是我们发出问题时,并不会马上得到答

覆,而等到系统取到资料时再告知我们。没错,这些非同步资料库函式的作用

就是这样。和 WSAAsyncSelect() 函式一样,我们要告诉 Winsock 系统一个接受

通知讯息的视窗及讯息代码,以便系统通知我们。



我们呼叫同步资料库函式时,return 值是一个指到相对资料的暂存区,而这

个资料暂存区是由系统所提供的;但是呼叫非同步资料库函式时,我们必须自

己准备资料暂存区,并将此暂存区的位址当成参数,传给系统,以便系统用来

储存取到的资料。读者们必须特别注意一点:在系统通知资料取得成功或失败

前,千万不可将传给系统的资料暂存区删除释放,不然当系统取得资料要写入

时,资料区已不见了,会导至当机的。除此之外,资料暂存区的大小一定要够

大,才足够让系统用来存放取得的资料。(Winsock 规格中的建议值是

MAXGETHOSTSTRUCT 1024 bytes 大小的暂存区,笔者认为太大了,100 byets

差不多就太够了?



呼叫非同步资料库函式时,得到的 return 值是一个代码,此代码代表的就

是此项呼叫在系统内的编号;由於是非同步,所以我们在得到答案前,仍可呼

叫 WSACancelAsyncRequest() 函式来取消原先的呼叫,这个取消的动作就要利

用到该代码了。另外,当我们收到结果通知时,wParam 的值也是这个代码;我

们此时可以利用 WSAGETASYNCERROR(lParam) 来得知资料取得是成功或失

败;如果失败的原因是原先传入的暂存区太小的话,我们亦可利用

WSAASYNCGETBUFLEN(lParam) 来得知至少要多大的暂存区才够。



◎ WSAAsyncGetHostByName():利用某一 host 的名称来获取该 host 的资

料。(非同步方式)

格 式: HANDLE PASCAL FAR WSAAsyncGetHostByName( HWND hWnd,

unsigned int wMsg, const char FAR *name, char FAR *buf, int

buflen );

参 数: hWnd 动作完成後,接受讯息的视窗 handle

wMsg 传回视窗的讯息

name host 名称

buf 存放 hostent 资料的暂存区

buflen buf 的大小

传回值: 成功 - 代表此非同步动作的 handle 代码

失败 - 0 (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是利用 host 名称来获取其他的资料,如 host 的位址、别名,

位址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的视窗 handle、

讯息代码、资料的存放位址指标等,以便得到资料时可以通知该视窗来使用资

料。呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle 代码,此代码

可用来辨别此非同步动作或用来取消此非同步动作。当资料取得後,系统会送一

个讯息到使用者指定的视窗。



◎ WSACancelAsyncRequest():取消某一未完成的非同步要求。

格 式: int PASCAL FAR WSACancelAsyncRequest( HANDLE

hAsyncTaskHandle );

参 数:hAsyncTaskHandle 要取消的 task handle 代码

传回值: 成功 - 0

失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)

说明: 此函式是用来取消原先呼叫但尚未完成的WSAAsyncGetXByY(),例

如 WSAAsyncGetHostByName(),的动作。参数 hAsyncTaskHandle 即为呼叫

WSAAsyncGetXByY() 时传回之代码值。若是原先呼叫之非同步要求已经完成,

则无法加以取消。





(图 3)hello 程式呼叫非同步资料库函式



【结语】



笔者已经为各位介绍了大部份 Winsock 应用程式设计时会用到的函式,不

知读者中是否已有人开始练习自己写 Winsock 网路程式了吗?下一期,笔者会

将剩下的函式都介绍完。再此笔者并期待各位除了使用别人设计的网路软体

外,大家也都能自己练习设计出一些不错的网路应用软体,让世界其他国家的

人知道台湾也有能人的;愿共勉之。




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值