winsock编程宝典(转贴)

--简单的 Winsock 应用程式设计

TCP连接建立与关闭

相信各位读者现在对於 Winsock 的定义、系统环境,以及一些 Winsock Stack Winsock 应用程式,都有基本的认识了。接下来笔者希望能分几期为各位读者介绍一下简单的 Winsock 网路应用程式设计。我们将以 Winsock 1.1 规格所定义的 46 个应用程式介面(API)为基础,逐步来建立一对 TCP socket 主从架构(Client / Server)的程式。在这两个程式中,Server 将使用 Winsock 提供的「非同步」(asynchronous)函式来建立 socket 连结、关闭、及资料收送等等;而 Client 则采类似传统 UNIX 的「阻拦式」(blocking)。由於我们的重点并不在於 MS Windows SDK 的程式设计,所以我们将使用最简便的方式来显示讯息;有关 MS Windows 程式的技巧,请各位读者自行研究相关的书籍及文章。

今天我们先要看一下主从架构 TCP socket 的建立连结(connect)及关闭(close)。以前笔者曾简单地介绍过主从架构的概念,现在我们再以生活上更浅显的例子来说明一下,读者稍後也较容易能明白笔者的叙述。我们可以假设 Server 就像是电信局所提供的一些服务,比如「104 查号台」或「112 障碍台」。

1)电信局先建立好了一个电话总机,这就像是呼叫 socket() 函式开启了一个 socket

2)接著电信局将这个总机的号码定为 104,就如同我们呼叫 bind() 函式,将 Server 的这个 socket 指定(bind)在某一个 port。当然电信局必须让用户知道这个号码;而我们的 Client 程式同样也要知道 Server 所用的 port,待会才有办法与之连接。

3)电信局的 104 查号台底下会有一些自动服务的分机,但是它的数量是有限的,所以有时你会拨不通这个号码(忙线)。同样地,我们在建立一个 TCP Server socket 时,也会呼叫 listen() 函式来监听等待;listen() 的第二个参数即是 waiting queue 的数目,通常数值是由 1 5。(事实上这两者还是有点不一样。)

4)用户知道了电信局的这个 104 查号服务,他就可以利用某个电话来拨号连接这个服务了。这就是我们 Client 程式开启一个相同的 TCP socket,然後呼叫 connect() 函式去连接 Server 指定的那个 port。当然了,和电话一样,如果 waiting queue 满了、与 Server 间线路不通、或是 Server 没提供此项服务时,你的连接就会失败。

5)电信局查号台的总机接受了这通查询的电话後,它会转到另一个分机做服务,而总机本身则再回到等待的状态。Server listening socket 亦是一样,当你呼叫了 accept() 函式之後,Server 端的系统会建立一个新的 socket 来对此连接做服务,而原先的 socket 则再回到监听等待的状态。

6)当你查询完毕了,你就可以挂上电话,彼此间也就离线了。ClientServer间的 socket 关闭亦是如此;不过这个关闭离线的动作,可由 Client 端或Server 端任一方先关闭。有些电话查询系统不也是如此吗?

接下来,我们就来看主从架构的 TCP socket 是如何利用这些 Winsock 函式来达成的;并利用资策会资讯技术处的「WinKing」这个 Winsock Stack 中某项功能来显示 sockets 状态的变化。文章中仅列出程式的片段,完整的程式请看附录的程式。

Server进入监听状态

首先我们先看 Server 端如何建立一个 TCP socket,并使其进入监听等待的状态。在图 1. 上,我们可以看到最先被呼叫到的是 WSAStartup() 函式。

WSAStartup

  式: int PASCAL FAR WSAStartup( WORD wVersionRequested,  LPWSADATA lpWSAData );

  数:   wVersionRequested 欲使用的 Windows Sockets API 版本

lpWSAData  指向 WSADATA 资料的指标

传回值:   成功 – 0

           失败 - WSASYSNOTREADY / WSAVERNOTSUPPORTED /  WSAEINVAL

