简单的 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)。(参见图 1.)
(图 1. 主从架构的 TCP socket 链接创建与关闭)
之前笔者曾简单地介绍过主从架构的概念,如今咱们再以生活上更浅显的例
子来讲明一下,读者稍後也较容易能明白笔者的叙述。咱们能够假设 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)当你查询完毕了,你就能够挂上电话,彼此间也就离线了。Client 和
Server 间的 socket 关闭亦是如此;不过这个关闭离线的动做,可由 Client 端或
Server 端任一方先关闭。有些电话查询系统不也是如此吗?
接下来,咱们就来看主从架构的 TCP socket 是如何利用这些 Winsock 函式来
达成的;并利用资策会资讯技术处的「WinKing」这个 Winsock Stack 中某项功能
来显示 sockets 状态的变化。文章中仅列出程式的片断,完整的程式请看附录的程
式。
【Server 端创建 socket 并进入监听等待状态】
首先咱们先看 Server 端如何创建一个 TCP socket,并使其进入监听等待的状
态。
在图 1. 上,咱们能够看到最早被呼叫到的是 WSAStartup() 函式。说明以下:
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,位址则由系统来设定。
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 环境的使用。)
--
! Origin: ● ZUCAI BBS ● From: 202.121.132.131
? Area: Internet.China ( Internet.中国 )架构