第一部分:传统网络API
第1章:
NetBIOS(NerWork Basic Input/Output System, NetBIOS)是一种标准的应用程序编程接口
“网络基本输入/输出系统”:
NetBIOS提供了异步调用,同时兼容于较老的操作系统。
OSI模型:
应用层: 为用户提供相应的界面,以便使用提供的连网功能
表示层: 完成数据的格式化
会话层: 控件两个主机间的通信链路
传输层: 提供数据传输服务
网络层: 在两个主机之间提供一套定址/寻址机制,同时负责数据包的路由选择
数据链路层:控制两个主机间的物理通信链路:同时还要负责对数据进行整形
物理层: 物理媒体负责以一系列电子信号的形式,传出数据
对于NetBIOS主要在会话的传输层发挥作用。
同步对多用户支持不如异步的好!
第二部分 Winsock API
Winsock都以不同的形式存在着。winSocket是网络编程接口而不是协议。
无保护消息边界的协议通常称作“基于流的协议”。流服务的定义是连续的数据传输;不管消息边界是否存在,接收端都会尽量地读取所有效数据。对发送端来说,意味着允许系统将原始消息分解成小消息或把几条消息积累在一起,变成一个较大的数据包。对接收端来说,则是数据到达网络堆栈,网络堆栈就开始读取它,并将它缓存下来选进程序处理。
面向连接和无连接:
面向连接的服务中,进行数据交换之前,必须与通信方建立一条路径。而无连接协议却不保证接收端是否下在收听。
不具备可靠性的协议则不能保证每个字节都能到达接收端,同样不能保证数据的完整性。次序性是指对数据到达接收端的顺序进行处理。保护次序性的协议保证接收端收到的数据的顺序就是数据的发送顺序。显然,没有保护次序性的协义就没有次序保证。面向连接的协义的确能保证数据的可靠性。
注意:可靠性和次序性两者不能兼而得之,保证了数据包顺序,就不能自动保证数据的完整性。当然无连接协议的过人之处就是速度:对接入接收端的虚拟连接来说,它们不会影响其建立。
从容关闭:
从容关闭只出现在面向连接的协议中。在这种关闭过程中,一方开始关闭通信会话,但另一方仍然可以读取线上或网络堆栈上已挂起的数据。如果面向连接的协议不支持从容关闭,只要其中一方关闭了通信信道,都会导致连接立即中断,数据丢失,接收端不能读取数据这些情况出现。如果使用TCP协议,连接双方都必须执行一次关闭,以便完全中断连接。
广播数据:
广播数据即数据从一个工作站发出,局域网内的其他所有工作站都能收到它。这一特征适用于无连接协议,因为LAN上的所有机器都可获得并处理广播消息。
多播数据:
多播是指一个进程发送数据的能力,这些数据即将由一个或多个接收端进行接收。
要想获得系统中安装的网络协议的相关信,调用这个函数WSAEnumProtocols即可,并像这样定义:
Int WSAEnumProtocols(
LPINT lpiProtocols,
LPWSAPROTOCOL_INFO lpProtocolBuffer,
LPDWORD lpdwBufferLenght
);
在可以调用一个WinSock函数之前,必须先加载一个版本正确的Winsock库。winSock启动全程是WSAStartup,它的定义是:
Int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData)
第一个参数是准备加载的WinSock库的版本号。MAKEWORD(2,2)高位字节指定副版本,而低位字节则指定主版本。
第二个参数是WSADATA结构,它是调用完成之后立即返回的WSADATA包含了WSAStartup加载的关于Winsock版本的信息,
WSADATA结构的真正定义:
Typedef struct WSAData{
WORD wVersion;//调用者希望使用的winsocket版本号
WORD wHighVersion;//加载的winsock库所支持的最高winsock版本,通常和wVersion //的值相同
Char szDescription[WSADESCRIPTION_LEN+1];//加载的Winsock库的文本说明
Char szSystemStatus[WSASYS_STATUS_LEN+1];//相应的状态或配置信息
Unsigned short iMaxSockets;//套接字的最大编号
Unsigned short iMaxUdpDg;//UDP数据报的最大容量
Char FAR* lpVendorInfo;//厂商专有信息
} WSADATA, FAR* LPWSADATA
在这个结构体中返回的唯一有用的信息是wVersion和wHighVersion.属于最大套接字和最大UDP长度的条目应该从自己正在使用的特定协议目录中获取。
在结束winsock库,而且不再需要调用任何winsock函数时,附带全程会卸载这个库,并释放资源。这个函数的定义是:
Int WSACleanup(void);
记住,每次调用WSAStartup都需要调用相应的WSACleanup因为每次启动调用都会增加对加载winsockDLL的引用次数。
Windows套接字:
所谓套接字,就是一个指向传输提供者的句柄。Win32中,套接字不同于文件描述符,所以它是一个独立的类型――SOCKET套接字是由两个函数建立的:
SOCKET WSASocket(
Int af,
Int type,
Int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,//组参数始终为0
DWORD dwFlags,//WSA_FLAG_OVERLAPPED
);
SOCKET socket(
Int af,//是协议的地址家族
Int type,//是协议的套接字类型
Int protocol//指定的地址家族和套接字类型有多个条目时,就可用这个字段来限定使用 //特定传输
);
协议 址家族af 套接字类型type 协议字段protocol
Internet AF_INEF TCP SOCK_STREAM IPPROTO_IP
Protocol
( IP )
UDP SOCK_DGRAM IPPROTO_UDP
Raw sockets SOCK_RAW IPPROTO_RAW
以上是我们常用的AF_INET IP协议
建立套接字的前三个参数组织成三级,第一个同时也是最重要的参数是地址家族。它指定准备使用哪种协议,另外还为第二和第三个参数有效选项。
Winsock API和OSI模型:
Winsock中的传输提供者位于OSI模型的传送层。也就是说,每个传输协议都会提供一种传输数据的方法;但是,它们本身又是另一个网络协议的成员,而网络协议位于网络层,因为它是为网络上各节点提供定址方法的协议。
winsockAPI安装在“会话层”和“传送层”之间。
第6章 地址家族和名字解析:
6.1 IP
网际协议(internet Protocol ,IP)IP是一个无连接的协议,不能保证数据投递万无一失。两个比它高级的协议(TCP、UDP)用于依赖IP协议的数据通信 。
6.1.1 TCP
面向连接的通信是通过“传输控制协议”(Transmission Control Protocol, TCP)来完成的。T C P提供两台计算机之间的可靠无错的数据传输。应用程序利用T C P进行通信时,源和目标之间会建立一个虚拟连接。这个连接一旦建立,两台计算机之间就可以把数据当作一个双向字节流进行交换。
6.1.2 UDP
无连接通信是通过“用户数据报协议”(User Datagram Protocol, UDP)来完成的。U D P不保障可靠数据的传输,但能够向若干个目标发送数据,接收发自若干个源的数据。简单地说,如果一个客户机向服务器发送数据,这一数据会立即发出,不管服务器是否已准备接收数据。如果服务器收到了客户机的数据,它不会确认收到与否。数据传输方法采用的是数据报。
T C P和U D P两者都利用I P来进行数据传输,一般称为T C P / I P和U D P / I P。Wi n s o c k通过A F _ I N E T地址家族为I P通信定址,这个地址家族的定义在Winsock 1.h和Winsock 2.h中。
6.1.3 定址:
IP中,计算机都分配有一个IP地址,用一个32位数来表示,正式的称呼是“IPv 4 ” 。
客户机需要通过TCP或UDP和服务器通信时,必须指定服务器的IP地址和服务端口号。服务器打算监听接入客户机请求时,也必须指定一个IP地址和一个端口号。Winsock中,应用通过SOCKADDR_IN结构来指定IP地址和服务端口号:
Struct sockaddr_in
{
Short sin_family;//AF_INET以告知Winsock我们此时正在使用IP地址家族
u_short sin_port;//使用端口号
struct in_addr sin_addr;
char sin_zero[8];//只充当填充项的职责,以使SOCKADDR_IN结构和SOCKADD结构的 //长度一样。
}
端口号选择:
■ 0 ~ 1 0 2 3由I A N A控制,是为固定服务保留的。
■ 1 0 2 4 ~ 4 9 1 5 1是I A N A列出来的、已注册的端口,供普通用户的普通用户进程或程序使用。
■ 4 9 1 5 2 ~ 6 5 5 3 5是动态和(或)私用端口。
一个有用的名为inet_addr的支持函数,可把一个点工IP地址转换成一个32位的无符号长整数。它的定义如下:
Unsigned long inet_addr(
Const char FAR* cp;//一个点式IP地址
)
1. 特殊地址:
特殊地址INADDR_ANY允许服务器应用监听主机计算机上面每个网络接口上的客户机活动。一般情况下,在该地址绑定套接字和本地接口时,网络应用才利用这个地址来监听连接。如果你有一个多址系统,这个地址就允许一个独立应用接受发自多个接口的回应。
2. 字节排序:
针对“大头”(big-endian)和“小头”(little-endian)形式的编号,不同的计算机处理器的表示方法有所不同,这由各自的设计决定。在计算机中把IP地址和端口号指定成多字节数时,这个数就按“主机字节”(host-byte)顺序来表示。但是如果网络上指定IP地址和端口号,“互联网联网标准”指定多字节值勤必须用“大头”形式来表示一般称之为“网络字节”顺序。
有一系列的函数可以用于多字节数的转换,把它们从主机字节顺序转换成网络字节顺序,反之亦然,下面四个api函数便将一个数从主机字节顺序转换成网络字节顺序:
U_long htonl(u_long hostlong);//网络字节顺序
Int WSAHtonl(
SOCKET s,
U_long hostlong,
U_long FAR * lpnetlong
);
U_short htons(u_short hostshort);
Int WSAHtons(
SOCKET s,
U_short hostshort,
U_short FAR * lpnetshort
)
下面四个是前面四个函数的反向函数,它们把网络字节顺序转换成主机字节顺序:
U_long ntohl(u_long netlong);
Int WSANtohl(
SOCKET s,
U_long netlong,
U_long FAR * lphostlong
);
U_short ntohs(u_short netshort);
Int WSANtohs(
SOCKET s,
u_short netshort,
u_short FAR* lphostshort
)
6.1.4 创建套接字
创建一个IP套接字的好处是便于应用能够通过TCP、UDP和IP协议进行通信。如要用TCP协议打开一个IP套接字需调用带有地址家族AF_INET和套接字类型SOCK_STREAM的SOCKET函数或WSASocket函数,并把协议字段设成0,
方式如下:
S = socket(AF_INET, SOCK_STREAM, 0);
S =WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
要利用UDP协议打开IP套接字,只须指定套接字类型,用这个指定的套接字类型代替socket函数中的SOCK_STREAM和上面的WSASocket调用,还可以打开打开一个套接字通过IP直接通信。
6.1.5 名字解析:
Winsock应用打算通过IP和主机通信时,必须知道这个主机的IP地址。依用户看来,IP地址是不容易记的,在指定机器时,许多人更愿意利用一个易记的友好的主机名而不是IP地址。Winsock提供了两个支持函数,它们有助于用户把一个主机名解析成IP地址。
Windows套接字gethostbyname和WSAAsynGetHostByName API函数从主机数据库中取回与指定的主机名对应的主机信息。两个函数均返回一个HOSTENT结构,该结构的格式如下:
Struct hostent{
Char FAR* h_name;//正式的主机名
Char FAR * FAR * h_aliases;//是一个由主机备用名组成的空中止数组
Short h_addrtype;//表示即将返回的地址家族
Short h_length;//h_addr_list字段中的每个地址定义字节长度进行定义
Char FAR *FAR * h_addr_list;//是一个由主机IP地址组成的空中止数组
}
Gethostbyname API函数的定义如下:
Struct hostent FAR * gethostbyname(
Const char FAR* name//参数表示准备查找的那个主机的友好名
)
保存HOSTENT 结构的是系统内存。应用程序不应该依靠它来维护状态。
WSAAsyncGetHostByName API函数是gethostbyname函数的异步版,后一个函数在结束时,利用windows消息向阳应用程序发出通知。WSAAsynGetHostByName的定义如下 :
HANDLE WSAAsyncGetHostByName(
HWND hWnd,
Unsigned int wMsg,//异步请求结束时收到的窗口消息
Const char FAR *name,//参数代表我们正在查找的主机用户名
Char FAR * buf,//参数是一个指针,它指向接收HOSTENT 数据的那个数据域。
Int buflen
);
另外两个用于获得主机信息的函数是: g e t h o s t b y a d d r和WSAAsynGetHostByName API函数,它们是为获得与I P网络地址相应的主机信息而设计的。在有了主机I P地址,并打算查找其用户友好名时,这两个函数非常有用。g e t h o s t b y a d d r函数的定义如下:
Struct HOSTENT FAR * gethostbyaddr(
Const char FAR * addr,
Int len,
Int type
);
a d d r参数是指向一个I P地址的指针,这个地址按网络字节顺序排列。l e n参数用于指定a d d r参数的字节长度。t y p e 参数将指定A F _ I N E T 值,这个值表明指定类型是I P地址。WSAAsyncGetHostByAddr API函数是g e t h o s t b y a d d r函数的异步版。
第七章:Winsock基础
7.1 Winsock的初始化
每个winsock应用都必须加载winsockDLL的相应版本,如果调用winsock之前,没有加载winsock库,这个函数就会返回一个SOCKET_ERROR,错误信息是WSANOTINITALISED。
加载winsock库是通过调用WSAStartup函数实现的。这个函数的定义如下:
Int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
)
这些在之前就说过了!这里不解释。
想要知道数据报的最大长度应该通过WSAEnumProtocols来查询协议信息,不要使用WSADATA中的iMaxSockets, iMaxudpDg来查看他们是假定的同时最多可打开多少套接字和数据报的最大长度。
7.2 错误检查和控制
不成功的winsock调用返回的最常见的值是SOCKET_ERROR。在详细介绍各个API调用时,我们打算指出和各个错误对应的返回值。实际上,SOCKET_ERROR常量是-1如果调用一个winsock函数,错误情况发生了,就可用WSAGetLastError函数来获得这段代码,这段代码明确地表明发生的状况。此函数定义如下:
Int WSAGetLastError(void);
7.3 面向连接的协议:
7.3.1 服务器API函数:
“服务器”其实是一个进程,它需要等待任意数量的客户机连接,以便为它们的请求提供服务。对服务器监听的连接来说,它必须在一个已知的名字上。在TCP/IP中,这个名字就是本地接口的IP地址,加上一个端口编号。在winsock中,第一步是将指定协议的套接字绑定到它已知的名字上,这个过程是通过API调用来bind来完成的。下一步是将套接字置为监听模式。这时,用API函数listn来完成的。最后,若一个客户机试图建立连接,服务器必须通过accept或WSAAccept调用来接受连接。
1. bind
一旦为某种特定协议创建了套接字,就必须将套接字绑定到一个已知地址。bind 函数可将指定的套接字同一个已知地址绑定到一起。声明如下:
Int bind(
SOCKET s, //等待客户机连接的那个套接字
Const struct sockaddr FAR* name,//在这个结构体中要指定协议簇和端口号以及发送方式
Int namelen //代表要传递的由协议决定的地址长度
);
创建和绑定的过程如下(TCP):
SOCKET s;
Struct sockaddr_in tcpaddr;
Int port = 5150;
S = Socket(AF_INET, SOKC_STREAM, IPPROTO_TCP);
Tcpaddr.sin_family = AF_INET;
Tcpaddr.sin_port = htons(port);
Tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind (s, (SOCKETDDR*)&tcpaddr, sizeof(tcpaddr));
最常见的错误是WSAEADRINUSE,表示一个进程已经同本地IP接口和端口号绑定到了一,
或者那个IP接口和端口号处理TIME_WAIT状态。
2. listen
我们接下来要做的是将套接字置入监听模式。bind 函数的作用只是将一个套接字和一个指定的地址关联起来。指示一个套接字等候进入连接的API函数则是listen,其定义如下:
Int listen(
SOCKET s, //监听的套接字
Int backlog //指定正在等待连接的最大队列长度
);
Backlog有多服务器连接请求时,会让socket等待的个数以便应用程序依次为它们提供服务。
一旦服务器接受了一个连接,那个连接请求就会从队列中删去,以便另人可继续发出请求。
忘记在listen之前调用bind时会返回WSAEINVAL的错误。与bind调用相反,使用listen时可能收到WSAEADDRINUSE。
3. accept 和WSAAccept
现在,我们已做好了接受客户连接的准备。这是通过accept或WSAAccept函数来完成的。Accept格式如下:
SOCKET accept(
SOCKET s, //限定套接字,它处在监听模式。
Struct sockaddr FAR* addr,//SOCKADDR_IN结构的地址,对于属于另一种协议的 //套接字应当用与那种协议对应的SOCKADDR结构来 //替换
Int FAR * addrlen
)
通过对accpet函数的调用,可为待解决连接队列中的第一个连接请求提供服务。Accept 函数返回后,addr结构中会包含发出连接请求的那个客户机的IP地址信息。此外accept会返回一个新的套接字描述符,它对应于已经接受的那个客户机连接。对于该客户机后续的所有操作,都应使用这个新套接字。至于原来的那个监听套接字,它仍然用于接受其他客户机连接,而且仍处于监听模式。
Winsock2引入一个名为WSAAccept的函数,它能根据一个条件函数的返回值,选择性地接受一个连接,这个新函数的定义如下:
SOCKET WSAAccept(
SOCKET s,
Struct sockaddr FAR* addr,
LPINT addrlen,
LPCONDITIONPROC lpfnCondition,//这是一个函数指针,是根据客户请求来调用
//该函数决定是否的连接请求请求
DWORD dwCallbackData
);
Int CALLBACK ConditionFunc(
LPWSABUF lpCallerId,//连接实体的地址
LPWSABUF lpCallerData,//由客户机发出的任何连接数据NULL
LPQOS lpSQOS,//
LPQOS lpGQOS,//这是对客户机请求的任何一个服务质量QOS参数进行指定,NULL
LPWSABUF lpCalleeId,
LPWSABUF lpCalleeData,
GROUP FAR * g,
DWORD dwCallbackData
)
Typedef struct __WSABUF{
u_long len;
char FAR * buf,
}WSABUF, FAR * LPWSABUF;
如果服务器和INADDR_ANY地址绑定在一起,任何一个网络接口都可为连接请求提供服务。随后,该参数会返回实际建立连接的那个接口。
如果服务器打算接受连接,那么条件函数就应返回CF_ACCEPT如果拒绝函数就应该返回CF_REJECT。如果出于某种原因,现在还不能做出决定,就应返回CF_DEFER。若服务器准备对这个连接请求进行处理,就应调用WSAAccept,要注意的是,条件函数在与WSAAccept函数相同的进程内运行,而且会立即返回。
如果发生错误就会返回INVALID_SOCKET最常见的错误是WSAEWOULDBLOCK如果监听套接字处于异步状态或非暂停模式,同时没有要接受的连接时,就会产生些类的错误。若条件函数返回CF_DEFER,WSAAccept就会返回WSATRY_AGAIN错误,如果条件函数返回CF_REJECT,WSAAccept错误就是WSAECONNREFUSED
7.3.2 客户机API函数:
建立连接所需要的步骤也有三个步骤:
1) 用socket或WSASocket创建一个套接字。
2) 解析服务器
3) 用connect或WSAConnect 初始化一个连接
Connect 函数和WSAConnect函数;
最后一步就是连接。这是通过调用connect 函数或WSACONNECT 函数来完成的。
Int connect(
SOCKET s,//TCP套接字
Const struct sockaddr FAR* name,//针对TCP的说明连接的服务器的套接字地址结构
Int namelen//名字参数的长度
);
Winsock2定义如下:
Int WSAConnect(
SOCKET s,
Const struct sockaddr FAR* name,
Int namelen,
LPWSABUF lpCallerData,// 字串缓冲区,用于收发请求连接时的数据
LPWSABUF lpCalleeData,//
LPQOS lpSQOS,
LPQOS lpGQOS,
)
如果你想连接计算机没有监听指定端口这一进程,connect调用就会失败,并发生错误WSAECONNREFUSED,另一个错误可能是WSAETIMEDOUT这种情况一般发生在试图连接计算机不能用时。
7.3.3 数据传输:
要在已建立连接的套接字上接收数据,可用API函数:send和WSASend。第二个函数是Winsock2中专用的。在已建立了连接的套接字上接收数据也有两个函数:recv和WSARecv。后者也是Winsock2函数。
必须牢牢记住一点:所有关系到收发数据的缓冲都属于简单的char类型。也就是说,这些函数没有“Unicode”版本。这一点对windows CE来说尤为重要,因为windowsCE默认使用Unicode。使用Unicode时有一种选择,即把字符串当作char* 或把它造型为char* 发送。需要注意的是,在利用字符串长充函数告诉WinsockAPI函数收发的数据有多少字符时,必须将这个值乘以2,因为每个字符占用字串组的两个字节。另一种选择是在将字串数据投给winsockAPI函数之前,用wideCharToMutiByte把UNICODE转换成SACII码。
另外,所有收发函数返回原错误代码都是SOCKET_ERROR。一旦返回错误,系统就会调用WSAGetLastError获得详细的错误信息。最常见的错误是WSAECONNABORTED和WSAECONNRESET。两者均涉及到即将关闭连接这一问题-要么通过超时,要么通过通信方关闭连接。别一个常见错误是WSAEWOULDBLOCK,一般出现在套接字处于非暂停模式或异步状态时。这个错误主要意味着指定函数暂时不能完成。
1. send和WSASend
要在已建立连接的套接字上发送数据,第一个可用的API函数是send,其原型为:
Int send(
SOCKET s,//已建立连接的套接字,将在这个套接字上发送数据
Const char FAR *buf,//字缓冲区,区内包含即将发送的数据。
Int len,//指定即将发送的缓冲区内的字符数。
Int flags//0,MS_DONTROUTE,MSG_OOB
)
MSG_DONTROUTE标志要求传送层不要将它发出的包路由出去。MSG_OOB标志预示数据应该被带外发送。
返回数据是send返回发送的字节数;若发生错误就返回SOCKET_ERROR。常见的错误是WSAECONNABORTED,这一错误一般发生在虚拟回路由于超时或协议有错而中断的时候。发生这种情况时,应该关闭这个套接字,因为它不能再用了。WSAETIMEOUT它发生在连接由于网络故障或远程连接系统异常死机而引起的连接中断时。
Send API函数的winsock2版本是WSASend它的定义如下:
Int WSASend(
SOCKET s,
LPWSABUF lpBuffers,//是指向一个或多个WSABUF结构的指针,每个WSABUF结构体 //身就是一个字符缓冲和缓冲长度
DWORD dwBufferCount,//准备投递的WSABUF结构数
LPDWORD lpNumberOfBytesSent,//包含字节总发送数。
DWORD dwFlags,//
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpComletionROUTINE
)
WSASend函数把lpNumberOfBytesSent设为写入的字节数。成功的话,该函数返回0,否则就返回SOCK_ERROR,常见错误和send函数的情形一样。
2. recv和WSARecv
对在已连接套接字上接受接入数据来说,recv函数是最基本的方式它的定义如下:
Int recv(
SOCKET s,//准备接收数据的套接字
Char FAR* buf,//即将收到数据的字符缓冲
Int len,//准备接收的字节数或buf缓冲的长度
Int flags//0,MSG_PEEK,MSG_OOB
);
MSG_PEEK会使有用的数据复制到所提供的接收端缓冲内,但是没有从系统缓冲中将它删除。另外还返回了待发字节数。
在面向消息或面向数据报的套接字上使用recv时,这几点应该注意。在待发数据大于所提代的缓冲这一事件中,缓冲内会尽量地填充数据。这时,recv调用就会产生WSAEMSGSIZE错误。注意,消息长错误是在使用面向消息的协议时发生的。流协议把接入的数据缓存下来,并尽量地返回应用所要求的数据,即使待发数据的数量比缓冲大。因此,对流式传输协议来说,就不会碰到WSAEMSGSIZE这个错误。
WSARecv函数在recv的基础上增加了一些新特性,比如说重叠I/O和部分数据报通知。
WSARecv的定义如下:
Int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,//接收数据的缓冲
Dword dwBufferCount, //接收数据的缓冲表明前一个数组中wsabuf结构的数目
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,//0,MSG_PEEK,MSG_OOB,MSG_PARTIAL
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE
);
7.3.4 流协议:
对于流套接字上收发数据所用的函数,需要明白的是:它们不能保证对请求的数据量进行读取或写入。
7.3.5 中断连接:
一旦完成任务,就必须关掉连接,释放关联到那个套接字句柄的所有资源。要真正地释放一个开着的套接字句柄关联的资源,执行closesocket调用即可。它可能会带来负面影响即可能会导致数据丢失。鉴于此应该在调用closesocket函数之前,利用shutdown函数从容中断连接。
1. shutdown
int shutdown(
SOCKET s,
Int how//SD_RECEIVE,SD_SEND,SD_BOTH
)
如果是SD_RECEIVE就表示不允许再调用接收函数。SE_SEND表示不允许再调用发送函数。SD_BOTH则表示取消连接两端的收发操作。
2. closesocket函数用于关闭套接字
int closesocket(SOCKET s);
closesocket的调用会释放套接字描述符,再利用套接字执行调用就会失败,并出现WSAENOTSOCK错误。
7.3 无连接协议
和面向连接的协议比较起来,无连接协议的行为极为不同,因此,收发数据的方式也会有所不同。
7.4.1 接收端
对于在一个无连接套接字上接收数据的进程来说,步骤并不复杂。先用socket或WSASocket建立套接字。再把这个套接字和准备接收数据的接口绑定在一起。这是通过bind函数来完成的。和面向会话不同的是,我们不必调用listen和accept。相反只需等待接收数据。由于它是无连接的,因此始发于网络上任何一台机器的数据报都可被接收端的套接字接收。最简单的接收函数是recvfrom它的定义如下:
Int recvfrom(
SOCKET s,
Char FAR* buf,
Int len,
Int flags,
Struct sockaddr FAR* from,//发送数据的工作站地址
Int FAR* fromlen
)
Recvfrom 函数的winsock2版本是WSARecvFrom
Int WSARecvFrom(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,//提供多个WSABUF缓冲
LPDWORD lpNumberOfBytesRecvd,//读取的字节总数返回
LPDWORD lpFlags,
Struct sockaddr FAR* lpFrom,
LPINT lpFromlen,指向SOCKADDR结构长度
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE
);
7.4.2 发送端
要在一个无连接的个套接字上发送数据,有两种选择。第一种,也就是最简单的一种,便是建立一个套接字,然后调用sendto或WSASendTo
Int sendto(
SOCKET s,
Const char FAR* buf,
Int len,
Int flags,
Const struct sockaddr FAR * to,
Int tolen
)
Int WSASendTo(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
Const struct sockaddr FAR * lpTo,
Int iToLen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUNTINE
)
第8章:Winsock I/O方法
本章重点是如何在windows套接字应用程序中对I/O操作进行管理。
Winsock分别提供了“套接字模式”和“套接字I/O模型”,可对一个套接字上的I/O行为加以控制。其中,套接字模式用于决定在随一个套接字调用时,那些winsock函数的行为。而另一方面,套接字模型描述了一个应用程序如何对套接字上进行的I/O进行管理及处理。要注意的是“套接字I/O模型”与“套接字模式”是无关的。套按字模型的出现,正是为了解决套接字模式存在的某些限制。
Windows提供了两种套接字模式:锁定和非锁定。所有windows平台都支持套接字以锁定或非锁定方式工作。然而,并非每种平台都支持每一种I/O模型。
8.1 套接字模式:
Windows套接字在两种模式下执行I/O操作:锁定和非锁定。
在锁定模式下,在I/O操作完成前,执行操作的winsock函数(send/recv)会一直等候下去,不会立即返回程序(将控制权交还给程序)。而在非模式下Winsock函数无论如何都会立即还回。
8.1.1 锁定模式:
对于处在锁定模式的套接字,我们必须多加留意,因为在一个锁定套接字上调用任何一个winsockAPI函数,都会产生相同的后果----耗费或长或短的时间“等待”。
对锁定套按字来说,它的一个缺点在于:应用程序很难同时通过多个建好连接的套接字通信 。
8.1.2 非锁定模式:
创建一个套接字,并将其置为非锁定模式。
SOCKET s;
Unsigned long ul = 1;
Int nRet;
S = socket(AF_INET, SOCK_STREAM, 0);
nRet = ioctlsocket(s, FIOBIO, (unsigned long*)&ul);
if(nRet == SOCKET_ERROR)
{
//Failed to put the socket into nonblocking mode
}
将一个套接字置为非锁定模式之后,winsockAPI调用会立即返回。大多数情况下,这些调用都会“失败”,并返回一个WSAEWOULDBLOCK错误。这表示请求的操作在调用期间没有时间完成。假如在系统的输入缓冲区中,尚不存在“待决”的数据,那么recv调用就会返回WSAEWOULDBLOCK错误。由于非锁定调用会频繁返回WSAEWOULDBLOCK错误,所以在任何时候,都应仔细检查所有返回代码,并作好“失败”的准备。综上所术一个锁定套接字和一个非锁定套接字都有其优点和缺点,所以我们可考虑使用“套接字I/O模型”,它有助于应用程序通过一种异步方式,同时对一个或多个套接字上进行的通信加以管理。
8.2 套接字I/O模型:
共有五种类型的套接字I/O模型,可让Winsock应用程序对I/O进行管理,它们包括:select(选择),WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及completion port(完成端口)。
8.2.1 select模型;
之所以称其为“select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理!最初设计该模型时,主要面向的是某些使用UNIX操作系统的计算机,它们采用的是Berkeley套接字方案。
利用select函数,我们判断套接字上是否存在数据,或者能否向一个套接字写入数据。之所以要设计这个函数,唯一的目的便是防止应用程序在处于锁定模式中时,在一次I/O绑定调用(send/recv)过程中,被迫进入“锁定”状态;同时防止在套接字处于非锁定模式中时,产生WSAEWOULDBLOCK错误。除非满足事先用参数规定的条件,否则select函数会在进行I/O操作时锁定。
Int select(
Int nfds,//没用
Fd_set FAR * readfds,//用于检查可读性
Fd_set FAR * writefds,//用于检查可写性
Fd_set FAR* exceptfds,//例外数据
Const struct timeval FAR* timeout
)
从根本上说,fd_set数据类型代表着一系列特定套接字的集合。
Readfds集合包括符合下任何一个条件的套按字:
1. 有数据可以读入。
2. 连接已经关闭、重设或中止。
3. 假如已调用listten,而且一个连接正在建立,那么accept函数调用会成功。
Writefds集合包括符合下述任何一个条件的套接字。
1. 有数据可以发出。
2. 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
最后,exceptfds集合包括符合下述任何一个条件的套接字:
1. 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
2. 有带外数据可代读取。
例如,假定我们想测试一个套接字是否“可读”,必须将自己的套接字增添到readfds集合,再等待select函数完成。Select完成后,必须判断自己的套接字是否仍为readfds集合的一部分。若答案是肯定的便表明该套接字“可读”,可立即着手从它上面读取数据。在三个参数中(readfds,writefds,exceptfds),任何两个都可以是空值,但是至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄;否则,select函数便没有任何东西可以等待。最后一个参数timeout对应的是一个指针,它指向一个timeval结构,用于决定select最多等待I/O操作完成多久的时间。如timeout是一个空指针,那么select调用会无限期“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束。
Timeval结构的定义如下:
Struct timeval
{
Long tv_sec;
Long tv_usec;
};
其中,tv_sec字段以秒为单位指定等待时间;tv_usec字段则以毫秒为单位指定等待时间。若将超时值设置为(0,0)表明select会立即返回,允许应用程序对select操作进行“轮询”。出于对性能方面的考虑,应避免这样的设计。Select成功完成后,会在fd_set结构中,返回刚好有未完成的I/O操作的所有套接字句柄的问题。若超过timeval设定的时间,便会返回0。不管由于什么原因,假如select调用失败,都会返回SOCKET_ERROR。
用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄分配给一个集合,设置好一个或全部读、写以及例外fd_set结构。将一个套接字分配给任何一个集合后,再来调用select,便可知道一个套接字上是否正在发生上述的I/O活动。Winsock提供了下列宏操作,可用来针对I/O活动,对fd_set进行处理与检查:
FD_CLR(s, *set);从set中删除套接字s
FD_ISSET(s, *set):检查s是否set集合的一名成员,如答案是肯定的是,则返回true;
FD_SET(s, *set):将套接字s加入集合set
FD_ZERO(*set):将set初始化成空集合。
如下:可以完成用select操作一个或多个套按妆接字句柄的全过程:
1) 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set.
2) 使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set
3) 调用select函数,然后等待在指定的fd_set集合中,I/O活动设置好一个或多个套接字句柄。Select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
4) 根据select的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成(待决)的I/O操作---具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。
5) 知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤1),继续进行select处理。
6) Select返回后,它会修改每个fd_set结构,删除那些不存在的待决I/O操作的套接字句柄。这正是我们在上述的步骤(4)中,为何要使用FD_ISSET宏来判断一个特定的套接字是否仍在集合中的原因。