说明: 此函式「必须」是应用程式呼叫到 Windows Sockets DLL 函式中的第一个,也唯有此函式呼叫成功後,才可以再呼叫其他 Windows  Sockets DLL 的函式。此函式亦让使用者可以指定要使用的 Windows Sockets API 版本,及获取设计者的一些资讯。程式中我们要用 Winsock 1.1,所以我们在程式中有一段为:

WSAStartup((WORD)((1<<8)|1),(LPWSADATA) &WSAData)

其中 ((WORD)((1<<8)|1) 表示我们要用的是 Winsock 1.1」版本,而WSAData 则是用来储存由系统传回的一些有关此一 Winsock Stack 的资料。

socket

再来我们呼叫 socket() 函式来开启 Server 端的 TCP socket socket():建立Socket

格 式: SOCKET PASCAL FAR socket( int af, int type, int protocol );

参 数: af 目前只提供 PF_INET(AF_INET)

type Socket 的型态 (SOCK_STREAMSOCK_DGRAM)

protocol 通讯协定(如果使用者不指定则设为0)

传回值: 成功 - Socket 的识别码

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

说明: 此函式用来建立一 Socket,并为此 Socket 建立其所使用的资源。Socket 的型态可为 Stream Socket Datagram Socket。我们要建立的是 TCP socket,所以程式中我们的第二个参数为SOCK_STREAM,我们并将开启的这个 socket 号码记在 listen_sd 这个变数。

listen_sd = socket(PF_INET, SOCK_STREAM, 0)

bind

接下来我们要指定一个位址及 port Server 的这个 socket,这样 Client 才知道待会要连接哪一个位址的哪个 port;所以我们呼叫 bind() 函式。

bind():指定 Socket Local 位址 (Address)

格式:

int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );

参数:

s:         Socket的识别码

name:      Socket的位址值

namelen:   name的长度

传回值: 成功 – 0

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

说明: 此一函式是指定 Local 位址及 Port 给某一未定名之 Socket。使用者若不在意位址或 Port 的值,那麽他可以设定位址为 INADDR_ANY,及 Port 0;那麽Windows Sockets 会自动将其设定适当之位址及 Port (1024 5000之间的值),使用者可以在此 Socket 真正连接完成後,呼叫 getsockname() 来获知其被设定的值。bind() 函式要指定位址及 port,这个位址必须是执行这个程式所在机器的 IP位址,所以如果读者在设计程式时可以将位址设定为 INADDR_ANY,这样Winsock 系统会自动将机器正确的位址填入。如果您要让程式只能在某台机器上执行的话,那麽就将位址设定为该台机器的 IP 位址。由於此端是 Server 端,所以我们一定要指定一个 port 号码给这个 socket。读者必须注意一点,TCP socket 一旦选定了一个位址及 port 後,就无法再呼叫另一次 bind 来任意更改它的位址或 port。在程式中我们将 Server 端的 port 指定为 7016,位址则由系统来设定。

struct sockaddr_in sa;

sa.sin_family = PF_INET;

sa.sin_port = htons(7016);      //port number

sa.sin_addr.s_addr = INADDR_ANY;//address

bind(listen_sd, (struct sockaddr far *)&sa, sizeof(sa))

我们在指定 port 号码时会用到 htons() 这个函式,主要是因为各机器的数值读取方式不同(PCUNIX系统即不相同),所以我们利用这个函式来将 host order 的排列方式转换成 network order 的排列方式;相同地,我们也可以呼叫ntohs() 这个相对的函式将其还原。

host order各机器不同,但network order都相同;htons是针对short数值,对於long数值则用hotnlntohl

listen

指定完位址及 port 之後,我们呼叫 listen() 函式,让这个 socket 进入监听状态。一个 Server 端的 TCP socket 必须在做完了 listen 的呼叫後,才能接受 Client 端的连接。

格式:

int PASCAL FAR listen( SOCKET s, int backlog );

参数:

s:         Socket 的识别码

backlog:   未真正完成连接前(尚未呼叫 accept )彼端的连接要求的最大个数

传回值:

成功 – 0

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

