WIN32汇编语言教程:第16章 TCP/IP和网络通信 · 16.4 UDP协议编程(3)
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
;####################################################################
; 程序开始
;####################################################################
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam,eax,DLG_MAIN,NULL,offset _ProcDlgMain,0
invoke ExitProcess,NULL
;####################################################################
end start
通过UDP协议收发数据时使用数据报套接字。程序在一开始创建一个SOCK_DGRAM类型的套接字并将它设置为非阻塞模式,由于数据报套接字并没有连接和断开的过程,所以并不需要设置FD_CONNECT和FD_CLOSE等通知码,只需要设置FD_READ和FD_WRITE通知码就可以了。一般来说,UDP客户端程序的结构如图16.14所示,WinSock库装入、数据报套接字的创建和设置工作放在窗口的初始化消息中完成(如图16.14中的①)。
图16.14 UDP客户端程序的常见结构
1. 发送UDP数据报
当数据报套接字被创建以后,就可以直接通过它向服务器端发送数据,16.3节中介绍的send函数没有目标地址参数,所以并不适合用来发送无连接的、发送时直接指定目标地址的UDP数据报,一般使用sendto函数来发送UDP数据报:
invoke sendto,s,lpbuf,len,flags,lpto,tolen
参数s指定用来发送数据的套接字句柄,lpbuf指向包含发送数据的缓冲区,len参数指定要发送的数据的长度,flags参数一般指定为0,参数lpto指向一个包含目标地址和端口号的sockaddr_in结构,tolen参数指定了这个结构的大小。由于使用sendto函数需要每次指定对方的地址,所以在例子中,程序每次在调用sendto函数之前首先获取对话框中输入的IP地址,填写到一个sockaddr_in结构中后再供sendto函数使用。
如果数据发送成功,函数将返回实际发送数据的字节数,否则函数返回SOCKET_ERROR,程序可以通过调用WSAGetLastError返回进一步的出错代码。
如果数据报过大,UDP协议不会自动将它分割成几个部分分别传送,对于这种情况,函数将执行失败并返回一个WSAEMSGSIZE出错代码,这时没有任何数据被发送,所以使用的时候要注意数据包最大不能超过预定义值SO_MAX_MSG_SIZE指定的大小。
当发送缓冲区满的时候,函数返回WSAEWOULDBLOCK错误,如图16.14的③,④所示,这时程序应该暂停发送直到收到FD_WRITE通知为止,这一点和使用TCP协议发送数据包时的处理方法是一样的。
对于数据报套接字来说,如果在对它调用sendto函数之前没有被bind函数绑定到一个固定的IP地址或端口上,那么这时它还没有被系统自动指派某个端口,这就意味着还无法使用它来接收数据(还没有地址和端口如何接收数据),直到第一次对它使用sendto函数来发送数据以后,系统才自动将它绑定到某个空闲的端口上,这时套接字才可用来接收数据。所以在图16.14中,必须首先有第②步的发送动作,才可能有第⑤步的接收动作,一个未经bind的数据报套接字在使用sendto函数之前是不可能接收到数据的。
当使用sendto函数时,如果返回的已发送数据数量和输入参数len 指定的值不同,则未发送的剩余数据需要被再次发送。
2. 接收UDP数据报
16.3节介绍的recv函数可以用来接收UDP数据包,但是recv函数接收数据时并不提供通信对端的地址,对于TCP协议来说这不是问题,因为TCP协议连接的时候已经确定了对方的IP地址,但UDP协议是无连接的,使用recv函数接收数据的话,程序将无法知道数据是从哪里发送过来的,这样也就无法将回复数据发回给发送方。
一般使用可以返回对端地址的recvfrom函数来接收UDP数据包:
invoke recvfrom,s,lpbuf,len,flags,lpfrom,lpfromlen
参数s指定套接字句柄,lpbuf指向一个用来接收数据的缓冲区,len参数指定缓冲区的大小。flags参数为标志。这个函数不同于recv函数的是多出来的最后两个参数,lpfrom参数指向一个缓冲区,函数在这里返回一个包含数据发送方地址的sockaddr_in结构,lpfromlen指向一个双字变量,函数在这里返回前面的sockaddr_in结构的长度。
除了可以返回对端地址以外,函数的使用方法和返回值与recv函数是一样的。
在使用UDP协议进行通信的程序中,应当使用适当的方法来做一些简单的确认动作。读者可能会问:如果要确认那就使用TCP协议好了,为什么还要使用UDP协议呢?其实这里的确认指的是确认双方是否在线的简单动作,而不是去实现TCP协议所具有的所有特性。
比如,例子程序在收到FD_READ通知后,调用_RecvData子程序来接收服务器发送过来的聊天语句,然后向服务器发送一个4个字节的数值为–1的双字数据当做应答,这是因为UDP协议没有连接,服务器端在收到客户端发送的聊天语句时将IP地址和端口当做ID登记下来,然后当其他客户端发送聊天语句的时候根据登记表中的ID将语句转发给所有客户端。但是某个客户端退出的时候,服务器是收不到通知的,没有应答机制将使服务器的客户端列表中逐步充满无效的地址,从而影响新客户端的进入,而发送应答信号可以让服务器端将没有应答的客户端适时地从列表中清除。
那么为什么不在客户端退出的时候通知服务器端“我已退出”呢?这是因为这个通知信息本身也是不可靠的(可能丢失),另外,当客户端被非正常终止或者网络物理连接异常断开时将没有机会发送“我已退出”信息(这种情况对于TCP协议来说是可以被检测到的,服务器可以得到FD_CLOSE通知),所以在对每条聊天语句进行确认是最好的办法。
16.4.2 UDP聊天室例子——服务器端
这里是和前面的客户端程序配套的服务器端代码,资源脚本文件请参考TCP聊天服务器使用的Server.rc文件,它们的界面定义是相同的。汇编源代码Server.asm如下:
.386
.model flat, stdcall
option casemap :none ; case sensitive
;####################################################################
; Include 数据
;####################################################################
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include wsock32.inc
includelib wsock32.lib
;####################################################################
; equ 数据
;####################################################################
DLG_MAIN equ 2000
IDC_INFO equ 2001
IDC_COUNT equ 2002
WM_SOCKET equ WM_USER + 100
UDP_PORT equ 9999
MAX_SOCKET equ 100 ;聊天室最大容量
RETRY_TIMES equ 5
;********************************************************************
CLIENT_ADDR struct
dwClientIP dd ?
上页:第16章 TCP/IP和网络通信 · 16.4 UDP协议编程(2) 下页:第16章 TCP/IP和网络通信 · 16.4 UDP协议编程(4)