通信网络程序的设计第6章TCPIP网络编程课件

上传人:公**** 文档编号:574284020 上传时间:2024-08-16 格式:PPT 页数:187 大小:582.03KB
返回 下载 相关 举报
通信网络程序的设计第6章TCPIP网络编程课件_第1页
第1页 / 共187页
通信网络程序的设计第6章TCPIP网络编程课件_第2页
第2页 / 共187页
通信网络程序的设计第6章TCPIP网络编程课件_第3页
第3页 / 共187页
通信网络程序的设计第6章TCPIP网络编程课件_第4页
第4页 / 共187页
通信网络程序的设计第6章TCPIP网络编程课件_第5页
第5页 / 共187页
点击查看更多>>
资源描述

《通信网络程序的设计第6章TCPIP网络编程课件》由会员分享,可在线阅读,更多相关《通信网络程序的设计第6章TCPIP网络编程课件(187页珍藏版)》请在金锄头文库上搜索。

1、1 1第6章 TCP/IP网络编程6.1 TCP/IP协议概述协议概述6.3 TCP编程编程6.4 UDP编程编程6.5 组播编程组播编程6.6 WinSock I/O模型模型小结小结2 2基于TCP/IP协议的网络程序是当前网络通信的主要方式,TCP/IP协议目前也处于鼎盛时期。随着TCP/IP由IPv4向IPv6过渡,可以预见,这种通信协议还会使用很长时间。因此,基于TCP/IP协议的编程方法也是本书所介绍的重点内容之一。本章首先简要地介绍TCP/IP协议的基本内容,然后介绍TCP/IP在Windows操作系统下网络编程的重要编程接口WinSock,基于WinSock进行TCP/IP传输层

2、两种通信方式(即TCP和UDP)的编程,接着介绍基于WinSock的TCP/IP组播编程。为了使网络编程更加具有灵活性,本章介绍了三种I/O控制方法。3 36.1.1 基本概念基本概念TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是一系列协议,或者说是一个协议族,它定义了数据传输如何通过因特网进行交换。TCP/IP起源于20世纪60年代末美国政府资助的一个分组交换网络研究项目,到20世纪90年代已发展成为计算机之间最常用的组网协议。6.1 TCP/IP协议概述协议概述4 4TCP/IP允许分布在各地安装着完

3、全不同系统的计算机互相通信,是一个真正的开放系统。TCP/IP是根据它最主要的两个协议命名的,已经实际应用了许多年,并在世界范围内证明了它的有效性。5 51协议栈结构协议栈结构TCP/IP模型进一步提炼与合并了OSI模型,它取消了OSI模型中的表示层和会话层,并合并了数据链路层和物理层(由于主要与连接有关并依赖于阐述介质,因此TCP/IP参考模型实际上对物理层并没有定义),使得逻辑更加简洁明晰,在此基础上逐步实现了各种子协议。TCP/IP模型与OSI模型的比较如图6-1所示。6 6TCP/IP模型每一层所负责的功能如下:链路层:有时被称做数据链路层或网络接口层,通常包括操作系统中的设备驱动程序

4、和计算机中对应的网络接口卡,它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。网络层:有时也被称为互连网层,负责分组在网络中的活动,包括IP(网际协议)、ICMP(Internet互联网控制报文协议)以及IGMP(Internet组管理协议)。这一层TCP/IP有两个基本组件:一个是IP协议,另一个是路由协议。7 7图6-1 TCP/IP模型与OSI模型的比较8 8传输层:该层主要为两台主机上的应用程序提供端到端的数据通信,它分为两个不同的协议,即TCP(传输控制协议)和UDP(用户数据报协议)。TCP提供端到端的保证质量的数据传输,该层负责数据的分组、质量控制和超时重发等,对于应用层来

5、说,就可以忽略这些工作。UDP则只简单地把数据报从一端发送到另一端,至于数据是否到达或按时到达、数据是否损坏,这都必须由应用层来做。这两种协议各有用途,前者可用于面向连接的应用,而后者则在及时性服务中有着重要的用途,如网络多媒体通信等。9 9应用层:该层负责处理实际的应用程序细节,包括Telnet、HTTP、SMTP、FTP、DNS和SNMP等协议和应用。层与层之间的联系与逻辑分离是利用封装与分用过程分别实现的。10102协议封装协议封装当应用程序传送数据时,数据按自上而下的方向被送入协议栈中,然后逐个通过每一层直到被当做一串比特流送入网络。其中,每一层对收到的上一层数据都要增加一些首部信息(

6、有时还要增加尾部信息),通过层层包裹完成数据的封装过程,使之适合网络传输,这相当于完成了1.1.2节提及的数字通信中的信道编码。以应用程序通过TCP协议传输数据为例,该过程如图6-2所示。1111图6-2 应用程序通过TCP协议传输数据封装过程1212用户数据首先被添加应用首部,传给TCP层;在TCP层,数据被再安装TCP首部后,传给IP层(TCP传给IP的数据单元称作TCP报文段或简称为TCP段);在IP层,数据被再次安装IP首部后,传给链路层(IP传给网络接口层的数据单元称作IP数据报);在链路层,数据再被安装以太网首部,并添加以太网尾部,形成适合网络(光、电信号)传输的数据比特流,该比特

7、流称做帧(Frame)。读者可能对首部和尾部的概念还不太了解。实际上,首部和尾部是一种协议指定的数据结构,按照一定的顺序规则填写数据。关于各种首部中的字段含义、用途将在后面章节进行详细介绍。13133协议分用协议分用网络接口分别发送和接收IP、ARP和RARP数据。IP层接口负责发送ICMP、IGMP、TCP、UDP数据包,因此必须在以太网的帧首部、IP特定字段中加入某种形式的标识,以指明生成数据的网络协议。例如:以太网的帧首部有一个16bit的帧类型域,当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议上加的报文首部。每层协议都要检查报文协议标识,以确定接收数

8、据的上层协议。这个过程称做分用(Demultiplexing),它解决了数据包接收时协议解析的问题,从而保证了各种不同的TCP/IP子协议能够被组合成一个整体(图6-3显示了该过程)。1414图6-3 TCP/IP协议分用过程15156.1.2 常用协议常用协议如前所述,TCP/IP协议是一个协议族,下面对常用的协议分别进行介绍。1MAC协议协议MAC(Media Access Control,媒体访问控制)协议最重要的功能是确定谁占有信道,即信道分配问题,其主要作用是保证信道的公平性和有效的资源共享。MAC的机制分为两类,即基于竞争的信道协议和无竞争的信道协议。基于竞争的信道协议是假定网络中

9、没有中心实体来分配信道资源,1616每个节点必须通过竞争媒体资源来进行传送,当超过一个节点同时尝试发送时,碰撞就会发生(见7.1.1节所介绍的CSMA/CD协议)。相反,无竞争的信道协议为每个需要通信的节点分配专用的信道资源。无竞争的信道协议能够有效地减少冲突,其代价是突发数据业务的信道利用率可能会比较低。不同的传输介质决定了所使用的MAC标准,如:以太网遵循IEEE 802.3标准,令牌总线遵循IEEE 802.4标准,令牌环网遵循IEEE 802.5标准,等等。本书重点关心的IEEE 802.3标准定义了一种具有七个字段的MAC帧,包括:1717前导符P、帧起始分界符SFD、目的地址DA、

10、源地址SA、表示数据字段字节数长度的字段LEN、要发送的数据字段、填充字段PAD和帧校验序列FCS等8个字段,这8个字段中除了数据字段和填充字段外,其余的长度都是固定的。图6-4就是以太网帧的结构,其首部由5个字段组成,包括前导符、起始帧分界符、目标地址、源地址、长度/类型构成;尾部由一个字段构成(CRC);上层协议的首部及数据夹在首部与尾部之间的数据字段里。1818图6-4 以太网帧结构1919这里使用的地址无论是DA还是SA,都是硬件地址,或称为MAC地址。MAC地址由网卡的生产厂商唯一设定给每一块不同的网卡。一块网卡依据数据帧的包头信息中是否写有它的MAC地址来决定是否接受并上传该帧。查