说明: 使用者可利用此函式来设定 Socket 进入监听状态,并设定最多可有多少个在未真正完成连接前的彼端的连接要求。(目前最大值限制为 5, 最小值为1)程式中我们将 backlog 设为 1

listen(listen_sd, 1)

呼叫完 listen 後,此时 Client 端如果来连接的话,Client 端的连接动作(connect)会成功,不过此时 Server 端必须再呼叫 accept() 函式,才算正式完成Server 端的连接动作。但是我们什麽时候可以知道 Client 端来连接,而适时地呼叫 accept 呢?在这里我们就要利用 WSAAsyncSelect 函式,将Server 端的这个 socket 转变成 Asynchronous 模式,让系统主动来通知我们有Client 要连接了。

WSAAsyncSelect

格式:

int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent );

参数:

s:         Socket 的编号

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

wMsg:      传回视窗的讯息

lEvent:    应用程式有兴趣的网路事件

传回值:

成功 – 0

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

说明:此函式是让使用者用来要求 Windows Sockets DLL 在侦测到某一 Socket有网路事件时送讯息到使用者指定的视窗;网路事件是由参数 lEvent 设定。呼叫此函式会主动将该 Socket 设定为 Non-blocking 模式。lEvent 的值可为以下之「OR」组合:(参见 WINSOCK1.18889) FD_READFD_WRITEFD_OOBFD_ACCEPTFD_CONNECTFD_CLOSE 使用者若是针对某一Socket再次呼叫此函式时,会取消对该 Socket 原先之设定。若要取消对该Socket 的所有设定,则 lEvent 的值必须设为 0。我们在程式中要求 Winsock 系统知道 Client 要来连接时,送一个ASYNC_EVENT 的讯息到程式中 hwnd 这个视窗;由於我们想知道的只有 accept事件,所以我们只设定 FD_ACCEPT

WSAAsyncSelect(listen_sd, hwnd, ASYNC_EVENT, FD_ACCEPT)

读者必须注意一点,WSAAsyncSelect 的设定是针对「某一个 socket」;也就是说,只有当您设定的这个 socket listen_sd)的那些事件(FD_ACCEPT)发生时,您才会收到这个讯息(ASYNC_EVENT)。如果您开启了很多 sockets,而要让每个 socket 都变成 asynchronous 模式的话,那麽就必须对「每一个 socket」都呼叫 WSAAsyncSelect 来一一设定。而如果您想将某一个 socket async 事件通知设定取消的话,那麽同样也是用 WSAAsyncSelect 这个函式;且第四个参数lEvent 一定要设为 0

WSAAsyncSelect( s, hWnd, 0, 0 ) -- 取消所有 async 事件设定

呼叫 WSAAsyncSelect 的同时也将此socket改变成「非阻拦」(non-blocking)模式。但是此时这个 socket 不能很简单地用 ioctlsocket() 这个函式就将它再变回「阻拦」(blocking)模式。也就是说WSAAsyncSelect ioctlsocket 所改变的「非阻拦」模式仍是有些不同的。如果您想将一个「非同步」(asynchronous)模式的 socket 再变回「阻拦」模式的话,必须先呼叫 WSAAsyncSelect() 将所有的 async 事件取消,再用 ioctlsocket() 将它变回阻拦模式。

ioctlsocket

ioctlsocket():控制 Socket 的模式。

格 式: int PASCAL FAR ioctlsocket( SOCKET s, long cmd, u_long FAR * argP );

参 数:    s Socket 的识别码

cmd 指令名称

argP 指向 cmd 参数的指标

传回值:   成功 – 0

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

说明: 此函式用来获取或设定 Socket 的运作参数。其所提供的指令有:(参见WINSOCK 1.1 3536 ) cmd 的值可为:

FIONBIO -- 开关 non-blocking 模式//允许或禁止套接字的非阻塞模式,允许为非0,禁止为0

