WIN32汇编语言教程:第16章 TCP/IP和网络通信 · 16.3 TCP协议编程(4)
比如,例子程序中的连接代码如下:
invoke connect,hSocket,addr @stSin,sizeof @stSin
.if eax == SOCKET_ERROR
invoke WSAGetLastError
.if eax != WSAEWOULDBLOCK
;真正的出错,关闭套接字并退出
.endif
.endif
那么,什么时候才知道连接的动作是否真正成功呢?那就要等待系统的FD_CONNECT通知消息了(如图16.10的③),当收到通知消息时,lParam参数的高16位包含了出错信息,如果检测到高16位等于0就表示连接成功,可以开始收发数据了(如图16.10的④);否则表示没有连接成功,程序应该关闭套接字并显示出错信息(如图16.10的⑤)。
FD_CONNECT通知的常见处理方法是:
mov eax,wMsg
.if eax == WM_SOCKET
mov eax,lParam
.if ax == FD_CONNECT
shr eax,16 ;取lParam的高16位
.if !ax
;连接成功,可以进行收发数据了
.else
;连接失败,显示出错信息
;并且关闭套接字
.endif
.endif
.endif
在进行连接的时候请读者注意:由于TCP协议的连接过程需要3次握手,也就是说确认连接用的数据包需要一定的往返时间,当连接互联网上的主机时,连接的过程往往需要几秒的时间,如果要连接的主机没有在线,等待连接失败的时间可能更长(可能会达到十几秒以上),因为这时客户端将一直等待到超时为止。
所以程序在开始连接的时候暂时禁止“连接”按钮,防止用户在连接的过程中再次按下按钮,而这时连接尚未确立,收发数据的功能也是被禁止的。
如果连接成功,收发数据的功能将被允许,而“连接”按钮也被改为“断开”按钮;如果连接失败,那么“连接”按钮被重新允许,以便用户再一次发起连接动作。
2. 接收数据
当FD_CONNECT通知显示连接成功以后,就可以开始收发数据了,但是什么时候有数据可供接收,是不是需要随时去读套接字?答案是并不需要,如果有数据过来,WinSock接口会发送包含FD_READ通知码的通知消息,程序可以在这里读数据(如图16.10中的A所示)。
系统在以下情况下发送FD_READ通知码:
● 调用WSAAsyncSelect函数对套接字设置非阻塞模式(参数中包括FD_READ)时,接收缓冲区中已经有数据存在。
● 原先系统的接收缓冲区是空的,然后有新的数据进入。
● 程序使用recv或recvfrom函数读取流套接字时,由于读出缓冲区比较小引起数据没有一次性被读完,这时系统会再发送一个FD_READ通知,表示缓冲区中仍有数据存在。
有一点要注意的是,如果收到FD_READ通知码后,由于程序比较忙而没有去读取数据,这时系统又收到了新的数据的话,那么系统并不会再次发送通知,而是必须在调用了recv或recvfrom函数以后才有可能再次收到FD_READ通知。
虽然recv和recvfrom函数都可以用于读取套接字,也都可以用于读取不同类型的套接字,但读取流套接字一般使用recv函数:
invoke recv,s,lpbuf,len,flags
s参数指定读取的套接字句柄,lpbuf指向一个用来返回数据的缓冲区,len参数指定缓冲区的大小,flags参数用来指定读取时的选项,它可以是MSG_PEEK和MSG_OOB的组合,MSG_PEEK表示返回数据后并不从缓冲区中清除数据。
如果读取成功函数将返回实际读取的字节数,否则函数返回SOCKET_ERROR,调用WSAGetLastError函数可以进一步获取出错代码。
如果读出缓冲区大于收到的数据包长度,函数会成功读出整个数据包。但是当缓冲区小于数据包长度时,recv函数对不同类型套接字的处理有所不同,对于流套接字来说这并不会返回错误,这时函数仅读取缓冲区大小的数据,多余的数据可以在下一次调用中继续读取;但是UDP数据包不能被分割,所以对于数据报套接字来说,函数仅读出缓冲区大小的数据,多余的数据将被丢弃,这时函数返回的是SOCKET_ERROR并且出错代码被指定为WSAEMSGSIZE。
如果缓冲区中没有数据却调用recv函数去读的话,对于阻塞模式的套接字,函数将一直等待到有数据到达为止,对于非阻塞的套接字,函数会马上返回SOCKET_ERROR,这时的错误代码是WSAEWOULDBLOCK。
3. 发送数据
发送流数据一般使用send函数:
invoke send,s,lpbuf,len,flags
s参数指定套接字句柄,lpbuf指向包含要发送数据的缓冲区,len参数指定发送的数据长度,flags参数指定选项。
当函数返回时,如果没有错误发生,那么返回值是实际发送的字节数,这个字节数不一定等于len参数指定的长度,具体发送了多少字节取决于系统发送缓冲区中剩余空间的大小。如果系统的发送缓冲区有足够剩余空间,那么指定的全部数据将被发送,否则发送缓冲区有多少剩余空间发送的数据就是多少。程序应该检测返回值是否等于输入的len参数,如果小于len参数,必须将余下的数据在下一次调用中继续发送。
要注意的是,函数执行成功并不意味着数据已经到达对端计算机,而只意味着数据已经进入系统的发送缓冲区。
如果函数执行失败,返回值是SOCKET_ERROR,调用WSAGetLastError函数可以进一步获取出错代码,典型的出错代码是当缓冲区全满的时候返回的WSAEWOULDBLOCK代码。那么当缓冲区全满的时候程序该怎么办呢?是不是需要不停地尝试发送呢?
回答是:没有这个必要,程序可以等待FD_WRITE通知码。但是这个通知会在什么时候收到呢?从字面上看,似乎系统在完成上一次发送后应该用FD_WRITE通知程序可以继续发送了,但实际情况并不是如此,如果没有因为缓冲区满而发生WSAEWOULDBLOCK错误,不管使用多少次send函数系统都不会得到FD_WRITE通知,得到FD_WRITE通知的情况是:
● 用WSAAsyncSelect函数为已连接的套接字设置FD_WRITE事件时,如果发送缓冲区有空闲空间,那么系统发送FD_WRITE通知。
● 套接字刚连接成功时。
● 当调用send或sendto函数发送数据,系统返回WSAEWOULDBLOCK错误时,表示发送缓冲区已经满了,在这以后,一旦系统将部分数据成功送抵对方,空出了部分发送缓冲区后,就会马上发一个FD_WRITE通知。
从这里可以看出,程序可以设置一个标志和FD_WRITE通知配合起来控制流量,标志一开始被设置为“可以发送”状态,如果使用send函数发送数据时发现返回WSAEWOULDBLOCK错误时,程序暂停发送并将标志设置为“暂停发送”(如图16.10的B所示),否则无论调用多少次send函数,返回的错误肯定都会是WSAEWOULDBLOCK。在收到FD_WRITE通知后(如图16.10的C),程序可以将标志恢复为“可以发送”状态并继续发送数据。
为了简化代码,例子程序在调用send函数得到WSAEWOULDBLOCK错误后,仅将“发送”按钮灰化来阻止用户继续输入聊天语句,当收到FD_WRITE通知后再恢复“发送”按钮的状态,这样一来本次要发送的内容实际上是被丢弃的,并且例子代码没有处理实际发送字节数少于要求发送字节数的情况,如果要求所有的数据都不能被丢弃,那么程序中必须处理这些情况。
当使用send函数时,如果返回的已发送数据数量和输入参数len 指定的值不同,则未发送的剩余数据需要被再次发送。
4. 连接的关闭
有好几种情况可以将连接断开:在本地使用closesocket函数关闭套接字可以主动断开连接(如图16.10中的⑥),远程计算机也可以断开连接,当发生其他错误的时候连接也会被断开,如超时,连接被复位,及拨号连接被挂断等。
当连接不是因为使用closesocket函数主动关闭套接字而断开的话,系统会通过FD_CLOSE通知码来通知应用程序(如图16.10中的⑦),这时程序也应该通过closesocket函数将套接字关闭,因为这时套接字已经不能用来收发数据了。
当处理FD_CONNECT通知的时候,如果lParam参数的高16位不为0,则表示连接不成功,但处理FD_CLOSE通知的时候,lParam参数的高16位不等于0并不代表“关闭连接”发生错误,或者理解为连接没有被关闭。
实际情况是,只要收到了FD_CLOSE通知,那么连接肯定已经断开,lParam高16位的错误代码是用来指明连接断开的原因的:假如高16位等于0,表示连接是被对方正常断开的;如果是WSAECONNRESET,表示连接被复位;如果是WSAECONNABORTED,则表示断开原因是超时或其他错误。
16.3.3 TCP聊天室例子——服务器端
在所附光盘的Chapter15\Chat-TCP目录中的Server.asm和Server.rc是服务器端的源代码,其中的Server.rc资源脚本文件定义了图16.9中的服务器端界面,内容如下:
//####################################################################
#include <resource.h>
//####################################################################
#define ICO_MAIN 1000
#define DLG_MAIN 2000
#define IDC_INFO 2001
#define IDC_COUNT 2002
//####################################################################
ICO_MAIN icon "Main.ico"
//####################################################################
DLG_MAIN DIALOG 94, 84, 245, 154
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "TCP聊天-服务器端"
FONT 9, "宋体"
{
EDITTEXT IDC_INFO, 4, 3, 237, 133, ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL
| ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP
LTEXT "当前连线客户端数量:", -1, 5, 141, 81, 8
LTEXT "0", IDC_COUNT, 88, 141, 81, 8
}
上页:第16章 TCP/IP和网络通信 · 16.3 TCP协议编程(3) 下页:第16章 TCP/IP和网络通信 · 16.3 TCP协议编程(5)