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)

第16章 TCP/IP和网络通信

版权所有 © 中山市飞娥软件工作室 证书:粤ICP备09170368号