FIONREAD -- Socket一次可读取的资料量(目前 in buffer 的资料量//确定套接字自动读入的数据量

SIOCATMARK -- OOB 资料是否已被读取完//确定是否所有带外数据都已被读入

由於我们 Server 端的 socket 是用非同步模式,且设定了 FD_ACCEPT 事件,所以当 Client 端和我们连接时,Winsock Stack 会主动通知我们;我们再先来看看Client 端要如何和 Server 端建立连接?

Client主动建立连接

Client 首先也是呼叫 WSAStartup() 函式来与 Winsock Stack 建立关系;然後同样呼叫 socket() 来建立一个 TCP socket。(读者此时一定要用 TCP socket 来连接Server 端的 TCP socket,而不能用 UDP socket 来连接;因为相同协定的 sockets 才能相通,TCP TCPUDP UDP)和 Server 端的 socket 不同的地方是:Client 端的 socket 可以呼叫 bind()函式,由自己来指定 IP 位址及 port 号码;但是也可以不呼叫 bind(),而由 Winsock Stack来自动设定 IP 位址及 port 号码(此一动作在呼叫 connect() 函式时会由 Winsock 系统来完成)。通常我们是不呼叫 bind(),而由系统设定的,稍後可呼叫getsockname() 函式来检查系统帮我们设定了什麽 IP port一般言,系统会自动帮我们设定的 port 号码是在 1024 5000 之间;而如果读者要自己用 bind设定 port的话,最好是 5000 以上的号码。

connect():要求连接某一 TCP Socket 到指定的对方。

格 式: int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );

参 数: s Socket 的识别码

name Socket 想要连接的对方位址

namelen name的长度

传回值: 成功 – 0

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

说明: 此函式用来向对方要求建立连接。若是指定的对方位址为 0 的话,会传回错误值。当连接建立完成後,使用者即可利用此一 Socket 来做传送或接收资料之用了。

我们的例子中, Client 是要连接的是自己机器上 Server 所监听的 7016 这个port,所以我们有以下的程式片段。(假设我们机器的 IP 存在my_host_ip

struct sockaddr_in sa; /* 变数宣告 */
sa.sin_family = PF_INET; /*
设定所要连接的 Server 端资料 */

sa.sin_port = htons(7016);

sa.sin_addr.s_addr = htonl(my_host_ip);

connect(mysd, (struct sockaddr far *)&sa, sizeof(sa)) /* 建立连接 */

Server接受连接

由於我们 Server 端的 socket 是设定为「非同步模式」,且是针对 FD_ACCEPT这个事件,所以当 Client 来连接时,我们 Server 端的 hwnd 这个视窗会收到Winsock Stack 送来的一个 ASYNC_EVENT 的讯息。(参见前面 WSAAsyncSelect 的设定)这时,我们应该先利用 WSAGETSELECTERROR(lParam) 来检查是否有错误;并由 WSAGETSELECTEVENT(lParam) 得知是什麽事件发生(因为WSAAsyncSelect 函式可针对同一个 socket 同时设定很多事件,但是只用一个讯息来代表)(此处当然是 FD_ACCEPT 事件);然後再呼叫相关的函式来处理此一事件。所以我们呼叫 accept() 函式来建立 Server 端的连接。

accept():接受某一 Socket 的连接要求,以完成 Stream Socket 的连接。

格 式: SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr, int FAR *addrlen );

参 数: s Socket的识别码

addr 存放来连接的彼端的位址

addrlen addr的长度

传回值:成功 - 新的Socket识别码

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

说明: Server 端之应用程式呼叫此一函式来接受 Client 端要求之 Socket 连接动作;如果Server 端之 Socket 是为 Blocking 模式,且没有人要求连接动作,那麽此一函式会被 Block 住;如果为 Non-Blocking 模式,此函式会马上回覆错误。accept()函式的答覆值为一新的 Socket,此新建之 Socket 不可再用来接受其它的连接要求;但是原先监听之 Socket 仍可接受其他人的连接要求。

