简单的 Winsock 应用程式设计.doc_第1页
简单的 Winsock 应用程式设计.doc_第2页
简单的 Winsock 应用程式设计.doc_第3页
简单的 Winsock 应用程式设计.doc_第4页
简单的 Winsock 应用程式设计.doc_第5页
已阅读5页,还剩29页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

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)当你查询完毕了,你就可以挂上电话,彼此间也就离线了。Client和Server间的 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 函式中的第一个,也唯有此函式呼叫成功後,才可以再呼叫其他 WindowsSockets DLL 的函式。此函式亦让使用者可以指定要使用的 Windows Sockets API 版本,及获取设计者的一些资讯。程式中我们要用 Winsock 1.1,所以我们在程式中有一段为:WSAStartup(WORD)(18)|1),(LPWSADATA) &WSAData) 其中 (WORD)(18)|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;/addressbind(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: 动作完成後,接受讯息的视窗 handlewMsg: 传回视窗的讯息lEvent: 应用程式有兴趣的网路事件传回值:成功 0失败 - SOCKET_ERROR (呼叫 WSAGetLastError() 可得知原因)说明:此函式是让使用者用来要求 Windows Sockets DLL 在侦测到某一 Socket有网路事件时送讯息到使用者指定的视窗;网路事件是由参数 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。我们在程式中要求 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() 将它变回阻拦模式。ioctlsocketioctlsocket():控制 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 模式/允许或禁止套接字的非阻塞模式,允许为非0,禁止为0FIONREAD - 自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 台北主机 . tw(0)的 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 的大小传回值: 成功 - 送出的资料长度失败

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论