11、阅本机MAC地址的方法很多,如Windows的ipcongfig命令、NetBIOS的Astatus命令等。需要注意的是,IEEE 802.3标准的MAC帧不提供任何对收到的帧进行确认的机制,其通信确认在高层完成,这表明它是一种不可靠的介质。以太网MAC协议承载了其他TCP/IP上层子协议。20202IP协议协议IP协议负责在TCP/IP主机之间提供数据报服务,进行数据封装,产生协议头。由于在以太网中帧的大小受限制,并且不同的帧可能由不同的网络路径传送,因此IP协议需要将较大的数据报文分割,并在目的主机处按正确顺序组合。另外,IP协议不负责包的校验,它是一种无连接、不可靠的传输。如果发生任何错

12、误,IP协议则丢弃该数据报,然后发送ICMP消息报给信源端。数据报的检测校验是由上层协议如TCP等提供的。无连接数据报并不维护任何关于后续数据报的状态,每个数据报的处理是相互独立的,即IP数据报可以不按发送顺序接收。2121IP协议还需要负责寻找路由,因此它需要配一个确定的IP地址。在IP报文的包头中包含了源与目的的IP地址。一般来说,不会有应用程序直接访问IP协议。IP数据报是Internet上数据通信的基本单元,这些数据报不超过1000字节长,当人们打开Web页、下载文件或者发送E-mail时,这些数据报就在世界各地来回传输。IP协议包裹的协议有:ARP、RAPRP、ICMP、IGMP、路

13、由协议。2222网络互联的目的是提供一个无缝的通信系统,为此,互联网协议必须屏蔽物理网络的具体细节,并提供一个虚拟网络的功能,使设计者可以在不考虑物理硬件细节的情况下自由地选择地址。在TCP/IP栈中,编址由IP协议规定,IP标准分配给每台主机一个32位的二进制数作为该主机的IP地址。在2019年6月即将正式投入运行的IPv6中,IP地址升至128位,这样IP资源变得更加丰富。IP协议将每个IP地址分割成前缀和后缀两部分。前缀用于确定计算机从属的物理网络,后缀则用于确定网络上一台单独的计算机。互联网中的每一个物理网络都有一个唯一的值作为网络号(Network Number)。2323IP地址的

14、层次性设计保证了以下两个重要性质:每台计算机分配一个唯一的地址;网络号分配全球统一,但后缀可本地分配,无需全球统一。IP地址共分五类:A类、B类、C类、D类和E类。其中,A类、B类和C类为基本类;D类用于多播传送;E类属于保留类,现在不用。这种地址分配方法的优点是,通过判断从左到右第一个0出现的位置就可以区分地址类型。它们的格式如表6-1所示(其中,*代表网络号位数,X代表主机号位数)。2424表6-1 IP地址分类2525IP地址一般采用点分十进制的方法表示,例如10000001 00110100 00000110 00000000129.52.6.0。此外,需要特别注意以下几个特殊的IP地

15、址:(1) 网络地址:IP中主机地址为0,表示网络地址,如128.211.0.0。(2) 广播地址:网络号后跟一个所有位全是1的后缀,即直接广播地址。(3) 回送地址:127.0.0.1用于测试。2626(4) 内网地址:B类地址中的10.0.0.010.255.255.255、172.16.0.0172.31.255.255,C类地址中的192.168.0.0192.168.255.255等三个地址段内的IP习惯上经常作为内部网络地址使用。除了给每个主机分配一个IP地址外,IP协议也规定给每个路由器分配IP地址。事实上,每个路由器会被分配了两个或更多个IP地址。一个路由器连接到多个物理网络,

16、每一个IP地址包含一个特定物理网络的网络号。这个IP地址并不标识一台特定的计算机,而是标识一台计算机和一个网络间的一个连接。2727现在所有的主机都要求支持子网编址(RFC950,J.Mogul and J.Postel,1985),该功能要求,不仅要把IP地址看成由单纯的一个网络号和一个主机号组成,还要把主机号再分成一个子网号和主机号。这样做是因为A类和B类地址为主机号分配了太多空间,但事实上在一个网络中并不会有这么多主机,因此在NIC(Network Information Center)获得某个IP网络号后,就由系统管理员来决定是否建立子网,以及分配多少位给子网号和主机号。例如,这里有一

17、个B类地址(140.252.0.0),在剩下的16位中,8位用于子网号,8位用于主机号,其格式如图6-5所示。这样就允许有254个子网,每个子网可有254台主机。2828图6-5 B类地址的子网编址举例2929除了地址类型以外,主机还需要知道地址中分别有多少位用于子网号与主机号。这是在引导过程中由子网掩码所确定的。这个掩码是一个32位的值,其中值为1的位留给网络号和子网号,为0的位留给主机号。在上面的例子中,子网掩码就是255.255.255.0。通常规定,具有相同网络号的主机属于网内关系,不同网络号的主机属于网间关系。IP协议包裹或承载了ARP/RARP、ICMP、IGMP、OSPF等TCP

18、/IP子协议。30303ARP/RARP协议协议ARP(Address Resolution Protocol,地址解析协议)和RARP(Reverse Address Resolution Protocol,逆向地址解析协议)是某些网络接口(如以太网和令牌环网)使用的特殊协议,用来转换IP层和MAC层使用的地址。由于IP地址只对TCP/IP有效,MAC地址只对网络访问层有意义,因此分配给主机使用的IP地址和它固有的MAC地址是互不相干的。3131在物理网络上的数据帧交换依赖于MAC地址,而在网络层层面的IP地址赋予用户设定逻辑地址的权利,要使二者配合工作必须进行正确的转换。ARP实现了从IP

19、地址到MAC地址的映射,而RARP负责根据NIC硬件地址去查询对应的IP地址。ARP要求网络接口有一个硬件地址。在硬件上进行的数据帧交换必须要有正确的接口地址。TCP/IP的地址是32位的IP地址。仅知道主机的IP地址并不能让内核(如以太网驱动程序)发送数据帧给主机,内核必须知道目的端的硬件地址才能发送数据。3232假设在一个以太网中,客户端要将一个IP报文发送到服务器端,那么客户端就必须把32位的IP地址转换成48位的以太网地址。ARP获取IP的过程可分为以下三个步骤:(1) ARP以广播的方式发送ARP Request数据帧给以太网的每个主机。ARP请求数据帧中包含目的主机的IP地址,意思

20、是“如果你是这个IP地址的拥有者,请回答你的硬件地址”。(2) 目的主机的ARP层收到这份广播报文后,识别出这是发送端在询问它的IP地址,于是发送一个ARP应答。这个ARP应答包含IP地址及对应的硬件地址。3333(3) 发送端收到ARP应答后,主机间通过使用ARP协议获得的硬件地址进行通信。ARP中规定了两种信息的基本类型:请求(Request)和应答(Response)。在以太网上解析IP地址时,ARP请求和应答分组的格式如附录4中的附表4-6所示(ARP亦可用于解析其他类型网络的IP地址以外的地址,紧跟着帧类型字段的前四个字段决定了最后四个字段的类型和长度)。34344ICMP协议协议I

21、CMP(Internet Control Message Protocol)是Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着至关重要的作用。ICMP协议是一种面向非连接的协议,用于传输出错报告控制信息。它是一个非常重要的协议,对网络安全具有极其重要的意义。3535它是TCP/IP协议族的一个子协议,属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。当遇到IP数据无

22、法访问目标、IP路由器无法按当前的传输速度转发数据包等情况时,IP路由器会自动发送ICMP消息。ICMP提供一致易懂的出错报告信息。发送的出错报文返回到发送原数据的设备,因为只有发送设备才是出错报文的逻辑接受者。发送设备随后可根据ICMP报文确定发生错误的类型,并确定如何才能更好地重发失败的数据报。但是ICMP唯一的功能是报告问题而不是纠正错误,纠正错误的任务由发送方完成。36365IGMP协议协议Internet 组管理协议(IGMP)是因特网协议家族中的一个组播协议,用于 IP 主机向任一个直接相邻的路由器报告它们的组成员情况。IGMP信息封装在IP报文中,其IP的协议号为2,用来在IP主

23、机和与其直接相邻的组播路由器之间建立、维护组播组成员关系。IGMP不包括组播路由器之间的组成员关系信息的传播与维护,这部分工作由各组播路由协议完成。所有参与组播的主机必须实现IGMP。参与IP组播的主机可以在任意位置、任意时间、成员总数不受限制地加入或退出组播组。3737组播路由器不需要也不可能保存所有主机的成员关系,它只是通过IGMP协议了解每个接口连接的网段上是否存在某个组播组的接收者,即组成员。而主机方只需要保存自己加入了哪些组播组。IGMP在主机与路由器之间是不对称的:主机需要响应组播路由器的IGMP查询报文,即以IGMP membership report报文响应;路由器周期性发送成