TCP socket Server 端在呼叫 accept() 後,会传回一个新的 socket 号码;而这个新的 socket 号码才是真正与 Client 端相通的 socket。比如说,我们用socket() 建立了一个 TCP socket,而此 socket 的号码(系统给的)为 1,然後我们呼叫的bind()listen()accept() 都是针对此一 socket;当我们在呼叫 accept()後,传回值是另一个 socket 号码(也是系统给的),比如说 3;那麽真正与 Client 端连接的是号码 3 这个 socket,我们收送资料也都是要利用 socket 3,而不是 socket 1;读者不可搞错。我们在程式中对 accept() 的呼叫如下;我们并可由第二个参数的传回值,得知究竟是哪一个 IP 位址及 port 号码的 Client 与我们 Server 连接。

struct sockaddr_in sa;

int sa_len = sizeof(sa);

my_sd = accept(listen_sd, (struct sockaddr far *)&sa, &sa_len)

Server 端呼叫完 accept() 後,主从架构的 TCP socket 连接才算真正建立完毕; Server Client 端也就可以分别利用此一 socket 来送资料到对方或收对方送来的资料了。

Server/Client结束连接

最後我们来看一下如何结束 socket 的连接。socket 的关闭很简单,而且可由Server Client 的任一端先启动,只要呼叫 closesocket() 就可以了。而要关闭监听状态的 socket,同样也是利用此一函式。

closesocket():关闭某一Socket

格 式: int PASCAL FAR closesocket( SOCKET s );

参 数: s Socket 的识别码

传回值: 成功 – 0

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

说明: 此一函式是用来关闭某一 Socket。若是使用者原先对要关闭之 Socket 设定 SO_DONTLINGER,则在呼叫此一函式後,会马上回覆,但是此一 Sokcet 尚未传送完毕的资料会继续送完後才关闭。若是使用者原先设定此 Socket SO_LINGER,则有两种情况:

(a) Timeout 设为 0 的话,此一 Socket 马上重新设定 (reset),未传完或未收到的资料全部遗失。

(b) Timeout 不为 0 的话,则会将资料送完,或是等到 Timeout 发生後才真正关闭。

程式结束前,读者们可千万别忘了要呼叫 WSACleanup() 来通知 WinsockStack;如果您不呼叫此一函式,Winsock Stack 中有些资源可能仍会被您占用而无法清除释放哟。

WSACleanup():结束 Windows Sockets DLL 的使用。

格 式: int PASCAL FAR WSACleanup( void );

参 数: 无

传回值: 成功 – 0

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

说明: 应用程式在使用 Windows Sockets DLL 时必须先呼叫WSAStartup() 来向 Windows Sockets DLL 注册;当应用程式不再需要使用Windows Sockets DLL 时,须呼叫此一函式来注销使用,以便释放其占用的资源。

结语

这期笔者先介绍主从架构 TCP sockets 的连接及关闭,以後会再陆续介绍如何收送资料,以及其他 API 的使用。想要进一步了解如何撰写 Winsock 程式的读者,可以好好研究一下笔者 demoserv democlnt 这两个程式;也许不是写的很好,但是希望可以带给不懂 Winsock 程式设计的人一个起步。读者们亦可自行用 anonymous ftp 方式到 SEEDNET 台北主机 tpts1.seed.net. tw139.175.1.10)的 UPLOAD / WINKING 目录下,取得笔者与 陈建伶 小姐所设计的WinKing 这个 Winsock Stack 的试用版,来跑 demoserv democlnt 这两个程式及其他许许多多的 Winsock 应用程式。(正式版本请洽 SEEDNET 服务中心,新版的WinKing 已含 Windows 拨接及 PPP 程式,适合电话拨接用户在 Windows 环境下用 SEEDNETWinKing 同样也提供 Ethernet 环境的使用。)

收送资料

在前一期的文章中,笔者为大家介绍了如何在 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 在传送资料时,并不保证资料能完整地送达对方,所以我们常用的一些应用程式(如 telnetmailftpnews...等)都是采用 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 的组合。(参见 WINSOCK1.148)

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_PEEKMSG_OOB 的组合; MSG_PEEK 代表将资料拷贝到使用者提供的 buffer,但是资料并不从系统的 input buffer 中移走;0 则表示拷贝并移走。(参考 WINSOCK 1.141 )

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 後才会返回;这是因为我们 ClientSocket 是阻拦模式。如果我们用的是非阻拦模式的 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 住,直到下列的情况之一发生,才会返回。

