第6章 网络程序设计基本知识 6.1 网络应用程序的概念 6.2 TCP/IP应用程序工作模型与网络编程接口 6.3 套接口的概念及其编程原理 6.4 Winsock API基本函数——套接口与连接的建立 6.5 Winsock API基本函数——数据传输 6.6 Winsock API基本函数——连接与套接口的关闭 习题
6.1 网络应用程序的概念 6.1.1 什么是网络应用程序 我们设计的应用程序可以简单地分为两种:一种程序不需要使用其他程序产生的数据并且其他程序也不使用它输出的数据;另一种程序需要与其他的应用程序进行数据交换才能完成其功能,也就是说程序之间存在通信问题。 进程间通信的问题也可以分为两种:一种是在操作系统中论述的单机系统中进程间的通信问题,另一种一般是在不同系统的进程间通过网络通信协议进行的进程间的通信问题。
Message semaphore shared memory
6.1.2 网络应用程序的标识问题 同一系统中不同进程间进行通信时,通过系统分配的进程号(Process ID)就可以惟一标识一个进程。也就是说,要通信的进程只要知道对方的进程号就可以进行通信。而网络情况下进程间的通信问题就要复杂得多,不能只简单地用进程号来标识不同的进程,因为各主机都独立地分配其进程号。 为了惟一地标识网络中通信的一个进程(即通信的某一方),就要使用一个如下的三元组: 多重协议的识别问题 (本地协议,本地IP地址,本地端口号)
这样一个三元组由于它只指定了通信时一条连接的半个部分,即通信的一方,因而称为半相关(Half-association)。如果要完整地表示网络中进行通信的两个进程,那么就要使用一个如下结构的六元组: (本地协议,本地地址,本地端口号,远地协议,远地地址,远地端口号) 在互联网中通信的两台主机在网络层都只能使用IP协议,但在网络层之上可以选择使用TCP协议或UDP协议,这样就可能得到以下4种类型的相关六元组: ● (本地TCP协议,本地IP地址,本地端口号,远程TCP协议,远程IP地址,远程端口号);
● (本地UDP协议,本地IP地址,本地端口号,远程UDP协议,远程IP地址,远程端口号); ● (本地TCP协议,本地IP地址,本地端口号,远程UDP协议,远程IP地址,远程端口号); ● (本地UDP协议,本地IP地址,本地端口号,远程TCP协议,远程IP地址,远程端口号)。 如果通信的两端使用不同的协议(后两类六元组),根据前面我们所学的知识,由于TCP协议和UDP协议使用的协议格式大不相同,通信时双方在传输层不能相互识别对方送来的数据,也就不可能进行正常的通信,因此后两种情况是不存在的。
换句话说,通信的两个进程在端到端的传输层只能使用相同的协议,因此一个完整的网间通信就可以简化为用一个五元组来标识通信的两个进程: (协议,本地IP地址,本地端口号,远程IP地址,远程端口号) 6.1.3 客户/服务器模型 1.客户/服务器模型的特点 客户/服务器模型的通信方式从所具有的资源角度来说,有明显的非对称性。服务器拥有较多的资源,它具有运算能力强,数据存储容量大,通信速度快,系统的可靠性高等优点。相对来说,客户则拥有较少的资源,它在各方面的性能一般要比服务器差。
客户/服务器模型在工作时,要求有一套客户机和服务器能共同识别的规则或约定,用来保证服务器方可以识别客户提出的请求是什么,客户方也能够解释收到的服务器应答。从本质上来说,这其实就是服务器方和客户方在通信中所使用的一套协议,它必须在通信的两端都被实现。 根据实际情况,协议可能是对称的也可能是非对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变地认为是主机(服务器),而另一方则是从机(客户机)。
2.服务器的分类 根据服务器提供服务方式的不同,服务器可以分为串行服务器和并发服务器。串行服务器只有一个进程用串行的方式对客户的请求提供服务;并发服务器可以为请求的每一个客户创建一个进程或线程,然后由对应的进程或线程给每一个客户提供服务。并发服务器又可以分为预先创建服务子进程(或线程)和按需创建服务子进程(或线程)两种方式。 根据上面的分析,我们来总结一下客户/服务器模型的特点。 对于服务器方来说: ● 服务进程一般在启动后就一直运行,以等待客户请求的到来,除非服务被禁止或执行强迫终止服务程序。
● 服务器方进程使用的是众所周知的端口,否则客户无法知道提供服务的端口,也就不可能提出服务的请求。 ● 服务器方通常拥有较多的资源(对称方式除外)。 ● 服务器方进程可以并行处理多个客户的请求,当然可以同时处理的客户请求数目是有一定限制的。 ● 服务器方在通信时属于被动的一方。 对于客户方来说: ● 在需要服务时向服务器提出请求,请求的服务得到满足并完成处理任务后,就终止客户程序的执行。
● 使用向系统申请的临时端口与服务器方进程进行通信。 ● 拥有相对较少的资源(对称方式除外)。 ● 客户方在通信时属于主动的一方。
6.2 TCP/IP应用程序工作模型 与网络编程接口 使用TCP/IP协议的网络,其协议核心内容在层次结构的低三层,即网络接口层、IP层和传输层,而这三层的功能一般是由操作系统的内核来实现的。 如图6-1所示的是两台主机的进程间通过网络编程接口进行通信的原理图。
图6-1 TCP/IP应用程序工作模型图
图6-1所示的是使用TCP/IP协议网络的典型应用方式,即客户/服务器模式。 通过图6-1用户还要明白一个问题,网络程序设计其实是使用系统提供的网络协议完成用户程序的功能,即在网络应用程序中使用网络协议提供的服务,而不是让用户去实现网络协议各层的功能。
6.2.2 Windows Sockets简介 1.Windows Sockets的概念 Windows Sockets是在Windows环境下使用的一套网络编程规范,常常简称为Winsock。 2.Windows Sockets的来源 Sockets本来是Unix操作系统下流行的一种网络编程接口(API),它是1983年在Berkeley(加州大学伯克利分校)4.2 BSD操作系统中被首先引入的,因此被称为“Berkeley Socket API”。
3.Windows Sockets的版本 目前常用的Winsock有两个版本:一个是16位的Winsock 1.1,由动态链接库WINSOCK.DLL提供支持;另一个是32位的Winsock 2.2,由动态链接库WSOCK32.DLL提供支持。 4.Winsock API函数的分类 在Winsock规范中把Winsock API函数集分为与BSD Socket(用在Unix中)相兼容的基本函数、网络数据信息检索函数和Windows专用扩展函数三类。
Winsock规范的核心内容是符合Berkeley Socket风格的库函数,但为了使程序员能充分利用Windows消息驱动机制进行编程,又定义开发了一组针对Windows的扩展库函数。 Windows Sockets规范中针对Windows的扩展部分,为应用程序开发者提供了开发Windows应用软件的功能,它有利于程序员写出更加稳定并且更加高效的程序。另外,除了WSAStartup( )和WSACleanup( )两个函数外(在6.5节介绍),其他Windows扩展函数的使用不是强制性的。
5.Windows Sockets对多线程的支持 Windows Sockets支持多线程的Windows进程。一个进程可以包含一个或多个同时执行的线程(在Windows 3.1非多线程版本中,一个任务对应了一个仅具有单个线程的进程)。 6.2.3 Windows Sockets规范的目标及几个相关的概念 1.Windows Sockets规范的目标 Windows Sockets规范的目标有两个:一是给网络应用程序的开发者提供一套简单的网络编程API;二是让各家网络软件供应商能够根据这套规范建立各自的符合Windows Sockets标准的实现和应用程序。
此外,在一个特定Windows版本的基础上,Windows Sockets也定义了一个二进制接口(ABI),以此来保证应用Windows Sockets API的应用程序能够在任何网络软件供应商的符合Windows Sockets协议的实现上工作。 2.Windows Sockets兼容和Windows Sockets提供者 遵守Windows Sockets规范的网络软件称之为是Windows Sockets兼容的,而Windows Sockets兼容实现的提供者称之为Windows Sockets提供者。一个网络软件供应商必须百分之百地实现Windows Sockets规范才能做到与Windows Sockets兼容。
3.Windows Sockets应用程序 任何能够与Windows Sockets兼容实现协同工作的应用程序都被认为是具有Windows Sockets接口的,称这种应用程序为Windows Sockets应用程序。应用程序通过调用Windows Sockets的API实现它们之间的相互通信。Windows Sockets又利用下层的网络通信协议功能和操作系统调用来实现实际的通信工作。
6.3 套接口的概念及其编程原理 6.3.1 套接口(Socket) Windows Sockets API依靠套接口(Socket)进行通信。套接口可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡(NIC)的传输介质将这段信息发送到另一台主机的Socket中,使这段信息能传送到其他程序中,如图6-2所示。
图6-2 套接口示意图
现在我们根据图6-2来分析一下使用套接口进行通信的过程。当主机A(Host A)上的网络应用程序(Program A)要发送数据时,通过调用数据发送函数首先将要发送的一段信息写入其Socket中,Socket中的内容通过主机A的网络管理软件由主机A的网络接口卡发送到主机B(Host B),主机B的网络接口卡接收到这段信息后,再传送给主机B的网络管理软件,网络管理软件将这段信息保存在主机B的Socket中,然后程序B(Program B)才能在Socket中读取并使用这段信息。
从以上的通信过程可以看出,如果不考虑通信过程中的网络接口卡和传输介质等,则网络通信的过程就是由数据的发送者将要发送的信息写入一个套接口,再通过中间环节将信息传输到接收端的套接口中,然后就可以由接收端的应用程序将信息从套接口中取出。因此,两个应用程序之间的数据传输要通过套接口来完成。在学习了后面更多的内容后我们将会体会到,套接口的本质是通信过程中所要使用的一些缓冲区及一些相关的数据结构。
6.3.2 套接口的分类 为了满足不同的通信程序对通信质量和性能的要求,一般的网络系统提供了三种不同类型的套接口,以供用户在设计网络应用程序时根据不同的要求来选择。这三种套接口分别是: (1) 流式套接口(SOCK_STREAM) 。 (2) 数据报套接口(SOCK_DGRAM) 。 (3) 原始套接口(SOCK_RAW) 。
6.3.3 套接口编程原理 该过程可以分为以下几个阶段: (1) 电信局必须要有一个电话总机,相当于套接口通信机制中提供服务的服务器。在Socket中通过调用socket()函数来开启一个服务,即创建一个提供服务的套接口。 (2) 电信局必须给电话总机分配一个号码(如114,即查号服务的号码是114),以便用户通过拨该号码得到电话服务,同时接入该电信局的用户必须知道该总机的号码。
(3) 电信局114查号台下会开设一些自动服务的分机,但是它们的数量是有限的。总机开通后就一直在监听(listen)用户的拨号,用户拨打114时,可能拨通,得到服务;也可能拨不通,就会听到忙音。同样,我们在建立一个Socket服务时,也会调用listen( )函数来监听客户的请求。 (4) 对于用户来说,如果知道电信局的查号号码,在想得到查号服务时就可以拨打114,请求得到电信局的服务,这相当于在客户端要进行的操作。
(5) 电信局的总机接受了某用户拨打的电话后,负责把用户与一个分机连通,而总机本身则又回到等待状态,等待其他客户的请求。 (6) 服务完成后,挂上电话,线路断开,一次服务过程结束,否则该线路将一直被占用,浪费了通信资源。在服务器和客户之间,最后也要使用closesocket( )函数关闭套接口,释放该套接口上的有关资源,这可以由通信的任何一方或双方同时提出。
6.3.4 Winsock套接口编程时对错误的处理机制 对Winsock函数来说,返回错误是很常见的。与大多数系统调用类似,Winsock函数发生的错误也有两种。 不成功的Winsock函数调用返回的最常见的值是宏定义SOCKET_ERROR,在Winsock的头文件中(如Winsock2.h),它的数值是−1。
该函数的使用非常简单,格式如下: int WSAGetLastError ( void ); 这是一个无参函数,调用时返回最新发生的网络错误代码。 调用该函数时要注意,当一特定的Windows Sockets API函数指出一个错误已经发生时,就应该调用本函数来获得对应的错误代码。WSAGetLastError( )函数返回的这些错误都已预定义成常量值,根据Winsock版本的不同,这些值的声明不在Winsock1.h中,就在Winsock2.h中。这两个头文件的惟一差别是Winsock2.h中包含的错误代码(针对Winsock 2而引入的一些新的API函数)更多。为各种错误代码定义的常量(带有#定义的宏)一般都以WSAE开头。
6.3.5 网络字节顺序 不同的主机对字节值的存储顺序不同。在存储由多个字节组成的一个字时,有的计算机在起始地址处存放整数的低序号字节,这种存储格式叫“小序在前”(Little-endian);而有的计算机在起始地址处存放整数的高序号字节,这种存储格式叫“大序在前”(Big-endian)。 计算机究竟采用那种字节存储顺序由各自的设计决定,如Windows系列的操作系统使用的是小序在前的存储方式,而Sun OS和Solaris等采用的大序在前的存储方式。
在计算机中,TCP/IP协议使用的16位整数(如端口号)和32位整数(如IP地址)是按计算机各自的“主机字节”(Host-byte)来表示的。 在网络中,为了保证数据的正确性,网络通信协议中必须指定网络字节顺序。如果在网络中使用IP地址和端口号,按“互联网联网标准”的要求,指定的多字节值必须用“大序在前”的形式来表示,一般称之为“网络字节”(Network-byte)顺序。
在Winsock中,有一系列的函数可用于多字节数的转换,把它们从主机字节顺序转换成网络字节顺序,反之亦然。下面4个API函数便将一个数从主机字节顺序转换成网络字节顺序: ● htonl():参数是主机字节顺序的一个4字节数,函数返回网络字节顺序的数; ● WSAHtonl():参数是主机字节顺序的一个4字节数,函数返回网络字节顺序的数; ● htons():参数是主机字节顺序的一个2字节数,函数返回网络字节顺序的数; ● WSAHtons():参数是主机字节顺序的一个2字节数,函数返回网络字节顺序的数。
6.4 Winsock API基本函数——套接口 与连接的建立 6.4.1 打开Winsock——WSAStartup( ) 应用程序或DLL只能在一次成功的WSAStartup( )调用之后才能进一步调用其他的Windows Sockets API函数。 1.函数格式 WSAStartup( )函数的格式如下: int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData );
2.函数参数说明 wVersionRequested:此参数是一个WORD型(双字节型)数值,它指定准备在应用程序中要使用的Winsock库的版本号。其中,用高位字节指定副版本,用低位字节指定主版本。就目前的Win32平台而言,Winsock 2库的最新版本是2.2(Win95为Winsock 1.1)。如果需要加载Winsock 2.2版,指定此参数的值为0x0202;也可使用宏MAKEWORD(X,Y),其中X为高位字节,Y为低位字节,如MAKEWORD(2,2)。 lpWSAData:此参数是一个指向WSADATA结构的指针。当该函数被调用时,它返回关于Windows Sockets实现的详细信息,该结构的定义如下:
typedef struct WSAData{ WORD wVersion; WORD wHighVersion; Char szDescription[WSADESCRIPTION_LEN+1]; Char szSystemStatus[WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADATA,FAR *LPWSADATA;
各字段的含义说明如下: ● wVersion:调用者希望使用的Winsock版本号; ●wHighVersion:加载的Winsock库所支持的最高Winsock版本, 通常和wVersion的值相同; ●szDescription:系统加载的Winsock库的说明字符串,如“Winsock 2.0”; ●szSystemStatus:系统状态或配置信息的说明字符串; ●iMaxSockets:套接口的最大编号(该字段被Winsock 2或其后的版本忽略);
● iMaxUdpDg:UDP数据报的最大容量(该字段被Winsock 2或其后的版本忽略); ●lpVendorInfo:厂商专有信息(该字段被Winsock 2或其后的版本忽略)。 3.函数返回信息 WSAStartup( )函数的返回值是一个整数,如果调用成功则返回0。 WSAStartup( )函数调用不成功时返回如下的错误信息: ●WSASYSNOTREADY:在Winsock的头文件Winsock2.h中,该错误代码定义的数值为10091,它表明加载的Winsock DLL不存在或底层的网络子系统无法使用。
● WSAVERNOTSUPPORTED:该代码的数值为10092,所需的Windows Sockets API的版本未由特定的Windows Sockets实现提供。 如果由wVersion返回的版本用户不能接受,则要调用WSACleanup( )函数清除对Winsock的加载。 ● WSAEINVAL:该代码的数值为10022,说明应用程序指出的Windows Sockets版本不能被该Winsock DLL的实现所支持。 ● WSAEINPROGRESS:该代码的数值为10036,说明一个阻塞的Winsock调用正在进行中。 ● WSAEPROCLIM:该代码的数值为10067,说明已经达到了Windows Sockets实现所支持的任务数量的极限。
● WSAEFAULT:该代码数值为10014,说明lpWSAData参数是一个无效的指针。 注意:在这里为了便于大家理解错误代码,列出了给错误代码定义的数值。以后为了节省篇幅,不再列出错误代码的数值。如果要查询对应错误代码对应的数值,可以在Winsock的头文件Winsock.h或Winsock2.h中去查找。
4.函数使用说明 如果用户在没有正确加载Winsock DLL的情况下使用了其他的Winsock API函数,则被调用的函数返回WSANOTINITIALISED错误信息,代码为10093。 该函数在程序中的基本使用方法如下: #include<Winsock2.h> …//其他代码 WORD wVersionRequested; WSADATA wsaData; wVersionRequested=MAKEWORD(2,2); if(WSAStartup(wVersionRequested,&wasData)!=0)
{ //Winsock初始化错误 //输出Winsock初始化错误提示信息,如“WSAStartup failed” return; } //下面可以用两种方法中的任一种进行版本号匹配的检查 //if(LOBYTE(wsaData.wVersion)!=2||HIBYTE(wsaData.wVersion)!=0) if(wsaData.wVersion!=wVersionRequested)
{ //Winsock版本号不匹配 //输出Winsock版本号不匹配的错误提示信息; WSACleanup( ); return; } //说明Winsock DLL的加载正确,可以执行以下的其他代码 …//其他程序代码 结束对Winsock DLL库的使用时,一定要调用WSACleanup( )函数卸载所加载的库。
6.4.2 创建套接口——socket( )或WSASocket( ) 1.函数格式 在Winsock 1中提供的创建套接口函数的格式如下: SOCKET socket( int af, int type, int protocol );
在Winsock 2中提供的该函数的扩展格式如下: SOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, Group g, int iFlags );
2.函数参数说明 以上两种格式中,前面三个参数的含义是一样的,说明如下: ● af:该参数说明套接口要使用的协议地址族,地址族与协议族的含义相同。如果想建立一个UDP或TCP套接口,只能用常量AF_INET表示使用互联网协议(IP)地址。当然,Winsock 2还支持其他的协议,但一般在程序中很少使用。 ●type:该参数描述套接口的协议类型。当第一个参数af是AF_INET时,它只能使用SOCK_STREAM、SOCK_DGRAM或SOCK_RAW三个协议类型中的任一个,分别表示要创建的是流式套接口、数据报套接口或原始套接口。
● protocol:该参数说明该套接口使用的特定协议。当协议地址族af和协议类型type已经确定后,协议字段可以使用的值是限定的,如表6-1所示。如果调用者不希望特别指定所使用的协议,可以将此参数设置为0,系统就根据前两个参数的值自动确定一个协议字段的取值。 以上三个参数就可以确定一个套接口,它们之间的对应关系可以用表6-1表示。
表6-1 套接口参数
系统可以根据这三个参数建立一个套接口,并给它分配相应的资源,同时返回一个整型套接口号。因此,socket( )函数调用实际上指定了相关五元组中的“协议”这一元。 使用Winsock 2时,一般可以先用WSAEnumProtocols( )函数(该函数在第8章介绍),以获得系统所安装协议的相关信息。当然对现在的绝大部分用户来说,直接使用AF_INET(IP协议族)就可以了,因为几乎所有的协议实现系统都支持IP协议族。但要编写通用性好的应用程序时,最好还是先使用WSAEnumProtocols( )函数查询一下系统安装的协议。
在Winsock 2提供的扩展格式中,增加了三个参数,其含义说明如下(因为常用的是格式1,所以这三个参数只需了解其大概的含义): ●lpProtocolInfo:一个指向WSAPROTOCOL_INFO结构的指针,该结构定义所创建套接口的特性。如果本参数不指向空(NULL),则前三个参数(af, type, protocol)被忽略,系统就根据该结构中三个字段的值确定套接口类型。 ● g:套接口组的描述字。组参数始终为0,因为目前尚无可支持套接口组的Winsock版本。
●iFlags:套接口属性描述。iFlags可用参数如下: WSA_FLAG_OVERLAPPED WSA_FLAG_MULTIPOINT_C_ROOT WSA_FLAG_MULTIPOINT_C_LEAF WSA_FLAG_MULTIPOINT_C_ROOF WSA_FLAG_MULTIPOINT_D_LEAF
第一个标志WSA_FLAG_OVERLAPPED用于指定这个套接口具备重叠I/O(是适用于Winsock的通信模式之一)的特性。调用socket( )建立一个套接字时,WSA_FLAG_OVERLAPPED便是默认设置。一般说来,在使用WSASocket时,最好始终保持设定该标志。后面4个标志用于处理多播套接口。 3.函数返回信息 该函数调用成功后,返回新创建的套接口号,它被定义成是一个无符号的整型数据。 函数调用错误时返回INVALID_SOCKET,应用程序可进一步调用WSAGetLastError( )函数来获取相应的错误代码。可能获得的错误代码说明如下:
●WSANOTINITIALISED:在调用本API之前应成功调用WSAStartup( ); ●WSAENETDOWN:网络子系统失效; ●WSAEAFNOSUPPORT:不支持指定的地址族; ●WSAEINPROGRESS:一个阻塞的Winsock调用正在进行中,或者服务提供者仍在处理一个回调函数; ●WSAEMFILE:无可用的套接口描述字; ●WSAENOBUFS:无可用的缓冲区空间,套接口无法创建;
● WSAEPROTONOSUPPORT:不支持指定的协议; ● WSAEPROTOTYPE:指定的协议对于本套接口类型错误; ● WSAESOCKTNOSUPPORT:本地址族不支持指定的套接口类型; ● WSAEINVAL:g参数非法。 4.函数使用说明
要创建一个流套接口时,可以使用下列三种格式之一: SOCKET sockid=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); SOCKET sockid=WSASocket(AF_INET,SOCK_STREAM,0); SOCKET sockid=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0, WSA_FLAG_OVERLAPPED);
要创建一个数据报套接口时,其格式如下(为节省篇幅,两种套接口的创建只给出了一种格式): SOCKET sockid=socket(AF_INET,SOCK_GDRAM,IPPROTO_UDP); 要创建一个原始套接口时,其格式如下: SOCKET sockid=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
6.4.3 指定本地地址——bind( ) 当用socket( )创建了一个套接口后,该套接口还是不能直接使用,因为它只存在于一个名字空间(地址族)中,也就是说它只确定了通信所希望使用的服务类型,并没有与该主机上提供服务的某端口联系在一起,这样的套接口可以叫未命名的套接口。bind( )函数通过给一个未命名的套接口分配一个本地名字来为套接口建立本地捆绑,即把一个套接口与一个主机地址和端口号联系起来。本函数适用于数据报或流类套接口。
1.函数格式 bind( )函数的格式如下: int bind( SOCKET s, const struct sockaddr FAR* name, int namelen );
2.函数参数说明 bind( )函数中各参数的说明如下: ● s:标识一未绑定套接口的描述字,它是socket( )函数调用成功时返回的值。 ● name:是一个与指定协议有关的地址结构指针, 它指向的值是赋予套接口的地址信息。在Winsock中使用sockaddr_in结构指定IP地址和端口信息,它的定义如下: struct sockaddr_in{ short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }
其中,sin_family字段必须为AF_INET,以告诉Winsock所使用的是IP地址族;sin_port是以网络字节顺序表示的16位端口号;sin_addr是以网络字节顺序表示的32位IP地址;sin_zero字段不用,一般用0填充,在程序中通常是使用sockaddr_in之前将整个结构置0。 ● namelen:bind( )函数的这个参数指地址参数(name)的长度。
3.函数返回信息 如果调用没有错误发生,则bind( )返回0,否则将返回SOCKET_ERROR,应用程序可进一步通过WSAGetLastError( )函数来获取相应的错误代码。下面是可能获取的错误代码: ● WSANOTINITIALISED:在使用此API之前应首先成功调用WSAStartup( ); ● WSAENETDOWN:Windows套接口实现检测到网络子系统失效;
● WSAEADDRINUSE:所指定的端口已在使用中; ● WSAEFAULT:namelen参数太小(小于sockaddr结构的大小); ● WSAEINPROGRESS:一个阻塞的Windows套接口调用正在运行中; ● WSAEAFNOSUPPORT:本协议不支持所指定的地址族; ● WSAEINVAL:该套接口已与一个地址捆绑; ● WSAENOBUFS:无足够可用缓冲区,连接过多; ● WSAENOTSOCK:指定的描述字不是一个套接口。
4.函数使用说明 各种IP地址和端口的设置可用表6-2表示。
表6-2 IP地址和端口的几种设置方式
在程序中使用bind( )调用的典型方式如下所示: #include <Winsock2.h> SOCKET s; sockaddr_in tcpaddr; int iSockErr; int port=5000; //端口号 //先创建一个使用IP地址族的流式套接口 s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//以下给结构类型的地址提供数值 tcpaddr.sin_family=AF_INET; //htons( )函数把一个16位主机字节顺序的端口号转化为网络字节顺序 tcpaddr.sin_port=htons(port); //htons( )函数把一个32位主机字节顺序的IP地址转化为网络字节顺序 tcpaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(s,(LPSOCKADDR)&tcpaddr, sizeof(tcpaddr))== SOCKET_ERROR) { //该函数的调用失败,进行错误处理 iSockErr=WSAGetLastError( ); //根据不同的错误类型进行输出提示信息 …… return; } //函数调用成功,进行其他处理
该程序段是使用bind( )调用常用的形式,由内核指定IP地址,应用程序指定端口号。至此,通信过程中一端的协议、地址和所使用的端口号已经确定,一个套接口就可以正常使用了,即可以进行数据的接收或发送操作了。
6.4.4 监听连接——listen( ) 在一个服务器端用socket( )调用成功创建了一个套接口,并用bind( )函数和一个指定的地址关联(即绑定)在一起后,要指示该套接口进入监听连接请求的状态以及接收由客户发出的连接请求,就要用到Winsock API函数listen( )。 1.函数格式 listen( )函数格式如下: int listen( SOCKET s, int backlog );
2.函数参数说明 listen( )函数中各参数说明如下: ● s:用于标识一个已绑定了地址,但还未建立连接的套接口描述字。 ● backlog:该参数指定了正在等待连接的最大队列长度。这个参数非常重要, 因为完全可能同时出现几个对服务器的连接请求。例如,假定backlog参数为2时有三个客户机同时发出连接请求,那么前两个会被放在一个“等待处理”队列中,以便应用程序依次为它们提供服务。
而第三个连接的请求会造成一个WSAECONNREFUSED错误。一旦服务器接受了一个连接请求,那个连接请求就会从队列中删去,以便可以继续接收其他客户发出的连接请求。要注意的是,backlog参数本身的大小就存在着限制,这个限制是由协议提供者决定的。 3.函数返回信息 如果listen( )函数的调用无错误发生,则返回值为0,否则返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError( )获取相应的错误代码。下面是可能获得的错误代码的说明:
●WSANOTINITIALISED:在使用此API之前应成功调用WSAStartup( ); ● WSAENETDOWN:Windows套接口实现检测到网络子系统失效; ● WSAEADDRINUSE:试图用listen( )去监听一个正在使用中的地址; ● WSAEINPROGRESS:一个阻塞的Windows套接口调用正在运行中; ● WSAEINVAL:该套接口未用bind( )进行捆绑,或已被连接;
● WSAEISCONN:套接口已被连接; ● WSAEMFILE:无可用文件描述字; ● WSAENOBUFS:无可用缓冲区空间; ● WSAENOTSOCK:描述字不是一个套接口 ●WSAEOPNOTSUPP:该套接口不能正常支持对listen( )的调用。 4.函数使用说明 listen( )仅适用于支持连接的套接口,如SOCK_STREAM类型的套接口。也就是说,在IP地址族中,它只适用于同时有多个连接请求的TCP服务器。如果当一个连接请求到来时队列已满,那么客户将收到一个WSAECONNREFUSED错误。
6.4.5 请求连接——connect( )或WSAConnect( )
1.函数格式 在Winsock 1中提供的connect( )函数格式是: int connect( SOCKET s, const struct sockaddr FAR* name, int namelen );
在Winsock 2中提供的扩展格式是: int WSAConnect( SOCKET s, const struct sockaddr FAR* name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS ); 它是Winsock 2对connect( )函数的扩展版本,支持连接数据交换和确定服务质量(QOS)。
2.函数参数说明 connect( )函数中各参数说明如下: ●s:将要建立连接的套接口描述字。 ●name:是一个指向远端套接口地址结构(sockaddr_in)的指针,表示s套接口欲与其建立一条连接。 ●namelen:name名字的长度。 Winsock 1中提供的请求建立连接函数的格式和Winsock 2提供的扩展格式前三个参数是完全一样的。Winsock 2中后四个参数的含义是: ●lpCallerData:指向用户数据缓冲区的一个指针,该缓冲区中包含有在建立连接时由本机传输到服务器端(远端)的数据。
● lpCalleeData:指向另一个用户数据缓冲区的指针,该缓冲区中包含在建立连接时从服务器端(远端)传送到本机的数据。 以上两个参数都是WSABUF结构型的指针,WSABUF是一种在Winsock 2中很常用的结构,该结构的定义如下: typedef struct _WSABUF { int len; // the length of the buffer char FAR * buf; // the pointer to the buffer }WSABUF, FAR* LPWSABUF;
根据该结构具体应用的不同,len字段要么指定由buf字段指向的那个缓冲区的长度,要么指定包含在数据缓冲区buf中的数据长度。在该函数中len字段显然表示缓冲区的长度。 ● lpSQOS:是一个指向服务质量(QOS)结构的指针,它用于指定流式套接口s需要的服务质量,在数据的发送与接收方各有一个;若为空值,则表明没有对该应用指定专用的QOS。 ● lpGQOS:也是一个指向服务质量(QOS)结构的指针,它指出套接口组所需要的服务质量;目前,尚未提供对套接字组的支持,因此该指针可设置为NULL。
这两个参数都是QOS型的指针,QOS结构的定义如下: typedef struct _QualityOfService { WSABUF SendingFlowspec; // the flow spec for data sending WSABUF ReceivingFlowspec; // the flow spec for data receiving } QOS, FAR * LPQOS;
3.函数返回信息 如无错误发生,connect( )和WSAConnect( )都返回0,否则将返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError( )获取相应错误代码。 对于阻塞套接口来说,返回值表示连接试图是否成功。 对于非阻塞套接口来说,连接试图不一定马上完成。在这种情况下,WSAConnect( )返回SOCKET_ERROR,且WSAGetLastError( )返回WSAEWOULDBLOCK,此时应用程序可以采用下列方法进一步做出判断(所使用的函数在第8章介绍):
● 利用select( )函数,通过检查套接口是否可写来判断连接请求是否完成; ● 如果应用程序已使用WSAAsyncSelect( )函数来确定对连接事件的兴趣,则当连接操作完成时应用程序将收到FD_CONNECT通知; ● 如果应用程序已使用WSAEventSelect( )函数来确定对连接事件的兴趣,则当连接操作完成时相应的事件对象将设置信号。 对于一个非阻塞套接口来说,在连接试图完成之前,任何对该套接口的WSAConnect( )调用都将以WSAEALREADY错误失败。
如果返回值指出连接试图失败(例如WSAECONNREFUSED,WSAENETUNREACH,WSAETIMEDOUT),则应用程序可对该套接口再次调用WSAConnect( )函数。 下面是由WSAGetLastError( )获得的错误代码的说明: ●WSANOTINITIALISED:在调用本API之前应成功调用WSAStartup( ); ● WSAENETDOWN:网络子系统失效; ●WSAEADDRINUSE:所指地址已被使用; ●WSAEINTR:通过WSACancelBlockingCall( )函数中止了阻塞调用;
●WSAEINPROGRESS:一个阻塞的Winsock调用正在进行中,或者服务提供者仍在处理一个回调函数; ● WSAEALREADY:在所指定的套接口上正在进行一个非阻塞的connect( )或WSAConnect( )调用; ●WSAEADDRNOTAVAIL:本地机器上所指定的地址不可用; ●WSAEAFNOSUPPORT:所指定地址族中的地址无法与本套接口一起使用; ●WSAECONNREFUSED:连接试图被拒绝;
● WSAEFAULT:name或namelen参数不是用户地址空间的一个有效部分,namelen参数太小,lpCalleeData、lpSQOS和lpGQOS的缓冲区太小,或者lpCallerData的缓冲区太大; ●WSAEINVAL:套接口没有准备好与一地址捆绑; ●WSAEISCONN:套接口已经连接(仅适用于面向连接的套接口); ● WSAENETUNREACH:当前无法从本主机访问网络; ● WSAENOBUFS:无可用缓冲区,套接口未连接;
● WSAENOTSOCK:描述字不是一个套接口; ●WSAEOPNOTSUPP:lpSQOS和lpGQOS中的流描述无法满足; ●WSAEPROTONOSUPPORT:服务提供者不支持lpCallerData参数; ● WSAETIMEDOUT:超时时间到,连接未建立; ● WSAEDESTADDREQ:需要目的地址; ● WSAEWOULDBLOCK:套接口标志为非阻塞,连接无法立即完成,当套接口用select( )函数设置为写时,可调用select( )函数;
● WSAEACCES:由于未使用setsockopt( )函数设置允许SO_BROADCAST,无法将一个数据报套接口与一个广播地址连接。 4.函数使用说明 在客户端使用该函数请求建立连接时,将激活建立连接的三次握手,用来建立一条与服务器的TCP连接。如果在调用该函数前没有调用bind( )来绑定本地地址,则由系统隐式地绑定一个地址到该套接口。 该函数用于使用UDP的客户端时,connect( )函数并不是真正地发出建立连接的请求,调用将从本地操作系统直接返回。
这样做的好处是可以将服务器的地址信息保存下来,在后续UDP端口上发送数据时,由套接口自动在发送函数中填入服务器地址信息,而不需要由应用程序在调用发送函数时填入。 6.4.6 接受连接——accept( )或WSAAccept( ) 在服务器端通过listen( )函数调用表示服务器进入监听客户的连接请求状态,而在服务器端调用accept( )函数表示可以接收来自客户端由connect( )发出的连接请求,这就好像是在通话过程中,被叫方听到电话铃声后提起话筒,然后双方进入通话状态。
1.函数格式 在Winsock 1中提供的accept( )函数的格式是: SOCKET accept( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen ); 在Winsock 2中提供的accept( )函数的扩展格式是: SOCKET WSAAccept( struct sockaddr FAR * addr, int FAR * addrlen, LPCONDITIONPROC lpfnCondition, DWORD dwCallbackData
该扩展函数可根据条件函数的返回值,有条件地接受连接,同时(可选地)创建和/或加入一个套接口组。 2.函数参数说明 accept( )函数的参数说明如下: ● s:标识一个套接口描述字,该套接口处于监听连接的状态。 ● addr:是一个地址结构的指针, 用来存放发出连接请求的那个客户机的IP地址信息。addr参数的实际格式由套接口创建时所使用的地址族确定。 ● addrlen:该参数指出客户套接口地址结构的长度。
以上三个参数在Winsock 1和Winsock 2扩展版本中是完全一样的。Winsock 2中后两个参数的含义是: ● lpfnCondition:是一个指向条件函数进程的指针。这个函数是根据客户的请求来调用的,该函数决定是否接受客户的连接请求。它的定义如下: int CALLBACK CONDITIONPROC (LPWSABUF lpCallerId, //指出客户连接实体的地址 LPWSABUF lpCallerData, //由客户在连接请求中发来的数据
LPQOS lpSQOS, //指定一个客户连接请求的服务质量 LPQOS lpGQOS, //指定套接口组的服务质量 LPWSABUF lpCalleeId, //指出本地连接实体的地址 LPWSABUF lpCalleeData, //建立连接时发给客户的数据 GROUP FAR * g,
//GROUP是一个无符号整型量,确定 //一个组 DWORD dwCallbackData //返回的数据 ); ● dwCallbackData:作为条件函数参数返回给应用程序的回调数据,Winsock不分析该参数。
3.函数返回信息 如无错误发生,该函数就返回一个新的套接口描述符,它对应于已经接受的那个客户机的连接。对该客户机后续的所有操作,都使用这个新的套接口描述符,因此把它叫已连接套接口(Connected Socket)描述符。至于原来那个监听套接口,它仍然用于接收其他客户机发送的连接请求,而且仍处于监听模式,因此把它叫监听套接口(Listening Socket)描述符。 如果发生错误,则返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError( )获取相应错误代码。
下面是由WSAGetLastError( )获得的错误代码的说明: ● WSANOTINITIALISED:在调用本API之前应成功调用WSAStartup( ); ● WSAECONNREFUSED:根据条件函数的返回值(CF_REJECT)强制拒绝连接请求; ● WSAENETDOWN:套接口实现检测到网络子系统失效; ● WSAEFAULT:addrlen参数太小(小于sockaddr结构的大小),或者lpfnCondition并不是用户空间的一部分; ● WSAEINTR:通过accept( )或WSACancelBlockingCall( )函数取消(阻塞)调用;
● WSAEINPROGRESS:一个阻塞Winsock调用正在进行。 ● WSAEINVAL:accept( )或WSAAccept( )调用前未执行listen( )调用,条件函数中的g参数非法,条件函数的返回值非法,套接口处于非法状态; ● WSAEMFILE:accept( )或WSAAccept( )调用时排队队列非空,且无可用套接口描述字; ● WSAENOBUFS:无可用缓冲区空间; ● WSAENOTSOCK:描述字不是一个套接口;
● WSAEOPNOTSUPP:所引用的套接口不是支持面向连接服务类型的; ● WSATRY_AGAIN:根据条件函数的返回值(CF_DEFER),连接请求被推迟; ● WSAEWOULDBLOCK: 套接口标志为非阻塞,无连接请求供接受; ● WSAEACCES:被推迟的连接请求超时或撤销。 4.函数使用说明 该函数用于面向连接的服务器端,在IP协议族中,只用于TCP服务器端。
6.5 Winsock API基本函数——数据传输 6.5.1 带外数据的概念 有时我们可能要传输一些相对来说比较重要的数据,如果按普通数据进行传送,那么在传输过程中要受流量、拥挤等控制的影响。 对于带外数据,从逻辑上看,好像用户进程使用了一个独立的通道发送这些带外数据,位于连接另一端的应用进程可通过一个独立的逻辑信道来接收和处理带外数据。这样对于标记成带外数据的数据流,在传输时就可减少传输系统对数据的干预。
在TCP协议中,OOB数据由一个紧急位(URG)和TCP段头中的一个16位的紧急数据指针标记,把指定的数据流当作紧急数据来处理。 在Winsock中,要查看待发数据中是否包含紧急数据,必须通过SIOCATMARK选项调用ioctlsocket函数来实现。Winsock提供了获得紧急数据的方法:一是紧急数据一旦在线插入,它就会出现在普通数据流中;二是可以关闭在线插入,这样,不连续调用接收函数就会只返回紧急数据。控制OOB数据行为的套接口选项是SO_OOBINLINE(见8.4节)。 不同的系统对带外数据可能有不同的解释,所以用户在编写应用程序时为了增强程序的通用性,应该尽量不用带外数据。
6.5.2 在已建立连接的套接口上发送数据——send( )或WSASend( ) 1.函数格式 send( )函数在Winsock 1中提供的格式是: int send( SOCKET s, const char FAR* buf, int len, int flags );
在Winsock 2中提供的WSASend( )函数的扩展格式是: int WSASend( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
2.函数参数说明 send( )函数中4个参数的含义是: ● s:用于标识已建立连接的套接口描述字。 ● buf:它是一个字符缓冲区,内有将要发送的数据。 ● len:即将发送的缓冲区中的字符数。 ● flags:用于控制数据传输方式,它可以是0、宏定义MSG_DONTROUTE或MSG_OOB。0表示按正常方式发送数据;MSG_DONTROUTE标志说明系统目标主机就在直接连接的本地网络中,无需路由选择,但如果传输协议的实现不支持该选项,则该标志被忽略;MSG_OOB标志指出数据是按带外数据发送的。
在Winsock 2中,WSASend( )函数的第一个参数s和第五个参数dwFlags与send( )函数一样。其他参数的含义如下: ● lpBuffers:一个指向WSABUF结构数组的指针。每个WSABUF结构包含缓冲区的指针和缓冲区的大小。在一个已建立连接的套接口上利用多缓冲来发送数据时,顺序是从第一个到最后一个WSABUF结构。 ● dwBufferCount:lpBuffers数组中WSABUF结构的数目。 ● lpNumberOfBytesSent:如果发送操作立即完成,则为一个指向所发送数据字节数的指针。
● lpOverlapped:指向WSAOVERLAPPED结构的指针(用于重叠套接口,见第8章)。 ● lpCompletionRoutine:一个指向发送操作完成后调用的完成例程的指针(用于重叠套接口)。 3.函数返回信息 若无错误发生,send( )函数和WSASend( )函数都返回所发送数据的字节数,连接结束,函数返回0。在发生错误的情况下,函数返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。下面是可获取的各种错误代码的含义:
● WSANOTINITIALISED:在使用此API之前应成功调用WSAStartup( ); ● WSAENETDOWN:Windows套接口实现检测到网络子系统失效; ● WSAEACESS:要求地址为广播地址,但相关标志未能正确设置; ●WSAEINTR:通过一WSACancelBlockingCall( )来取消一个(阻塞的)调用;
● WSAEINPROGRESS:一个阻塞的Windows套接口调用正在运行中; ● WSAEFAULT:buf或lpBuffer(WSASend)参数不在用户地址空间中的有效位置; ● WSAENETRESET:由于Windows套接口实现放弃了连接,故该连接必须被复位; ● WSAENOBUFS:Windows套接口实现报告一个缓冲区死锁; ● WSAENOTCONN:套接口未被连接; ● WSAENOTSOCK:描述字不是一个套接口;
● WSAEOPNOTSUPP:已设置了MSG_OOB,但套接口非SOCK_STREAM类型; ● WSAESHUTDOWN:套接口已被关闭,一个套接口以1或2(WSASend:对一个套接口以SD_SEND或SD_BOTH)的how参数调用shutdown( )关闭后,无法再用send( )函数; ● WSAEWOULDBLOCK:太多的重叠输入/输出操作; ● WSAEMSGSIZE:套接口为SOCK_DGRAM类型,且数据报大于Windows套接口实现所支持的最大值;
● WSAEINVAL:套接口未用bind( )捆绑,或者套接口未用重叠标志位创建; ● WSAECONNABORTED:由于超时或其他原因引起虚电路的中断; ● WSAECONNRESET:虚电路被远端复位; ● WSA_IO_PENDING:成功启动重叠操作,过后将有完成指示; ● WSA_OPERATION_ABORTED:重叠操作由于套接口的关闭而被取消,或者在WSAIoctl( )调用中执行了SIO_FLUSH。
4.函数使用说明 send( )适用于已建立连接的数据报或流式套接口发送数据。对于数据报类套接口,必须注意发送数据长度不应超过通信子网的IP包最大长度。IP包最大长度在WSAStartup( )调用返回的WSAData结构量的iMaxUdpDg元素中。如果数据太长无法自动通过下层协议,则返回WSAEMSGSIZE错误,数据不会被发送。 请注意,成功地完成send( )调用并不意味着数据已可靠传送到目标。
如果传送系统的缓冲区空间不够保存需传送的数据,除非套接口处于非阻塞I/O方式,否则send( )将阻塞。对于非阻塞SOCK_STREAM类型的套接口,实际写的数据数目可能在1到所需大小之间,其值取决于本地和远端主机的缓冲区大小。可用select( )调用来确定何时能够进一步发送数据。 在相关套接口的选项之上,还可通过标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口的选项,又取决于标志位。
6.5.3 在已建立连接的套接口上接收数据——recv( )或WSARecv( ) 1.函数格式 在Winsock 1中提供的recv( )函数的格式是: int recv( SOCKET s, char FAR* buf, int len, int flags );
在Winsock 2中提供的WSARecv( )函数扩展格式是: int WSARecv( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPINT lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
2.函数参数说明 recv( )函数中各参数说明如下: ● s:已建立连接的套接口描述字。 ● buf:用于接收数据的缓冲区。 ● len:缓冲区长度。 ● flags:指定调用方式。flags参数可以是下面的值:0、 MSG_PEEK或MSG_OOB。0表示接收的是正常数据,无特殊行为。MSG_PEEK表示会使有用的数据复制到所提供的接收端缓冲区内,但是没有从系统缓冲区中将数据删除。
但要注意,把数据留在系统缓冲区中,则系统缓冲区中可容纳的接收数据的系统空间就会越来越少,其结果是系统减少各发送端的TCP窗口的容量,用户的应用程序就不能获得最大的数据流通量,因此,最好把所有数据都复制到自己的缓冲区中,并在那里处理数据。MSG_OOB标志表示处理带外数据。 WSARecv( )函数在recv( )函数的基础上新增加了一些特性,比如重叠I/O和部分数据报通知。它的参数含义如下: ● s:已建立连接的套接口描述字。
● lpBuffers:一个指向WSABUF结构数组的指针。每一个WSABUF结构包含一个缓冲区的指针和缓冲区的长度。 ● dwBufferCount:lpBuffers数组中WSABUF结构的数目。 ● lpNumberOfBytesRecvd:如果接收操作立即结束,一个指向本调用所接收的字节数的指针。 ● lpFlags:一个指向标志位的指针。 ● lpOverlapped:一个指向WSAOVERLAPPED结构的指针(对于非重叠套接口则忽略)。 ● lpCompletionRoutine:一个指向接收操作结束后调用的例程的指针(对于非重叠套接口则忽略)。
3.函数返回信息 在无错误发生时,这两个函数都返回所接收的字节数,函数在连接结束时返回0。如果发生错误,则函数返回SOCKET_ERROR错误信息,应用程序可通过WSAGetLastError( )获取相应错误代码。下面是可获取的各种错误代码的含义: ●WSANOTINITIALISED:在调用本API之前应成功调用WSAStartup( ); ● WSAENETDOWN:网络子系统失效; ● WSAENOTCONN:套接口未连接;
● WSAEINTR:通过WSACancelBlockingCall( )函数取消(阻塞)调用; ● WSAEINPROGRESS:一个阻塞的Winsock调用正在进行中,或者服务提供者仍在处理一个回调函数; ● WSAENETRESET:由于远端的复位造成连接的中止; ● WSAENOTSOCK:描述字不是一个套接口; ● WSAEOPNOTSUPP:设置了MSG_OOB, 但是该套接口不是SOCK_STREAM流类型的,与套接口相关的通信域不支持带外数据,或者套接口是单向的,只支持发送操作;
● WSAESHUTDOWN:套接口已经关闭;一个套接口以SD_RECEIVE(0)或SD_BOTH(2)的how参数调用shutdown( )后,就再无法进行WSARecv( )或recv( )函数的调用了; ●WSAEWOULDBLOCK:对于重叠套接口来说, 有太多重叠的输入/输出请求,对于非重叠套接口来说,套接口被标志为非阻塞,但是操作不能立即完成; ● WSAEINVAL:套接口未用bind( )捆绑,或者套接口未用重叠标志创建; ●WSAECONNABORTED:由于超时或其他错误导致虚电路中止;
● WSAECONNRESET:虚电路被远端复位; ● WSAEDISCON:远端“优雅”地结束了连接; ● WSA_IO_PENDING:成功启动一个重叠操作,过后将有完成指示; ● WSAEMSGSIZE:数据报太大无法全部装入缓冲区,故被剪切。
4.函数使用说明 本函数用于在已建立连接的数据报或流式套接口s上进行数据的接收。 对SOCK_STREAM类型的套接口来说,本函数将返回所有可用的信息,最大可达缓冲区的大小。如果套接口被设置为在线接收带外数据(选项为SO_OOBINLINE),且有带外数据未读入,则返回带外数据。应用程序可通过调用ioctlsocket( )的SIOCATMARK命令来确定是否有带外数据待读入。
对于数据报类套接口,队列中第一个数据报中的数据被解包,但最多不超过缓冲区的大小。如果数据报大于缓冲区,那么缓冲区中只有数据报的前面部分,其他的数据都丢失了,并且recv( )函数返回WSAEMSGSIZE错误。但对于流式传输协议来说,就不会碰到WSAEMSGSIZE这个错误。如果没有数据待读,那么除非是非阻塞模式,否则套接口将一直等待数据的到来,此时将返回SOCKET_ERROR错误,错误代码是WSAEWOULDBLOCK。 用select( )或WSAAsynSelect( )可以获知数据何时到达。
如果套接口为SOCK_STREAM类型,并且远端“优雅”地中止了连接,那么recv( )一个数据也不读取,立即返回。如果连接被强制中止,那么recv( )将以WSAECONNRESET错误失败返回。在套接口所设的选项之上,还可用标志位flag来影响函数的执行方式。也就是说,本函数的语义既取决于套接口选项,也取决于标志位参数。
6.5.4 在无连接的套接口上接收数据——recvfrom( )或WSARecvFrom( ) 1.函数格式 在Winsock 1中提供的recvfrom( )函数的格式是: int recvfrom( SOCKET s,
char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen ); 在Winsock 2中提供的WSARecvFrom( )函数的扩展格式是: int WSARecvFrom( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd,
LPINT lpFlags, LPVOID lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
2.函数参数说明 recvfrom( )函数中各参数说明如下: ● s:标识一个套接口的描述字; ● buf:接收数据缓冲区; ● len:缓冲区长度; ● flags:调用操作方式,同recv( )中的flags; ● from:(可选)指针,指向装有源地址的缓冲区,对监听套接字的具体协议来说,from参数是一个指向SOCKADDR结构的指针,它的长度由指针fromlen指向的数确定,这个API调用返回数据时,SOCKADDR结构内便填入发送数据的那个工作站的地址; ● fromlen:(可选)指针,指向from缓冲区长度值;
WSARecvFrom( )函数参数的含义如下: ● lpBuffers:一个指向WSABUF结构数组的指针,每个WSABUF结构包含缓冲区的指针和缓冲区的大小; ● dwBufferCount:lpBuffers数组中WSABUF结构的数目; ● lpNumberOfBytesRecvd:如果接收操作立即完成,则为一个指向所接收数据字节数的指针; ● lpFlags:一个指向标志位的指针;
● lpFrom:(可选)指针,指向重叠操作完成后存放源地址的缓冲区; ● lpFromlen:指向from缓冲区大小的指针,仅当指定了lpFrom时才需要; ● lpOverlapped:指向WSAOVERLAPPED结构的指针(对于非重叠套接口则忽略); ● lpCompletionRoutine:一个指向接收操作完成后调用的完成例程的指针(对于非重叠套接口则忽略)。
3.函数返回信息 在无错误发生时,这两个函数都返回所接收的字节数,函数在连接结束时返回0;否则返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError( )获取相应错误代码。可获取的各错误代码的含义如下: ● WSANOTINITIALISED:在调用本API之前应成功调用WSAStartup( ); ● WSAENETDOWN:网络子系统失效; ● WSAEFAULT:lpFromlen参数非法,lpFrom缓冲区太小,无法容纳远端地址(对recvfrom( )来说,fromlen参数非法,from缓冲区大小无法装入端地址);
● WSAEINTR:通过WSACancelBlockingCall( )函数取消(阻塞)调用; ● WSAEINPROGRESS:一个阻塞的Winsock调用正在进行中,或者服务提供者仍在处理一个回调函数; ● WSAEINVAL:套接口未用bind( )捆绑,或者套接口未用重叠标志创建; ● WSAENETRESET:由于远端的复位造成连接中止; ● WSAENOTCONN:套接口未连接(仅适用于面向连接的套接口);
● WSAENOTSOCK:描述字不是一个套接口; ● WSAEOPNOTSUPP:设置了MSG_OOB, 但是该套接口不是SOCK_STREAM流类型的,与套接口相关的通信域不支持带外数据,或者套接口是单向的,只支持发送操作; ● WSAESHUTDOWN:套接口已经关闭,一个套接口以SD_RECEIVE或SD_BOTH(对recvfrom( )来说,0或2)的how参数调用shutdown( )后,就无法进行WSARecvFrom( )调用了; ● WSAEWOULDBLOCK:对于重叠套接口来说, 有太多重叠的输入/输出请求,对于非重叠套接口来说,套接口被标志为非阻塞,但是操作不能立即完成;
● WSAEMSGSIZE:消息太大无法全部装入指定的缓冲区,故被修剪; ● WSAECONNABORTED:由于超时或其他错误导致虚电路中止; ● WSAECONNRESET:虚电路被远端复位; ● WSAEDISCON:远端“优雅”地中止了连接; ● WSA_IO_PENDING:成功启动一个重叠操作,过后将有完成指示; ● WSA_OPERATION_ABORTED:由于套接口的关闭,重叠操作已经被取消。
4.函数使用说明 该函数的用法与有连接时recv( )或WSARecv( )函数的用法类似,要注意的是该函数也可以用于有连接时数据的接收。 6.5.5 在无连接的套接口上发送数据——sendto( )或WSASendTo( ) 对于无连接的套接口来说,要从套接口上发送一个数据报,就要使用sendto( )函数或WSASendTo( )函数。
1.函数格式 在Winsock 1中提供的sendto( )函数的格式是: int sendto( SOCKET s, const char FAR* buf, int len, int flags, const struct sockaddr FAR* to, int tolen );
在Winsock 2中提供的WSASendTo( )函数的扩展格式是: int WSASendTo( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, int iFlags, LPVOID lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
2.函数参数说明 sendto( )函数中各参数的说明如下: ● s:一个标识套接口的描述字; ● buf:待发送数据的缓冲区; ● len:指明buf缓冲区中要发送的数据长度; ● flags:调用方式标志位,同send( )中该参数的含义相同; ● to:(可选)指针,指向接收数据的目的套接口的地址; ● tolen:to所指地址的长度。
WSASendTo( )函数中各参数的含义如下: ● s:用于标识一个已连接的套接口,该套接口以WSA_FLAG_OVERLAPPED标志调用WSASocket( )函数; ● lpBuffers:一个指向WSABUF结构数组的指针,每个WSABUF结构包含缓冲区的指针和缓冲区的大小; ● dwBufferCount:lpBuffers数组中WSABUF结构的数目; ● lpNumberOfBytesSent:如果发送操作立即完成,则为一个指向所发送数据字节数的指针;
● iFlags:标志位; ● lpTo:(可选)指针,指向目标套接口的地址; ● lpTolen:lpTo中地址的大小; ● lpOverlapped:指向WSAOVERLAPPED结构的指针(对于非重叠套接口则忽略)。 ● lpCompletionRoutine:一个指向发送操作完成后调用的完成例程的指针(对于非重叠套接口则忽略)。
3.函数返回信息 在无错误发生时,这两个函数都返回所发送的字节数,函数在连接结束时返回0;否则返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError( )获取相应错误代码。可获取的错误代码的含义说明如下: ● WSANOTINITIALISED:在调用本API之前应成功调用WSAStartup( ); ● WSAENETDOWN:网络子系统失效; ● WSAEACCES:请求的地址为广播地址,但未设置相应的标志位;
● WSAEINTR:通过WSACancelBlockingCall( )函数取消(阻塞)调用。 ● WSAEINPROGRESS:一个阻塞的Winsock调用正在进行中,或者服务提供者仍在处理一个回调函数; ● WSAEFAULT:lpBuffer或lpTo参数不是用户地址空间的一部分,或者lpTo参数太小(小于sockaddr结构的大小); ● WSAENETRESET:远端主机复位造成连接中止; ● WSAENOBUFS:Winsock提供者报告了一个缓冲区死锁;
● WSAENOTCONN:套接口未连接(仅适用于面向连接的套接口); ● WSAENOTSOCK:描述字不是一个套接口; ● WSAEOPNOTSUPP:设置了MSG_OOB,但是该套接口不是SOCK_STREAM流类型的,与套接口相关的通信域不支持带外数据,或者套接口是单向的,只支持接收操作; ● WSAESHUTDOWN:套接口已经关闭,一个套接口以SD_SEND或SD_BOTH的how参数调用shutdown( )后,就无法进行WSASendTo( )调用了;
● WSAEWOULDBLOCK:套接口被标志为非阻塞,但该调用会产生阻塞,有太多重叠的输入/输出请求; ● WSAEMSGSIZE:套接口是面向消息的,且消息大于底层传送所支持的最大长度; ● WSAEINVAL:套接口未用bind( )捆绑,或者套接口未用重叠标志位创建; ● WSAECONNABORTED:由于超时或其他错误导致虚电路中止; ● WSAECONNRESET:虚电路被远端复位; ● WSAEADDRNOTAVAIL:本地主机无法获取所指定的地址;
● WSAEAFNOSUPPORT:指定地址族中的地址无法与本套接口一起使用; ● WSAEDESTADDRREQ:需要目标地址; ● WSAENETUNREACH:当前无法从本主机联系网络; ● WSA_IO_PENDING:成功启动一个重叠操作,过后将有完成指示; ● WSA_OPERATION_ABORTED:重叠操作由于套接口的关闭而被取消,或者在WSAIoctl( )调用中执行了SIO_FLUSH。
4.函数使用说明 这两个函数的使用方法类似于send( )和WSASend( )函数。当用于无连接的套接口时,调用函数之前要设置,指出目标IP地址和目标端口号。如果用于有连接的套接口时,则不能指定目标地址和端口,应将to设置为空,地址长度设置为0。当然在有连接的情况下很少使用该函数。
6.6 Winsock API基本函数——连接与套接口的关闭 6.6.1 关闭读写通道——shutdown( ) 在一个套接口上的读写操作完成后,应该首先使用shutdown( )函数来关闭套接口的读通道、写通道或读写通道。这样做的好处是当通信的双方不再有数据要发送或接收时,它可以通知对方,以防止数据丢失,并能“优雅”地关闭连接。
1.函数格式 shutdown( )函数的格式如下: int shutdown( SOCKET s, int how ); 2.函数参数说明 shutdown( )函数中各参数说明如下: ● s:标识一个套接口的描述字; ● how:标志,用于描述禁止哪些操作,how参数可以取表6-3所示的任何一个值。
表6-3 读写关闭方式选择
3. 函数返回信息 如果没有错误发生,shutdown( )返回0;否则返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError( )获取相应错误代码。错误代码说明如下: ● WSANOTINITIALISED:在使用此API之前应成功调用WSAStartup( ); ● WSAENETDOWN:Windows套接口实现检测到网络子系统失效; ● WSAEINVAL:how参数非法;
● WSAEINPROGRESS:一个阻塞的Windows套接口调用正在运行中; ● WSAENOTCONN:套接口未连接(仅适用于SOCK_STREAM类型的套接口); ● WSAENOTSOCK:描述字不是一个套接口。 4.函数使用说明 请注意,shutdown( )函数并不关闭套接口,且套接口所占有的资源将被一直保持到closesocket( )调用之前。
6.6.2 关闭套接口——closesocket( ) 一个套接口不再使用时一定要关闭这个套接口,以释放与该套接口关联的所有资源,包括等候处理的数据。 1. 函数格式 closesocket( )函数的格式如下: int closesocket( SOCKET s ); 2.函数参数说明 closesocket( )函数中的参数s标识一个将被关闭的套接口。
3.函数返回信息 如果没有错误发生,closesocket( )返回0;否则返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError( )获取相应错误代码。错误代码说明如下: ● WSANOTINITIALISED:在使用此API之前应首先成功调用WSAStartup( ); ● WSAENETDOWN:Windows套接口实现检测到网络子系统失效; ● WSAEINPROGRESS:一个阻塞的Windows套接口调用正在运行中;
●WSAEINTR:通过一个WSACancelBlockingCall( )来取消一个(阻塞的)调用; ● WSAENOTSOCK:描述字不是一个套接口; ● WSAEWOULDBLOCK:该套接口设置为非阻塞方式且SO_LINGER设置为非零超时间隔。 4.函数使用说明 对一个有连接的TCP来说,closesocket( )调用还要关闭TCP连接。 使用closesocket( )函数关闭一个连接时,如果在套接口的数据队列中还有未发送的数据,对这些数据的处理方式要受套接口选项SO_LINGER和SO_DONTLINGER的影响,有关这方面的详细内容将在8.4节介绍。
6.6.3 终止使用Winsock——WSACleanup( ) 当在应用程序中不再使用Winsock API中的任何函数时,必须调用WSACleanup( )将其从Windows Sockets的实现中注销,以释放为应用程序或DLL分配的任何资源。因此,对应于一个任务进行的每一次WSAStartup( )调用,必须有一个WSACleanup( )调用,因为每次WSAStartup( )函数的启动调用都会增加对加载Winsock DLL的引用次数,它要求调用同样多次的WSACleanup( )调用,以此抵消引用次数。
1.函数格式 WSACleannp( )函数的格式为 int WSACleanup (void); 2.函数参数说明 该函数是一个无参函数。 3.函数返回信息 该函数调用成功返回0。调用失败时,返回的错误信息有下面几类: ● WSANOTINITIALISED:使用本API前必须要进行一次成功的WSAStartup( )调用;
● WSAENETDOWN:Windows Sockets的实现已经检测到网络子系统故障; ● WSAEINPROGRESS:一个阻塞的Windows Sockets操作正在进行。 Winsock API函数调用的错误信息可以用WSAGetLastError( )函数来检查(在Unix Socket中是存入errno变量中的)。
4.函数使用说明 WSACleanup( )函数是任何一个Winsock应用程序在最后必须要调用的函数。在一个多线程的环境下,WSACleanup( )函数中止了Windows Sockets在所有线程上的操作。 只有在一次成功调用WSAStartup( )函数后,才能调用WSACleanup( )函数,否则,WSACleanup( )函数返回WSANOTINITIALISED错误信息。
习题 1.下载并安装一个NetXRay软件,学习它的操作使用方法,总结它有哪些功能。 2.根据截获的数据包说明TCP/IP协议的四个协议层是怎样联系的? 3.说明应用层数据是如何被封装传输的? 4.根据截获的数据包说明IP和TCP协议头信息各字段的含义。 5.说明TCP连接的建立和关闭是如何进行的? 6.应用层的命令和响应是如何被传输的?