24、员资格查询报文,然后根据收到的响应报文确定某个特定组在自己所在子网上是否有主机加入,并且当收到主机的退出组的报告时,发出特定组的查询报文(IGMP版本2),以确定某个特定组是否已无成员存在。38386路由协议路由协议当数据跨网传输时需要进行路由,通常路由包括两个基本的动作:确定最佳路径和让信息群(或称为分组)通过网络传输。通过网络传输分组相对较简单,而路径的确定却相对复杂,一般采用的方法就是查询路由表。路由表保存了通过路由器可能到达的目标网络以及如何到达该目标网络的信息,基本项包括目标网络地址、子网掩码以及到达目标网络的下一站路由器的地址,简介记录了网络间的位置关系。路由表由路由协议维护和更新

25、,此外,路由协议还完成发送路由更新信息且基于路由算法决定路由的功能,常见的路由协议有RIP、OSPF、BGP等。3939RIP协议使用V-D算法在局域网上实现,它将参加者分为主动机和被动机两种。主动机主动地向外广播路径刷新报文,被动机被动地接受路径刷新报文。一般情况下,网关作主动机,主机作被动机。RIP规定一条路径的距离为该路径(从信源机到信宿机)上的网关数。为防止寻径回路的长期存在,RIP规定,长度为16的路径为无限长路径,即不存在路径。所以一条有限的路径长度不得超过15。正是这一规定限制了RIP的使用范围,使RIP局限于小型的局域网中。4040OSPF(Open Shortest Path

26、 First)是一个内部网关协议(Interior Gateway Protocol,IGP),用于在单一自治系统(Autonomous System,AS)内决策路由,一个自治系统的经典定义是在一个管理机构控制之下的一组路由器。与RIP相对,OSPF是链路状态路由协议,而RIP是距离向量路由协议。链路是路由器接口的另一种说法,因此OSPF也称为接口状态路由协议。OSPF通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,每个OSPF路由器使用这些最短路径构造路由表。4141BGP(Border Gateway Protocol)是一种在自治系统之间动态交换路由信息的路由协议

27、,它使用IGP(内部网关协议)和普通度量值向其他自治系统转发报文。BGP中使用自治系统这个术语是为了强调这样一个事实:一个自治系统的管理对于其他自治系统而言是提供一个统一的内部选路计划,它为那些通过它可以到达的网络提供了一个一致的描述。三种协议分别采用不同的下层协议承载自己,即RIP使用UDP、OSPF使用IP、BGP使用TCP。42427TCP协议协议TCP(Transmission Control Protocol,传输控制协议)使用IP作为网络层协议。在网络通信传输机制中,它属于面向连接、可靠传输的类型。面向连接的传输意味着在进行通信以前,需要在两个系统之间建立逻辑连接,在每个数据传输的

28、过程中都需要进行应答以保证数据包的完整。这种方法需要的网络开销较大,可是数据传输的可靠性可以保证。虽然TCP使用不可靠的IP服务,但它却提供了一种可靠的传输层服务。TCP承载的高级应用与路由协议有HTTP、FTP、SMTP、BGP等。43438UDP协议协议UDP(User Datagram Protocol,用户数据报协议)属于面向无连接、不可靠传输的类型。该协议只负责接收和传送由上层协议传递的消息,它本身不做任何检测、修改与应答,上层协议需要自己处理这些事务。UDP的报头格式较简单,主要是地址信息、包的长度和校验信息。与此对应,TCP包的头信息有十多个域。因此UDP的网络开销一般要小于TC

29、P。由于UDP在传送数据过程中没有建立连接,亦不进行检查,因此在良好的网络环境中,其工作效率较TCP要高。4444由于UDP的这种特点,因此亦是进行网络广播的首选协议。UDP承载的高级应用与路由协议有DNS、SNMP及RIP等。45459应用协议应用协议应用协议是平时使用最广泛的协议,这层的每个协议都由两部分组成:客户程序和服务程序。程序通过服务器与客户机的交互来工作,如SNMP、FTP、SMTP、POP3等。应用协议被下层协议所承载,提供更加具体的应用服务,详见第12章。更多关于TCP/IP协议的介绍请参考相关书籍。46466.1.3 TCP/IP地址函数地址函数TCP/IP协议下的WinS

30、ock函数与5.2.3节介绍的函数基本一致(实现产生的区别见8.1节),特殊之处在于其地址编码。TCP/IP协议的地址编码遵循IP协议(参见6.1.2节),采用结构sockaddr_in实现。同时,为了解决不同情况下的地址格式转换与查询问题,还分别提供了一组地址转换函数和地址信息查询函数,下面分别进行介绍。47471地址结构地址结构TCP/IP协议采用一种不同于IPX/SPX和NetBIOS的逻辑地址编码方法,使用IP地址结构体sockaddr。TCP/IP协议WinSock的bind、connect、recvfrom、sendto等都用这个地址结构来指明地址信息,该结构的定义如下:struc

31、t sockaddr unsigned short sa_family; /地址协议族char sa_data14; /地址数据字符数组然而,一般在编程中,并不直接使用sockaddr结构,而是使用其等价结构sockaddr_in,定义如下:4848struct sockaddr_inshort sin_family;/地址协议族unsigned short sin_port;/IP端口struct in_addr sin_addr;/IP地址结构char sin_zero8 ;/填充,使总长度与sockaddr一致其中,in_addr结构体存放的IP地址可以用标准点分式字符串、无符号短整型、

32、无符号长整型等三种不同的方式描述。该结构是一个联合体(union),定义如下:struct in_addr union4949 struct unsigned char s_b1, s_b2,s_b3, s_b4;/标准点分式字符串 S_un_b; struct unsigned short s_w1, s_w2;/两个无符号短整型 S_un_w; unsigned long S_addr;/一个无符号长整型5050 S_un;51512地址转换地址转换在TCP/IP地址的使用中,不仅由于具有不同的字节顺序(网络字节顺序和主机字节顺序)需要转换,再加上地址本身的三种数据类型有时需要进行适当的转

33、换,因此WinSock提供了以下一组转换函数。(1) ntohl()函数:将一个u_long类型数(32位无符号整数)从TCP/IP网络字节顺序转换成主机字节顺序,原型如下:u_long WSAAPI ntohl(u_long netlong);/netlong是TCP/IP网络字节顺序表示的32位5252/无符号整数(2) ntohs()函数:将一个u_short类型数(16位无符号整数)从TCP/IP网络字节顺序转换成主机字节顺序,原型如下:u_short WSAAPI ntohs(u_short netshort);/netshort是TCP/IP网络字节顺序表示的16位/无符号整数53

34、53(3) htonl()函数:将一个u_long类型数(32位无符号整数)从主机字节顺序转换成TCP/IP网络字节顺序,原型如下:u_long WSAAPI htonl(u_long hostlong); /hostlong是指主机字节顺序表示的32位无符号整数(4) htons()函数:将一个u_short类型数(16位无符号整数)从主机字节顺序转换成TCP/IP网络字节顺序,原型如下:u_short WSAAPI htons(u_short hostshort);/hostshort是按主机字节顺序表示的16位无符号整数5454上述函数很容易记忆,h代表主机字节顺序,n代表网络字节顺序,

35、s代表无符号短整型,l代表无符号长整型,to代表转换目标。另外,还有两个重要的函数,即inet_addr()和inet_ntoa(),用于IP地址点分表示法的转换。(1) inet_addr()函数将一个用点分表示法表示的地址的字符串地址转换成网际地址in_addr形式,所有网际地址都以网络字节顺序(字节顺序从左到右)返回,原型如下:unsigned long WSAAPI inet_addr(const char FAR * cp);/cp是含有用点分表示法表示的5555/地址字符串(2) inet_ntoa()函数将一个网际地址转换成点分十进制表示法表示的字符串。它接收由参数in指定的网际