1Server 端送来资料。(此时 return 值是读取的资料长度)

2Server 端将相对的 Socket 关闭了。(此时的 return 值会是 0

3Client 端自己呼叫 WSACancelBlockingCall() 来取消 recv() 的呼叫。(此时 return 值是 SOCKET_ERROR 错误,错误码 10004 WSAEINTR

同样地,资料全部送收完毕後,我们也呼叫 closesocket() 来将 Socket 关闭。

WSACancelBlockingCall():取消目前正在进行中的 blocking 动作。

格式:  int PASCAL FAR WSACancelBlockingCall( void );

参数:  

传回值:成功 – 0

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

说明: 此函式用来取消该应用程式正在进行中的 blocking 动作。通常的使用时机有:(a) Blocking 动作正在进行中,该应用程式又收到某一讯息(MouseKeyboardTimer 等),则可在处理该讯息的段落中呼叫此函式。(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 事件。

sendtorecvfrom

一般言,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 的值可设为 0MSG_DONTROUTE MSG_OOB 的组合。 (参见 WINSOCK1.151)

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 SocketUDP)言,一次读取一个 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_READFD_WRITE 等事件的发生时机;读者们综合前一期的文章,应该已经可以建立出一对主从架构的程式,并利用 TCP Socket 来传送资料了。下一期,笔者将继续介绍有关如何获取网路资讯的函式,如gethostnamegetsocknamegetpeername,以及同步与非同步的网路资料库撷取函式 getXbyYWSAAsyncGetXByY。本文中所提到的 WinKing 试用版可自 SEEDNET 台北主机 tpts1.seed.net.tw139.175.1.10)的 UPLOAD/WINKING 目录中取得,档名为 wkdemo.exeWinKing 提供 Ethernet PPP 连线功能,适用於一般 Ethernet 网路,亦可用来以电话、数据机连上 SEEDNET PPP 伺服主机; demoservdemoclnt,以及一些笔者所写的 Winsock 程式(含原始程式码)则存放在UPLOAD/WINKING/JNLIN 目录下;有兴趣的读者可自行用 anonymous ftp 方式取得。

获取网路资讯

在前两期的文章中,笔者介绍了如何在 Winsock 环境下建立主从架构的TCP Socket,以及如何利用 Socket 来收送资料;今天,我们接著来看一看如何利用 Winsock 所提供的函式来取得一些基本的网路资料,包括我们本身主机的名称是什麽、系统主动指定给我们的 Socket IP 位址及 port number、我们的 Socket 所连接的对方是谁、如何查得某些主机的 IP 位址或名称、以及某些well-known 服务(如 ftptelnet 等)所用的 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_pingwintalk)时,在呼叫 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 位址与portnumber

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 资料,与我们呼叫函式所得结果相同)

读者必须注意一点,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」字串格式。

struct sockaddr {

        u_short sa_family;              /* address family */

        char    sa_data[14];            /* up to 14 bytes of direct address */

};

struct in_addr {

        union {

                struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;

                struct { u_short s_w1,s_w2; } S_un_w;

                u_long S_addr;

        } S_un;

#define s_addr  S_un.S_addr          /* can be used for most tcp & ip code */

#define s_host  S_un.S_un_b.s_b2     /* host on imp */

#define s_net   S_un.S_un_b.s_b1     /* network */

#define s_imp   S_un.S_un_w.s_w2     /* imp */

#define s_impno S_un.S_un_b.s_b4     /* imp # */

#define s_lh    S_un.S_un_b.s_b3     /* logical host */

};

struct sockaddr_in {

        short   sin_family;

        u_short sin_port;

        struct  in_addr sin_addr;

        char    sin_zero[8];

};

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() 这两个函式。大部份的读者应该都用过 telnetmailftpnews 等服务应用程式;这些应用程式的协定,比如服务名称、伺服器端所用的 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() 函式是用来取得一些「协定」的资料,比如 tcpudpigmp 等。一般而言,我们是不太会用到的。

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」档;如无需要,我们也不用去变更这个档案的内容。

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

