简单的 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 障碍台」。less

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

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

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

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

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

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

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

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_STREAM、SOCK_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() 这个函式,主要是由于各机器的数值读取方式不一样(PC与UNIX系统即不相同),因此咱们利用这个函式来将 host order 的排列方式转换成 network order 的排列方式;相同地,咱们也能够呼叫ntohs() 这个相对的函式将其还原。

host order各机器不一样,但network order都相同;htons是针对short数值,对於long数值则用hotnl及ntohl。

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」组合:(参见 WINSOCK第1.1版8八、89页) FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、FD_CONNECT、FD_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 版 3五、36 页) 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 对 TCP,UDP 对 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. tw(139.175.1.10)的 UPLOAD / WINKING 目录下,取得笔者与陈建伶小姐所设计的WinKing 这个 Winsock Stack 的试用版,来跑 demoserv 与 democlnt 这两个程式及其余许许多多的 Winsock 应用程式。(正式版本请洽 SEEDNET 服务中心,新版的WinKing 已含 Windows 拨接及 PPP 程式,适合电话拨接用户在 Windows 环境下用 SEEDNET;WinKing 一样也提供 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 在传送资料时,并不保证资料能完整地送达对方,因此咱们经常使用的一些应用程式(如 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 方式取得。

获取网路资讯
在前两期的文章中,笔者介绍了如何在 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 位址与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() 这两个函式。大部份的读者应该都用过 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( HANDLEhAsyncTaskHandle );

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

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

【结语】

笔者已经为各位介绍了大部份 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() 函式,在参数的个数及资料型态上是同样,都有 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 的值,包括有:

²           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 表明的就是 readfds、writefds 或 exceptfds。

读者们要知道参数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 时)。

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

(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 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 函式(如 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)  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可用来辨别此 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/msgsnd/archive/2008/03/06/2153768.aspx