简单的 Winsock 应用程式设计2.docx
《简单的 Winsock 应用程式设计2.docx》由会员分享,可在线阅读,更多相关《简单的 Winsock 应用程式设计2.docx(12页珍藏版)》请在冰豆网上搜索。
![简单的 Winsock 应用程式设计2.docx](https://file1.bdocx.com/fileroot1/2023-1/10/0805c161-e683-4860-8305-6c43457a88d2/0805c161-e683-4860-8305-6c43457a88d21.gif)
简单的Winsock应用程式设计2
简单的Winsock应用程式设计
(2)
在前一期的文章中,笔者为大家介绍了如何在Winsock环境下,建立主从
架构(Client/Server)的TCPsocket的连接建立与关闭;今天笔者将继续为大家
介绍如何利用TCPsocket来收送资料,并详细解说WSAAsyncSelect函式中的
FD_READ及FD_WRITE事件(笔者曾发现有相当多人对这两个事件甚不了
解)。
相信读者们已经知道TCPsocket的连接是在Client端呼叫connect函式成
功,且Server端呼叫accept函式後,才算完全建立成功;当连接建立成功後,
Client及Server也就可以利用这个连接成功的socket来传送资料到对方,或是
收取对方送过来的资料了。
(图1.TCPsocket的资料收送)
在介绍资料的收送前,笔者先介绍一下TCPsocket与UDPsocket在传送资
料时的特性:
Stream(TCP)Socket提供「双向」、「可靠」、「有次序」、「不重覆」之
资料传送。
Datagram(UDP)Socket则提供「双向」之沟通,但没有「可靠」、「有次
序」、「不重覆」等之保证;所以使用者可能会收到无次序、重覆之资料,甚至
资料在传输过程中也可能会遗漏。
由於UDPSocket在传送资料时,并不保证资料能完整地送达对方,所以我
们常用的一些应用程式(如telnet、mail、ftp、news...等)都是采用TCP
Socket,以保证资料的正确性。
(TCP及UDP封包的传送协定不在我们讨论□
围,想要了解的读者们,请自行参考相关书籍)
TCP及UDPSocket都是双向的,所以我们是利用同一个Socket来做传送及
收取资料的动作;一般言TCPSocket的资料送、收是呼叫send()及recv()这两
个函式来达成,而UDPSocket则是用sendto()及recvfrom()这两个函式。
不过
TCPSocket也可用sendto()及recvfrom()函式,UDPSocket同样可用send()及
recv()函式;这一点我们稍後再加以解释。
现在我们先看一下send()及recv()的函式说明,并回到我们的前一期程
式。
◎send():
使用连接式(connected)的Socket传送资料。
格式:
intPASCALFARsend(SOCKETs,constcharFAR*buf,
intlen,intflags);
参数:
sSocket的识别码
buf存放要传送的资料的暂存区
lenbuf的长度
flags此函式被呼叫的方式
传回值:
成功-送出的资料长度
失败-SOCKET_ERROR(呼叫WSAGetLastError()可得知原因)
说明:
此函式适用於连接式的Datagram或StreamSocket来传送资料。
对
DatagramSocket言,若是datagram的大小超过限制,则将不会送出任何资料,并
会传回错误值。
对StreamSocket言,Blocking模式下,若是传送(transport)系统
内之储存空间(outputbuffer)不够存放这些要传送的资料,send()将会被block
住,直到资料送完为止;如果该Socket被设定为Non-Blocking模式,那麽将视目
前的outputbuffer空间有多少,就送出多少资料,并不会被block住。
使用者亦须
注意send()函式执行完成,并不表示资料已经成功地送抵对方了,而是已经放到
系统的outputbuffer中,等待被送出。
flags的值可设为0或MSG_DONTROUTE
及MSG_OOB的组合。
(参见WINSOCK第1.1版48页)
◎recv():
自Socket接收资料。
格式:
intPASCALFARrecv(SOCKETs,charFAR*buf,intlen,intflags);
参数:
sSocket的识别码
buf存放接收到的资料的暂存区
lenbuf的长度
flags此函式被呼叫的方式
传回值:
成功-接收到的资料长度(若对方Socket已关闭,则为0)
失败-SOCKET_ERROR(呼叫WSAGetLastError()可得知原因)
说明:
此函式用来自连接式的DatagramSocket或StreamSocket接收资料。
对StreamSocket言,我们可以接收到目前inputbuffer内有效的资料,但其数量
不超过len的大小。
若是此Socket设定SO_OOBINLINE,且有out-of-band的资
料未被读取,那麽只有out-of-band的资料被取出。
对DatagramSocket言,只取
出第一个datagram;若是该datagram大於使用者提供的储存空间,那麽只有该空
间大小的资料被取出,多馀的资料将遗失,且回覆错误的讯息。
另外如果Socket
为Blocking模式,且目前inputbuffer内没有任何资料,则recv()将block到有任
何资料到达为止;如果为Non-Blocking模式,且inputbuffer无任何资料,则会马
上回覆错误。
参数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,(structsockaddrfar*)&sa,&sa_len)
我们在呼叫完accept()函式,成功地建立了Server端与Client端的连接後,
此时便可利用新建的Socket(my_sd)来收送资料了。
由於我们同样希望用
Asynchronous的方式,因此要再利用WSAAsyncSelect()函式来帮新建的
Socket设定一些事件,以便事件发生时WinsockStack能主动通知我们。
由於我
们的Server是被动的接受Client的要求,然後再做答覆,所以我们设定
FD_READ事件;我们也希望WinsockStack在知道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事件时,
inputbuffer中已有资料。
(2)原先系统的inputbuffer是空的,当系统再收到资料时,会通知我们。
(3)使用者呼叫recv或recvfrom函式,从inputbuffer读取资料,但是并
没有一次将资料读光,此时会再驱动一个FD_READ事件,表示仍有资料在
inputbuffer中。
读者必须注意:
如果我们收到FD_READ事件通知的讯息,但是我们故意
不呼叫recv或recvfrom来读取资料的话,尔後系统又收到资料时,并不会再次
通知我们,一定要等我们呼叫了recv或recvfrom後,才有可能再收到
FD_READ的事件通知。
【FD_CLOSE事件】
当系统知道对方已经将Socket关闭了的情况下(收到FIN通知,并和对方
做关闭动作的hand-shaking),我们会收到FD_CLOSE的事件通知,以便我
们也能将这个相对的Socket关闭。
FD_CLOSE事件只会发生於TCPSocket,因
为它是connection-oriented;对於connectionless的UDPSocket,即使设了
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,表示我们要将资料从系统的inputbuffer中读取并移走。
收到要求後,我们要答覆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端的TCPsocket连接就算成功了。
这时,我们便可利用这个连
接成功的Socket来送收资料了。
由於我们并没有要设定为Asynchronous模式,
所以也不用呼叫WSAAsyncSelect()来设定事件。
Client端通常是会先主动发出要求到Server端,因此我们呼叫send()来传送
此一资料。
我们的资料量很小,所以并不会被send()函式Block住;不过如果
您要送的资料量很大,那麽可能会等一段时间才会自send()函式返回;也就是
说必须等资料都放到系统的outputbuffer後才会返回;这是因为我们Client的
Socket是阻拦模式。
如果我们用的是非阻拦模式的Socket,那麽send()函式会
视系统的outputbuffer的空间有多少,只拷贝那麽多的资料到outputbuffer,然
後就返回,并告知使用者送出了多少资料,并不须等所有资料都放到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错误,错误码10004WSAEINTR)
同样地,资料全部送收完毕後,我们也呼叫closesocket()来将Socket关
闭。
◎WSACancelBlockingCall():
取消目前正在进行中的blocking动作。
格式:
intPASCALFARWSACancelBlockingCall(void);
参数:
无
传回值:
成功-0
失败-SOCKET_ERROR(呼叫WSAGetLastError()可得知原因)
说明:
此函式用来取消该应用程式正在进行中的blocking动作。
通常的
使用时机有:
(a)Blocking动作正在进行中,该应用程式又收到某一讯息
(Mouse、Keyboard、Timer等),则可在处理该讯息的段落中呼叫此函式。
(b)
Blocking动作正在进行中,而WindowsSockets又呼叫回应用程式?
「blockinghook」函式时,在该函式内可呼叫此函式来取消blocking动作。
使用者必须注意,在某一Winsockblocking函式动作进行时,除了
WSAIsBlocking()及WSACancelBlockingCall()外,不可以再呼叫其它任何
WindowsSocketsDLL提供的函式,否则会产生错误。
另外若取消的
blocking动作不是accept()或select()的话,那麽该Socket可能会处於未定
状态,使用者最好是呼叫closesocket()来关闭该Socket,而不该再对它做任
何动作。
(图2.)demoserv与democlnt在资策会WinKing上收送资料的画面
(图3.)demoserv与democlnt在资策会WinKing上关闭Socket後的画面
介绍完了TCPSocket的资料收送,笔者接著为读者介绍sendto()及
recvfrom()这两个函式,以及许多人可能很容易搞错的FD_WRITE事件。
【sendto及recvfrom函式】
一般言,TCPSocket使用的是send()及recv()这两个函式;而UDPSocket
用的是sendto()及recvfrom()函式。
这是因为TCP是Connection-oriented,必须
做完Socket真正的连接程序後,才可以开始收送资料,此时系统已经知道了连
接的对方,所以我们不用再指定资料要送到哪里。
而UDP是Connectionless,
收送资料的双方并没有建立真正的连接,所以我们要利用sendto()及recvfrom()
来指定收资料的对方及获知是谁送资料给我们?
TCPSocket也可以用sendto()及recvfrom()来送收资料,只是此时这两个
函式的最後两个参数没有作用,会被系统所忽略。
而UDPSocket如果呼叫了
connect()函式来指定对方的位址(这个connect并不会真的和对方做连接的动
作,而是告知我们本身的系统说我们只想收、送何方的资料),那麽也可以利
用send()及recv()来送收资料。
◎sendto():
将资料送到使用者指定的目的地。
格式:
intPASCALFARsendto(SOCKETs,constcharFAR*buf,
intlen,intflags,conststructsockaddrFAR*to,int
tolen);
参数:
sSocket的识别码
buf存放要传送的资料的暂存区
lenbuf的长度
flags此函式被呼叫的方式
to资料要送达的位址
tolento的大小
传回值:
成功-送出的资料长度
失败-SOCKET_ERROR(呼叫WSAGetLastError()可得知原因)
说明:
此函式适用於Datagram或StreamSocket来传送资料到指定的
位址。
对DatagramSocket言,若是datagram的大小超过限制,则将不会
送出任何资料,并会传回错误值。
对StreamSocket言,其作用与send()相
同;参数to及tolen的值将被系统所忽略。
若是传送(transport)系统内之储
存空间不够存放这些要传送的资料,sendto()将会被block住,直到资料都被
送出;除非该Socket被设定为non-blocking模式。
使用者亦须注意sendto()
函式执行完成,并不表示资料已经成功地送抵对方了,而可能仍在系统的output
buffer中。
flags的值可设为0、MSG_DONTROUTE及MSG_OOB的组合。
(参见WINSOCK第1.1版51页)
◎recvfrom():
读取资料,并储存资料来源的位址。
格式:
intPASCALFARrecvfrom(SOCKETs,charFAR*buf,intlen,intflags,
structsocketaddrFAR*from,intFAR*fromlen);
参数:
sSocket的识别码
buf存放接收到的资料的暂存区
lenbuf的长度
flags此函式被呼叫的方式
from资料来源的位址
fromlenfrom的大小
传回值:
成功-接收到的资料长度(若对方Socket已关闭,则为0)
失败-SOCKET_ERROR(呼叫WSAGetLastError()可得知原因)
说明:
此函式用来读取资料并记录资料来源的位址。
对DatagramSocket
(UDP)言,一次读取一个Datagram;对StreamSocket(TCP)言,其作用与
recv()相同,参数from及fromlen的值会被系统忽略。
如果Socket为Blocking模
式,且目前inputbuffer内没有任何资料,则recvftom()将block到有任何资料到
达为止;如果为Non-Blocking模式,且inputbuffer无任何资料,则会马上回覆错
误。
【FD_WRITE事件】
笔者在前面介绍过FD_READ事件的发生时机,现在继续介绍FD_WRITE
这个较易使人混淆的事件,因为真的有相当多的人对此一事件的发生不明了。
由字面上看,FD_WRITE应该是要求系统通知我们某个Socket现在是否可
以呼叫send()或sendto()来传送资料?
答案可以说「是」,但是它和FD_READ
却又有不同的地方。
在前面我们知道呼叫一次recv()後,如果inputbuffer中尚有资料未被取出
的话,系统会再通知我们一次FD_READ。
那麽如果我们呼叫一次send()後,
系统的outputbuffer仍有空间可写入的话,它是否会再通知我们一个
FD_WRITE,叫我们继续传送资料呢?
这个答案就是「否定」的了!
系统并不
会再通知我们了。
系统会通知我们FD_WRITE事件的讯息,只有下列几种情况:
(1)呼叫WSAAsyncSelect()来设定FD_WRITE事件时,Socket已经可以
传送资料(TCPscoket已经和对方连接成功了,或UDPsocket已建立完成),
且目前outputbuffer仍有空间可写入资料。
(2)呼叫WSAAsyncSelect()来设定FD_WRITE事件时,Socket尚不能传
送资料,不过一旦Socket与对方连接成功,马上就会收到FD_WRITE的通
知。
(3)呼叫send()或sendto()传送资料时,系统告知错误,且错误码为
10035WSAEWOULDBLOCK(呼叫WSAGetLastError()得知这项错误),这
时表示outputbuffer已经满了,无法再写入任何资料(此时即令呼叫再多次的
send()也都一定失败);一旦系统将部份资料成功送抵对方,空出outputbuffer
後,便会送一个FD_WRITE给使用者,告知可继续传送资料了。
换句话说,读
者在呼叫send()传送资料时,只要不是返回错误10035的话,便可一直继续呼
叫send()来传送资料;一旦send()回返错误10035,那麽便不要再呼叫send()
传送资料,而须等收到FD_WRITE後,再继续传送资料。
【结语】
在这一期的文章中,笔者介绍了各位有关TCPSocket的资料收、送方式及