【非同步资料库函式】

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( HANDLEhAsyncTaskHandle );

参 数:hAsyncTaskHandle 要取消的 task handle 代码
传回值: 成功 - 0
失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)
说明: 此函式是用来取消原先呼叫但尚未完成的WSAAsyncGetXByY(),例如 WSAAsyncGetHostByName(),的动作。
参数 hAsyncTaskHandle 即为呼叫WSAAsyncGetXByY() 时传回之代码值。若是原先呼叫之非同步要求已经完成,则无法加以取消。

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

【结语】

笔者已经为各位介绍了大部份 Winsock 应用程式设计时会用到的函式,不知读者中是否已有人开始练习自己写 Winsock 网路程式了吗?下一期,笔者会将剩下的函式都介绍完。再此笔者并期待各位除了使用别人设计的网路软体外,大家也都能自己练习设计出一些不错的网路应用软体,让世界其他国家的人知道台湾也有能人的;愿共勉之。

其它

接著笔者要再为各位介绍剩下的几个函式,包括 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 time val FAR *timeout )

参数:

nfds     此参数在此并无作用

readfds  要被检查是否可读的 Sockets

writefds 要被检查是否可写的 Sockets

exceptfds:要被检查是否有错误的 Sockets

timeout  此函式该等待的时间

传回值: 成功 - 符合条件的 Sockets 总数 ( Timeout 发生,则为0)

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

说明:使用者可利用此函式来检查 Sockets 是否有资料可被读取,或是有空间可以写入,或是有错误发生。Winsock 1.1 所提供的 select() 函式与 UNIX BSD select() 函式,在参数的个数及资料型态上是一样,都有 nfdsreadfdswritefdsexceptfds、及 timeout五个参数;但是 Winsock nfds 是没有作用的,有这个参数的目的只是为了与 UNIX BSD select() 函式一致。至於 readfdswritefdsexceptfds 同样是一组 sockets 的集合,所以您可以同时设定许多 sockets 的号码在这三个参数里面;当然这些 sockets 必须是属於您的这个应用程式所建立的。如果您设定的 socket 号码中有任一个不是属於您的这个程式的话,呼叫 select() 函式便会失败(错误码为 10038 WSAENOTSOCK)。

Winsock 同样也提供了一些 macros 来让您设定或检查 readfdswritefdsexceptfds 的值,包括有:

²           FD_ZERO(*set) -- set 的值清乾净

²           FD_SET(s, *set) -- s 加到 set

²           FD_CLR(s, *set) -- s set 中删除

²           FD_ISSET(s, *set) -- 检查 s 是否存在於 set

其中 s 代表的是某一个 socket 的号码,set 代表的就是 readfdswritefds exceptfds

读者们要知道参数readfdswritefdsexceptfds 都是「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-blockingsocket在呼叫connect()後,也可利用select() writefds 来检查连接是否已经成功了(当这个non-blockingsocket被设定在 writefds,且被 select 成功时);此外,我们亦可利用readfds来检查 TCP socket 连接的对方是否已经关闭了(当此socket被设定在readfds,且被select成功,但呼叫recv去收资料却 return 0 时)。

UNIX 系统上因为没有提供 WSAAsyncSelect() 函式,所以我们要用 select()函式来做 polling 的动作;但是 Winsock 系统上已经有了可以设定非同步事件的WSAAsyncSelect() 函式,为了让 MS Windows「讯息驱动」(message driven)的环境更有效率,读者们应该尽量使用 WSAAsyncSelect(),而少用 select() 的方式;这也是当初为什麽要定义一个 WSAAsyncSelect() 函式的最大目的。

setsockopt【变更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

1SO_BROADCAST -- 适用於 UDP socket。其意义是允许UDP socket「广播」broadcast讯息到网路上。

