Winsock 应用程式设计(1)

相信各位读者如今对於 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 障碍台」。socket

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

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

(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 则再回到监听等待的状态。 ip

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

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

在图 1. 上,咱们能够看到最早被呼叫到的是 WSAStartup() 函式。说明下: cmd

WSAStartup():连结应用程式与 Winsock.DLL 的第一个函式。
格式: 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() 函式来开启 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)

接下来咱们要指定一个位址及 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,位址则由系统来设定。

【Server 端创建 socket 并进入监听等待状态】

首先咱们先看 Server 端如何创建一个 TCP socket,并使其进入监听等待的状态。

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)指定完位址及 port 之後,咱们呼叫 listen() 函式,让这个 socket 进入监听状态。一个 Server 端的 TCP socket 必须在作完了 listen 的呼叫後,才能接受 Client 端的链接。

listen():设定 Socket 为监听状态,准备被链接。
格式: 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 要链接了。(图1. 中并未将此函式绘出)

WSAAsyncSelect():要求某一 Socket 有事件 (event) 发生时通知使用者。
格 式: 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。

(图2) WSAAsyncSelect 函式参数与应用程式关系

  咱们在程式中要求 Winsock 系统知道 Client 要来链接时,送一个ASYNC_EVENT 的讯息到程式中 hwnd 这个视窗;由於咱们想知道的只有 accept事件,因此咱们只设定 FD_ACCEPT。

WSAAsyncSelect(listen_sd, hwnd, ASYNC_EVENT, FD_ACCEPT)

(图 3)demoserv 在 WinKing 系统上创建 socket 并进入监听状态

  读者必须注意一点,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():控制 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 模式
FIONREAD -- 自 Socket 一次可读取的资料量(目前 in buffer 的资料量)

SIOCATMARK -- OOB 资料是否已被读取完

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

【Client 端向 Server 端主动创建链接】

  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 端接受 Client 端的链接】

  由於咱们 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 来送资料到对方或收对方送来的资料了。(有关资料的收送,咱们等下一期再谈)

(图 4) demoserv 与 democlnt 在 WinKing 上链接成功後状态

【Server 及 Client 端结束 socket 链接】

最後咱们来看一下如何结束 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() 来通知 Winsock Stack;若是您不呼叫此一函式,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 环境的使用。)