📄 016.txt
字号:
通知消息的ID可以由程序自己定义,当不同的动作完成以后,不同的通知码将被包含在消息的参数中传递给指定的窗口过程,wMsg参数用来定义通知消息使用的ID,可以在WM_USER以上数值中任意选择一个用做ID,最后的参数lEvent指定哪些通知码需要发送,它可以被指定为几个通知码的组合,一般常用的通知码有下面这些:
● FD_READ——套接字收到对端发送过来的数据包,表明这时可以去读套接字。
● FD_WRITE——当短时间内向一个套接字发送太多数据造成缓冲区满以后,发送函数会返回出错信息,当缓冲区再次变空的时候,WinSock接口通过这个通知码通知应用程序,表示可以继续发送数据了。但是缓冲区未溢出的情况下数据被发送完毕的时候并不会发送这个通知码。
● FD_ACCEPT——监听中的套接字检测到有连接进入(适用于流套接字)。
● FD_CONNECT——如果用一个套接字去连接对方主机,当连接动作完成以后将收到这个通知码(适用于流套接字)。
● FD_CLOSE——检测到套接字对应的连接被关闭(适用于流套接字)。
在使用中并不需要指定全部这些通知码。例如,对于数据报套接字来说,它并不需要一个连接的过程,所以FD_CONNECT,FD_CLOSE和FD_ACCEPT等通知码是没有意义的,而在流套接字中,如果一个套接字不是用于监听,那么FD_ACCEPT也是没有意义的,所以要根据套接字的类型选用合适的通知码。
4. 将套接字绑定到IP地址和端口
对于一个流套接字和数据报套接字来说,它使用的IP地址和端口的组合在系统中必须是惟一的,这样WinSock内核才能将它收发的数据和其他套接字分辨开来。当创建一个流套接字和数据报套接字用于通信的时候,如果不去人工指定它使用哪个IP地址和哪个端口,那么系统自动将套接字的IP地址指定为本机的IP地址,端口指定为1 024 ~ 65 536之间的任意一个未使用的端口号(编号在1 024以下的端口被视为保留端口而不予自动分配)。
但是在某些情况下,需要人工指定IP地址或端口号,比如,当创建的套接字用于提供特定服务的时候,必须使用一个固定的端口号,如提供Web服务的套接字一般工作在80号端口,而提供FTP服务的套接字一般工作在21号端口,不然每次由系统随机指定端口号的话,其他计算机就不知道该与哪个端口连接。另外,当一台计算机上指定有多个IP地址,而我们希望套接字使用其中的一个IP地址而不是全部IP地址时(比如,用做虚拟主机服务的计算机往往指定多个IP地址,然后指派IP地址1的端口80提供网站A服务,IP地址2的端口80提供网站B服务等),这时就需要指定套接字只使用其中的一个IP地址。
使用bind函数可以为套接字指定IP地址和端口,这个过程称为绑定。函数用法如下:
invoke bind,s,lpsockaddr,len
参数s指定套接字句柄,lpsockaddr参数是一个指向sockaddr_in结构的指针,len参数指定sockaddr_in结构的长度。sockaddr_in的定义是:
sockaddr_in STRUCT
sin_family WORD ? ;地址格式
sin_port WORD ? ;端口号(使用网络字节顺序)
sin_addr in_addr <> ;IP地址(使用网络字节顺序)
sin_zero BYTE 8 dup (?) ;空字节
sockaddr_in ENDS
结构中的sin_family 字段用来指定地址格式,这个字段和socket函数中af参数的含义是相同的,所以惟一可以使用的值就是AF_INET。sin_port字段和sin_addr字段分别指定套接字需要绑定的端口号和IP地址,放入这两个字段的数据的字节顺序必须是Internet顺序,由于字节顺序和Intel CPU的字节顺序刚好相反,所以必须首先经过转换,比如当端口号为9 999时,转换成16进制是270Fh,那么放入sin_port字段的数值就应该是转换以后的0F27h。
sockaddr_in 结构是WinSock编程中最常用的结构,凡是涉及通信地址的时候都会用到这个结构,比如使用connect发起连接和使用sendto发送数据时的目标地址都是使用这个结构指定的。
读者可能会问一个问题,结构中存在一个定义IP地址的sin_addr字段,这样的话岂不是必须先获取本机的IP地址后才能绑定,否则如何填写这个字段呢?实际上仅在当前计算机配置有多个IP地址,而程序又只想绑定到其中一个IP地址的时候才需要具体指定使用哪一个地址,否则可以使用预定义值ADDR_ANY,这样系统会自动使用当前主机配置的所有IP地址。
使用bind函数绑定IP地址和端口的时候,端口号可以指定为任意一个16位值,也可以使用小于1 024的值,惟一的限制就是不能去绑定已经在使用中的端口号。
如果绑定成功,函数返回0,否则函数返回SOCKET_ERROR,这时程序可以继续调用WSAGetLastError函数获取进一步的出错代码。一般来说,绑定错误是由下面几个原因引起的,对应的出错代码如下:
● WSAEADDRINUSE——指定的IP地址或端口已经在使用中。
● WSAEFAULT——指定的结构、地址和端口等数据无效。
● WSAEINVAL——套接字已经被绑定到某个地址,绑定只能进行一次,对一个已经绑定过的套接字再次调用bind函数就会出现这个错误。
5. 用于连接和收发数据的函数
当完成了前面的库初始化,创建套接字,设置套接字模式以及绑定工作后,就可以使用套接字收发数据了。流套接字和数据报套接字数据收发时适用的函数有所不同,一般对流套接字使用send和recv函数来收发数据,对数据报套接字使用sendto和recvfrom函数。对于流套接字来说,开始收发数据之前还需要有一个监听或连接的过程,这时还要用到listen,accept和connect函数。另外,WinSock接口还提供一些与主机名解析有关的函数,如gethostbyname和gethostname等。
所有这些函数将在下面的章节中逐一介绍。
6. 常用的转换函数
WinSock接口中也提供了一些常用的转换函数,比如将32位的IP地址和 “aa.bb.cc.dd”类型的10进制IP地址字符串互相转换,或者将IP地址和端口号等数据在不同的字节顺序之间进行转换,这些函数可以为我们带来很多方便。
inet_addr函数和inet_ntoa可以在IP地址和字符串之间进行转换。
inet_addr函数将一个由小数点分隔的10进制IP地址字符串转换成由32位二进制数表示的IP地址(网络字节顺序):
invoke inet_addr,lpString
.if eax != INADDR_NONE
mov dwIP,eax
.endf
lpString参数指向“aa.bb.cc.dd”类型的IP地址字符串。如果转换成功,函数将返回已经按网络字节顺序排列的32位IP地址,否则返回INADDR_NONE。
inet_ntoa则是inet_addr函数的逆函数,它将一个网络字节顺序的32位IP地址转换成字符串:
invoke inet_ntoa,in
.if eax
mov lpsz,eax
.endif
参数in是需要转换的32位IP地址,如果转换失败函数返回NULL,转换成功的话函数返回一个指针,指向转换后的IP地址字符串。这个字符串位于WinSock接口的内部缓冲区中,所以若以后需要继续使用的话,那么在调用下一个WinSock函数之前必须将它拷贝到程序自己定义的缓冲区中。
除了IP地址转换函数,WinSock接口还提供了一系列的字节顺序转换函数。
htons函数完成的功能是“Host to Network Short”,即将16位的以当前主机字节顺序排列的数据转换成按网络顺序排列的数据:
invoke htons,hostshort
mov netshort,ax
函数的输入值是按主机字节顺序排列的16位数据(当然需要扩展到32位以便当做参数输入),返回值的低16位是转换后的按网络字节顺序排列的数据。
htonl函数完成的功能是“Host to Network Long”,即将32位的以当前主机字节顺序排列的数据转换成按网络顺序排列的数据:
invoke htonl,hostlong
mov netlong,eax
ntohs函数完成的功能是“Network to Host Short”,即将16位的按网络顺序排列的数据转换成以当前主机字节顺序排列的数据(输入参数同样需要首先被扩展到32位):
invoke ntohs,netshort
mov hostshort,ax
ntohl函数则完成“Network to Host Long”功能,即将32位的按网络顺序排列的数据转换成以当前主机字节顺序排列的数据:
invoke ntohl,netlong
mov hostlong,eax
一般来说,当涉及当前主机字节顺序和网络顺序数据的转换时,不管当前主机使用的字节顺序是否和网络顺序相同,最好都进行一次转换,而且转换时必须使用这些WinSock函数而不是自定义的函数,这样程序可以在不同的主机上进行移植。
比如,现在使用的是Intel 80x86平台,它的字节顺序和网络字节顺序是不一样的,如果使用自定义的函数将字节顺序反过来,万一Windows移植到和网络字节顺序一致的硬件平台上,转换的结果就不对了,但使用WinSock接口提供的转换函数肯定是正确的,所以永远使用WinSock接口提供的转换函数是有必要的。
反之,在运行于Motorola平台上的UNIX系统中,CPU字节顺序和网络字节顺序是相同的,如果程序因此取消了字节顺序转换这一步骤,那么程序移植到运行于Intel平台上的Linux系统中时就会出错,所以不管主机的字节顺序是否和网络字节顺序相同,应该总是使用转换函数进行字节顺序转换。
读者可以注意到Win32 API函数的名称一般由几个单词组成,每个单词的首字母是大写的,但是一部分WinSock函数如socket和ioctlsocket等的命名却是全部小写的,造成这种现象的原因是这些函数名称源于UNIX socket,而UNIX socket中的函数命名全部是小写的。读者也可以注意到:WinSock接口中由Windows系统扩展的函数使用的就是标准的Win32 API命名方式,如WSAStartup和WSAAsyncSelect等。从这里也可以看出哪些函数是WinSock接口特有的。
16.3 TCP协议编程
介绍了WinSock编程的一般流程和常用函数以后,现在来看如何使用流套接字传输数据。
16.3.1 TCP协议简介
1. TCP协议的特征
TCP协议是一种传输层上的协议,它提供一种面向连接的、可靠的字节流服务。
面向连接意味着两个使用TCP协议的应用程序在开始传输数据之前必须先建立一个连接。其含义就是由一方首先向另一方发送请求信息,等对方确认以后才能开始通信。这一过程与打电话很相似,一方先拨号振铃,等待对方摘机后才开始对话,如果对端没有程序响应,那就像没有人接电话一样,通信是无法开始的。另外,面向连接意味着TCP协议不能用于广播,即一方不能用一个套接字同时向多方发送数据。
TCP通信是可靠的,它采用超时以及重传机制来保证不丢失数据。当一个TCP发送一个数据包后,它将启动一个定时器,等待对端确认收到这个包,如果在指定的时间内没有得到确认,它将重发这个数据包。而接收数据包的时候,它将发送一个确认,如果检测发现数据包的校验和有错,TCP协议丢弃这个数据包并且不发送确认,那么对端会因为超时而重新发送这个数据包。
数据包在传输的时候会通过多个路由器,不同的数据包到达终点前经过的路由器可能是不同的,因此所花的时间也是不同的,这就可能造成后发的数据先到的情况,TCP协议在TCP首部保存数据包序号,如果有必要,它将对收到的数据进行重新排序,并将收到的数据以正确的顺序交给应用层。
接收方收到的IP报文段也可能重复,原因之一是在发送和确认之间还有个时差,发送方可能因为接收方的确认信息还在路上就发生超时而重发数据,这时接收方收到的数据包就会发生重复,对于这种情况,TCP的接收端会丢弃重复的数据。
TCP还提供流量控制机制,发送方可以根据接收方应答的时间和速率适当调整自己的数据发送速度,这样可以防止速度较快的主机使速度较慢主机的接收缓冲区溢出。
TCP协议的工作过程也和打电话的过程很相似。当一方滔滔不绝地讲话时,需要另一方偶尔回复“是的”、“嗯”之类的短语来确认,如果有一段时间没有听到对方的简短确认,发言方就会停下来问一句“你在听吗”。当另一方没有听清楚某句话的时候,他会说:“你刚才说什么,我没有听清楚”,这样发言方会复述前面的句子。如果一方讲得太快,另一方会要求他适当放慢速度。TCP协议实现的机制就与此类似。
2. TCP程序的客户机/服务器模型
从TCP协议的特征可以看出通信双方的工作方式必然是不同的,这种工作方式的不同可以用客户机/服务器模型(Client/Server或C/S)来描述,通信的发起端被称为客户机(Client,也称为工作站端或者客户端),通信的等待方被称为服务器(Server),图16.8显示了两者工作方式的不同,图中的括号内显示了各步骤使用的WinSock函数。
图16.8 TCP服务器和客户机模型
就像打电话一样,A对B说:“某某时候Call我”,那么B给A打电话的过程就可以用这种客户机/服务器模型来描述。为了等待B的电话,A必须在双方约定的某个特定的电话旁边等待,否则B将不知道如何Call他。“特定的电话”意味着服务器端的地址必须是特定的,所以服务器端的套接字必须首先使用bind函数绑定到指定的IP地址和端口上。“等待”意味着服务器必须随时监听客户端的连接动作,所以套接字绑定地址后必须使用listen函数进入监听状态。而对于B来说,他可以在任何时刻从任何电话给A打电话。由此可见,客户端使用的套接字并不需要绑定过程,让系统自动指定任意值并不影响它向服务器端发起连接。
客户端可以随时使用connect函数连接到服务器,服务器检测到这个连接后,需要使用accept函数接受这个连接,当服务器接受连接后,一个稳定的连接就建立了,双方就可以开始互相通过send和recv函数收发数据了。
下面用两个互相配合的例子—聊天室服务器和客户端例子来演示如何用WinSock接口来实现这个模型。当在一台计算机上运行聊天室服务器程序以后,网络上可以有多个聊天客户端程序同时连接到服务器,当任意一个客户端发出一句话后,服务器将它转发到所有在线的客户端,这样所有的客户端就可以通过服务器进行聊天。
16.3.2 TCP聊天室例子——客户端
TCP聊天室的例子代码放在所附光盘的Chapter15\Chat-TCP目录中,包括服务器端代码和客户端代码。编译链接后执行将产生如图16.9所示的界面,图中后面的窗口是服务器端的界面,对话框中的编辑框中将显示所有客户端的对话内容,对话框最下面显示当前有多少个客户端在线。
图中前面的窗口是客户端的界面,使用时必须首先输入服务器IP地址并进行连接,当连接成功后,在下面的输入框中输入聊天内容并单击“发送”键,就可以将它发送给服务器端程序,服务器端会将它转发给所有的在线客户端。
图16.9 TCP聊天室例子的运行界面
先来看相对比较简单的客户端程序,客户端代码由Client.asm和Client.rc文件组成。Client.rc文件定义了图16.9前端的窗口,内容如下:
#include
#define ICO_MAIN 1000
#define DLG_MAIN 2000
#define IDC_SERVER 2001
#define IDC_CONNECT 2002
#define IDC_INFO 2003
#define IDC_TEXT 2004
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN icon "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 94, 81, 245, 155
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "TCP聊天-客户端"
FONT 9, "宋体"
{
LTEXT "服务器IP地址:", -1, 6, 7, 57, 8
EDITTEXT IDC_SERVER, 63, 5, 116, 12
PUSHBUTTON "连接(&C)", IDC_CONNECT, 186, 4, 56, 14
EDITTEXT IDC_INFO, 4, 22, 237, 110, ES_MULTILINE | ES_AUTOVSCROLL
| ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_VSCROLL | WS_TABSTOP
LTEXT "输入", -1, 6, 140, 19, 8
EDITTEXT IDC_TEXT, 28, 138, 150, 12, ES_AUTOHSCROLL | WS_DISABLED
| WS_BORDER | WS_TABSTOP
DEFPUSHBUTTON "发送(&S)", IDOK, 185, 137, 56, 14, BS_DEFPUSHBUTTON
| WS_DISABLED | WS_TABSTOP
}
客户端的汇编源代码Client.asm文件的内容如下:
.386
.model flat, stdcall
option casemap :none ; case sensitive
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -