📄 016.txt
字号:
协议或应用程序
TCP端口号
UDP端口号
HTTP
80
POP3
110
DNS查询
53
TFTP协议
69
NetBIOS名字服务
137
NetBIOS数据包服务
138
SQL Server数据库
139、1433
Oracle数据库
1521
3. 字节顺序
TCP/IP协议是一组开放的协议,它被设计用来在不同的计算机平台之间进行通信,所以在协议的实现细节中不能包括与特定平台相关的东西,凡是与平台相关的都需要转换为规定的格式,其中最主要的就是对字节顺序的处理。
不同CPU对字节顺序的处理方式是不一样的,当数据包在基于这些处理器的平台上传输时,这一点可能会引发一些问题。CPU对字节顺序的处理方式有两种:大尾方式(big Endian)和小尾方式(little Endian)。在大尾方式中,数据的高字节被放置在连续存储区的首位,比如一个32位的16进制数12345678h在内存中的存放方式是12h,34h,56h,78h,同样,IP地址192.168.0.1在内存中的存放方式是192,168,0,1;而在小尾方式中,数据的低字节被放置在连续存储区的首位,上面的数据在内存中的存放方式变为78h,56h,34h,12h以及1,0,168,192。Intel 80x86系列处理器和DEC VAX处理器使用的是小尾方式(所以我们常常看到内存中的多字节数是倒过来放置的),而Motorola的680x0和其他大部分的RISC芯片都使用大尾方式。
大尾和小尾方式各有好处,不同的处理器采用不同的方式本身无可厚非,但是要在它们之间进行通信的话就必须选定其中一种方式当做标准,否则会造成混淆,比如某个采用Intel CPU的计算机要向某个采用Motorola CPU的计算机的0100h号端口发送数据,它按照自己的字节处理顺序在TCP首部填入代表端口号的数据00h,01h(字节流按小尾方式排列),而接收方收到后却按照自己的方式理解为0001h端口,那就成问题了。
TCP/IP协议统一规定使用大尾方式传输数据(也称Internet顺序),非常遗憾的是,这与Intel 80x86系列处理器使用的方式不同,所以在80x86平台下的WinSock编程中,当使用IP地址和端口号等参数时,必须首先将它们转换为Internet顺序。
16.2 WinSock接口
16.2.1 WinSock接口简介
Microsft为Win32环境下的网络编程提供了Windows Sockets接口(简称WinSock接口),正如Windows下的各种接口都是以API的形式出现的一样(如GDI),WinSock也是以一组API的方式提供的,从1991年推出1.0版开始,经过不断地完善后,WinSock接口现在已成为Windows下网络编程的标准。
WinSock接口是以BSD UNIX操作系统中流行的Socket接口为范例定义的,它包含了 UNIX Sockets接口中一系列的同名函数,但是Windows系统的运行方式和UNIX系统有显著的不同,为了和Windows系统相适应,WinSock接口在移植这些函数的同时对它们进行了改造,并针对Windows的消息驱动机制定义了一部分新的函数,所以使用WinSock接口的程序和UNIX网络通信程序的结构还是有很多的不同点的。
WinSock接口允许应用程序通过它来利用下层的网络通信协议进行通信。如图16.6所示,通过WinSock接口可以在TCP/IP模型的传输层上利用TCP或UDP协议进行通信,也可以在Internet层上直接使用IP协议,以便处理ICMP等数据包。
图16.6 WinSock接口
但WinSock接口并没有对应用层上的各种协议提供支持,所以应用程序无法通过WinSock接口提供的函数来实现HTTP,FTP与Telnet等协议,WinSock接口也不对绑定到TCP/IP协议上的NetBIOS协议提供支持,应用程序必须用另外的接口或者自己编程来实现这些高层协议。
大部分使用WinSock接口的程序主要是利用TCP协议来进行数据流通信或者利用UDP协议进行数据报通信,但是程序也可以通过WinSock接口直接使用IP协议来实现某些特殊功能,如实现ICMP与IGMP等Internet层上面的协议,或者用来发送非正常的数据包,比如在2000年2月份,雅虎、亚马逊和CNN等著名网站受到D.o.S(Denial of Service,拒绝服务)攻击而陷入瘫痪,D.o.S攻击程序制造了高流量的无用数据或反复发送畸形数据,以此引发服务器程序错误而大量占用系统资源,从而使主机处于假死状态甚至死机,D.o.S攻击程序使用的非正常的数据包就可以通过WinSock接口用IP协议实现。
1. WinSock接口中的套接字
为了使用WinSock接口进行通信,必须首先建立一个用来通信的对象,这个对象就称为套接字(Socket),套接字的定义是“通信的一端”,在通信的另一端必定有另一个套接字与之相对应以互相传递数据。使用WinSock接口进行通信的时候,第一个步骤就是建立一个套接字。
套接字的种类有很多种,最主要的两种是流套接字(stream socket)和数据报套接字(datagram socket)。流套接字使用传输层的TCP协议进行通信,所以它具有TCP协议所拥有的各种特征,比如它是面向连接的、稳定的,及数据包是顺序发送的等;而数据报套接字使用UDP协议进行通信,所以它的特征同样来自于UDP协议,如数据包可能丢失,可能重复,及可能不按顺序到达等。
另外,也存在其他一些不常用的套接字类型,如原始套接字(raw socket)、可靠信息分递套接字(rdm socket)和连续小分包套接字(seqpacket socket)等,其中值得一提的是原始套接字,使用这种套接字可以直接在Internet层上处理IP数据包的首部,所以可以用它来实现各种特殊的功能,如伪造发送者地址等。
一个套接字必须能与使用它的进程一一对应,所以对于流套接字和数据报套接字来说,IP地址和端口号是两个必不可少的参数,而对于原始套接字来说,由于它是在Internet层上工作的,在这里还没有开始使用端口号进行复用,所以它的有效参数只有IP地址。
有一部分介绍WinSock的书籍中讲到:“套接字的种类有两种——流套接字和数据报套接字”,这种说法是不确切的,因为正如上面介绍的,实际上还存在其他类型的套接字。
2. 套接字的工作模式
WinSock套接字的使用分为两种模式:阻塞模式和非阻塞模式。阻塞模式也称为同步模式,在这种模式下,WinSock函数直到全部操作完成后才返回。比如要接收数据包时,必须等到对方将数据包发送过来为止,调用WinSock函数的线程在这期间是被挂起的,所以程序看起来好像是停止响应了。显然,要以这种方式执行WinSock函数的话,几乎每个函数的使用都会违反“1/10秒规则”,所以必须考虑在不同的线程中执行每个WinSock函数,这显然是比较麻烦的。
在BSD UNIX中,套接字是以阻塞模式执行的,这对以命令行方式执行的UNIX程序来说并不是问题,但阻塞模式不是很适合于Windows下的消息驱动体系,所以WinSock为所有的函数提供了非阻塞模式的版本,非阻塞模式又称异步模式,在这种模式下,一个函数执行后会立即返回,即使是操作还没有全部完成,但是当函数最终完成操作的时候,WinSock接口会通过某种形式(如窗口消息)通知应用程序,显然这种方式非常适合于Windows下的消息驱动体系。
一般来说,WinSock接口强烈推荐程序员使用非阻塞模式进行通信编程,仅在绝对有必要的情况下才采用阻塞方式进行通信编程,因为非阻塞模式的操作能够更好地在Windows环境下进行。
16.2.2 WinSock编程概述
使用WinSock接口编程的一般步骤是:
(1)装入并初始化WinSock动态链接库。
(2)创建一个套接字。
(3)指定这个套接字的工作方式:使用阻塞模式还是非阻塞模式。
(4)如果使用TCP协议,则将套接字连接到对端主机的套接字(不使用TCP协议则不要这一步)。
(5)如果需要手工指定套接字使用的IP地址和端口号,那么将套接字绑定到指定的地址和端口(如果由系统自动分配地址和端口则不需要这一步)。
(6)通过套接字收发数据。
(7)关闭套接字。
(8)释放WinSock动态链接库。
1. WinSock库的初始化和释放
当前可以使用的WinSock接口动态链接库从版本上看有1.1版本和2.0版本两种,从位数分可以分为16位版本和32位版本,如图16.7所示,WinSock接口函数的代码主要包括在WS2_32.dll库文件中,这个库文件提供了对2.0版本WinSock接口的支持。但是在早期的Windows中,16位和32位的1.1版本的文件名分别是WinSock.dll和WSock32.dll,为了给使用这些库文件的程序提供兼容性支持,系统中仍然存在这两个文件,只不过在这两个文件中也是间接调用了WS2_32.dll文件而已。
为了使用WinSock接口,需要在源程序中包含对应的inc和lib文件,如果使用2.0版本,在源程序中必须包括WS2_32.inc和WS2_32.lib文件,如果要使用的是1.1版本的WinSock函数,那么既可以使用上面两个文件,也可以使用WSock32.inc和WSock32.lib文件。
图16.7 WinSock接口使用的动态链接库
在源文件中包含了对应的inc文件和lib文件后,在使用其他WinSock函数之前,必须首先使用WSAStartup函数来装入并初始化动态链接库,否则对其他任何WinSock函数的调用都不会成功:
invoke WSAStartup,wVersionRequested,lpWSAData
.if eax
;初始化库出现错误
.endif
wVersionRequested是一个16位的参数,用来指定动态链接库将支持哪个版本的WinSock函数,其中的低8位指定主版本号,高8位用来指定副版本号,假如要使用1.1版的,可以在这里使用0101h,假如需要使用2.0版本函数,则可以将参数指定为0002h。
lpWSAData参数指向一个WSADATA结构,用来返回动态链接库的详细信息,结构的定义为:
WSADATA STRUCT
Wversion WORD ? ;库文件建议应用程序使用的版本
wHighVersion WORD ? ;库文件支持的最高WinSock版本
szDescription BYTE WSADESCRIPTION_LEN + 1 dup (?) ;库描述字符串
szSystemStatus BYTE WSASYS_STATUS_LEN + 1 dup (?) ;系统状态字符串
iMaxSockets WORD ? ;同时支持的最大套接字数量
iMaxUdpDg WORD ? ;2.0版中已废弃的参数
lpVendorInfo DWORD ? ;2.0版中已废弃的参数
WSADATA ENDS
szDescription字段中返回的字符串一般是“WinSock 2.0”之类的库描述串,szSystemStatus字段中返回的是类似于“Running”一类的运行状态字符串。如果库装入成功,函数将返回0,否则将返回下面的出错代码:
● WSASYSNOTREADY——网络子系统未准备好。
● WSAVERNOTSUPPORTED——不支持指定的版本。
● WSAEINPROGRESS——另一个阻塞方式的WinSock 1.1操作正在进行中。
● WSAEPROCLIM——WinSock接口已经到达了所支持的最大任务数。
● WSAEFAULT——输入参数lpWSAData指定的指针无效。
如果不再需要使用WinSock函数了,那么必须使用WSACleanup 函数将库释放:
invoke WSACleanup
WSACleanup函数没有输入参数,它将释放动态链接库并自动释放所有被创建的套接字等资源。如果函数执行成功将返回0,否则将返回SOCKET_ERROR。
WinSock函数中有些参数是16位的,如WSAStartup函数中的wVersionRequested参数,但是在传递这些参数时,并不是堆栈中压入一个字(word),而是将它扩展到32位的双字(dword)以后再压入这个双字,所以在源程序的invoke语句中并不需要特殊的处理。
2. 套接字的创建和关闭
套接字是通信的一端,当装载WinSock库以后,在开始通信之前必须为通信进程建立一个套接字,如果在一个程序中同时使用多个通信进程(比如用TCP协议同时连接到几个不同的主机),那就必须为每个通信连接创建一个套接字,创建套接字使用socket函数:
invoke socket,af,type,protocol
.if eax != INVALID_SOCKET
mov hSocket,eax
.endif
函数的第一个参数af用来指定套接字使用的地址格式。在不同操作系统下,这个参数可以指定为AF_UNSPEC,AF_UNIX或AF_OSI等不同的值,但是在WinSock中只能使用AF_INET(也可以使用PF_INET,在Windows.inc中PF_INET被定义为与AF_INET等效)。
第二个参数type用来指定套接字的类型。正如前面介绍的,套接字有流套接字、数据报套接字和原始套接字等,下面是最常用的几种套接字类型定义:
● SOCK_STREAM——流套接字,使用TCP协议提供有连接的和可靠的传输。
● SOCK_DGRAM——数据报套接字,使用UDP协议提供无连接的和不可靠的传输。
● SOCK_RAW——原始套接字,WinSock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据包以及协议首部。
protocol参数配合type参数使用,用来指定使用的协议类型,当type参数指定为SOCK_STREAM或者SOCK_DGRAM的时候,系统已经明确确定使用TCP和UDP协议来工作,所以这时这个参数并没有什么意义,可以指定为0,但是当type参数指定为SOCK_RAW类型的时候,使用protocol参数可以更详细地指定原始套接字的工作方式。
当type参数指定为SOCK_RAW类型时,可以将protocol参数指定为以下的数值:
● IPPROTO_IP,IPPROTO_ICMP,IPPROTO_TCP和IPPROTO_UDP——分别指定使用IP,ICMP,TCP和UDP协议,这时系统会自动为数据加上IP首部,并且将IP首部中的上层协议字段设置为指定的这些协议名称。但是使用这个套接字接收数据时,系统却不会将IP首部自动去除,需要程序自行分析处理(如果在以后将套接字的属性设置上IP_HDRINCL选项的话,那么发送时系统将不自动添加IP首部,这时需要自己封装数据包)。
● IPPROTO_RAW——系统将数据包直接送到网络访问层发送,程序需要自己添加IP首部以及其他协议首部,并且需要自己计算和填充协议首部中的校验和字段。当使用IPPROTO_RAW协议类型的原始套接字时,这个套接字只能用来发送数据包而无法接收数据包。这是因为所有的IP包都是先递交给系统核心,然后再传输到用户程序,当发送这样一个原始数据包的时候,核心并不知道,也没有这个数据被发送或者连接建立的记录,因此当远端主机回应的时候,系统核心就把这些包给丢弃而不是送到应用程序中。
当套接字被成功创建的时候,函数将返回一个套接字句柄,否则函数将返回INVALID_SOCKET,这时可以继续调用WSAGetLastError函数获取更详细的出错信息。
当不再需要使用套接字的时候,需要使用closesocket函数将它关闭:
invoke closesocket,s
参数s就是创建套接字时返回的套接字句柄。
当出错的时候,大部分WinSock函数的返回值是INVALID_SOCKET或者SOCKET_ERROR,如果需要进一步的出错代码,必须马上调用WSAGetLastError来获取,在以后说明“出错代码”时,指的就是这样得到的出错代码而不是出错函数的返回值。但惟一的例外是WSAStartup,它出错时会直接返回出错代码,因为这时WinSock库尚未装入,程序无法通过调用WSAGetLastError函数来获取出错代码。
3. 设置套接字的工作模式
当一个套接字被创建的时候,它默认工作在阻塞模式下,所以如果不需要改变它的工作模式,可以直接跳过“设置为非阻塞模式”这一步,但是因为Windows程序的工作特点,WinSock强烈建议程序员使用非阻塞模式。有两个函数可以用来改变一个套接字的模式:ioctlsocket函数和WSAAsyncSelect函数。
ioctlsocket函数从BSD UNIX Socket规范中延续过来,它的用法是:
invoke ioctlsocket,s,cmd,argp
参数s指定需要被设置模式的套接字句柄,cmd为命令参数,argp是一个指针,指向一个被cmd命令使用的参数。当cmd被指定为FIONBIO时,函数被用来改变套接字模式,这时如果argp指向的变量值为1,那么套接字的工作模式被设置为非阻塞模式;如果变量中的值为0,那么套接字被设置为阻塞模式。如下面的代码将一个套接字设置为非阻塞模式:
.data
dwArg dd 1
.code
mov dwArg,1
invoke ioctlsocket,hSocket,FIONBIO,addr dwArg
.if eax == SOCKET_ERROR
;发生错误
.endif
但是,这个函数用起来并不方便,因为用这种方法将套接字设置为非阻塞模式后,其结果就是对这个套接字的操作会马上返回,而在操作最终完成以后,函数并不会通过某种机制通知程序操作已经完成,还需要程序去不停地查询操作结果,所以建议使用WinSock接口的WSAAsyncSelect函数来设置工作模式。
WSAAsyncSelect函数是WinSock接口中的特有函数,它仅针对Windows的消息体系工作,所以BSD UNIX Socket中并没有这个函数。这个函数将套接字设置为非阻塞模式,并且打开操作完成后的通知机制,通知消息可以被绑定到某个窗口句柄中,这样程序就不必不停地去查询套接字的操作是否已经完成,而是在窗口过程中等收到通知消息后再进行处理。
WSAAsyncSelect函数的用法是:
invoke WSAAsyncSelect,s,hWnd,wMsg,lEvent
s参数指定需要设置的套接字句柄,hWnd指定一个窗口句柄,套接字的通知消息将被发送到与其对应的窗口过程中。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -