1.6.3 数据传输
收发数据是网络编程的主题。要在已建立连接的套接字上发送数据,可用这两个 API函数:sed和 WSASend。第2个函数是 Winsock 2中特有的。同样地,在已建立了连接的套接字上接收数据也有两个函数:recv 和 WSARecV。后将也是 Winsock 2函数。必须牢记住这点: 所有关系到收发数据的缓冲区都属于简单的 char 类型,即面向字节的数据。事实上,它可能是一个包含任何原始数据的缓冲区,至]这个原始数据是…进制数据,还是字符型数据,则无关紧要。
另外,所有收发函数返回的错误代码都是 SOCKET ERROR。一旦有错误返回,系统就会调用WSAGetLastError 获得详细的错误信息。最常见的错误是WSAECONNABORTED和WSAECONNRESET。两者均涉及到连接正在被关闭这一问题–要么由于超时被关闭,要么由于通信方止在关闭连接。另··个常见错误是 WSAEWOULDBLOCK,·般出现在套接字处于非阻寒模式或只步状态时。这个错误意味着指定函数暂时不能完成。第5 将详细说明各种 Winock1/0 方法,以避免出现此类错误。
1.6.3.1 send 和 WSASend
要在已建立连接的套接子上发送数据,第1个可用的API 函数是 send,其原型为:
int send(SOCKET s, const char FAR * buf, int len,int flags);
SOCKET 参数是已建义了连接、将用于发送数据的套接子。第2个参数 buf则是指向学符缓冲区的指针,该缓冲区中包含即将发送的数据、第 3 个参数 e,指定即将发送的缓冲区内的字符数。最后,fags 可为0、MSG DONTROUTE MSG OOB。另外,flags 还可以是对这些标志进行按位“或运算的一个结果。MSG DONTROUTE 标态要求传输层不要将它发出的数据包路中出去。是否实现这请求由下尽的传输米决定(例如,若传输协议个文持该选项,这一请求就会被忽略》。MSG OOB 标志衣示数据应该进行带外发送。
在顺利的情况下 send 将返回发送的字节数:发借误,就返回 SOCKET ERROR。常见的误是 WSAECONNABORTED,这一错误一般在虚拟回路出于超时或协议有错而中断的时候发生。发生这种情况时,因为这个套接字不能再用了,所以应该将它关闭。当远程主机上的应用程序通过执行强行关闭或意外中断模作重新设置虑拟回路时,或远程主机重新启动时,发生的则是WSAECONNRESET 错误。再次需要注意的是,发生这·错误时,应该大闭这个接字。最后-个常见错误是 WSAETIMEOUT,它在连接由丁网络故障或远程连接系统并常死机而导致连接中断时发牛。
Send AP 函数的 Winsock 2 版本是 WSASend,它的定义如下:
int WSASend{ SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDNORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpoverlapped, LPWSAOVERLAPPED COMPLETION RCUTINE IpCompletionRoutine );
这个套接字是一个连接会话的有效句柄。第2个参数是指向一个或多个 WSABUF 结构的指针它既可是一个独立的结构,又可以是一个结构数组。第3个参数指明准备传递的 WSABUF 结构数量.
记住,每个 WSABUF 结构本身就代表了一个字符缓冲及其长度。为何打算同时发送多个缓冲呢? 也许人家不太明白其小的原因。这就是稍后要讲到的散播一聚集I/O 模式,但是,在·个已建立连接的套接子上利用多个缓冲来发送数据时,每个缓冲区的发送顺序都是从数组中的第一个到最后一个WSABUF 结构、IpNumbcrOBytesSent 参数是指向DWORD(是 WSASend 调用返回)的指针,其小包含已发送的字节总数。
dwFlags 参数相当于它 send 中的等效参数。最后两个参数pOverlappedlpCompletionROUTINE-用于重I/0,重叠1/0是Winsok 支持的异步/0 模式的·种。关于重1/0在第5章将进行详细讲解。
WSASend函数把IpNumberOfBytesSent 设为写入的宁节数。执行成功时,该函数就返回0,否则就返阿 SOCK ERROR,该函数的常见错误通常send 函数一样。
必须注意这最后一个发送函数WSASendDisconnect
1.6.3.2 WSASendDisconnect
这个雨数非常特殊,-般不用。其原型是:
jnt NSASendDisconnect fSOCKET S, LPWSABUF 1pOptboundDiseonnectData
带外数据
对已建立连接的流套接宇上的应用程序来说,如果需要发送的数据比流上的普通数据重要得多,使可将这些重要数据标记成 00B(Oul-of-band,带外)数据,位于连接另一的应用程序可通过一个独立的逻辑信道(从理论上讲,该逻辑信道与数据流无关)来接收和处理 OOB 数据,在TCP中,00B 数据由一个紧急的!位标记(叫作 URG)和 TCP 分段头中的一个 16 位的指针组成。这里的标记和指针把特定的下行流宇节当作紧急数据,实现紧急数据的两种特殊方法目前只能在TCP.RFC793 中见到,该索引对 TCP 进行了描述,并引入了“紧急数据”这一概念,指明 TCP 头中的紧息指针是紧急数据字节之后那个字节的绝对偏移。在 RFC1122 中,紧急偏移被描述成指向紧急字节本身、
Winsock 规范中,独立于协议的 00B 数据和 TCP的 OOB 数(紧息数)实现均来用了OOB这一术语。要查看被搁置的数据中是否包含紧急数据,必须通过 SIOCATMARK 选项调用 joctlsocket 西教。第7章将介绍 SIOCATMARK 的用法
Winsock 提供了获得紧急数据的几种方法。可以内联紧急数据,使其出现在普通数据流中,也可以禁用内联,这样,对接收函数的不连续调用就只返回紧急数据,至于控制 OOB 数据行为的套接宇选项 SO OOBINLINE,本书也将在第 7章详细讨论
Telnet和 Rlogin 使用紧急数据是有原因的,尽管如此,除非计划编写自己的 Telnet 和 Rlogin,否刚就应该避开紧急数据,原因在于,它定义得不完善,而且它在其他平台上的实施情况可能和 Windows有所不同,如果在紧急的情况下需要一种方法来发信号通知通信方,可以为紧急数据执行独立的控制套接字,并为普通数据的传输保留主要的套接字连接
函数 WSAScndDisconnect 起初将套接字置为关闭状态,并发送断开的数。当然,它只能用于支持从容关机和断]数据的伙输协议、目前还没有传输提供科序支持断开数据。WSASendDisconnect 函数的行为和利用 SD SEND 参数调用 shutdown 两数(后面将讲到该函数)差不多,但它另外还要发送包含在 boundDisconnectData 参数中的数据,之后的发送禁在这个套接字上进行。如果调用失败,WSASendDisconnection 就会返 SOCKET ERROR。使用该函数可能会出现 send 函数中出现的某些错误。
1.6.3.3 recv和WSARecv
对于已建立连拨的套接字上接受数据的传入来说,rccv 函数是最基的方式。它的定义如下
int recv( SOCKET e char FAR* buf, int len, int flag );
第1个参数 5,是准备用来接收数据的那个套接。第 2 个参数 buf,是于接收数据的字符缓冲区,而 len 则是准备接收的字节数或 buf 缓冲区的长度。最后的 ags 参数可以是下面的值:0MSG PEEK或MSG OOB。另外,还可对这些标点中的每一个进行按位“或”运算。当然,0表示没有特殊的行为。MSG PEEK 示要将可用的数据复制到所提供的接收端缓冲区内,但是并不从系统缓冲区中将这些数据删除。待发字节数也将被返回。
消息查看功能有一些缺点,它不仅导致性能下降(因为需要进行两次系统调用,·次是古看,另一次是无 MSG PEEK 标点的真正删除数据的调用),在某些情况下还可能不可靠。返回的数据可能没有反映出真正可用的总单。与此同时,把数据留任系统缓冲区内,可容纳传入数据的系统空间就会越来越少。结果使得系统减少各发送端的 TCP 窗容量,这样,应用程序就不能得最人的数据吐率。因此,最好是尽量把所有数都复制到自己的缓冲区中,并在那里操作数据。
在基丁消息或基于数据报的套接字(例如 UDP)使用 recv 时,儿点应该注意当挂起数据大丁所提供的缓冲区时,缓冲区会尽地计数据填满。这时,recv 调州会产生 WSAEMSGSIZE 错误。注点,消息大小的错识是在使用面向消息的协议时发生的。而流协议(如 TCP)刚把么入的数据缓下来并尽量地返国应用科序所要求的数据,即使被挂起的数据量比缓冲大。闪此,对流传输协议来说,就下金碰到 WSAEMSGSIZE 这个错误。
WSARecv 函数在 recv 的基础下增加了一些新特性。比如说叠1/0 和部分数据报通知WSARccy的定义如下:
int WSARecv ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD IpNumberOfBytesRecvd,LPDNORD lpFlagS, LPWSAOVERLAPPED IpOverlapped,LPWSADVERLAPPED COMPLETION ROUTINE IpCompletonRoutine );
参数 s 是建立连接的套接。第 2 和第3 个参数是用收数据的缓冲pBuffers 参数是个由WSABUF 结构组成的数组,而dwBufferCouni 则表明这个数组中 WSABUF 结构的数量。如果接收操作立即元成,IpNumber0BytesRcccived 参数就会指向这个丽数调用所收到的字节数。lpFlags 参数可以是下面任何一个值:MSG PEEK、MSG OOB、MSG PARTIAL,者是对这些值进行按位“或”运算之后的结果。MSG PARTIAL 标志如果使和出现的地方不同,其义也不同,对支持部分消息的血向消息的协议(如 Apple Talk)水说,这个标志是在 WSARecv 调用返后设置的(如果因为缓冲区空间不够,导致整条消息未能在这次调用中返回)。这时,后的 WSARecv 调用都会设置这个标志,直到整条消息返回,才把这个MASG PARTIAL 忐清除。如果把这个标志当作·个输入参数传递则接收操作应该·–收到可用数据就结束,即使它收到的只是整条消息中的·部分,MSG PARTIAL 标志只随面向消息的协议“起使用。另外,不是所有的协议都支持部分消息。每个协议的协议条口都包含·个标志,农明是否文持这一特性,有关详情参见第2章。pOverlapped 和ipCompletionROUTINE参数用丁重叠 /O 的操作,第5 将对此进行详细讨论。此外,还必须注意另外一个特殊的接收雨数WSARecvDisconnect。
1.6.3.4 WSARecvDisconnect
这个函数与WSASendDisconnect 函数相对应,其定义如下:
int WSARecvDisconnct( SOCKET s, LPWSABUF lpInboundDisconnectData );
和WSASendDisconnect 函数的参数一样,该函数的参数也是山建立连接的套接字柄,以及一个有效的 WSABUF 结构(带有接收到的数据)。接收到的数据可以只是断数。这个断开数据是另一端的 WSASendDisconnect 调用发出的,它不能用于接收普通数据。另外,一日收到数据,WSARecvDisconnect 雨数就会取消接收远程通信方的数据,其作用和用SD RECEIVE 调用 shutdown函数(稍后讲述)时相同。
1.6.4 流协议
由于大多面向连接的协议(如 TCP)同时也是流传输协议,所以,在此对流协议作一些介绍。在流协议中,发送者和接收者可以将数据分解成小块数据,或将数据合并为大块数掘。对于流套接字小收发数据所用的函数,需要了解的是:它们不能保证要求进行读取或写入的数据量。
比如说,用 send函数来发送 个有 2048 字节的字符缓冲区,采用的代码是:
char sendbuffl2048];\ int nBytes = 2048; //用2048 字节的数填允 sendbuff //假定 s 是一个有效的、已连接的流套接字 ret = send(s,sendbuff,nBytes,0);
对 send 函数而言,可能会返已发出的少下2048 的子节。
因为对每个收发数据的套接子来说系统都为它们分配了相当充足的缓冲区空间,所以ret 变量将被设为发送的字节数。在发送数据时内部缓冲区会将数据一直保留到可以将它发到线上为止。几种常见的情况都可导致这一情形的发生。比方说,传输大量的数据以令缓冲区快速填满。同时,对TCP/IP 来说,还一个窗口大小的问题接收端会对窗口大小进行调节,以指示它可以接收多少数据。如果有大量数据涌入接收端,接收端就会将窗口大小设为 0,为挂起的数做好准备。
对发送来说,这样会强制它在收到一个新的大下0的窗口大小之前,不得再发送数据。在使用 send 调用时,缓冲区可能只能容纳 1024 个字节,这时,使有必要重新提交剩下的 1024 个字节。要保证将所有的字节发出去,可采用下面的代码。
char sendbuff[2048]; int nBytes = 2048, nLeft, idx; //用 2048字节的数据填充 sendbuff //假定 s 是一个有效的、已连接的流复接字 nLeft = nBytes; idx = 0; while (nLeft >0) ret = send(s,sendbuff[idx],nLef-,0); if (ret == SOCKET ERROR) { //出错 } nLeft -= ret; idx += ret;
在流套接子上接收数据时,这一原则照样适用,但意义不大。因为流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读多少数据。如果应用程序需要通过流协议获取离散消息,您需要做一些额外的T.作。如果所有消息长度都一样,则处理起来比较简单,比如说,需要读取的 512个字节的消息看起来像下面这样:
char recvduff[1024]; int ret,nleft,idx; nLeft = 512; ldx = 0; while (nLeft >0){ ret = recv(s,&recvbufflidx],nLeft,0); if (ret == SOCKET ERROR){ //出错 } idx += ret; nLeft -= ret; }
如果消息长度不同,处理起来就会麻烦-·点。因此,有必要利用自己的协议来通知接收端,让它知道即将到来的消息长度是多少。
比方说,写入接收端的前 4 个字节总是整数,大小为即将到来的消息的字节数。接收端每次开始读取时,会先食看前 4 个学节,把它们转换成一个整数,并查看构成消息的字节数是多少。
散播一聚集1/0
散播-聚集(scatter-gather) 支持是 Berkeley Socket中首次随 Recv和 Writev 这两个函数一起出现的概念,它随WSARecV、WSARecvFrom、WSASend和WSASendTo这几个 Winsock2函数一起使用对那些收发特殊格式数据的应用程序来说,这个功能是非常有用的。比方说,客户机发到服务器的消息可能一直都是这样构成的: 一个指定某种操作的固定的 32 字节的头,一个64字节的数据块和一个16字节的尾,这时,就可用一个由3个WSABUF 结构组成的数组调用 WSASend,这3个结构分别对应3种消息类型、在接收端,则用3个WSABUF 结构来调用 WSAReCV,各个结构包含的数据缓冲区分别是32字节、64字节和 16字节。
在使用基于流的套接字时,散播一聚集 I/0 模式只是把 WSABUF 结构中提供的数据缓冲区当作一个连续的缓冲区。另外,接收调用可能在所有缓冲区填满之前就返回,在基于消息的套接字上,每次对接收操作的调用都会收到一条消息,其长度由所提供的缓冲区大小决定。如果缓冲空间不够,调用就会失败,并出现 WSAEMSGSIZE 错误,为了适应可用的缓冲空间,数据就会被截断。当然,如果使用支持部分消息的协议,就可用 MSG_PARTIAL 标志来避免数据的丢失
1.6.5 中断连接
一旦完成了套接字连接,就必须将它关掉,并释放关联到那个套按字创树的所何资源。
要真正地释放与一个打开的套接宁句柄关联的资源,执行 closesocket 调用即可,但要明白这·点,closesocket可能会带来负面影响(和如何调用它有关), 即可能会导致数据的丢失。
鉴于此,应该在调用 closesocket函数之前,利用 shutdown 函数从容地终止连接。接下来讨论这两个AP[函数。
1.6.5.1 shutdown
为了保证通信方能够收到应用程序发出的所有数据,对一个好的应用程序来说,应该通知接收端“不再发送数据”。同样,通信对方也应该如此,这就是所谓的”正常关闭”方法,由 shutdown 函数来执行。shutdown 的定义如下;
int shutdown( SOCKET s, int how)
how
参数可以是后面的任何一个值:
- SD RECEIVE、
- SD SEND
- SD BOTHL:
如是SD RECEIVE,就表示不允许再调用接收函数。这对底部的协议层没有影响。另外,对 TCP 套接宁来说,不管数据在是等候接收,还是将随后抵达,都要重新设置连接。尽管如此,在 UDP 套字上仍然接受并排列传入的数据(因为对于无连接协议而言,shutdown 毫无意义)。如果选择 SE SEND,表示不允许再调用发送函数。对 TCP 套接宁来说,这样会在所有数据发出,并得到接收端确认之后生成··个FIN包。最后,如果指定 SD BOTH,则表示取消连接两端的收发操作。
应注意到,并非所有的面向连接协议都支持从容关闭,这是 shutdown API 执行的功能。对那些不支持从容关闭的协议来说(如 ATM),仪调用 closesocket 使可结束进程
1.6.5.2 closesocket
closesocket 函数用于关闭套接宁,它的定义如下:
int closesocket (SOCKET s):
对 closesocket 的调用会释放套接字描述符,然后再利用套接字执行的调用就会失败,并山现WSAENOTSOCK
错误。
如果没有对该套接字的其他引用,那么所有与套接字描述关联的资源都被释放。
其中包括丢弃所有队列中的数据,对这个进程中任何一个线程来说,它们发出的被挂起的同步调用都会在未投递任何通知消息的情况下被删除。被挂起的的重叠操作也会被删除。与该重叠换作关联的任何事件、完成例程或完成端口能被执行,但后都会失败,并出现 WSA_OPERATION_ABORTED
错误。套接字I/O 模式在第5章中有详细讲解。
另外,还有一点会对 closesocket 的行为产生影响: 套接字选项 SO LINGER 是否已经设置。要了解这一点,请参考第7章中对 SO_LINGER 选项的描述。
1.7 无连接通信
和面向连接的协议比较起来,无连接协议的行为有很大不同,因此,收发数据的方法也会有所差别。和面向连接的服务器比较起来,无连接接收端改动不大,所以先讨论接收端(如果愿意,也可称之为服务器)。接下来再谈发送端。
在IP 中,无连接通信是通过 UD/P 协议完成的。UDP 不能确保可靠的数据传输,但能将数据发送到多个目标,或著接收的多个源数据。例如,如果· 个客户机向一个服务器发送数据,则数据将立刻被传输,不管服务器是否准备接收这些数据。如果服务器接收来自客户机的数据,该服务器也不会发消息确认数据已收到。数据的传输使用数据报,即离散信息包。
1.7.1 接收端
对于在一个无连接套接字上接收数据的进程来说,操作过程并不复杂先用 socket或 WSASocket创建套接字。再把这个套接字和准备接收数据的接口绑定在 -起,这是通过 bind 函数(和面向会话的示例··样)来完成的。和面向连接不同的是,不必调用 listen 和 accept。相反,只需等待接收数据。由于设有连接,始发于网络上任何一台机器的数据报都可被接收端的套接学接收。最简单的接收函数recvfrom,它的定义如下:
int recvfrom( SOCKET char FAR* bufs int len, int flags, struct sockaddr FAR* from, int FAR* fromlen)
前面4个参数和 recv 的参数是一样的,其中包括标志的可能值:MSG_OOB 和MSG_PEEK。在使用无连接套接字时,和前面一样,仍然应该慎用 MSG_PEEK 标志。对监听接字的给定协议来说from参数是个SOCKADDR 结构,带有指向地址结构长度的 fromlen这个API调用的返回数据时SOCKADDR结构内便填入了发送数据的那个T作站的地址。
recvfrom 函数的Winsock 2 版本是WSARecvFrom。后者的原型是:
int WSARecvFrom( SOCKET S, LPWSABUF lpBuffersDWORD dwBufferCount, LPDWORD lpNumberofBytesRecvd, LPDWORD IpFlags, struct sockadde FAR * IpFrOilt, LPINT lpFromlen, LPWSAOVERLAPPED IpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE IpCompletionRoutine )
两者的差别在于接收数据时 WSABUF
结构的用法。可以利用 dwBufferCount 为 WSARecvFrom提供一个或多个 WSABUF 缓冲区。提供多个缓冲区后,就可以用散播一聚集 I/0了。读取的字节总数放在INumberOfBytesRecvd 中返回。
在调用WSARecvFrom 时,ipFlags 参数可以是0代表无选项).MSG 0OB、MSG PEEK或MSG PARTIAL。这些标志还可以一起按位“或”运算。如果在调用这个函数时,指定了MSG PARTIAL,提供程序就知道返回数据,即使只收到了部分消息。当调用返回之后,如果只收到部分消息,应用程序就会设置 MSG PARTIAL 标志。另外调用返回后,WSARecvFrom就会把IpFrom参数(它是-个指向 SOCKADDR结构的指针设置为发送端的地划。LpFromLn 同样指向SOCKADDR 结构的长度,不过,在这个函数中,它还是-·个指针,指向DWORD。最后两个参数lpOverlapped 和1pCompletionROUTNE,用于重叠I/O(第S章将就此展开讨论)
在无连接套接字上接收(发送)数据的另一种方法是建立连接。听起来有些奇怪吧,但事实的确如此。无连接的套接字-·旦建立,便可利用 SOCKADDR 参数(它被设为准备与之通信的远程接收端地址)调用connect 或 WSAConnect。但事实上并没有建立真正的连接。传递到连接函数的套接字地址是与接字关联在一起的,如此一来,才能够用 Rcv和WSARecv 来代替recvfrom和WSARecvFrom。
为什么呢? 其原因是数据的始发处是已知的。如果在应用程序中,一次只和一个端点进行通信,便能很容易地与数据报套接字建立连接。
下面的示例代码展示了如何创建一个简单的 UDP 接收端应用程序。本应用程序的完整版本可在补充材料中名为UDPRECEIVER的文件中找到。
#include <winsork2 .n> void main(void){ WSADATA wsaData; SOCKET ReceivingSocket; SOCKADDR_IN ReceiverAddr; int Port = 5150; char ReceiveBuf[1024]; int BufLength = 1024; SOCKADDR_IN SenderAddr; int SenderAddrsize = sizeof(SenderAddr); //初始化 Winsock 2.2 版本 WSAStartup(MAKEWORD(2,2),&wsaData); //创建一个新的套接宁来接收数据报ReceivingSocket = socket(AF INET,SOCK DGRAM,IPPROTO UDP);//建立一个SOCKADDR IN结构,这个结构将告知bind 我们想要使用 5150 端口接收米自所 //有接口的数据报 ReceiverAddr.sin_famlly = AF INET; ReceiverAddr.sin port = htons(Port}; ReceiverAddr.sin_addr.s_addr = htonl(INADDR ANY);//使用 bind 将这个地址信息和套接字关联起来bind(ReceivingSocket,(SOCKADDR *)5SenderAddr,sizeof(SenderAddr)); //此时可以在绑定套接字上接收数据报了 recvfrom(ReceivingSocket,ReceiveBuf,BufLength,0,(SOCKADDR *)sSenderAddr,&SenderAddrsize); //应用程序结束接收数据报后,关闭套接字 closesocket(ReceivingSocket) //应用程序结束后,调用 WSACLeanup WSACleanup();
在了解到如何创建一个能够接收数据报的接收端之后,下面叙述怎样创建发送端。
1.7.2 发送端
要在一个无连接的套接字上发送数据,有两种选择。最简单的一种,使是建立一个套接字,然后调用 sendto或WSASendTo。先讲解 sendto 函数,它的定义是这样的:
int sendto( SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR * to, int tolen );
除了 buf是发送数据的缓冲区,len 指明发送多少字节外,其余参数和 recvfrom 的参数-·样。另外,0参数是一个指向SOCKADDR 结构的指针,该结构带有接收数据的那个丁作站的目标址址。另外,也可以用Winsock 2 函数WSASendTo。
它的定义如下:
int WSASendTo( SOCKET S, LPWSABUF lpBuffers DWORD dwBufferCount, LPDWORD lpNumberofBytesSent, DWORD dwFlags, const struct sockaddr FAR * IpTo, int iToLen, LPWSAOVERLAPPED IpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUIINE IpCompletionRoutine );
同样,WSASendTo和前版本中的 sendto 函数类似。它把指针当作 ipBuffers 参数,该指针指向带有发给接收端数据的结构,而dwBufferCount 参数则指明当前有多少个结构。发送多个WSABUF结构来启用散播-聚集/0。在函数返回之前,WSASendTo 把第4个参数pNumber0BytesSent设为真正发送到接收端的早节数。IpTo 参数是针对具体协议的个 SOCKADDR 结构,并带有接收端的地址。iToLen 参数是SOCKADDR 结构的长度。最后两个参数,IpOverlapped 和ipCompletionROUTINE用于重叠 I/0(将在第5中讨论)。
通过接收数据的方式,就可以把一个无连接的套接子连接到一个端点地址,并以用 send 和WSASend发送数据。这种关联旦建立,就不能再用带有地址的 sendto 和 WSASendTo,除非这个地址足传递到其中 个连接函数的地址,否则调用就会失败,出现 WSAEISCONN 错误。要取消套接宁柄与目标地址的关联,惟一的办法是在这个套接字句柄上以 INADDR ANY 为目标地址调用connect。
下面的示例代码展示了如何创建一个简单的 UDP 发送端应用程序。本应用程序的完整版本可在配套光盘中名为UDPSENDER 的文件中找到。
// Module Name: udpsender.cpp // // Description: // // This sample illustrates how to develop a simple UDP sender application // that can send a simple "hello" message to a UDP receiver awaiting datagrams // on port 5150. This sample is implemented as a console-style application and // simply prints status messages as data is sent to the server. // // Compile: // // cl -o udpsender udpsender.cpp ws2_32.lib // // Command Line Options: // // udpsender.exe <receiver IP address> // #include <winsock2.h> #include <stdio.h> void main(int argc, char **argv) { WSADATA wsaData; SOCKET SendingSocket; SOCKADDR_IN ReceiverAddr; int Port = 5150; int Ret; if (argc <= 1) { printf("USAGE: udpsender <receiver IP address>.\n"); return; } // Initialize Winsock version 2.2 if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0) { // NOTE: Since Winsock failed to load we cannot use WSAGetLastError // to determine the error code as is normally done when a Winsock // API fails. We have to report the return status of the function. printf("ERROR: WSAStartup failed with error %d\n", Ret); return; } // Create a new socket to receive datagrams on. if ((SendingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) { printf("ERROR: socket failed with error %d\n", WSAGetLastError()); WSACleanup(); return; } // Setup a SOCKADDR_IN structure that will identify who we // will send datagrams to. For demonstration purposes, let's // assume our receiver's IP address is 136.149.3.29 and waits // for datagrams on port 5150. Obviously you will want to prompt // the user for an IP address and port number and fill these // fields in with the data from the user. ReceiverAddr.sin_family = AF_INET; ReceiverAddr.sin_port = htons(Port); ReceiverAddr.sin_addr.s_addr = inet_addr(argv[1]); // Send a datagram to the receiver. if ((Ret = sendto(SendingSocket, "Hello", 5, 0, (SOCKADDR *)&ReceiverAddr, sizeof(ReceiverAddr))) == SOCKET_ERROR) { printf("ERROR: sendto failed with error %d\n", WSAGetLastError()); closesocket(SendingSocket); WSACleanup(); return; } // When your application is finished sending datagrams close // the socket. printf("We successfully sent %d byte(s) to %s:%d.\n", Ret, inet_ntoa(ReceiverAddr.sin_addr), htons(ReceiverAddr.sin_port)); closesocket(SendingSocket); // When your application is finished call WSACleanup. WSACleanup(); }
1.7.3 基于消息的协议
正如面向连接的通信同时也是流协议,无连接通信几乎都是基于消息的。因此,在收发数据时,需要考虑这几点。首先,由于面向消息的协议保留了数据边界,所以提交给发送函数的数据在被发送完之前会形成阻塞。对非阻塞 I/0 模式而言,如果数据未能完全发送,发送函数就会返四WSAEWOULDBLOCK 错误。这意味着下层的系统不能对不完整的数据进行处理,应该稍后再次调用发送函数。第5 章将对此进行详述。需要记化的是,对丁基于消息的协议而言,写入操作只能作为一种自动行为发生。
在连接的另一端,对接收雨数的调用必须提供一个足够大的缓冲空间。如果提供的缓冲区不够大,接收调用就会失败,将出现借误 WSAEMSGSIZE。发生这种情况时,缓冲区会填满,但未接收完的数据会被丢弃。被截断的数据也无法恢复。唯 的例外是支持部分消息的协议,比方说 AppleTalkPAP协议。当接收调用仅接收到部分消息时,WSARecv、WSARecEx 或WSARecvFrom函数会在返网之前,将出入 flag 参数设为 MSG PARTIAL。
对以支持部分消息的协议为基础的数据报来说,可考虑使用某个 WSARecv 函数因为在调用 recy或recvfrom 时,不会有“读取的数据只是消息的-·部分”这样一个通知。至丁接收端怎样判断是否已卖取完整条消息,具体方法则由程序员决定。
随后的recy/recvrom 调用将返回这个数据报的其余部分由十有这个限制,利用 WSAReCvEx 函数就显得非常方便,因为它允许设置和读取 MSG PARTIAL标志,而 MSG PARTIAL 标志指明整条消息是否已读取完毕。
Winsock 2 函数 WSARecy 和WSARecvFrom 也支持这一标志。关于这个标志的更多内容,请参见对 WSARecv、WSARecvEx和WSARecvFrom 这3个函数的描述最后要讲的使是在有多个网络接口的机器发送 UDP/IP 消息。这方面的问题颇多,先来看个最常见的问题:在·个 UDP 套接字被显式地绑定到-·个地 P 接口,并发送数据报时,会发生什么情况?UDP 套接字并不会真正和网络接口绑定在“起,而是建立一种关联,即被绑定的[P 接口成为发出去的 UDP数据报的源]P 地址。真止决定数据报在哪个物接口上发送出去的,是路由表。如柴不调用 bind,而是先调用 sendto/WSASendTo 或执行连接,网络堆栈就会根据路由衣,自动选出最件本地 IP 地址。这意味着,如果先执行显式绑定,源IP 地址就可能误。也就是说,源 可能不是具正存它上面发送数据报的那个接口的 IP 地址。