2SO_DONTLINGER -- 适用於 TCP socket。其意义是让 socket 在呼叫 closesocket() 关闭时,能马上 return,而不用等到资料都送完後才从函式呼叫returnclosesocket() 函式 return 後,系统仍会继续将资料全部送完後,才真正地将这个 socket 关闭。一个 TCP socket 在开启时的预设值即是 Don't Linger

3SO_LINGER -- 适用於 TCP socket 来设定 linger 值之用。如果 linger的 值设为 0,那麽在呼叫 closesocket() 关闭 socket 时,如果该 socket output buffer 中还有资料的话,将会被系统所忽略,而不会被送出,此时 closesocket() 也会马上 return;如果 linger 值设为 n 秒,那麽系统就会在这个时间内,尝试去送出output buffer 中的资料,时间到了或是资料送完了,才会从 closesocket() 呼叫return

4SO_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 options

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)

WSASetBlockingHook

【什麽是 Blocking Hook 函式及如何设定自己的 Blocking Hook 函式】

什麽是「Blocking Hook」函式呢?在解释之前,我们要先来剖析一下Winsock 1.1 提供的 Blocking 函式(如 acceptconnect 等)的内部究竟做了哪些事?在 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)  continue;/* timeout */

...

}

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 中任何资料送出; 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 );

我们在设定自己的 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 环境可能就完蛋了。

【结语】

四期的「Winsock 应用程式设计篇」在此结束了;笔者除了介绍 Winsock API  外,也将自己亲身设计 winsock.dll 的经验与各位读者分享了;希望这几期的文章,对於国内想要在 Winsock 1.1 环境上开发网路应用程式的读者有些许的帮助。谢谢大家。

[Microsoft Windows-specific Extensions]

(1)   WSAAsyncGetHostByAddr():利用某一 host 的位址来获取该 host 的资料。(非同步方式)

  式: HANDLE PASCAL FAR WSAAsyncGetHostByAddr( HWND hWnd,  unsigned int wMsg, const char FAR *addr, int len, int type, char FAR *buf, int buflen );

  数:

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

wMsg  传回视窗的讯息

addr network 排列方式的位址

len addr 的长度

type PF_INET(AF_INET)

buf  存放 hostent 资料的区域

buflen buf 的大小

传回值: 成功 - 代表此 Async 动作的 handle

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

说明: 此函式是利用位址来获取 host 的其他资料,如 host 的名称、别名, 位址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的视窗handle、讯息代码、资料的存放位置指标等,以便得到资料时可以通知该视窗来使用资料。呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle,此 handle 可用来辨别此非同步动作或用来取消此非同步动作。当资料取得後,会送一个讯息到使用者指定的视窗。

(2)   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 的大小

传回值: 成功 - 代表此 Async 动作的 handle

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

说明: 此函式是利用 host 名称来获取其他的资料,如 host 的位址、别名, 位址的型态、长度等。使用者呼叫此函式时必须传入要接收资料的视窗handle、讯息代码、资料的存放位置指标等,以便得到资料时可以通知该视窗来使用资料。呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle,此handle 可用来辨别此非同步动作或用来取消此非同步动作。当资料取得後,会送一个讯息到使用者指定的视窗。

(3) WSAAsyncGetProtoByName():依照通讯协定的名称来获取该通讯协定的其他资料。(非同步方式)

  式: HANDLE PASCAL FAR WSAAsyncGetProtoByName( HWND hWnd, unsigned int wMsg, const char FAR *name, char FAR *buf, int buflen );

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

wMsg 传回视窗的讯息

name 通讯协定名称

buf  存放 protoent 资料的区域

buflen buf 的大小

传回值: 成功 - 代表此 Async 动作的 handle

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

说明: 利用通讯协定的名称来得知该通讯协定的别名、编号等资料。使用者呼叫此函式时必须传入要接收资料的视窗 handle 、讯息代码、资料的存放位置指标等,以便得到资料时可以通知该视窗来使用资料。呼叫此函式後会马上回到使用者的呼叫点并传回一个 handle,此  handle可用来辨别此 

你可能感兴趣的:(winsock编程宝典(转贴))