36、地址结构,返回以点分表示法表示的地址的ASCII字符串,原型如下:char FAR * WSAAPI inet_ntoa(struct in_addr in);/in表示主机网际地址结构56563地址查询地址查询TCP/IP还提供地址查询函数,可以在编程过程中根据需要获取主机信息。主机信息被放入一个hostent的结构体里。函数如下:struct hostent char FAR * h_name; /PC的官方名 char FAR * FAR * h_aliases; /PC的别名 short h_addrtype; /地址类型5757 short h_length; /地址长度 char

37、FAR * FAR * h_addr_list;/主机地址列表可以依据主机的地址得到该主机信息,函数如下:struct HOSTENT FAR * gethostbyaddr ( const char FAR * addr, /主机地址 int len,/地址长度 int type);/地址类型5858可以依据主机名得到该主机信息,函数如下:struct hostent FAR * gethostbyname (const char FAR * name);/指向主机名的指针 可以依据主机名得到扩展gethostbyname,函数如下:HANDLE WSAAsyncGetHostByName

38、(HWND hWnd,/接收异步请求的窗口句柄unsigned int wMsg, /接收该异步请求的消息const char FAR * name, /指向主机名的指针char FAR * buf, /接收信息的缓冲区指针int buflen );/缓冲区长度5959获得本地主机名的函数如下:int gethostname ( char FAR * name, /接收主机名的缓存区int namelen);/缓冲区长度上述部分函数需要PSDK(参见2.8.2节)的支持。6060TCP/IP协议中的面向连接服务是TCP协议提供的,本节对TCP编程进行介绍。在WinSock编程中,TCP协议编程

39、是用流套接字实现的。流套接字的服务进程和客户进程在通信前必须创建各自的套接字并建立连接,然后才能对相应的套接字进行读/写操作,实现数据的传输。6.3 TCP编程编程61616.3.1 TCP程序结构程序结构在TCP通信中主要有连接的建立、数据的传输、连接的关闭等三个主要过程(如图6-6所示)。每个过程完成不同的工作,而且TCP包的序列号和确认号在每个过程中的变化都是不同的。TCP建立连接也就是我们常说的三次握手,它需要三步完成。在 TCP 建立连接后,就可以开始传输数据了。TCP工作在全双工模式,它可以同时进行双向数据传输。以服务器向客户端发送数据为例,服务器向客户端发送一个数据包,客户端收到

40、这个数据包后,会向服务器发送一个确认数据包。TCP连接的关闭经历四次挥手的过程。6262图6-6 流套接字程序时序图6363下面是从套接字编程角度进行的服务端与客户端的函数过程,TCP通信的三个过程蕴含于其中。 建立套接字。用socket()函数完成。 将指定协议的套接字绑定到它已知的名字上,这个名字就是本地的IP地址端口号。这个过程通过bind()函数完成。 服务进程要处于监听状态,等待任意数量的客户端连接,以便为它们的请求提供服务。此服务进程必须在所绑定的名字上进行监听,所以要把套接字置为监听模式。通过listen()函数来实现。6464 服务进程调用函数accept()或WSAAccep

41、t()准备接收来自客户端的连接,如果一个客户端用connect()函数试图建立连接,服务进程就可以接受连接。 建立连接后,服务器和客户端之间就可以使用send()和recv()函数进行通信。注意,默认情况recv()函数处于阻塞模式,在接收到数据前,程序不向下执行。 通信结束后,调用closescoket()函数关闭套接字。注意:在此编程过程中,服务器必须首先启动,直到执行完accept()调用,进入等待状态后,方能接收客户请求。如客户在此之前启动,则connect()将返回出错代码,表示连接不成功。65656.3.2 TCP服务器端服务器端下列程序是一个面向连接的服务器端程序,能够侦听客户端

42、的连接请求。当使客户端发送字符串“list0”到“list5”,服务器端每接收到一个字符串就回应一个echo信号,例如:接收到“list4”,回应“echo list4”。#include stdafx.h#include winsock.h#include windows.h#include stdio.h6666#pragma comment(lib,wsock32.lib)#define RECV_PORT 3000SOCKET sock,sock1;sockaddr_in ServerAddr;sockaddr_in ClientAddr;int Addrlen;DWORD Start

43、Sock()WSADATA WSAData; 6767 if (WSAStartup(MAKEWORD(2,2),&WSAData)!=0) /初始化套接字printf(sock init fail !n);return(-1); return(1);DWORD CreateSocket()6868sock=socket(AF_INET,SOCK_STREAM,0); /创建套接字if (sock=SOCKET_ERROR)printf(sock create fail !n);WSACleanup();return(-1);ServerAddr.sin_family=AF_INET; 696

