发信人: wusi() 
整理人: yueqs(2000-12-05 19:47:56), 站内信件
 | 
 
 
【 在 puple (student) 的大作中提到: 】
 : 【 在 yueqs (Kingson) 的大作中提到: 】
 : : 【 在 puple (student) 的大作中提到: 】
 : : 
 : : 我有本c语言的,要不要先看看? pdf格式的
 :    .......
 发信人: digger (欧阳疯), 信区: Socket
 标  题: [转载] winsock编程(1)
 发信站: 华南网木棉站 (Tue Aug  4 15:21:32 1998), 转信
 
 【 以下文字转载自 Hacker 讨论区 】
 【 原文由 cpu 所发表 】
 
 简单的 Winsock 应用程式设计(1)
 
         林 军 鼐
 
 相信各位读者现在对於 Winsock 的定义、系统环境,以及一些 Winsock Stack
  及 Winsock 应用程式,都有基本的认识了。接下来笔者希望能分几期为各位读者 
 介绍一下简单的 Winsock 网路应用程式设计。
 
 我们将以 Winsock 1.1 规格所定义的 46 个应用程式介面(API)为基础,逐
 步来建立一对 TCP socket 主从架构(Client / Server)的程式。在这两个程式 中,
 Server 将使用 Winsock 提供的「非同步」(asynchronous)函式来建立 socke t 连
 结、关闭、及资料收送等等;而 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():建立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 *nam e,
                                                 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 or der 都
 相同)(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 进入监听状态,并设定最多可有多少 
 
 程式中我们将 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 在侦测到某一  Sock et
 有网路事件时送讯息到使用者指定的视窗;网路事件是由参数 lEvent 设定。呼 叫此
 函式会主动将该 Socket 设定为 Non-blocking 模式。lEvent 的值可为以下之「 OR」
 组合:(参见 WINSOCK第1.1版88、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 版 35、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 来连接;因为相同协定的 soc kets 才
 能相通,TCP 对 TCP,UDP 对 UDP)
 
 和 Server 端的 socket 不同的地方是:Client 端的 socket 可以呼叫 bind()  函式,
 由自己来指定 IP 位址及 port 号码;但是也可以不呼叫 bind(),而由 Winsoc k Stack
 来自动设定 IP 位址及 port 号码(此一动作在呼叫 connect() 函式时会由 Wi nsock 系
 统来完成)。通常我们是不呼叫 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  *add r,
                         int FAR *addrlen );
 参  数:        s       Socket的识别码
                 addr    存放来连接的彼端的位址
                 addrlen addr的长度
 传回值:成功 - 新的Socket识别码
         失败 - INVALID_SOCKET (呼叫 WSAGetLastError() 可得知原因)
 说明: Server 端之应用程式呼叫此一函式来接受 Client 端要求之 Socket 连 接动
 作;如果Server 端之 Socket 是为 Blocking 模式,且没有人要求连接动作,那 麽此一
 函式会被 Block 住;如果为 Non-Blocking 模式,此函式会马上回覆错误。acc ept()
 函式的答覆值为一新的 Socket,此新建之 Socket 不可再用来接受其它的连接要 求;
 但是原先监听之 Socket 仍可接受其他人的连接要求。
 
 TCP socket 的 Server 端在呼叫 accept() 後,会传回一个新的 socket 号码; 而这
 个新的 socket 号码才是真正与 Client 端相通的 socket。比如说,我们用 so cket() 建
 立了一个 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 环境的使用。)
 
 简单的 Winsock 应用程式设计(1)
 
         林 军 鼐
 
 相信各位读者现在对於 Winsock 的定义、系统环境,以及一些 Winsock Stack
  及 Winsock 应用程式,都有基本的认识了。接下来笔者希望能分几期为各位读者 
 介绍一下简单的 Winsock 网路应用程式设计。
 
 我们将以 Winsock 1.1 规格所定义的 46 个应用程式介面(API)为基础,逐
 步来建立一对 TCP socket 主从架构(Client / Server)的程式。在这两个程式 中,
 Server 将使用 Winsock 提供的「非同步」(asynchronous)函式来建立 socke t 连
 结、关闭、及资料收送等等;而 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():建立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 *nam e,
                                                 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 or der 都
 相同)(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 在侦测到某一  Sock et
 有网路事件时送讯息到使用者指定的视窗;网路事件是由参数 lEvent 设定。呼 叫此
 函式会主动将该 Socket 设定为 Non-blocking 模式。lEvent 的值可为以下之「 OR」
 组合:(参见 WINSOCK第1.1版88、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 版 35、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 来连接;因为相同协定的 soc kets 才
 能相通,TCP 对 TCP,UDP 对 UDP)
 
 和 Server 端的 socket 不同的地方是:Client 端的 socket 可以呼叫 bind()  函式,
 由自己来指定 IP 位址及 port 号码;但是也可以不呼叫 bind(),而由 Winsoc k Stack
 来自动设定 IP 位址及 port 号码(此一动作在呼叫 connect() 函式时会由 Wi nsock 系
 统来完成)。通常我们是不呼叫 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  *add r,
                         int FAR *addrlen );
 参  数:        s       Socket的识别码
                 addr    存放来连接的彼端的位址
                 addrlen addr的长度
 传回值:成功 - 新的Socket识别码
         失败 - INVALID_SOCKET (呼叫 WSAGetLastError() 可得知原因)
 说明: Server 端之应用程式呼叫此一函式来接受 Client 端要求之 Socket 连 接动
 作;如果Server 端之 Socket 是为 Blocking 模式,且没有人要求连接动作,那 麽此一
 函式会被 Block 住;如果为 Non-Blocking 模式,此函式会马上回覆错误。acc ept()
 函式的答覆值为一新的 Socket,此新建之 Socket 不可再用来接受其它的连接要 求;
 但是原先监听之 Socket 仍可接受其他人的连接要求。
 
 TCP socket 的 Server 端在呼叫 accept() 後,会传回一个新的 socket 号码; 而这
 个新的 socket 号码才是真正与 Client 端相通的 socket。比如说,我们用 so cket() 建
 立了一个 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 环境的使用。)
 
 --
 ※ 来源:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 202.38.212.2 22]
 --
 ※ 转载:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 202.96.151.2 19]
 --
 ※ 转寄:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 202.38.198.8 5]
 
 
  -- ※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.38.198.85]
  | 
 
 
 |