《TCPIP协议规范及UIP处理流程》由会员分享,可在线阅读,更多相关《TCPIP协议规范及UIP处理流程(31页珍藏版)》请在金锄头文库上搜索。
1、目录 简要历史 1973 年,ARPANET 核心组成员 Vint Cerf 和 Bob Kahn 发表了一篇里程碑论文,阐述了实现分组的端到端交付的协议。这篇关于传输控制协议(TCP)的论文包括:封装、数据报,以及网关的功能。 后来,TCP 被划分为两个协议:传输控制协议(TCP)和网际互联协议(IP) 。IP 处理数据报的路由选择,而 TCP 负责高层的一些功能,如分段、重装和差错检测。这个用来进行网际互联的协议后来就被称为TCP/IP。 TCP/IP 协议族 简介 TCP/IP 协议族由 5 层组成:物理层、数据链路层、网络层、运输层和应用层。前四层与 OSI 模型的前四层相对应, 提供
2、物理标准、网络接口、 网际互联、以及运输功能。 而应用层与 OSI 模型中最高的三层相对应。 TCP/IP 协议族中的各层包含了一些相对独立的协议。 在物理层和数据链路层, TCP/IP 并没有定义任何协议。在网络层 TCP/IP 支持网际互联协议(IP) ,而 IP 又由四个支撑协议组成:ARP、RARP、ICMP 和 IGMP。在传统上, TCP/IP 协议族在运输层有两个运输协议: TCP 和 UDP, 然而现在已经设计出一个新的运输层协议 SCTP以满足新的应用的需要。IP 是主机到主机的协议,即把分组从一个物理设备交付到另一个物理设备。UDP和 TCP 是运输机协议,负责把报文从一个
3、进程(运行着的程序)交付到另一个进程。 编址 使用 TCP/IP 协议的互联网使用 3 个等级的地址:物理(链路)地址、逻辑(IP)地址以及端口地址。每一种地址属于 TCP/IP 体系结构中的特定层。 物理地址 物理地址也叫链路地址,是结点的地址,由它所在的局域网或广域网定义。物理地址包含在数据链路层使用的帧中。 以太网的地址是 6 字节(48 位)长,通常用十六进制记法,如:07:01:02:01:2C:4B。以太网的地址共3 种:单播、多播和广播。在单播地址中的第一个字节的最低位 0;在多播地址中的第一个字节的最低位是 1。广播地址是 48 个 1。 逻辑地址 因特网的逻辑地址是 32 位
4、地址,可以用来标志连接在因特网上的每个主机。在因特网上没有两个主机有相同的 IP 地址。同样,逻辑地址也可以是单播地址、多播地址和广播地址。 Internet 被各种路由器和网关设备分隔成很多网段, 为了标识不同的网段, 需要把 32 位的 IP 地址划分成网络号和主机号两部分,网络号相同的各主机位于同一网段,相互间可以直接通信,网络号不同的主机之间通信则需要通过路由器转发。 把所有 IP 地址分为五类,如下图 1 所示: 图 2-1 A 类到 B 类到 C 类到 D 类到 E 类到 在分类编址的 A 类、B 类、C 类地址中,IP 地址可划分为 net-id(网络标识)和 host-id(主
5、机标识) 。对于A类地址, 1字节定义net-id而3字节定义host-id。 对于B类地址, 2字节定义net-id, 2字节定义host-id。对于 C 类地址,3 字节定义 net-id 而 1 字节定义 host-id。D 类地址和 E 类地址不划分 net-id 和 host-id。 网络地址是一个地址块的第一个地址,向因特网的其余部分定义这个网络。路由器就是根据网络地址来选择分组的路由。若给出网络地址,我们就能够找出这个地址的类别、地址块以及这个地址块的地址范围。 这种划分方案有很大的局限性,它对网络的划分是 flat 的而不是层级结构(hierarchical)的。Interne
6、t 上的每个路由器都必须掌握所有网络的信息,随着大量 C 类网络的出现,路由器需要检索的路由表越来越庞大,负担越来越重。 于是提出了新的划分方案,称为 CIDR(Classless Interdomain Routing) 。 网络号和主机号的划分需要用一个额外的子网掩码(subnet mask)来表示,而不能由 IP 地址本身的数值决定, 也就是说, 网络号和主机号的划分与这个 IP 地址是 A 类、 B 类还是 C 类无关, 因此称为 Classless的。这样,多个子网就可以汇总(summarize)成一个 Internet 上的网络。 IP 地址与子网掩码做与运算可以得到网络号,主机号
7、从全 0 到全 1 就是子网的地址范围。IP 地址和子网掩码还有一种更简洁的表示方法,例如/24,表示 IP 地址为,子网掩码的高 24 位是 1,也就是。 目的地址为,表示本网络内部广播,路由器不转发这样的广播数据包。 目的地址的主机号为全 1,表示广播至某个网络的所有主机,例如目的地址表示广播至网络(假设子网掩码为) 。 端口地址 计算机是多进程设备,即可以在同一时间运行多个进程。因特网通信的最终目的是使一个进程能够和另一个进程通信。为了能够同时发生这些事情,需要有一种方法对不同的进程打上标号,就是说这些进程需要地址。 在 TCP/IP 体系结构中,给一个进程指派的标号叫做端口地址。TCP
8、/IP 中的端口地址是 16 位长,通常用 10 进制数表示。 分层数据包介绍 以太网帧 图 2-2 目的地址(DA) DA 字段有 6 字节,是下一站的物理地址(也叫 MAC 地址) 。 源地址(SA) SA 字段有 6 字节,是前一站的物理地址。 类型 类型字段有三种值,分别对应 IP、ARP、RARP。 数据 携带从上层协议封装起来的数据。它的最小长度是 46 字节,最大长度是 1500 字节。ARP、RARP 的数据包长度不够 46 字节,要在后面补填充位。最大值 1500 称为以太网的最大传输单元(MTU) ,如果一个数据包从以太网路由到链路上,数据包的长度大于链路的 MTU 了,则
9、需要对数据包进行分片 CRC 差错检测信息,4 字节。 ARP 报文格式 图 2-3 如上图 3 所示,ARP 分组的格式如下: 硬件类型 16 位字段,用来定义运行 ARP 的链路层网络的类型。以太网是类型 1。 协议类型 16 位字段,指要转换的地址类型。0x0800 位 IP 地址。 硬件长度 8 位字段,定义以字节为单位的物理地址长度。对以太网这个值为 6。 协议长度 8 位字段,定义以字节为单位的逻辑地址长度。对 IPv4 协议这个值是 4。 操作 16 位字段,定义分组的类型。为 1 表示 ARP 请求,为 2 表示 ARP 应答。 发送端硬件地址 可变长度字段,定义发送端的物理地
10、址。 发送端协议地址 定义发送端的逻辑地址。 目标硬件地址 定义目标的物理地址。对于 ARP 请求报文,这个字段是全 0,因为发送端不知道目标的物理地址。 目标协议地址 定义目标的逻辑(如,IP)地址。 IP 数据报格式 图 2-4 如上图 4 所示,IP 数据报的结构包括: 版本(VER) 这个 4 位字段定义 IP 协议的版本。 首部长度(HLEN) 这个 4 位字段定义 IP 首部总长度,以 4 字节为单位计算。当没有选项时,首部长度是 20 字节,这个字段的值是 5(5*4=20) 。当选项字段位最大值时,这个字段的值是 15(15*4=60) 。 服务类型(DS) TOS 位是 4
11、位子字段,共有 5 种不同的服务类型。 总长度 这个 16 位字段定义了以字节计的数据报总长度(首部加上数据) 。要找出上层传来的数据长度,可以从总长度减去首部长度。总长度字段是 16 位,因此 IP数据报的长度限制是 65535(216 - 1)字节。 标识(Identification) 这个 16 位字段与源 IP 地址一起唯一地定义这个数据报。 IP 协议使用一个计数器来标志数据报,当 IP 协议发送数据时,就把这个计数器的当前值复制到标识字段中,并加 1。当数据报被分片时,标识字段的值就复制到所有的分片中。换言之,所有的分片具有相同的标识号,即原始数据报的标识号。在终点重装数据报时,
12、终点就知道所有具有相同标识号的分片必须组装成一个数据报。 标志(Flags) 3 位字段。第一位保留。第二位为不分片位,为 1 表示不对数据报进行分片;为0 表示在需要时对数据报进行分片。第三位为分片位,为 1 表示这个数据报不是最后的分片,在其后还有分片;为 0 表示这个数据报是最后的分片。 分片偏移(Fragment Offset) 这个 13 位字段表示该分片在整个数据报中的相对位置, 以 8 字节为度量单位。 生存时间(TTL) 用来控制数据报所通过的最大路由跳数,这个生存时间的单位不是秒,而是跳(hop) 。 协议 这个 8 位字段定义使用 IP 层服务的高层协议。 如: TCP、
13、UDP、 ICMP 和 IGMP 等。 检验和 IP 分组中的检验和只在首部而不在数据部分进行。因为,所有将数据封装在 IP数据报中的高层协议, 都有覆盖整个分组的检验和; 其次, , 每经过一个路由器,IP 数据报的首部就要改变一次,但数据部分不变。因此检验和只对发生变化的部分进行检验。 源地址 这个 32 位字段定义源点的 IP 地址。在 IP 数据报从源主机发送到目的主机的时间内,这个字段必须保持不变。 目的地址 这个 32 位字段定义了终点的 IP 地址。在 IP 数据报从源主机发送到目的主机的时间内,这个字段必须保持不变。 ICMP 报文格式 类型 8 位字段,定义 ICMP 报文的
14、类型。ICMP 报文的类型有:终点不可达、源点抑制、超时、参数问题、改变路由、回送请求或回答、时间戳请求或回答、地址掩码请求或回答、路由器询问和通告。 代码 8 位字段,指明了发送这个特定报文类型的原因。 检验和(icmpchksum) 16 位字段。在 ICMP 中,检验和的计算覆盖了整个报文(首部和数据) 。 ICMP 回送请求或回答报文头格式如下图 5 所示: 图 2-5 ICMP 终点不可达报文头格式如下图 6 所示: 图 2-6 ICMP 超时报文头格式如下图 7 所示: 图 2-7 IGMP 报文格式 图 2-8 类型 8 位字段, 定义了查询、 成员关系报告、 退出报告三种报文类
15、型, 类型值分别为 0x11、0x16、0x17。 最大响应时间 8 位字段,定义了查询必须在多长时间内回答。它的值以十分之一秒位单位。在查询报文中这个值不是零,但在其他两种报文中则置为零。 检验和 16 位字段,检验和在 8 字节的报文上计算。 组地址 在一般查询报文中这个字段的值为 0,在特殊查询报文、成员关系报告报文以及退出报告报文中定义 groupid(组多播地址)。 UDP 用户数据报首部格式 图 2-9 UDP 数据报格式如上图 9 所示。 用户数据报有 8 个字节的固定首部。 源端口号 16 位字段,定义源主机上运行的进程所使用的端口号。 目的端口号 16 位字段,定义目的主机上
16、运行的进程使用的端口号。 长度 16 位字段,定义了用户数据报的总长度,首部加上数据。 检验和 16 位字段,UDP 的检验和包括三部分:伪首部、UDP 首部以及从应用层来的数据。位首部是 IP 分组的首部的一部分,包括:源 IP 地址、目的 IP 地址、8 位协议和 16位 UDP 总长度。位首部可以保证在 IP 首部受到损伤时,用户数据报可以交付到正确的主机。协议字段的加入,可以确保这个分组是属于 UDP 而不是属于 TCP。 TCP 报文段格式 图 2-10 如上图 10 所示,TCP 报文段的结构包括: 源端口地址 这个 16 位字段定义发送报文段的应用程序端口号。 目的端口地址 这个
17、 16 位字段定义了接收该报文段的应用程序端口号。 序号 这个 32 位字段定义了指派给本报文段第一个数据字节的一个号。为了保证连通性,要发送的每一个字节都要编号。序号告诉终点,这个序列中的哪一个字节是报文段中的第一个字节。在连接建立时,每一方使用随机数产生器产生初始序号(ISN) 。 确认号 32 位字段,定义了报文段接收端期望从对方接收的下一个序号。如果报文段的接收端成功地发送了对方发来的序号 x,它就把确认号定义为 x+1。 首部长度(tcpoffset) 4 位字段, 指出 TCP 首部共有多少个 4 字节字。 即 TCP 数据在 IP 数据中的偏移大小。同 IP 首部长度,可以在 5
18、 至 15 之间。 保留位 该 6 位字段留待今后使用。 控制/标志位 该字段定义了 6 种不同的控制位或标志,在同一时间可设置一位或多位标志。 表 2-1 控制字段各标志说明(从高位到低位) 紧急指针字段值有效 确认字段值有效 推送数据 连接必须复位 在连接建立时对序号进行同步 终止连接 窗口值 该字段定义接收方必须维持的窗口值(以字节为单位) 。注意,该字段是 16 位长,因此窗口值的最大长度为 65535 字节。这个值由接收端来确定,发送端必须服从接收端的决定。 检验和 这个 16 位字段包含检验和,TCP 使用检验和是强制性的。 紧急指针 当紧急标志位置位时,这个 16 位字段才有效,
19、这时的报文段中包括紧急数据。紧急指针定义了一个数,把这个数加到序号上就得出报文段数据部分中最后一个紧急字节。 选项 包括无操作(NOP) 、最大报文段长度(MSS) 、窗口扩大因子、时间戳等。 分层协议讲解 总的来说,TCP/IP 协议的多路选择过程可以表示为下图 2-11: 图 2-11 ARP 和 RARP 地址解析协议 ARP 在任何时候, 当主机或路由器有数据报要发送给另一个主机或路由器时, 它必须有接收端的逻辑 (IP)地址。但是 IP 数据报必须封装成帧才能通过物理网络。这就表示,发送端必须有接收端的物理地址,因此需要有从逻辑地址到物理地址的映射。 地址解析协议(ARP)用来把 I
20、P 地址与其物理地址联系起来。任何时候当主机或路由器需要找出这个网络上的另一个主机或路由器的物理地址时,它就发送 ARP 查询分组。这个分组包括发送端的物理地址和IP 地址,以及接收端的 IP 地址。 因为发送端不知道接收端的物理地址, 查询就在网络上广播。 例如, 数据包要发送给 IP 地址为的主机,过程如下: 源主机发出 ARP 请求,询问“IP 地址是的主机的硬件地址是多少” ,并将这个请求广播到本地网段(以太网帧首部的硬件地址填 FF:FF:FF:FF:FF:FF 表示广播) ,目的主机接收到广播的 ARP 请求,发现其中的 IP 地址与本机相符,则发送一个 ARP 应答数据包给源主机
21、,将自己的硬件地址填写在应答包中。 ARP 报文格式如前所述。 ARP 软件包由 5 个构件组成: 高速缓存表: 每台主机都维护一个 ARP 高速缓存表,由于高速缓存表的空间非常有限,所以缓存表中的表项有过期时间(一般为 20 分钟) ,如果 20 分钟内没有再次使用某个表项,则该表项失效,下次还要发 ARP 请求来获得目的主机的硬件地址。 队列: 队列用来在 ARP 试图解析硬件地址时保留 IP 分组。输出模块把未解析的分组发送到相应的队列,输入模块从一个队列中拿走一个分组,并连同解析出的物理地址一同发送给数据链路层来传输。 输出模块: 输出模块从 IP 软件等待 IP 分组。输出模块检查高
22、速缓存表,寻找是否有某个项目对应于这个分组的目的 IP 地址。这个 IP 分组的目的 IP 地址必须与这个项目的协议地址相匹配。 输入模块: 输入模块一直等待,直到有 ARP 分组到达。检查高速缓存表,寻找对应这个 ARP 分组的项目。输入模块设置这个项目的超时时间 TIME-OUT。若队列为空,则从相应队列中把分组一个接一个地取出,连同其硬件地址一起交给数据链路层来处理。 高速缓存控制模块: 负责维护高速缓存表,它周期性地逐项检查高速缓存表,判断有哪些项目到期,哪些队列需要撤销。 逆地址解析协议 RARP 当一个主机知道自己的物理地址时,RARP 可用来找出其逻辑地址。每一个主机或路由器都被
23、指派一个或多个逻辑地址,这些地址与机器的物理地址无关。要创建 IP 数据报,主机或路由器要知道它自己的 IP地址。可以使用 RARP 协议从物理地址得到逻辑地址。 知道物理地址后,先创建 RARP 请求,并在本地网络上广播。在本地网络上的另一个机器知道所有的IP 地址,它就用 RARP 回答来响应。请求的机器必须运行 RARP 客户程序;而响应的机器必须运行 RARP 服务器程序。 IP 协议 IP 数据报的格式如前所述。 IP 是不可靠的无连接协议,负责源点到终点的交付。 在 IP 层的分组叫做数据报。 数据链路层有自己的帧格式,在这个格式中有一个字段是“数据字段最大长度” 。当数据报封装成
24、帧时,数据报的总长度必须小于这个数据字段最大长度(MTU)。 对数据报进行分割,叫做分片。源站通常不对 IP 分组进行分片。运输层会进行分片工作,把数据划分成 IP 和在使用的数据链路层都可能接纳的大小。数据报在到达终点之前可以经过多次分片,可以被源主机或在其路径上任何路由器进行分片。然而数据报的重组却只能在目的主机上进行。 在 IP 分组中的检验和只在首部而不在数据部分心进行。因为,首先所有将数据封装在 IP 数据报中的高层协议,都有覆盖整个分组的检验和;其次,每经过一个路由器,IP 数据报的首部就要改变一次,但数据部分不变。因此检验和只对发生变化的部分进行检验。 IP 软件包包括 8 个构
25、件:首部添加模块、处理模块、转发模块、分片模块、重装模块、路由表、MTU表以及重装表,还有输入和输出队列。 首部添加模块,从高层协议接收数据(连同其 IP 地址) ,添加 IP 首部后,把数据封装成 IP 数据报。 处理模块,从一个接口或从首部添加模块接收数据报,首先检查数据报是否为回环地址,还是这个分组已到达最后终点。 输入队列把从数据链路层或从高层协议发来的数据存放起来。 输出队列把要发送到数据链路层或高层协议的数据报存放起来,处理模块从中取出数据报,分片和重装模块则把这个数据报加入输出队列中。 路由表是在转发模块中使用的,用来确定分组的下一跳地址。 分片模块从转发模块接收 IP 数据报。
26、转发模块给出 IP 数据报、下一站的 IP 地址。以及发送这个数据报所必须通过的接口号。分片模块使用 MTU 表以便找出对于特定接口的最大传送单元 MTU。若数据报的长度大于 MTU,则分片模块对数据报进行分片,为每一个分片添加首部,并把它们发送到 ARP 软件包进行地址解析和交付。 重装模块从处理模块接收已到达最终目的地的数据报分片。重装模块将未分片的数据报看成是属于仅有一个分片的数据报。使用重装表找出一个分片是属于哪一个数据报,将属于同一个数据报的各分片进行排序,并在所有分片到达时把它们重新组装成一个数据报。 ICMP 协议 IP 协议没有差错报告或差错纠正机制和管理查询机制。网际控制报文
27、协议(ICMP)就是为了补偿这两个缺点而设计的。它是配合 IP 协议使用的。 ICMP 本身是网络层协议,但是它的报文不是如设想的那样直接传送给数据链路层,而是首先要封装成 IP 数据报,再传送给下一层。 在 IP 数据报中的协议字段值是 1 就表示其 IP 数据是 ICMP 报文。 ICMP 报文类型如下表 2-2 所示: 表 2-2 ICMP 报文类型 ICMP 报文分为两大类:差错报告报文和查询报文。报文格式如前所述。 差错报告报文 差错报告报文报告当路由器或主机在处理 IP 数据报时可能遇到的一些问题。ICMP 不能纠错,只能报告差错,差错纠正留给高层协议去做。 ICMP 总是使用源
28、IP 地址把差错报文发送给数据报的源点。 一共有 5 种差错可处理:终点不可达、源点抑制、超时、参数问题以及改变路由。 终点不可达报文 当路由器不能够给数据报找到路由或主机不能够交付数据报时,就丢弃这个数据报,然后这个路由器或主机就向发出这个数据报的源主机发回终点不可达报文。 源点抑制 ICMP 的源点抑制报文就是为了给 IP 增加一种流量控制而设计的。 当路由器或主机因拥塞而丢弃数据报时,它就向数据报的发送端发送源点抑制报文。目的有二:第一,通知源点,数据报已被丢弃。第二,它警告源点,在路径中的某处出现了拥塞,因而源点需放慢发送过程。注意,必须为每一个丢弃的数据报向源点发送源点抑制报文。 超
29、时 超时有两种情况:第一,当路由器接收到生存时间字段值为零的数据报时,就丢弃这个数据报,并向源点发送超时报文;第二,当最后的终点在规定时间内没有收到所有的分片时,就丢弃已收到的分片,并向源点发送超时报文。 参数问题 如果路由器或主机在数据报的首部中发现任何二义性,或在数据报的某个字段中缺少了某个值,就丢弃这个数据报,并发送参数问题报文。 改变路由 路由器的路由选择是动态的, 而主机为了提高效率, 通常使用静态路由选择。 当主机开始连网工作时,其路由表中的项目数很有限。它通常只知道默认路由器这一个路由器的 IP 地址,因此主机有可能会把某个数据报发送给一个错误的路由器。此时,收到这个数据报的路由
30、器会把数据报转发给正确的路由器,并向主机发送改变路由报文,以更新主机中的路由表。 查询报文 查询报文都是成对出现的。 在这种类型的 ICMP 报文中,一个结点发送报文,然后由目的结点用特定的格式进行回答。 回送请求和回答报文 为诊断目的而设计的。 主机或路由器可以发送回送请求报文给另一个主机或路由器。收到回送请求报文的主机或路由器产生回送回答报文,并将其返回给原来的发送者。 回送请求和回答报文可用来确定是否在 IP 这级能够通信。 还可由主机使用,以检查另一个主机是否可达。在用户级,调用分组因特网搜寻器(ping)命令可做到这点。 时间戳请求和回答 两个机器可使用时间戳请求和回答来确定 IP
31、数据报在这两个机器之间来往所需的往返时间。 地址掩码请求和回答 主机通过向局域网上的路由器发送地址掩码请求报文来获得自己的掩码。若主机知道这个路由器的地址,则直接将请求发送给该路由器,若主机不知道,则广播这个请求报文。 路由器收到地址掩码请求报文,就以地址掩码回答报文进行响应,向主机提供所需的掩码。 路由询问和通告 主机若想把数据发送给另一个网络上的主机,就需要知道连接到该网络上的路由器的地址。此外,这个主机还需要知道这些路由器是否正常工作。就可以通告路由询问和通告报文。 主机把路由器询问报文进行广播,收到询问的路由器就使用路由通告报文广播其路由选择信息。路由器发送通告报文时,不仅通告自己的存
32、在,而且通告了它所知道的所有在这个网络上的路由器。 在 ICMP 中,检验和的计算覆盖了整个报文(首部和数据) 。 网际组管理协议(IGMP) 网际组管理协议(IGMP)是与多播有关的一个必要的但不是充分的协议。IGMP 并不是多播路由选择协议,而是个管理组成员关系的协议。每当主机需要加入或离开某个特定的多播群组时,该协议允许该主机去通知邻近的路由器。 该协议只用在主机与路由器之间的网络上。 而且, 协议只把计算机 (不是应用进程) 定义为群组成员。 如果在一个给定计算机上有多个进程要加入到一个多播群组,计算机必须要把接收到的每个数据报复制多个副本给每个进程。只有当最后一个进程离开群组时,计算
33、机才利用 IGMP 通知本地的路由器,表明它不再是群组的成员了。IGMPv2 有 3 种报文类型:查询、成员关系报告和退出报告。 IGMP 可分为两个阶段: 第一阶段:当某个主机加入新的多播组时,该主机应向组播组的多播地址发送一个 IGMP 报文,声明自己要成为该组的成员。本地的多播路由器收到 IGMP 报文后,将组成员关系转发给因特网上的其他多播路由器。 第二阶段:因为组成员关系是动态的,因此本地多播路由器要周期性地探询本地局域网上的主机,以便知道这些主机是否还连续是组的成员。只要对某个组有一个主机响应,那么多播路由器就认为这个组是活跃的。但一个组在经过多次的探询后仍然没有一个主机响应,则多
34、播路由器就认为本网络上的主机已经都离开这个组了因此就不再将该组的成员关系转发给其他的多播路由器。 IGMP 报文格式如前所述。 IGMP 协议的优点: 主机和多播路由器的所有通信使用 IP 多播,只要有可能,携带 IGMP 报文的数据报都使用硬件多播来传送。 多播路由器在探询组成员关系时,只需要对所有多播组只发一个查询,而不是对每一个组发送一个查询,默认 125S 一次。 用户数据报(UDP) UDP 数据报的格式如前所述。 UDP 位于应用层和 IP 层之间,作为应用程序和网络操作的中介物。 IP 是负责在计算机级的通信 (主机到主机的通信) , 作为网络层协议, IP 只能把报文交付给目的
35、主机。但是,这是一种不完整的交付。这个报文还必须送交到正确的进程。UDP 就是负责把报文交付给适当的进程。 完成进程到进程的通信最常用的方法是通过客户- 服务器范例。 在本地主机上叫做客户的进程主动发起请求, 远程主机上叫做服务器的进程被动地等待、接收和应答请求。客户端的 IP 地址和端口号唯一标识了该主机上的客户端进程,服务器的 IP 地址和端口号唯一标识了该主机上的服务端进。由于客户端是主动发起请求的一方,它必须知道服务器的 IP 地址和服务进程的端口号,所以,一些常见的网络协议有默认的服务器端口。 TCP/IP 协议族中,端口号是在 065535 之间的整数。ICANN 把端口号划分为
36、3 个范围:熟知端口号、注册端口号和动态(或专用)端口号。 熟知端口范围从 01023;注册端口范围从 102449151;动态端口范围从 4915265535. 已知 UDP 需要两个标识符,即 IP 地址和端口号,各用在一端以建立一条连接。一个 IP 地址和一个端口号合起来叫做套接字地址。这些信息是 IP 首部和 UDP 首部的一部分。 UDP 提供物连接服务, 即 UDP 发出的每一个用户数据报都是独立的数据报, 每一个用户数据报可以走不同的路径到达目的进行。UDP 缺少流量控制和差错控制。 要从一个进程把报文发送到另一个进程,UDP 协议就要把报文进行封装和拆装。 封装 当进程有报文要
37、通过 UDP 发送时,它就把这个报文连同一对套接字地址以及数据的长度传递给 UDP,加上 UDP 首部后,UDP 把用户数据报连同套接字地址一起传递给 IP。IP 加上自己的首部,在协议字段使用值 17,指出该数据是从 UDP 协议来的。再将 IP 数据报传递给数据链路层,数据链路层收到 IP 数据报后,再加上自己的首部传递给物理层。物理层将这些位编码为电信号或光信号,把它发送到远程机器。 拆装 报文到达目的主机时,物理层对信号解码,将它变为位,传递给数据链路层。数据链路层使用这个首部(和尾部)检查数据。若无差错,则去掉首部和尾部,并把数据报传递给 IP。IP 软件进行检查,若无差错,就剥去首
38、部,把用户数据报连同发送端和接收端的 IP 地址一起传递给 UDP。UDP 使用检验和对整个用户数据报进行检查。若无差错则剥去首部,把应用数据传递给接收进程。在需要回答收到的报文时,应把发送端的套接字地址一起传递给接收进程。 UDP 软件包共包括 5 个构件:一个控制块表、若干个输入队列、一个控制块模块、一个输入模块和一个输出模块。在 UDP 中,队列是与端口相关联在一起的。这里的实现只创建与每一个进程相关联的输入队列,而不创建输出队列。 控制块表 UDP 控制块表来记录打开的端口。表中的每一个项目有最小的 4 个字段:状态(FREE 或 IN-USE) 、进程 ID、端口号以及相应的队列号。
39、 输入队列 使用了一组输入队列,每一个对应于一个进程。 控制块模块 负责管理控制块表。当进程启动时,它就从操作系统请求得到一个端口号。操作系统把熟知端口号指派给服务器,而把短暂端口号指派给客户。进程把进程 ID 和端口号传递给控制块模块,以便在表中为这个进程创建一个项目。这个模块不创建队列。队列数字段值为零。 输入模块 输入模块从 IP 接收用户数据报。它查找控制块表,查找具有和这个用户数据报同样端口号的项目。若找到这样的项目,模块就利用这项目中的信息把这个数据放入队列。若未找到这样的项目,它就产生 ICMP“端口不可达”报文,并丢弃这个项目。 输出模块 负责创建和发送用户数据报。 传输控制协
40、议(TCP) TCP 叫做面向连接的、可靠的运输协议。它提供进程到进程、全双工和面向连接的服务。TCP 使用滑动窗口机制实现流量控制,来避免接收端因数据过多而过载;使用差错控制来提供可靠的服务。 两个设备之间使用 TCP 软件传送的数据单元叫做报文段,它有 2060 字节的首部,首部后面是来自应用程序的数据。首部结构如前所述。 TCP 连接 TCP 的连接通常包括 3 个阶段:连接建立、数据传送和连接终止。 连接建立需要三向握手: 客户发送第一个报文段,SYN 报文段,在这个报文段中只有 SYN 标志位置 1.这个报文段的作用是使序号同步。SYN 报文段是控制报文段,不携带任何数据,但是消耗一
41、个序号。当数据传送开始时,每发送一个字节,序号应该加 1. 在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。 服务器发送第二个报文段,SYN+ACK 报文段,有两个标志位置 1(SYN 和 ACK) 。服务器使用这个报文段同步初始序号,以便从服务器向客户发送字节。使用 ACK 确认已从客户端收到了 SYN 报文段,确认号为客户端发送 SYN 报文段序号值加 1. 客户发送第三个报文 ACK,确认号为服务器发送报文段的序号值加 1。该报文段的序号与 SYN 报文段使用的序号一样。 ACK 报文段如果不携带数据就不消耗序号。 连接建立后,数据开始双向传送: 在数据传输过程中,ACK
42、 和确认序号是非常重要的,应用程序交给 TCP 协议发送的数据会暂存在 TCP层的发送缓冲区中, 发出数据包给对方之后, 只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的 ACK 段,经过等待超时后 TCP 协议自动将发送缓冲区中的数据包重发。 以上情况只描述了最简单的一问一答的情景,事实上 TCP 协议为应用层提供了全双工(full-duplex)的服务,双方都可以主动甚至同时给对方发送数据。如果通讯过程只能采用一问一答的方式,收和发两个方向不能同时传输,在同一时间只允许一个方向的数据传输,则称为半双工(ha
43、lf-duplex),假设某种面向连接的协议是半双工的,则只需要一套序号就够了,不需要通讯双方各自维护一套序号了。 参加交换数据的双方中的任何一方都可以关闭连接, 连接终止的四向握手: 在正常情况下,客户机 TCP 接收到客户进程发来的关闭命令后,就发送第一个报文段把 FIN位置 1。如果 FIN 报文段不携带数据,它消耗一个序号。同时更改状态为 FIN_WAIT_1,关闭应用程序进程。 服务器 TCP 在收到这个 FIN 报文段后,向自己对应的进程发送一个文件结束符 EOF,同时更改状态为 CLOSE_WAIT,并发送第二个报文段ACK,以证实从客户端收到了 FIN 报文段。如果不携带数据,
44、客户端接到 ACK 后状态更改为 FIN_WAIT_2。 服务器关闭应用程序进程, 更改状态为 LAST_ACK。 并发送第三个报文段FIN, 若不携带数据,FIN 消耗一个序号。 客户 TCP 接收到 FIN 后,更改状态为 TIME-WAIT,同时发送最后一个报文段ACK,证实从 TCP服务器收到了一个 FIN 报文段,该报文段的确认号等于从服务器发送的 FIN 报文段的序号加 1 。 除上述的情况外,建立连接时,客户端和服务器端可以同时打开;关闭连接时,可以同时关闭或者进行三向握手。 TCP 的状态机转换图如下所示: 图 2-12 TCP 的各种状态如下表 2 所示: 表 2-3 TCP
45、 的各种状态 没有连接 收到了被动打开,等待 SYN 已发送 SYN;等待 ACK 已发送 SYN+ACK;等待 ACK 连接已建立;数据传送在进行 第一个 FIN 已发送;等待 ACK 对第一个 FIN 的 ACK 已收到;等待第二个 FIN 收到第一个 FIN,已发送 ACK;等待应用程序关闭 收到第二个 FIN,已发送 ACK;等待 2MSL 超时 已发送第二个 FIN;等待 ACK 双方都已决定同时关闭 流量控制: 如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP 协议通过滑动窗口(Sliding Window)机制解决这一问
46、题。 TCP 在接收缓存上定义一个窗口,TCP 发送数据的多少由滑动窗口协议定义。 为了完成流量控制,TCP 使用滑动窗口协议。两个主机为向外通信(发送数据)各使用一个窗口,这个接收窗口覆盖了缓存的一部分。这个窗口有两个沿:一个在左边,另一个在右边。这个窗口叫做滑动窗口,因为左沿和右沿都可以滑动。窗口可以展开、合拢或缩回,这三种活动受接收端而不是发送端的控制(取决与网络上的拥塞状态) ,发送端必须听从接收端的命令。一般,窗口缩回必须避免。 TCP 的滑动窗口是面向字节的,窗口大小取决于接收窗口(rwnd)和拥塞窗口(cwnd)中的较小值。 接收端在一段时间内不愿意从发送端接收任何数据时, 可以
47、发送rwnd为0的报文段来暂时关闭窗口,此时发送端窗口大小并非真正地缩回,而是暂停发送数据,直到一个新的通告收到为止。 差错控制: 差错控制由检验和、确认和超时来处理。受损伤的和重复的报文段要重传,重复的报文段要丢弃。数据可能不按序到达,接收端 TCP 把它们暂时存储下来,但 TCP 保证交付给进程的报文段都是按序的。 重传发生在:当重传超时(RTO)计时器时间到,或已到达 3 个重复的 ACK 报文段。 TCP 使用拥塞机制来避免和检测网络中的拥塞。在拥塞控制中使用曼开始(指数增大) 、拥塞避免(加法增大)和拥塞检测(乘法减小)等策略。 TCP 在运行中使用 4 个计时器(重传计时器、持久计
48、时器、保活计时器和时间等待计时器) 。 UIP 处理流程 3.1. 简介 uIP 协议栈去掉了完整的 TCP/IP 中不常用的功能,简化了通讯流程,但保留了网络通信必须使用的协议,设计重点放在了 ARP/ IP/ICMP/UDP/TCP 这些网络层和传输层协议上,保证了其代码的通用性和结构的稳定性。 由于 uIP 协议栈专门为嵌入式系统而设计,它具有如下的优点: 代码非常少,其协议栈代码不到 6K; 占用的内存数非常少,RAM 占用仅几百字节; 其硬件处理层、协议栈层和应用层共用一个全局缓存区,不存在数据的拷贝,且发送和接收都是依靠这个缓存区,极大的节省空间和时间。 支持多个主动连接和被动连接
49、并发; 通用性强,移植起来基本不用修改就可以通过; 对数据的处理采用轮循机制,不需要操作系统的支持。 3.2. 层次结构 uIP 相当于一个代码库,通过一系列的函数实现与底层硬件和高层应用程序的通讯,对于整个系统来说它内部的协议组是透明的,从而增加了协议的通用性。 uIP 协议栈与系统底层和高层应用之间的关系如图 11 所示: 图 3-1 实现设备驱动与 UIP 对接需要的 7 个接口程序,定义在 uip.h: 应用程序 UIP 网卡驱动 系统定时应用层 UIP协议栈 硬件驱动 #define uip_input() uip_process(UIP_DATA) #define uip_peri
50、odic(conn) do uip_conn = &uip_connsconn; uip_process(UIP_TIMER); while (0) #define uip_conn_active(conn) (uip_connsconn.tcpstateflags != UIP_CLOSED) #define uip_periodic_conn(conn) do uip_conn = conn; uip_process(UIP_TIMER); while (0) #define uip_poll_conn(conn) do uip_conn = conn; uip_process(UIP_P
51、OLL_REQUEST); while (0) #define uip_udp_periodic(conn) do uip_udp_conn = &uip_udp_connsconn; uip_process(UIP_UDP_TIMER); while (0) #define uip_udp_periodic_conn(conn) do uip_udp_conn = conn; uip_process(UIP_UDP_TIMER); while (0) 还有一个变量,在接口中要用到: u8_t uip_bufUIP_BUFSIZE+2; 对以上接口进行详细介绍: #define uip_inp
52、ut() 处理输入数据包。 当设备从网络上接收到数据包时调用此函数。在调用此函数之前,应将接收到的数据包内容存入uip_buf 缓冲区,并将其长度赋给 uip_len. 以太网内使用的 uip 需要用到 ARP 协议,因此在调用此函数之前先调用 uip 的 ARP 代码。 此函数返回时,如果系统有数据要输出,会直接将数据存入 uip_buf,并将其长度值赋给 uip_len。如果没有数据要发送,则 uip_len 值为 0. 使用举例如下: uip_len = tapdev_read(uip_buf); if(uip_len 0) if(BUF-type = htons(UIP_ETHTYPE
53、_IP) uip_arp_ipin(); uip_input(); if(uip_len 0) uip_arp_out(); tapdev_send(uip_buf,uip_len); else if(BUF-type = htons(UIP_ETHTYPE_ARP) uip_arp_arpin(); if(uip_len 0) tapdev_send(uip_buf,uip_len); #define uip_periodic(conn) 周期性的处理一个连接,需用到该连接的连接号,conn 为将要轮询的连接号。 该函数对一个 uip 的 TCP 连接进行一些必要的周期性处理(如定时器、轮询
54、等) ,它应该在周期性 uip定时器期满消息到来时被调用。每一个连接都应该调用该函数,不论连接是否打开。 该函数返回时, 若缓冲区内有需要被发送出去的数据包等待处理, 就将uip_len的值置为大于零的数。以太网内使用的 uip 需要用到 ARP 协议,因此在调用驱动程序之前先调用 uip 的 ARP 代码 uip_arp_out(),再调用设备驱动程序将数据包发送出去。 使用举例如下: for(uint32_t i = 0; i 0) uip_arp_out(); tapdev_send(uip_buf,uip_len); #define uip_conn_active(conn) #def
55、ine uip_periodic_conn(conn) 对一个连接进行周期性处理,需用到指向该连接结构体的指针。 该函数与 uip_periodic 执行的操作是相同的, 不同之处在于传入的参数是一个指向 uip_conn 结构体的指针。 此函数可用于对某个连接强制进行周期性处理。 #define uip_poll_conn(conn) 请求对特定连接进行轮询。 该函数功能与 uip_periodic()相同,但是不执行任何定时器处理。通过轮询从应用程序得到新数据。 #define uip_udp_periodic(conn) 周期性处理连接号指定的连接。 此函数基本上与 uip_period
56、ic()相同,区别在于这里处理的是 UDP 连接。其调用方式也与 uip_periodic()类似: for(i = 0; i 0) uip_arp_out(); tapdev_send(); #define uip_udp_periodic_conn(conn) 周期性处理一个 UDP 连接,需用到指向该连接结构体的指针。 此函数功能与 uip_periodic_conn()相同,只是用来处理的是 UDP 连接。 u8_t uip_bufUIP_BUFSIZE+2; uip 数据包缓冲区,长度固定。 Uip_buf 数组用于存放接收、发送的数据包。设备驱动程序应将接收到的数据放入缓冲区。发送
57、数据时,设备驱动程序从缓冲区中读取链路层的首部和 TCP/IP 首部。链路层头的大小在 UIP_LLH_LEN 中定义。 注:应用程序数据无需放入这个缓冲区中,而是需要设备驱动程序从 uip_appdata 指针所指的地方读取数据。 u16_t uip_len; 全局变量,uip_buf 缓冲区中数据包的长度。 当网络设备驱动调用 uip 输入函数时,uip_len 要被设为传入数据包的大小。 当发送数据包时,设备驱动程序通过这个变量来确定要发送的数据包的大小。 应用层要调用的函数,包括一些宏定义与函数,定义在 uip.h: 宏定义: #define uip_outstanding(conn)
58、 (conn)-len) #define uip_datalen() uip_len #define uip_urgdatalen() uip_urglen #define uip_close() (uip_flags = UIP_CLOSE) #define uip_abort() (uip_flags = UIP_ABORT) #define uip_stop() (uip_conn-tcpstateflags |= UIP_STOPPED) #define uip_stopped(conn) (conn)-tcpstateflags & UIP_STOPPED) #define uip_
59、restart() do uip_flags |= UIP_NEWDATA; uip_conn-tcpstateflags &= UIP_STOPPED; while(0) #define uip_udpconnection() (uip_conn = NULL) #define uip_newdata() (uip_flags & UIP_NEWDATA) #define uip_acked() (uip_flags & UIP_ACKDATA) #define uip_connected() (uip_flags & UIP_CONNECTED) #define uip_closed()
60、(uip_flags & UIP_CLOSE) #define uip_aborted() (uip_flags & UIP_ABORT) #define uip_timedout() (uip_flags & UIP_TIMEDOUT) #define uip_rexmit() (uip_flags & UIP_REXMIT) #define uip_poll() (uip_flags & UIP_POLL) #define uip_initialmss() (uip_conn-initialmss) #define uip_mss() (uip_conn-mss) #define uip_
61、udp_remove(conn) (conn)-lport = 0 #define uip_udp_bind(conn, port) (conn)-lport = port #define uip_udp_send(len) uip_send(char *)uip_appdata, len) 函数: void uip_listen(u16_t port); void uip_unlisten(u16_t port); struct uip_conn *uip_connect(uip_ipaddr_t *ripaddr, u16_t port); void uip_send(const void
62、 *data, int len); struct uip_udp_conn *uip_udp_new(uip_ipaddr_t *ripaddr, u16_t rport); 对以上函数进行详细介绍: #define uip_outstanding(conn) 检查一个连接是否有特殊的(例如,未答复的)数据。 conn 为指向该连接结构体的指针。 #define uip_datalen() uip_appdata 缓冲区中,当前可用的传入数据的长度。必须先调用 uip_data()查明是否有当前可用的传入数据。 #define uip_urgdatalen() 所有到达连接的缓冲区外的(紧急数
63、据)数据长度。要使用此宏,应配置 UIP_URGDATA 宏为真。 #define uip_close() 此函数会以一种谨慎的方式关闭当前连接。 #define uip_abort() 中止(重置)当前连接,多用于出现错误导致无法使用 uip_close()的场合。 #define uip_stop() 告诉发送主机停止发送数据。该函数会关闭接收者的窗口,以停止从当前连接接收数据。 #define uip_stopped(conn) 找出当前连接先前是否已经被 uip_stop()停止了。 #define uip_restart() 如果连接先前被 uip_stop()停止了,该函数会重启连
64、接。接收者的窗口会被重新打开,并从当前连接开始接收数据。 #define uip_udpconnection() 检查当前连接是否是一个 UDP 连接。 #define uip_newdata() 如果uip_appdata指针所指之处有新的应用数据, 就得到一个非零值。 数据的大小可通过uip_len得到。 #define uip_acked() 若先前发送的数据得到了远程主机的确认信息,就得到一个非零值。这表示应用程序可以发送新数据了。 #define uip_connected() 如果当前与远程主机的连接建立,则得到一个非零值。这包括两种情形:连接被主动打开(uip_connect()
65、) ,或者被动打开(uip_listen()) 。 #define uip_closed() 如果连接被远程主机关闭,则返回一个非零值。这时应用程序会做一些必要的清理工作。 #define uip_aborted() 如果连接被远程主机中止(重置) ,则返回一个非零值。 #define uip_timedout() 如果当前连接是因为多次重传而超时中止,则返回一个非零值。 #define uip_rexmit() 如果先前发送的数据在网络中丢失,应用程序需重传时,返回一个非零值。应用程序需调用 uip_send()函数来重传与上一次所发送的完全一致的数据。 #define uip_poll()
66、 解决连接是否由 uip 轮询的问题。 如果应用程序被调用的原因是当前连接因空闲太久而被 uip 轮询,则返回一个非零值。该轮询事件可以被用来发送数据,而无需等待远程主机发送数据。 #define uip_initialmss() 获得当前连接的初始最大报文段长度(MSS) 。 #define uip_mss() 获得当前连接所能发送的最大报文段长度。该长度是由接收者的窗口大小和连接的 MSS 计算出来的。 #define uip_udp_remove(conn) 移除一个 UDP 连接。conn 指向该连接的 uip_udp_conn 结构体。 #define uip_udp_bind(co
67、nn, port) 绑定一个 UDP 连接到本地端口。conn 指向该连接的 uip_udp_conn 结构体,port 为本地端口号,以网络字节序。 #define uip_udp_send(len) 在当前连接上发送长度为 len 的 UDP 数据包。该函数只有在答复一个 UDP 事件(轮询或有新数据)时才可被调用。数据必须提前放入 uip_buf 缓冲区中 uip_appdata 指针指向的地方。 void uip_listen(u16_t port); 开始监听指定的端口。 由于 port 应该为网络字节序,所以需要用到转换函数 HTONS()或者 htons()。 void uip_
68、unlisten(u16_t port); 停止监听指定端口。 struct uip_conn *uip_connect(uip_ipaddr_t *ripaddr, u16_t port); 使用 TCP 协议连接到远程主机。 此函数用来与特定主机上的特定端口建立新的连接,它分配一个新的连接标识符,并将连接的状态转为 SYN_SENT,将重传计时器设置为 0.当该连接下次被周期性处理时,将会发送一个 TCP 的 SYN 报文段。这个过程一般在 uip_connet()被调用后 0.5 秒后完成。 该函数只有在主动打开配置项 UIP_ACTIVE_OPEN 被置 1 时可用。 ripaddr
69、为远程主机的 IP 地址,port 为网络字节序的端口号。该函数返回一个指向新连接的 uip 连接标识符的指针,当没有连接能被分配时返回 NULL。 void uip_send(const void *data, int len); 在当前连接上发送数据。 该函数用来发送单个 TCP 数据报文段,只有为了事件处理而被 uip 调用的应用程序,才能发送数据。该函数能够发送的数据大小是由 TCP 所允许的最大数据量来决定的。uip 会自动的分割数据,以保证发送出去的数据量是合适的。可以通过 uip_mss()来查询 uip 实际将要发送的数据量。 该函数不保证发送的数据能到达目的地,如果数据在网络
70、中丢失,uip_rexmit()事件将被置位。应用程序会被调用,并通过该函数来重新发送数据。 struct uip_udp_conn *uip_udp_new(uip_ipaddr_t *ripaddr, u16_t rport); 建立一个新的 UDP 连接。 该函数会自动为新连接分配一个不用的端口,但是在调用该函数之后,还可以通过 uip_udp_bind()还重新选择一个端口。 ripaddr 为远程主机(服务器)的 IP 地址,rport 为远程主机网络字节序的端口号。该函数返回新连接的 uip_udp_conn 结构体指针,或者当没有连接能够分配时返回 NULL。 UIP 中所用到的
71、主要结构体 Struct uip_conn /TCP 连接 uip_ipaddr_t ripaddr; /* The IP address of the remote host. */ u16_t lport; /* The local TCP port, in network byte order. */ u16_t rport; /* The local remote TCP port, in network byte order. */ u8_t rcv_nxt4; /* The sequence number that we expect to receive next. */ u8_
72、t snd_nxt4; /* The sequence number that was last sent by us. */ u16_t len; /* Length of the data that was previously sent. */ u16_t mss; /* Current maximum segment size for the connection. */ u16_t initialmss; /* Initial maximum segment size for the connection. */ u8_t sa; /* Retransmission time-out
73、 calculation state variable. */ u8_t sv; /* Retransmission time-out calculation state variable. */ u8_t rto; /* Retransmission time-out. */ u8_t tcpstateflags; /* TCP state and flags. */ u8_t timer; /* The retransmission timer. */ u8_t nrtx; /* The number of retransmissions for the last segment sent
74、. */ /* The application state. */ uip_tcp_appstate_t appstate; ; struct uip_udp_conn /UDP 连接 uip_ipaddr_t ripaddr; /* The IP address of the remote peer. */ u16_t lport; /* The local port number in network byte order. */ u16_t rport; /* The remote port number in network byte order. */ u8_t ttl; /* De
75、fault time-to-live. */ /* The application state. */ uip_udp_appstate_t appstate; ; struct uip_stats /统计信息结构体 struct uip_stats_t drop; /* Number of dropped packets at the IP layer. */ uip_stats_t recv; /* Number of received packets at the IP layer. */ uip_stats_t sent; /* Number of sent packets at th
76、e IP layer. */ uip_stats_t vhlerr; /* Number of packets dropped due to wrong IP version or header length. */ uip_stats_t hblenerr; /* Number of packets dropped due to wrong IP length, high byte. */ uip_stats_t lblenerr; /* Number of packets dropped due to wrong IP length, low byte. */ uip_stats_t fr
77、agerr; /* Number of packets dropped since they were IP fragments. */ uip_stats_t chkerr; /* Number of packets dropped due to IP checksum errors. */ uip_stats_t protoerr; /* Number of packets dropped since they were neither ICMP, UDP nor TCP. */ ip; /* IP statistics. */ struct uip_stats_t drop; /* Nu
78、mber of dropped ICMP packets. */ uip_stats_t recv; /* Number of received ICMP packets. */ uip_stats_t sent; /* Number of sent ICMP packets. */ uip_stats_t typeerr; /* Number of ICMP packets with a wrong type. */ icmp; /* ICMP statistics. */ struct uip_stats_t drop; /* Number of dropped TCP segments.
79、 */ uip_stats_t recv; /* Number of recived TCP segments. */ uip_stats_t sent; /* Number of sent TCP segments. */ uip_stats_t chkerr; /* Number of TCP segments with a bad checksum. */ uip_stats_t ackerr; /* Number of TCP segments with a bad ACK number. */ uip_stats_t rst; /* Number of recevied TCP RS
80、T (reset) segments. */ uip_stats_t rexmit; /* Number of retransmitted TCP segments. */ uip_stats_t syndrop; /* Number of dropped SYNs due to too few connections was avaliable. */ uip_stats_t synrst; /* Number of SYNs for closed ports,triggering a RST. */ tcp; /* TCP statistics. */ #if UIP_UDP struct
81、 uip_stats_t drop; /* Number of dropped UDP segments. */ uip_stats_t recv; /* Number of recived UDP segments. */ uip_stats_t sent; /* Number of sent UDP segments. */ uip_stats_t chkerr; /* Number of UDP segments with a bad checksum. */ udp; /* 0)/如果 len0,表示设备里有新数据,下面对新数据进行处理。 if(BUF-type = htons(UIP
82、_ETHTYPE_IP)/表示收到的是 IP 数据包 uip_arp_ipin();/ARP 对收到的 IP 包进行处理,如果收到的 IP 包中的源 IP 地址是本地网络上主机的 IP 地址,则只进行 ARP 缓存表的更新(更新源 IP 地址对应的 MAC 地址)或者插入(若表中没有该 IP 地址项,插入源 IP-MAC 对应关系)操作 uip_input();/调用 uip_process(UIP_DATA)对数据包进行处理 if(uip_len 0)/表示数据包处理完后,立即产生了要发送的数据 uip_arp_out();/查看是否需要发送 ARP 请求, 并为传出的 ARP 请求或 IP
83、 包添加以太网头 tapdev_send(uip_buf,uip_len);/调用驱动程序发送缓存中的以太网帧 else if(BUF-type = htons(UIP_ETHTYPE_ARP)/表示要处理的是 ARP 包(请求或应答) uip_arp_arpin();/直接进行 ARP 处理。 若为应答, 则从包中取出需要的 MAC 地址加入 ARP缓存表;若为请求,则将自己的 MAC 地址打包成一个 ARP 应答,发送给请求的主机。 if(uip_len 0)/表示有 ARP 应答要发送 tapdev_send(uip_buf,uip_len); else if(timer_expired
84、(&periodic_timer)/设备里没有新数据,检查周期性定时器是否期满,若期满进行下面的处理。 timer_reset(&periodic_timer);/复位定时器,将开始时间设为当前时间,使重新开始计时 for(uint32_t i = 0; i 0)/表示缓存中有数据要发送 uip_arp_out();/查看是否要发送 ARP 请求, 为 ARP 请求或 IP 数据包添加以太网帧头 tapdev_send(uip_buf,uip_len); #if UIP_UDP /如果有 UDP 连接,也对 UDP 连接进行处理 for(i = 0; i 0) uip_arp_out(); t
85、apdev_send(); #endif / Call the ARP timer function every 10 seconds. /检查 ARP 缓存表中的表项是否到期,若到期则将该表项清 0(表项保存时间为 10 秒) if(timer_expired(&arp_timer) timer_reset(&arp_timer); uip_arp_timer(); 由上可知,uip 在不停的读设备、发数据,同时,当周期性定时器期满时,对定时器进行复位,并对连接和 ARP 表项进行周期性处理操作。 主要的处理函数 uip_process() 该函数的处理过程总的来说分两种情况:有数据要处理;
86、周期性定时器被激发。 数据处理又可细分为三种情况: #define UIP_DATA 1 /* Tells uIP that there is incoming data in the uip_buf buffer. The length of the data is stored in the global variable uip_len. */ #define UIP_POLL_REQUEST 3 /* Tells uIP that a connection should be polled. */ #define UIP_UDP_SEND_CONN 4 /* Tells uIP tha
87、t a UDP datagram should be construc- ted in the uip_buf buffer. */ 定时器处理分为两种情况: #define UIP_TIMER 2 /* Tells uIP that the periodic timer has fired. */ #define UIP_UDP_TIMER 5 首先分析 UIP_DATA,对接收到的 IP 数据报的处理,对应大循环里的 uip_input(): /首先进行基础的检查,判断 IP 数据报首部的合法性 /如果收到的数据报长度比IP首部中记录的长度要大, 说明是数据报被补0了, 将正确的长度赋给l
88、en;否则,IP 数据报传送过程中被破坏了,将其丢弃。 /检查分片标志,若非第一个分片需进行重组;若没有定义分片操作,而分片偏移非 0,则将该 IP 数据报丢弃 /若主机 IP 地址全 0,若定义了 ping 操作,且高层协议为 ICMP,则接收到的为回送请求报文,进入icmp_input 进行处理;否则,丢弃该报文 /若主机 IP 地址非全 0 ,IP 广播被支持,且目的地址为全 1,则接收到的应该为 UDP 广播报文, 重新设定检验和,并进入 udp_input 处理 /非以上两种情况,则比较目的 IP 地址与本主机 IP 地址是否相符,并检查检验和是否合法,不符则丢弃该报文 /若该 IP
89、 数据报的上层协议为 TCP,则进入 tcp_input 进行处理 /若上层协议为 UDP,则进入 udp_input 进行处理 /若既非 ping 命令, 非广播, 非 TCP 报文, 也非 UDP 报文, 则应该为 ICMP 报文, 若上层协议不是 ICMP则将该 IP 数据报丢弃,并更新统计信息。 icmp_input: /首先接收到的 ICMP 报文数加 1 /如果报文类型不是回送请求,就将报文丢弃,相应的统计数据加 1。 /将目的 IP 地址存入 /将报文类型改为回送回答 /重新计算检验和 /交换源和目的 IP 地址 /将 ICMP 发送报文数加 1,之后转到 send 进行回送回答
90、报文 Send: DEBUG_PRINTF(Sending packet with length %d (%d)n, uip_len, (BUF-len0 len1); /打印要发送了的 IP 数据报总长度/将发送的 IP 数据报个数加 1 uip_flags = 0; /将 uip 的标志位置 0 return; /返回,并让被调用程序进行数据报发送(驱动程序 tapdev_send()) udp_input: /* 我们不对 UDP/IP 头做任何更改, 所有复杂的事情都交给 UDP 应用程序来处理。 如果 UDP 应用程序设置了 uip_slen, 表示有应用数据要发送*/ /如果定义了
91、检验和, /uip_len 里存放应用数据长度 /获得应用数据的偏移地址 /判断检验和是否合法,不合法,将该数据包丢弃 /如果没有定义 UDP 检验和,则只取应用数据长度到 uip_len /遍历 UDP 连接,如果本地端口号非零,且与目的端口号相等,同时远程端口号为 0 或远程端口号与源端口号相等,且远程的 IP 地址为全 0/全 1 或远程 IP 地址与源 IP 地址相同(有对应的 UDP 连接) ,就转到 udp_found 进行处 /没有找到对应的连接,则丢弃该 UDP 报文 udp_found: /接收到的 UDP 报文数据加 1 uip_conn = NULL; uip_flags
92、 = UIP_NEWDATA; /表示远程主机的进程已向该主机进程发送了数据 uip_sappdata = uip_appdata = &uip_bufUIP_LLH_LEN + UIP_IPUDPH_LEN; /获得发送数据的偏移地址 uip_slen = 0; UIP_UDP_APPCALL();/ 调用对应的 UDP 应用程序接收数据 Tcp_input: / TCP 报文接收统计数据加 1 /检查检验和是否合法,不合法就将该数据包丢弃 /检查所有活动连接,若找到对应的 TCP 连接,则转入 found 处理 /如果没有找到希望接收该报文的活动连接,要么该报文是旧的复制品,要么该报文是去
93、往监听进程的SYN 报文 /若没有设置 SYN 标志位,则该报文为旧的复制品,需要重置连接,转入 reset 处理。 /再来检查监听连接,进入 found_listen 处理 /若没有匹配的连接,则发送一个 RST 报文 Found: /若 TCP 标志为 RST,就将连接状态改为 CLOSED,将 uip_flags 设为 UIP_ABORT,再调用 UIP_APPCALL() ,之后将产生的 IP 数据包丢弃。 /计算接收到的 TCP 数据长度。若接收到的报文段的序号非我们所希望接收到的下一个报文段,则转到 tcp_send_ack 发送包含正确序号的确认号 ACK。 /若接收到的序号即为
94、希望收到的报文段的序号,检查该报文段里是否有紧急数据,如果有,则更新上次发送报文段序号,计算往返时间 RTT,并重置超时重传计数器和待确认数据长度。 /根据当前 TCP 连接的不同状态(UIP_SYN_RCVD、UIP_SYN_SENT、UIP_ESTABLISHED、UIP_LAST_ACK、UIP_FIN_WAIT_1、UIP_FIN_WAIT_2、UIP_TIME_WAIT、UIP_CLOSING) ,进行相应的处理。 found_listen: uip_connr = 0; /选择出关闭的连接或处于等待关闭且使用时间最久的旧连接作为新连接,并配置参数; /更新希望接收的下一个报文序号
95、/若没有这样的连接则将该 SYN 报文丢弃 /如果有选项的话,分析 TCP MSS 选项(END、 NOOP、 MSS 等) 再来分析 UIP_UDP_SEND_CONN,主要处理 UDP 报文的发送: /首先获得指向当前 uip_conn 的指针 /如果程序里要用到 UDP 协议,且连接标志位是 UIP_UDP_SEND_CONN,则转到 udp_send 处理 接下来分析 udp_send: udp_send: /如果要发送的数据长度为 0,即缓存中没有数据要发送,将数据包丢弃 /若缓存区有数据要发送,将 uip_len 设为要发送数据长度加上 IP 首部(20)和 UDP 首部(8)的长
96、度。 注意: #define BUF (struct uip_tcpip_hdr *)&uip_bufUIP_LLH_LEN) #define FBUF (struct uip_tcpip_hdr *)&uip_reassbuf0) #define ICMPBUF (struct uip_icmpip_hdr *)&uip_bufUIP_LLH_LEN) #define UDPBUF (struct uip_udpip_hdr *)&uip_bufUIP_LLH_LEN) /将 uip_len 的值放入 IP 首部中对应的位置,即总长度 /设置 IP 数据报在网络上的生存时间 /表示该 IP
97、数据报的高层协议类型为 UDP。 /设定 UDP 数据报的长度域,为要发送的数据长度加上 UDP 头的长度。 /设置 UDP 检验和为 0 /设置 UDP 数据报中的源和目的端口号为本地端口号和远程主机上进程端口号。 /设定IP数据报中源和目的IP地址, 源IP地址为当前主机的IP地址, 目的IP地址为存储在udp_conn的 ripaddr 中的值。 /取得应用数据的偏移地址 /如果用到 UDP 的检验和,重新计算检验和 /将 udp 发送数据包数目加 1 /跳转到该处进行接下来的 IP 数据报的处理 再来分析 ip_send_nolen: (略过 IPV6 部分) BUF-vhl = 0x
98、45; BUF-tos = 0; BUF-ipoffset0 = BUF-ipoffset1 = 0; +ipid; BUF-ipid0 = ipid 8; BUF-ipid1 = ipid & 0xff; /* Calculate IP checksum. */ BUF-ipchksum = 0; BUF-ipchksum = (uip_ipchksum(); DEBUG_PRINTF(uip ip_send_nolen: chkecum 0x%04xn, uip_ipchksum(); /将发送的 TCP 数据报个数加 1 接下来转入 send,代码如前所示,准备进行数据包的发送。 设定
99、IP 数据报首部的内容,ipid 为 16位标识位 接下来,分析 UIP_POLL_REQUEST uip_sappdata = uip_appdata = &uip_bufUIP_IPTCPH_LEN + UIP_LLH_LEN; if(flag = UIP_POLL_REQUEST) if(uip_connr-tcpstateflags & UIP_TS_MASK) = UIP_ESTABLISHED & !uip_outstanding(uip_connr) /如果 TCP 连接的状态(低 4 位为 state)为 UIP_ESTABLISHED,且没有未确认数据 uip_flags =
100、 UIP_POLL;/将 uip 标志设定为 POLL,对应用程序进行轮询,查看是否有应用数据要发送(uip_flags 总是在调用应用程序之前被设置) UIP_APPCALL();/调用宏定义的应用程序 goto appsend; /否则,就将之前的数据丢弃 goto drop; 再来分析 appsend: if(uip_flags & UIP_ABORT) /若应用程序希望中止连接,则将发送数据长度设为 0,连接状态设为关闭,将TCP 报文首部中的标志位设为 TCP_RST | TCP_ACK。后转入 tcp_send_nodata uip_slen = 0; uip_connr-tcps
101、tateflags = UIP_CLOSED; BUF-flags = TCP_RST | TCP_ACK; goto tcp_send_nodata;/将 uip_len 设为 IP 和 TCP 首部长度之和 if(uip_flags & UIP_CLOSE) /若应用程序发起主动关闭请求,发送数据长度设为 0,之前发送数据长度设为1,TCP 连接状态变为 UIP_FIN_WAIT_1 uip_slen = 0; uip_connr-len = 1; uip_connr-tcpstateflags = UIP_FIN_WAIT_1; uip_connr-nrtx = 0; BUF-flags
102、 = TCP_FIN | TCP_ACK; goto tcp_send_nodata; if(uip_slen 0) /若应用程序有数据要发送 if(uip_flags & UIP_ACKDATA) != 0) /若已经收到发送数据的确认信息,则应用程序应该发送新的数据,而不是重传上次发送数据,将上次发送数据长度 len 置 0 uip_connr-len = 0; if(uip_connr-len = 0) /取合适的发送数据长度 if(uip_slen uip_connr-mss) uip_slen = uip_connr-mss; uip_connr-len = uip_slen; el
103、se /若存在未得到确认的数据,确定应用程序发送不超过上一次数据长度的数据(例如要进行重传时) uip_connr-nrtx = 0; 对定时器期满的处理流程 UIP_TIMER /重组计数器清 0 /对初始序号、uip_len、uip_slen 进行设定 /若连接状态为 UIP_TIME_WAIT 或 UIP_FIN_WAIT_2,且重传定时器刚启动,将连接置为 CLOSED /若连接状态非前两种,也非 UIP_CLOSED, /若存在未得到确认的已发送数据 /就将重传超时定时器 RTO 的值减 1 /若 RTO 到期, /重传次数达到最大值, /或当前连接状态为 UIP_SYN_SENT
104、或 UIP_SYN_RCVD 且重传次数达到最大值, /将连接状态置为 CLOSED /Uip_flags 设为 UIP_TIMEDOUT,表示连接因重传次数过多而被中止 /调用 UIP_APPCALL() /向远程主机发送 RST 报文 /转到 tcp_send_nodata /分情况进行重传,TCP 重传统计值加 1 UIP_SYN_RCVD: 重传 SYNACK 报文,tcp_send_synack UIP_SYN_SENT: 重传 SYN 报文,tcp_send_syn UIP_ESTABLISHED: Uip_flags 设为 UIP_REXMIT, 调用 UIP_APPCALL()
105、 转到 apprexmit 进行重传处理 UIP_FIN_WAIT_1: UIP_CLOSING: UIP_LAST_ACK: 重传 FINACK 报文,tcp_send_finack。 /不需要进行重传,也连接状态为 UIP_ESTABLISHED,则轮询应用程序来获得新数据 /转到 appsend 对接收到的新数据进行发送 /否则丢弃该消息 对 UIP_UDP_TIMER 的处理流程 /若本地端口号已分配(非 0) , /将连接置为 NULL /取得要发送数据在缓存区内的偏移地址 /将 uip_len 和 uip_slen 设为 0 /对 UDP 应用程序 UIP_UDP_APPCALL(
106、)进行轮询 /转到 udp_send 发送接收到的数据 /若本地没有建立 UDP 连接,则将该消息丢弃 原始套接字和原始线程 网络数据从用户进程传输到网络设备要经过四个层次,如图 33 所示: 图 3-3 一个套接字就是网络中的一个连接,和网络协议紧密地联系在一起,它是网络传输的入口。它是网络编程的入口,提供了大量的系统调用。 原始套接字和原始线程函数的宏定义 #define PSOCK_INIT(psock, buffer, buffersize) psock_init(psock, buffer, buffersize) #define PSOCK_BEGIN(psock) PT_BEGI
107、N(&(psock)-pt) #define PSOCK_SEND(psock, data, datalen) PT_WAIT_THREAD(&(psock)-pt), psock_send(psock, data, datalen) #define PSOCK_SEND_STR(psock, str) PT_WAIT_THREAD(&(psock)-pt), psock_send(psock, str, strlen(str) #define PSOCK_GENERATOR_SEND(psock, generator, arg) PT_WAIT_THREAD(&(psock)-pt), ps
108、ock_generator_send(psock, generator, arg) #define PSOCK_CLOSE(psock) uip_close() #define PSOCK_READBUF(psock) PT_WAIT_THREAD(&(psock)-pt), psock_readbuf(psock) #define PSOCK_READTO(psock, c) PT_WAIT_THREAD(&(psock)-pt), psock_readto(psock, c) #define PSOCK_DATALEN(psock) psock_datalen(psock) #define
109、 PSOCK_EXIT(psock) PT_EXIT(&(psock)-pt) #define PSOCK_CLOSE_EXIT(psock) do PSOCK_CLOSE(psock); PSOCK_EXIT(psock); while(0) #define PSOCK_END(psock) PT_END(&(psock)-pt) #define PSOCK_NEWDATA(psock) psock_newdata(psock) #define PSOCK_WAIT_UNTIL(psock, condition) PT_WAIT_UNTIL(&(psock)-pt), (condition)
110、; #define PSOCK_WAIT_THREAD(psock, condition) PT_WAIT_THREAD(&(psock)-pt), (condition) 初始化: #define PT_INIT(pt) LC_INIT(pt)-lc) 声明和定义: #define PT_THREAD(name_args) char name_args #define PT_BEGIN(pt) char PT_YIELD_FLAG = 1; LC_RESUME(pt)-lc) #define PT_END(pt) LC_END(pt)-lc); PT_YIELD_FLAG = 0; PT_I
111、NIT(pt); return PT_ENDED; 阻塞等待: #define PT_WAIT_UNTIL(pt, condition) do LC_SET(pt)-lc); if(!(condition) return PT_WAITING; while(0) #define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL(pt), !(cond) 分等级的原始线程: #define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE(pt), PT_SCHEDULE(thread) #define PT_SPAWN(pt, chil
112、d, thread) do PT_INIT(child); PT_WAIT_THREAD(pt), (thread); while(0) 退出和重启: #define PT_RESTART(pt) do PT_INIT(pt); return PT_WAITING; while(0) #define PT_EXIT(pt) do PT_INIT(pt); return PT_EXITED; while(0) 调用一个原始线程: #define PT_SCHEDULE(f) (f) = PT_WAITING) #define PT_YIELD(pt) do PT_YIELD_FLAG = 0;
113、LC_SET(pt)-lc); if(PT_YIELD_FLAG = 0) return PT_YIELDED; while(0) #define PT_YIELD_UNTIL(pt, cond) do PT_YIELD_FLAG = 0; LC_SET(pt)-lc); if(PT_YIELD_FLAG = 0) | !(cond) return PT_YIELDED; while(0) 原始套接字函数详细介绍,定义在 Psock.h 中 #define PSOCK_INIT(psock, buffer, buffersize) 初始化一个原始套接字,必须在使用套接字之前被调用。该初始化同时
114、为套接字指定了 input buffer. Psocket (struct psock*)指向一个要被初始化的原始套接字,buffer(char*)指向原始套接字的输入缓存区,buffersize 为输入缓存区的大小。 #define PSOCK_BEGIN(psock) 在函数中启用原始套接字的原始线程。必须在它所使用的原始套接字在函数中被调用之前启用。 #define PSOCK_SEND(psock, data, datalen) 此宏通过原始套接字来发送数据。发送时,原始套接字的原始线程阻塞,直到所有的数据发送完毕并被确认已被远程 TCP 终端接收。 #define PSOCK_SEN
115、D_STR(psock, str) 通过原始套接字发送以 0 结束的字符串。Psock 指向一个套接字,str 指向要发送的字符串。 #define PSOCK_GENERATOR_SEND(psock, generator, arg) 调用一个函数产生数据,并发送数据。Generator 指向产生数据的函数,arg 为要传给函数的参数。 #define PSOCK_CLOSE(psock) 关闭一个套接字。该宏只能在包含该原始套接字的原始线程中被调用。Psock 指向要关闭的套接字。 #define PSOCK_READBUF(psock) 该宏会一直阻塞等待数据,并将数据读入由 PSOCK
116、_INIT()所指定的 input buffer,直到缓冲区被填满才停止读入数据。 #define PSOCK_READTO(psock, c) 该宏会一直阻塞等待数据,将数据读入由 POSCK_INIT()指定的输入缓冲区,直到出现某一指定的字符时停止。 #define PSOCK_DATALEN(psock) 该宏返回之前被 PSOCK_READTO() 或者 PSOCK_READBUF()读入的数据的长度。Psock 指向保存这些数据的原始套接字。 #define PSOCK_EXIT(psock) 该宏停止一个原始套接字的原始线程,且该宏总是配合 PSOCK_CLOSE()来一起完成操
117、作。 #define PSOCK_CLOSE_EXIT(psock) 该宏选择一个原始套接字,并退出该原始套接字的原始线程。 #define PSOCK_END(psock) 此宏用来声明一个原始套接字的原始线程结束。必须与 PSOCK_BEGIN()一起使用。 #define PSOCK_NEWDATA(psock) 该宏与 PSOCK_WAIT_UNTIL()一起检查是否有数据到达该套接字。 #define PSOCK_WAIT_UNTIL(psock, condition) 该宏阻塞原始线程直到特定的条件为真。当线程等待时,PSOCK_NEWDATA()可以被用来检查是否有新数据到达。
118、原始线程函数详细介绍,定义在 Pt.h 中 #define PT_INIT(pt) 必须在执行原始线程之前初始化原始线程。 #define PT_THREAD(name_args) 该宏用来声明一个原始线程,所有的原始线程必须被该宏声明。Name_args 为该原始线程执行的 C 函数的名字和参数。 #define PT_BEGIN(pt) 该宏用来声明一个原始线程的开始点。该宏应该被放在原始线程所在的函数的开始。所有在该宏之前的语句,在原始线程被调度时都将被执行。 #define PT_END(pt) 该宏用来声明一个原始线程结束。必须与 PT_BEGIN()一起使用。 #define PT
119、_WAIT_UNTIL(pt, condition) 阻塞原始线程直到特定条件出现。 #define PT_WAIT_WHILE(pt, cond) 当条件为真时一直阻塞等待。 #define PT_WAIT_THREAD(pt, thread) 阻塞并等到,直到子线程完成。 该宏调度一个子线程。当前的原始线程会阻塞直到该子线程完成。Thread 为带参数的子线程。 #define PT_SPAWN(pt, child, thread) 产生一个子线程,并等待直到它退出。该宏只能在原始线程内使用。 #define PT_RESTART(pt) 该宏将阻塞,因为当正在运行的原始线程在 PT_BE
120、GIN()被调用时,该原始线程要重启执行。 #define PT_EXIT(pt) 该宏使原始线程退出。若果该原始线程由其他线程产生,父线程将不再阻塞,并可以继续执行。 #define PT_SCHEDULE(f) 该函数调度一个原始线程。 当原始线程正在运行时, 返回非零值, 若该原始线程已经退出, 则返回 0 。 #define PT_YIELD(pt) 该宏使当前线程让位,并让其他处理过程先执行。 #define PT_YIELD_UNTIL(pt, cond) 该宏使当前线程让位,直到特定的值为真。 在真正调用应用程序的函数之前,若有新的连接建立,则先初始化应用程序状态结构里的套接字。 接下来,再调用真正进行通信管理的函数,进行数据通信。套接字函数必须返回整数值,但是不明确的返回所有的返回值都被藏在 PSOCK 宏中。 例如:static int handle_connection(struct hello_world_state *s) PSOCK_BEGIN(&s-p); PSOCK_CLOSE(&s-p); PSOCK_END(&s-p);