44、9 /填充服务器地址ServerAddr.sin_addr.s_addr=htonl(INADDR_ANY);ServerAddr.sin_port=htons(RECV_PORT);if (bind(sock,(struct sockaddr FAR*)&ServerAddr,sizeof(ServerAddr)=SOCKET_ERROR) /绑定套接字为创建的sock指定通信对象 printf(bind is the error); return(-1); return (1);7070DWORD ConnectProcess() char buff80;char buffecho80=e

45、cho ;int i,length;Addrlen=sizeof(sockaddr_in);if (listen( sock, 5 ) 0) 7171 printf(Listen error); return(-1); else printf(Listening.n);sock1 = accept( sock,(struct sockaddr FAR *) &ClientAddr,&Addrlen);printf(connect ok n);printf(wait for receive.n); for (i=0;i6;i+)7272 memset(buff,0,80); if (recv(s

46、ock1,buff,80,0)=0) return -1; else printf(recive %sn,buff); strncpy(buffecho+5,buff,5);/依次生成ehco list0、ehco list1、ehco list2、ehco list3、/ ehco list4、ehco list5字符串并发送 length=send(sock1,buffecho,strlen(buffecho),0); if (length=0)7373 printf(send data error !n); closesocket(sock); WSACleanup(); return(

47、-1); return(1);7474int main(int argc, char* argv)if (StartSock()=-1)return(-1);if (CreateSocket()=-1)return(-1);if (ConnectProcess()=-1)return(-1);closesocket(sock);WSACleanup();return 0;75756.3.3 TCP客户端客户端下面程序是6.3.2节程序对应的客户端程序,首先向服务端发出连接请求。连接建立后向服务端发送字符串“list0”到“list5”,之后接收来自服务端的响应。#include stdafx.

48、h#include winsock.h#include windows.h#include stdio.h#include #pragma comment(lib,wsock32.lib)7676#define SEND_PORT 3000SOCKET sock;sockaddr_in ServerAddr;DWORD CreateSocket() sock=socket(AF_INET,SOCK_STREAM,0); /创建套接字if (sock=SOCKET_ERROR)printf(sock create fail !n);7777WSACleanup();return(-1);retu

49、rn (1);DWORD CallServer() CreateSocket();if(connect(sock,(struct sockaddr*) &ServerAddr,sizeof( ServerAddr)=SOCKET_ERROR) 7878 printf(Connect fail n); closesocket( sock ); return(-1); return(1);DWORD TCPSend(char data)int length;length=send(sock,data,strlen(data),0);7979 if (length=0)printf(send dat

50、a error !n);closesocket(sock);WSACleanup();return(-1);return(1);8080DWORD TCPRecv(char data)int length;length=recv(sock,data,80,0); if (length=0)printf(send data error !n);closesocket(sock);WSACleanup();8181return(-1);return(1);DWORD StartSock()WSADATA WSAData; if (WSAStartup(MAKEWORD(2,2),&WSAData)

51、!=0)/初始化套接字8282printf(sock init fail !n);return(-1); ServerAddr.sin_family=AF_INET; /填充服务器地址ServerAddr.sin_addr.s_addr=inet_addr(127.0.0.1);ServerAddr.sin_port=htons(SEND_PORT);return(1);8383int main( )char buff80=list;char buffrecv80=;char num; int i;StartSock();while (CallServer()=-1);printf(conne

52、ct ok!n);8484 for (i=0;i6;i+) _itoa(i,&num,10); strncpy(buff+4,&num,1); printf(press any key to send %s!,buff); getchar(); TCPSend(buff);/发送list并接收echo ZeroMemory(buffrecv,80); TCPRecv(buffrecv);8585 printf(%sn,buffrecv); closesocket(sock);WSACleanup(); return(0);86866.3.4 TCP连接与断开连接与断开TCP连接与断开是其通信的

53、关键,TCP采用三次握手与四次挥手的方式来实现。1三次握手三次握手TCP提供的一个可靠连接的方式是通过三次握手(Three-way Handshake)来完成的。三次握手是指通信双方彼此交换三次信息。三次握手是指在存在包丢失、重复和延迟的情况下,确保通信双方信息交换确定性的充分必要条件。三次握手的操作过程如图6-7(a)所示。8787(1) 请求端(通常称为客户)发送一个TCP报文,并设置了SYN标志,指明客户打算连接的服务器的端口,以及初始序号(ISN)。这个TCP报文为报文段1。(2) 服务器发回一个设置了SYN标志和ACK标志的TCP报文(报文段2)作为应答,并将该报文中确认序号设置为客

54、户的ISN加1,并将ISN设置为服务器端初始序号,用以对客户的SYN报文段进行确认,一个SYN占用一个序号。(3) 客户开始向服务器发送数据,并设置ACK标志,将确认序号设置为服务器的ISN加1,用以对服务器的SYN报文段进行确认(报文段3)。8888图6-7 三次握手与四次挥手的过程8989在程序设计中,WinSock的listen、connect、accept配合完成该过程,三次握手在WinSock函数TCP编程中所处的位置如图6-8所示。上述三个过程的依次完成表明建立了TCP连接。9090图6-8 三次握手在Winsock函数TCP编程中所处的位置91912四次挥手四次挥手建立一个TCP

55、连接需要三次握手,而正常终止一个连接则要经过四次挥手,这是由TCP的半关闭(half-close)特性所造成的。由于TCP是全双工连接,每个方向的连接必须单独关闭,因此当一方完成数据发送任务后必须发送一个FIN标志来终止这个方向的连接。当一端收到一个FIN后,必须通知应用层另一端已经终止了该方向的数据传送。发送FIN通常是应用层关闭连接的结果。TCP连接收到一个FIN标志只意味着对方已不再发送数据,但己方仍能发送数据,这是半关闭型应用。正常关闭过程如图6-7(b)所示。9292(1) 通常情况下,一方完成主动关闭而另一方完成被动关闭(但也存在双方都执行主动关闭的特例)。首先进行关闭的一方(即发

56、送第一个FIN)执行主动关闭,而另一方(收到这个FIN)执行被动关闭。图6-8中的报文段1发起终止连接,TCP客户端发送一个FIN,用来关闭从客户到服务器的数据传送。(2) 当服务器收到这个FIN,它发回一个ACK,确认序号为收到的ISN加1(报文段2)。9393(3) 接着,这个服务器程序就关闭它的连接,TCP端发送一个FIN(报文段3),客户必须发回一个确认,并将确认序号设置为收到的ISN加1(报文段4)。四次挥手的WinSock函数在TCP编程中所处的位置如图6-9所示。9494图6-9 四次挥手的WinSock函数在TCP编程中所处的位置95953优雅地关闭连接优雅地关闭连接TCP连接

57、的关闭处于通信程序的末端,一般容易被程序设计者所忽视,认为直接调用shutdown()或closesocket()函数即可,这种做法通常过于武断和直接,会导致四次挥手没有全部完成,从而造成传输数据丢失、关闭不完善等问题。在这种关闭过程中,一方开始关闭通信会话,但另一方仍然可以读取线上或网络堆栈上已挂起的数据。因此必须提倡优雅地关闭TCP连接,这也是MSDN专门强调的问题。建议按照如下的方法关闭TCP连接9696(C代表客户,S代表服务器)优雅地关闭TCP连接:C:注册(用WSAAsyncSelect)FD_CLOSE事件(套接字起初创建之后就应当注册),调用shutdown,how=SD_SE

58、ND(实际上发了一个FIN包);S:注册FD_CLOSE事件(套接字起初创建之后就应当注册),当发现此事件发生后(C的shutdown和全部数据都处理这两个条件均满足才会引起的,参见MSDN中FD_CLOSE的解释),为了使FD_CLOSE可以顺利发生,则要调用recv直到返回值是0或SOCKET_ERROR,调用shutdown,how=SD_SEND(实际上发了一个FIN包),等上一会,调用closesocket;9797C:当发现FD_CLOSE事件发生后(S的shutdown和全部数据都处理这两个条件均满足才会引起的,参见MSDN中看FD_CLOSE的解释),为了使FD_CLOSE可以

59、顺利发生,则要调用recv直到返回值是0或SOCKET_ERROR,等上一会再调用closesocket。图6-10是利用网络嗅探器软件Wireshark抓到的TCP服务器(IP:192.168.100.200)与客户端(IP:192.168.100.15)之间的三次握手(包13)和四次挥手(包13)过程中的数据包。从图中包列表面板(packet list)的info字段可以清楚地看到SYN、ACK、FIN等标识,以及报文长度等信息,9898如果想得到一个数据包更加详细的信息,可以在包列表窗口选中该包,在包细节面板(packet details)里看到该数据包的各字段数据,或者在包字节面板(p

60、acket byte)里查看该数据包的十六进制数据。9999图6-10 三次握手与四次挥手过程100100数据报套接字是无连接的,其编程过程与流套接字编程类似,但是要比流套接字简单。6.4.1 UDP程序结构程序结构数据报套接字是无连接的,所使用的基本WinSock函数与流套接字相同,而数据传输函数则使用的是sendto()函数和recvfrom()函数。6.4 UDP编程编程101101图6-11 UDP通信时序图102102具体步骤如下(如图6-11所示): 服务器和客户端都要建立一个数据报套接字。 服务器调用bind()函数给套接字分配一个公认的端口,在开发应用程序时,这个公认端口通常是

61、指定的。客户端同样需要对套接字进行绑定(如果在套接字建立之后直接调用sendto()函数,该步骤可以省略)。 客户端和服务器都可以使用sendto()函数发送数据,使用recvfrom()函数接收数据,从而完成数据报传递。默认情况recvfrom()函数处于阻塞模式,在接收到数据前,程序不向下执行。103103 通信结束后,调用closescoket()关闭套接字。注意:无连接服务器也必须先启动,否则客户请求传不到服务进程。无连接客户不调用connect(),因此在数据发送之前,客户与服务器之间尚未建立完全相关,但各自通过socket()和bind()建立了半相关。发送数据时,发送方除指定本地

62、套接字号外,还需指定接收方套接字号,从而在数据收发过程中动态地建立了全相关。1041046.4.2 UDP服务器端服务器端本节展示的是用UDP实现点对点会话的一个实例程序。以下程序实现的是一个计算机对计算机的点对点会话。程序循环接收另外一台发送的消息(也可以增加发送功能)。实现原理是程序首先建立一个socket(),然后同一个本地地址与端口100绑定在一起。这样,如果对方计算机向这个地址上的这个端口发送数据,此计算机就可以循环接收数据了。105105#include #include #pragma comment(lib, wsock32.lib)DWORD StartSock() /略,同

63、6.3.2节int main()printf(初始化.n);StartSock();SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); /创建套接字106106if(s = INVALID_SOCKET)printf(Failed socket() n);return 0;sockaddr_in sin; /填充sockaddr_in结构sin.sin_family = AF_INET;sin.sin_addr.S_un.S_addr = INADDR_ANY;sin.sin_port = htons(100);107107if(bind(

64、s, (LPSOCKADDR)&sin, sizeof(sin) =SOCKET_ERROR) /绑定该套接字到一个本地地址printf(Failed bind() n);return 0;printf(开始数据接收.n); /接收数据char buff1024;108108sockaddr_in client;int nLen = sizeof(client);while(TRUE)int nRecv = recvfrom(s, buff, 1024, 0, (sockaddr*)&client, &nLen); if(nRecv 0)buffnRecv = 0;109109printf(接

65、收到数据(%s):%s,inet_ntoa(client.sin_addr), buff);closesocket(s);1101106.4.3 UDP客户端客户端此程序实现的是一个与上节程序相配合的客户端程序。程序向服务端发送一条消息(也可以增加接收功能)。实现原理是程序首先建立一个socket(),然后直接调用sendto()函数向服务器程序(假设地址为“24.84.60.228”、端口为100)发送信息。#include #include #pragma comment(lib, wsock32.lib)111111DWORD StartSock() /略,同6.3.2节int main

66、()printf(初始化.n);StartSock();SOCKET s = :socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); /创建套接字if(s = INVALID_SOCKET)112112printf(Failed socket() %d n, :WSAGetLastError();return 0;sockaddr_in addr; /填写远程地址信息addr.sin_family = AF_INET;addr.sin_port = htons(100);113113addr.sin_addr.S_un.S_addr = inet_addr(24.

67、84.60.228);/填写服务器程序所在机器的IP地址char szText = Msg from TCP Client! rn; /发送数据sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr);closesocket(s);return 0;114114如果上述程序在调用sendto()函数发送信息时,使用的是广播地址(见IP协议的6.1.2节的描述),则所有子网内的计算机都可以收到该信息。115115除了UDP套接字技术,TCP/IP程序设计还采用了多播技术。在聊天软件中,用户进行群聊时常常需要将某条信息发

68、送给一组用户。多播通信技术,不仅可以实现一个发送者和多个接收者之间进行通信的功能,还可以有效地减轻网络通信的负担,避免资源的无谓浪费。多播技术与NetBIOS中的数据报组播技术非常类似。6.5 组组 播播 编编 程程1161166.5.1 IGMP程序结构程序结构多播技术基于IGMP协议,其程序的结构非常简单,主要有加入多播组、收发多播数据、离开多播组等三个主要步骤。(1) 将套接字加入到多播组。加入多播组需要使用本地地址和多播地址两个地址,这里多播地址为IP地址中的任意D类地址,如224.0.0.8。为了方便加入多播组时setsockopt()函数的调用,使用如下一个ip_mreq结构变量:

69、struct ip_mreq 117117 struct in_addr imr_multiaddr; /* IP multicast address of group */ struct in_addr imr_interface; /* local IP address of interface */ ;接着调用setsockopt()函数设置套接字选项IP_ADD_MEMBERSHIP和要加入的ip_mreq变量即可,具体如下面例程所示:/加入多播组ip_mreqmcast;118118mcast.imr_interface.S_un.S_addr = INADDR_ANY;mcast.

70、imr_multiaddr.S_un.S_addr = inet_addr(224.0.0.8); setsockopt(s,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char*)&mcast,sizeof(mcast);(2) 收发多播数据时,对于接收没有特殊要求,只要在从加入到多播组的套接字上使用recvfrom()函数就可以了;向组发送数据的要求更宽松,只要向组地址和端口号发送数据就可以了(发送者甚至没有必要加入组)。119119(3) 离开多播组只要调用setsockopt()函数并设置套接字选项IP_DROP_MEMBERSHIP和要离开的ip_mreq变量即可,具

71、体如下面例程所示:setsockopt(s,IPPROTO_IP,IP_DROP_MEMBERSHIP,(char*)&mcast,sizeof(mcast);1201206.5.2 IGMP程序设计程序设计下面程序演示了将套接字加入地址为“224.0.0.8”的多播组,并接收组播数据后退出该多播组。#include #include #pragma comment(lib,wsock32.lib)DWORD StartSock() /略,同6.3.2节void main()printf(初始化.n);121121StartSock();/初始化WinSock库SOCKET s=socket(

72、AF_INET, SOCK_DGRAM, 0);BOOL bReuse = TRUE; /允许其他进程使用绑定的地址setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&bReuse, sizeof(BOOL);sockaddr_in si; /绑定到4800端口122122si.sin_family = AF_INET;si.sin_port = ntohs(4800);si.sin_addr.S_un.S_addr = INADDR_ANY;bind(s, (sockaddr*)&si, sizeof(si);ip_mreqmcast; /加入多播

73、组mcast.imr_interface.S_un.S_addr = INADDR_ANY;mcast.imr_multiaddr.S_un.S_addr = inet_addr(224.0.0.8);/多播地址为224.0.0.8123123setsockopt(s,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char*)&mcast,sizeof(mcast);printf(开始接收多播组224.0.0.8上的数据.n); /接收多播组数据char buf1280;int nAddrLen = sizeof(si);while(TRUE)int 124124nRet=rec

74、vfrom(s,buf,strlen(buf),0,(sockaddr*)&si,&nAddrLen);if(nRet!=SOCKET_ERROR)if(strcmp(buf,quit)break;elsebufnRet = 0;printf(buf);125125elseint n = WSAGetLastError();break;126126setsockopt(s,IPPROTO_IP,IP_DROP_MEMBERSHIP,(char*)&mcast,sizeof(mcast); /退出多播组WSACleanup();从多播组中接收数据时可以对发送数据的源地址进行选择,主要是通过在se

75、tsockopt()函数中使用套接字选项IP_ADD_SOURCE_MEMBERSHIP、IP_DROP_SOURCE_MEMBERSHIP、IP_ADD_MEMBERSHIP等来设置。127127WinSock套接字在非阻塞方式下工作,为了判明是否所需的网络事件发生了就需要重复调用一个函数,直至获得一个成功返回代码,所以程序要通过不断地检查函数返回代码以判断一个套接字何时可以进行读/写操作,这样做非常耗费系统资源。为了避免出现这种情况,WinSock提供了三种套接字I/O模型以对I/O操作进行管理,即select(选择)、WSAAsyncSelect(异步选择)和WSAEventSelect

76、(事件选择)模型。6.6 WinSock I/O模型模型1281286.6.1 select模型模型1select操作操作select模型是WinSock中最常见的I/O模型,它通过将感兴趣的套接字置入一个类型为fd_set的套接字集合中(通常是同一类动作),再使用一个select()函数来轮询观察WinSock的I/O状态。fd_set的结构如下:typedef struct fd_set u_int fd_ count; /集合中的套接字个数 129129 SOCKETfd_arrayFD_SETSIZE; /套接字集合 fd_set;WinSock提供的FD_SETSIZE变量用于确定一

77、个集合中最多的套接字描述字数目(FD_SETSIZE的缺省值为64,可在包含WinSock.h前用宏定义语句“#define FD_SETSIZE”来改变FD_SETSIZE的值)。130130对一个fd_set集合可以有四种操作,即初始化、添加、删除、查询,分别由以下四个宏完成(在WinSock2.h中定义):FD_ZERO (*set)/将set初始化为空集NULLFD_SET (s,*set)/向集合添加描述字FD_CLR (s,*set)/从集合set中删除描述字sFD_ISSET (s,*set)/若s为集合中一员,非零,否则为零这里,s为任意一个套接字。131131在为fd_set

78、填充套接字后,通过调用select()函数可以确定一个或多个套接字的状态,判断套接字上是否存在数据,或者能否向一个套接字写入数据。它既能防止应用程序(当套接字处于阻塞模式时,在I/O操作后被阻塞),同时也能防止在套接字处于非锁定模式中时,产生WSAEWOULDBLOCK错误。select()函数如下:int WSAAPI select( int nfds,132132fd_set FAR*readfds, /指向等待可读性检查的fd_set套接集合fd_set FAR*writefds, /指向等待可写性检查的fd_set套接集合fd_set FAR*exceptfds,/指向等待错误检查的f

79、d_set套接集合或带外数据const struct timeval FAR *timeout );133133其中:参数nfds指明被检查的套接字描述符的值域,一般被忽略;参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据;参数writefds指向要做写检测的套接字描述符集合的指针;参数exceptfds指向要检测是否出错的套接字描述符集合的指针;参数timeout指向select()等待的最大时间,如设为NULL,则为阻塞时间;参数timeout为timeval结构数据,若将超时值设置为(0,0),则select会立即返回,允许应用程序对select进行轮询,

80、处于对性能方面的考虑,应避免这样的设置。timeval结构的格式为:134134struct timeval long tv_sec;/以秒为单位指定等待时间long tvusec; /以毫秒为单位指定等待时间返回值:如果没有错误产生,select()返回包含在fd_set结构中已准备好的套接字描述符的总数目;若调用超过timeval设定的时间,则返回0;若调用失败,则返回SOCKET_ERROR。1351352select模型操作模型操作完成select操作一个或多个套接字句柄的典型步骤如下:(1) 使用FD_ZERO宏,初始化自己感兴趣的每个fd_set。(2) 使用FD_SET宏,将套接

81、字句柄分配给感兴趣的每个fd_set。(3) 调用select()函数,等待I/O活动设置好一个或多个套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。136136(4) 监测select的返回值,判断哪些套接字存在着尚未完成的I/O操作,具体的办法是使用FD_ISSET宏,并对每个fd_set集合进行检查。(5) 知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回第(1)步,继续进行select处理。1371373select模型应用模型应用下面实例演示了进行selcet模型I/O操作的实例。SOCKET s;

82、fd_setfdread; intret; timeval tv; tv_sec = 2; /让select等待两秒后返回 tv_usec=0; while(true) /管理套接字上138138的I/O操作 FD_ZERO( &fdread ); /初始化,在调用select之前清除读列表 FD_SET( s,&fdread ); /在读列表中添加套接字s ret = select(0,&fdread,NULL,NULL, & tv) /或用宏FD_ISSET if(ret!0) 139139/监测select的返回值 ;break; /发生读事件处理 else /未发生读事件处理由上述内容

83、可以看出select模型较为原始,且可监控的只有三种网络事件,因此在实际中使用较少。1401406.6.2 WSAAsyncSelect模型模型WSAAsyncSelect模型是WinSock中另一个常用的异步I/O模型。利用这个模型可在一个套接字上接收以Windows消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsyncSelect()函数自动将套接字设置为阻塞模式,并向WinSock dll注册一个或多个感兴趣的网络事件,并提供一个通知时使用的窗口句柄。当注册的网络事件发生时,对应的窗口将收到一个基于消息的通知。1411411Windows窗口窗口WSAAsyncSelect

84、必需要有一个窗口句柄接收网络事件,这是该模型的一个限制。为此,如果应用中没有窗口,就需要注册生成一个。下面代码演示了如何注册一个Windows窗口句柄。int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)142142MSG msg;HACCEL hAccelTable;char *szClassName = WndClass; WNDCLASSEX wndclass; wndclass.cbSize = sizeof(wndclass); wndclas

85、s.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0;143143 wndclass.cbWndExtra = 0; wndclass.hIcon = :LoadIcon(NULL,IDI_APPLICATION); wndclass.hCursor = :LoadCursor(NULL,IDC_ARROW); wndclass.hbrBackground = (HBRUSH):CreateSolidBrush(RGB(255,255,255);

86、 wndclass.hInstance = hInstance; wndclass.lpszClassName = szClassName;144144 wndclass.lpszMenuName = NULL; wndclass.hIconSm = NULL; :RegisterClassEx(&wndclass); HWND hWnd = :CreateWindowEx(0,szClassName,NULL,WS_POPUP|WS_SYSMENU|WS_SIZEBOX|WS_VSCROLL,100,100,300,300,0,0,145145hInstance,NULL);ShowWind

87、ow(hWnd, nCmdShow); UpdateWindow(hWnd);hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_GF);while (GetMessage(&msg, NULL, 0, 0) /主消息循环146146if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg) TranslateMessage(&msg);DispatchMessage(&msg);return msg.wParam;LRESULT CALLBACK WndProc(HWND hWnd,U

88、INT uMsg,WPARAM wParam,LPARAM lParam)147147 ; /略,可参考VC向导生成的Win32工程文件中的WindowProc函数上述代码是一个基于Win32主函数WinMain()的注册窗口程序以及窗口处理函数,读者可以隐约看出Windows的窗口消息处理过程,其主要过程包括:(1) 声明一个WNDCLASSEX窗口变量和一个MSG消息变量(MSG的成员变量wParam和lParam通常会放入一些关于消息状况的参量,要特别注意,具体的参数是由操作系统规定的,可以通过MSDN了解),然后给该变量的关键成员变量赋值;148148(2) 调用RegisterCla

89、ssEx()函数注册该窗口;(3) 利用注册的窗口类型调用CreateWindowEx()函数,创建一个窗口;(4) 利用ShowWindow()函数显示该窗口(也可以隐藏方式运行);(5) 进入消息处理循环(即程序中while循环部分),当系统对该窗口进行操作时,操作系统将操作产生的消息投递到WndProc()回调函数中。WSAAsyncSelect使用的就是上述程序的hWnd句柄接收网络事件。1491492WSAAsyncSelect()函数函数该模型使用WSAAsyncSelect()函数注册感兴趣的网络事件,函数的定义如下:int WSAAPI WSAAsyncSelect(SOCKE

90、T s,HWND hWnd,unsigned int wMsg,long lEvent);其中:参数s标识请求事件通知的套接字的描述符;参数hWnd标识网络事件发生时,接收消息的窗口的句柄;参数wMsg为网络事件发生时,窗口接收到的消息;参数lEvent为指定应用程序感兴趣的网络事件组合的位屏蔽(使用“|”符号连接),可由表6-2左侧关键字所列的值组成。150150表6-2 网络事件表151151返回值是:如果应用程序对感兴趣的网络事件集的注册登记成功,返回0;否则,返回SOCKET_ERROR。例如:我们要在套接字读准备好或写准备好时接到通知,语句如下:rc = WSAAsyncSelect

91、( s,hWnd,wMsg,FD_READ|FD_WRITE);如果我们需要注销对套接字网络事件的消息发送,只要将lEvent设置为0。1521523WSAAsyncSelect()的应用实例的应用实例下面演示了一个WSAAsyncSelect()的应用实例,主要过程有:注册窗口,建立套接字半相关,设置感兴趣消息,进行消息发生后的处理等几步。int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)/注册窗口类,处理消息为msg,窗口处理回调函数为WndPro

92、c;153153/建立TCP的服务端套接字s的半相关 /将套接字设为窗口通知消息类型:WSAAsyncSelect(s, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);:listen(s, 5); / 进入监听模式/进入窗口消息处理循环while(:GetMessage(&msg, NULL, 0, 0):TranslateMessage(&msg);:DispatchMessage(&msg);154154return msg.wParam;LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPA

93、RAM lParam)switch (uMsg)case WM_SOCKET:SOCKET s = wParam; 155155/取得有事件发生的套接字句柄(系统已将其放入wParam)if(WSAGETSELECTERROR(lParam)/查看是否出错(错误码系统已将其放入lParam):closesocket(s);return 0;156156switch(WSAGETSELECTEVENT(lParam) / 处理发生的事件case FD_ACCEPT:/监听中的套接字检测到有连接进入后将其交给工作套/接字,并对该套接字进行关于FD_READ、FD_WRITE、/FD_CLOSE三种

94、事件的监测,监测消息同样发送给/hWnd窗口157157SOCKET client = :accept(s, NULL, NULL);WSAAsyncSelect(client,hWnd,WM_SOCKET,FD_READ|FD_WRITE|FD_CLOSE);break;case FD_WRITE:158158printf(发生套接字数据写入!n);break;case FD_READ:printf(发生套接字数据读入!n );159159break;case FD_CLOSE: printf(发生套接字关闭!n );:closesocket(s);break;160160return 0;

95、case WM_DESTROY:PostQuitMessage(0) ;return 0 ;return :DefWindowProc(hWnd, uMsg, wParam, lParam); /将缺省的消息交给系统处理如窗/口的最大化、最小化之类的操作运用WSAAsyncSelect()实现TCP的关闭连接。1611616.6.3 WSAEventSelect模型模型WSAEventSelect模型是WinSock提供的另一个有用的异步I/O模型,与WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上接收以事件为基础的网络事件通知,并且它支持的网络事件与WSAAsy

96、ncSelect模型一样。它与WSAAsyncSelect模型的主要区别在于网络事件会被发送到一个事件对象句柄,而不是发送到一个窗口。1621621WSAEventSelect的相关函数的相关函数WSAEventSelect模型的运用需要用到5个主要函数,分别是:WSACreateEvent()、WSAResetEvent()、WSAEventSelect()、WSAWaitForMultipleEvents()、WSAEnumNetworkEvents(),下面分别进行介绍。1) WSACreateEvent()函数该函数创建事件对象来接收网络事件,其函数原型为WSAEVENT WSACre

97、ateEvent (void);163163其返回值是一个事件对象句柄,该事件有两种工作状态:已传信(signaled)和未传信(nonsignaled);两种工作模式:人工重设(manual reset)和自动重设(auto reset)。默认状态下,事件处于未传信的工作状态和人工重设模式。1641642) WSAResetEvent()函数由于事件对象创建后默认处于人工重设模式,因此在完成一个I/O请求的处理之后,应用程序需要调用WSAResetEvent()函数将事件对象的工作状态从“已传信”更改为“未传信”。WSAResetEvent()函数的原型为BOOL WSAResetEvent

98、 ( WSAEVENT hEvent);其中,参数hEvent是一个事件句柄,成功则返回TRUE,失败则返回FALSE。1651653) WSAEventSelect()函数该函数将创建的事件对象与某个套接字关联在一起,同时注册感兴趣的网络事件类型,使事件对象的工作状态从“未传信”转变成“已传信”,WSAEventSelect()函数的原型为int WSAEventSelect( SOCKET s, WSAEVENT hEventObject,long lNetworkEvents);其中,参数s为一个标识套接字的描述字。参数hEventObject用于指定与所提供的FD_XXX网络事件集合相

99、关的一个事件对象句柄。参数lNetworkEvents是一个屏蔽位,用于指定感兴趣的网络事件组合,同表6-2。166166这里要注意hEventObject、lNetworkEvents的区别:前者是用户声明,用于包裹某些感兴趣网络事件的“事件容器”;后者是系统预定义的网络事件,可以认为前者包裹后者。1671674) WSAWaitForMultipleEvents()函数套接字与事件对象句柄关联在一起后,应用程序就可以通过WSAWaitForMultipleEvents()函数等待网络事件来触发多个这样的事件句柄,然后进行I/O处理,其原型为DWORD WSAWaitForMultipleE

100、vents(DWORDcEvents,/等候事件的总数量const WSAEVENT FAR *lphEvents, /事件数组的指针BOOLfWaitAll,/是否等待所有事件传信168168DWORD dwTimeout,/超时时间BOOLfAlertable); /线程是否为alertable等待状态其中,参数lphEvents为指向一个事件对象句柄数组的指针,因此可以是多个事件。参数cEvents指出lphEvents所指向的数组中事件对象句柄的数目,事件对象句柄数组的最大值为WSA_MAXIMUM_WAIT_EVENTS。参数fWaitAll指定等待类型,若为TRUE,则当lphEv

101、ents数组中的所有事件对象同时有信号时,函数返回;若为FALSE,则当任意一个事件对象有信号时函数就返回。169169后一种情况的返回值指出是哪一个事件对象造成函数返回。通常把该参数设置为FALSE,一次只为一个套接字事件提供服务。参数dwTimeout指定超时等待时间(以毫秒计),当超时间隔到时,不论fWaitAll参数所指定的条件是否满足,函数即返回。如果dwTimeout为零,则函数测试指定的事件对象的状态,并立即返回。如果dwTimeout为WSA_INFINITE,则函数的超时间隔永远不会到。参数fAlertable指定当系统将一个I/O完成例程放入队列以供执行时,函数是否返回,若

102、为TRUE,则函数返回并执行完成例程;若为FALSE,函数不返回,不执行完成例程。170170返回值是:若函数成功,则指出造成函数返回的事件对象(WSAWaitForMultipleEvents()在等待的事件没有发生时处于阻塞状态,即无返回值,除非dwTimeout指定的时间已到或有事件发生);若函数失败,返回值为WSA_WAIT_FAILED。WSAWaitForMultipleEvents()的返回值就是lphEvents指针指向数组的索引值,表示该索引值指向的事件是导致WSAWaitForMultipleEvents返回的根源,也就是说,该索引值指向的事件发生了。借助下面代码可以解析该

103、事件:171171index = WSAWaitForMultipleEvents();MyEvent = lphEvents index - WSA_WAIT_EVENT_0 ;WSA_WAIT_EVENT_0是事件数组的起始位置,通常为0。当两个事件同时发生时,该返回值返回索引数小的一个。这种情况下,有可能另外一个被“淹没”,因此当事件发生时应当对返回值后面的事件进行遍历查询(详见例程)。1721725) WSAEnumNetworkEvents()函数由WSAEventSelect()函数可知,多个网络事件被包裹在一个WSAEVENT事件中,当被包裹的任何一个网络事件发生后,都会被WSA

104、WaitForMultipleEvents()捕捉到,就会产生问题,即:到底是该WSAEVENT中的哪一个网络事件发生了。这时,就需要调用WSAEnumNetworkEvents()函数,该函数了解发生的网络事件的类型。该函数原型为int WSAEnumNetworkEvents(SOCKET s,/套接字WSAEVENT hEventObject, /事件数组句柄173173LPWSANETWORKEVENTS lpNetworkEents); /结构体指针其中,参数s为标识套接字的描述字;参数hEventObject是可选参数,用于标识需要重设的相应事件对象;参数lpNetworkEven

105、ts是一个WSANETWORKEVENTS结构的数组,每一个元素记录了一个网络事件和相应的错误代码。执行WSAEnumNetworkEvents函数后,hEventObject 中包裹的网络事件及其错误码会一一对应地提交给lpNetworkEvents。WSANETWORKEVENTS结构的格式为174174typedef struct_WSANETWORKEVENTS long lNetworkEvents; /事件数组句柄 int iErrorCodesFD_MAX_EVENTS; /错误代码数组其中,参数lNetworkEvents用于指定套接字上发生的所有网络事件类型;参数iError

106、Codes指定的是一个错误代码数组,同lNetworkEvents中的事件关联在一起。通过对该参数的判断就可以知道hEventObject 中包裹的某网络事件的状态情况,从而解决上述网络事件的辨别问题。下述代码片断对此进行了阐释(针对FD_READ事件):175175/处理FD_READ通知事件 if(NetworkEvents.lNetworkEvents & FD_READ) if(NetworkEvents.iErrorCodeFD_READ_BIT!=0) printf(“FD_READ failed with error %dn”, NetworkEvent.iErrorCodeFD

107、_READ_BIT); else /处理网络事件FD_READ 176176针对每个网络事件类型,都存在着一个特殊的事件索引,名字与事件类型的名字类似,只是要在事件名字后添加一个“_BIT”后缀字串即可。如上述代码,对FD_READ事件类型来说,iErrorCodes数组的索引标识符便是FD_READ_BIT。1771772WSAEventSelect的使用步骤的使用步骤WSAEventSelect的使用步骤如下:(1) 创建事件对象并进行人工重设;(2) 注册感兴趣的网络事件;(3) 用WSAWaitForMultipleEvents()函数等待事件发生;(4) 当WSAWaitForMul

108、tipleEvents()有返回,则解析事件数组中的那一个事件;(5) 解析该事件中的哪一个网络事件发生了;(6) 进行事件处理;178178(7) 对处理后的事件进行重设并继续等待。用WSAEventSelect可以非常方便地管理多个WinSock上的I/O。1791793WSAEventSelect实例实例下面程序演示了WSAEventSelect对ACCEPT事件的I/O监控。 SOCKET SocketArrayWSA_MAXIMUM_WAIT_EVENTS; /SOCKET数组WSAEVENT EventArrayWSA_MAXIMUM_WAIT_EVENTS, NewEvent;/

109、事件数组180180WSANETWORKEVENTS NetworkEvents; /网络事件DWORD EventTotal = 0,Index = 0,i=0; /相应的创建和bindNewEvent = WSACreateEvent();WSAEventSelect(Listen,NewEvent,FD_ACCEPT | FD_CLOSE);listen(Listen, 5);SocketArrayEventTotal = Listen;181181EventArrayEventTotal = NewEvent;EventTotal +; While (TRUE) /等待所有SOCKET

110、上的网络事件Index = WSAWaitForMultipleEvents(EventTotal,EventArray,FALSE WSA_INFINITE, FALSE);Index = Index WSA_WAIT_EVENT_0; /解析发生网络事件的索引来遍历所有事182182/件,查看是否还有别的事件也发生了for(int i= Index; i WSA_WAIT_EVENTS) Printf(Too many connects); Closesocket(Accept);Break; 185185NewEvent = WSACreateEvent();WSAEventSelect

111、(Accept, NewEvent, FD_READ | FD_CLOSE); SocketArrayEventTotal = Accept;EventArrayEventTotal = NewEvent;EventTotal +;Printf(Socket %d Connected n,Accept);/end FD_ACCEPT186186 /end else /end for /end while 187187本章重点介绍了TCP/IP协议下的编程技术,是关于三大协议编程的第三部分,还涉及了TCP、UDP这两种传输层的编程技术;以及介绍了IGMP编程技术。为了使读者对I/O进行更好地控制,本章最后介绍了WinSock I/O模型。鉴于TCP/IP协议的重要性,基于TCP/IP协议的网络编程是目前最主要的编程方法,也是构建后续高级编程方法的基础。小小 结结

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 资格认证/考试 > 自考

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号