contiki学习笔记(十二)UIPTCP/IP协议

Contiki包含两个通信协议栈
uIP和Rime

UIPTCP/IP协议

导言
随着Internet的成功,TCP/IP协议套件已经成为全球通信的标准。TCP/IP是一种底层协议,用于网页传输、电子邮件传输、文件传输和Internet上的对等网络。对于嵌入式系统,能够运行本机TCP/IP使系统能够直接连接到Intranet,甚至是全局Internet。具有完全TCP/IP支持的嵌入式设备将成为一流的网络公民,从而能够与网络中的其他主机充分通信。

在小型8位或16位系统中,传统的TCP/IP实现在代码大小和内存使用方面都需要太多的资源。几百千字节的代码大小和几百千字节的RAM要求使整个TCP/IP堆栈无法容纳几十千字节的RAM和不足100 KB的代码。

UIP实现设计为只有一个完整的TCP/IP堆栈所需的绝对最小特性集。它只能处理单一的网络接口,包含IP、ICMP、UDP和TCP协议。UIP是用C语言编写的。

许多其他用于小型系统的TCP/IP实现假设嵌入式设备总是与运行在工作站类计算机上的完整TCP/IP实现通信。在这种假设下,可以删除在这种情况下很少使用的某些TCP/IP机制。然而,如果嵌入式设备要与另一个同样有限的设备通信,例如在运行分布式对等服务和协议时,许多这些机制是必不可少的。UIP被设计为符合RFC的,以便让嵌入式设备充当一流的网络公民。不适合任何特定应用程序的UIPTCP/IP实现。

TCP/IP通信
完整的TCP/IP套件由许多协议组成,从低层协议(如将IP地址转换为MAC地址的ARP)到应用程序级协议(如用于传输电子邮件的SMTP)。UIP主要关注TCP和IP协议,上层协议被称为“应用程序”。底层协议通常在硬件或固件中实现,并被称为“网络设备”,由网络设备驱动程序控制。

TCP为上层协议提供可靠的字节流。它将字节流分解为适当大小的段,每个段以自己的IP数据包发送。IP包由网络设备驱动程序在网络上发送。如果目的地不在物理连接的网络上,则IP分组由位于两个网络之间的路由器转发到另一个网络。如果另一个网络的最大数据包大小小于IP数据包的大小,则该数据包被路由器分割成较小的分组。如果可能,选择TCP段的大小,以便将碎片最小化。数据包的最终接收者必须重新组装任何支离破碎的IP数据包,然后才能传递到更高的层。

TCP/IP协议栈中的协议的形式要求是由Internet工程任务组(IETF)发布的许多RFC文档中指定的。堆栈中的每个协议都在另一个RFC文档中定义,RFC 1122收集所有需求并更新以前的RFC。

RFC 1122要求可分为两类:处理主机到主机通信的需求和处理应用程序与网络堆栈之间通信的需求。第一类示例是“TCP必须能够在任何段中接收TCP选项”,第二类示例是“必须有向应用程序报告软TCP错误条件的机制”。违反第一类要求的TCP/IP实现可能无法与其他TCP/IP实现通信,甚至可能导致网络故障。违反第二类要求只会影响系统内部的通信,不会影响主机与主机之间的通信。

在UIP中,所有影响主机到主机通信的RFC需求都被实现.然而,为了减少代码大小,我们删除了应用程序和堆栈之间的某些机制,如软错误报告机制和TCP连接的动态可配置的服务类型位。由于使用这些特性的应用程序很少,因此可以在不失去通用性的情况下删除它们。

主控制回路
UIP堆栈可以作为多任务系统中的任务运行,也可以作为单任务系统中的主程序运行。在这两种情况下,主控制循环重复执行两件事情:

检查数据包是否已从网络到达。
检查是否发生了定期超时。
如果数据包已经到达,则输入处理程序函数,UIP_INPUT(),应该由主控制循环调用。输入处理程序函数永远不会阻塞,但会立即返回。当它返回时,堆栈或用于接收数据包的应用程序可能产生了一个或多个应发送的应答包。如果是这样的话,应该调用网络设备驱动程序来发送这些数据包。

周期性超时用于驱动依赖于定时器的TCP机制,例如延迟确认、重传和往返时间估计。当主控制循环推断周期性计时器应该触发时,它应该调用计时器处理函数uip_周期()。因为TCP/IP堆栈在处理计时器事件时可能执行重传,所以网络设备驱动程序应该被调用来发送可能已经产生的数据包。

特定于体系结构的功能
UIP需要为运行UIP的体系结构专门实现几个功能。这些函数应该针对特定的体系结构进行手工调整,但是一般的C实现是UIP发行版的一部分。

校验和计算
TCP和IP协议实现了包含TCP和IP数据包的数据和报头部分的校验和。由于此校验和的计算是对发送和接收的每个数据包中的所有字节进行的,因此计算校验和的函数是有效的。最常见的情况是,这意味着校验和计算必须针对运行UIP堆栈的特定体系结构进行微调。

虽然uip包含一个泛型校验和函数,但它也为这两个函数的特定于体系结构的实现保留了一个打开状态。uip_ipchksum()和uip_tcpchksum()…这些函数中的校验和计算可以用高度优化的汇编程序(而不是泛型C代码)编写。

32位算法
TCP协议使用32位序列号,作为正常协议处理的一部分,TCP实现必须进行一些32位的添加。由于32位算法在许多UIP打算使用的平台上本机不可用,因此uip将32位添加留给特定于体系结构的模块来实现,并且不使用主代码库中的任何32位算法。

虽然uip实现了一个通用的32位加法,但是支持有一个特定于体系结构的实现。UIP_add32()功能。

内存管理
在UIP的体系结构中,RAM是最稀缺的资源。由于只有几千字节的RAM可供TCP/IP堆栈使用,因此不能直接应用传统TCP/IP中使用的机制。

UIP堆栈不使用显式动态内存分配。相反,它使用一个全局缓冲区来保存数据包,并且有一个固定的表来保存连接状态。全局数据包缓冲区足够大,足以包含一个最大大小的数据包。当数据包从网络到达时,设备驱动程序将其放入全局缓冲区并调用TCP/IP堆栈。如果数据包包含数据,TCP/IP堆栈将通知相应的应用程序。由于缓冲区中的数据将被下一个传入数据包覆盖,因此应用程序要么必须立即对数据采取行动,要么将数据复制到辅助缓冲区中以便稍后处理。在应用程序处理数据之前,数据包缓冲区不会被新数据包覆盖。当应用程序处理数据时到达的数据包必须由网络设备或设备驱动程序排队。大多数单片以太网控制器都有足够大的片上缓冲区,至少可以容纳4个最大大小的以太网帧。由处理器处理的设备,如RS-232端口,可以在应用程序处理期间将传入的字节复制到单独的缓冲区中。如果缓冲区已满,则丢弃传入的数据包。这将导致性能下降,但仅当多个连接并行运行时。这是因为UIP广告的接收窗口非常小,这意味着每个连接的网络中只有一个TCP段。

在UIP中,用于传入数据包的相同的全局数据包缓冲区也用于传出数据的TCP/IP报头。如果应用程序发送动态数据,它可以使用全局数据包缓冲区中未用于报头的部分作为临时存储缓冲区。要发送数据,应用程序将指向数据的指针以及数据的长度传递给堆栈。TCP/IP报头被写入全局缓冲区,一旦产生报头,设备驱动程序就将报头和应用程序数据发送到网络上。数据没有排队等待重传。相反,如果需要重传,应用程序将不得不复制数据。

UIP的内存使用总量在很大程度上取决于要运行实现的特定设备的应用程序。内存配置决定了系统应该能够处理的通信量和同时连接的最大数量。一个设备将发送大量电子邮件,同时运行一个具有高度动态网页和多个同时客户端的Web服务器,它将比简单的Telnet服务器需要更多的RAM。只需200字节的RAM就可以运行UIP实现,但是这样的配置将提供极低的吞吐量,并且只允许少量的同时连接。

应用程序接口(API)
应用程序接口(API)定义了应用程序与TCP/IP堆栈的交互方式。TCP/IP最常用的API是BSD套接字API,它在大多数Unix系统中使用,严重影响了MicrosoftWindowsWinSock API。由于套接字API使用停止和等待语义,它需要底层多任务操作系统的支持。由于任务管理、上下文切换和任务堆栈空间分配的开销在预期的UIP目标体系结构中可能过高,因此BSD套接字接口不适合我们的目的。

uip为程序员提供了两个api:protosocket,一个类似bsd套接字的api,没有完全多线程的开销,以及一个基于“原始”事件的api,它比protosocket级别低,但使用的内存更少。

另见:
Protosocket库
原螺纹
UIP原始API
“RAW”UIPAPI使用事件驱动的接口,其中应用程序在响应某些事件时被调用。运行在UIP之上的应用程序作为一个C函数实现,UIP响应某些事件调用该函数。UIP在接收到数据、数据已成功地传递到连接的另一端、新连接已经设置或数据必须重新传输时调用应用程序。还定期对应用程序进行新数据的轮询。应用程序只提供一个回调函数,由应用程序处理将不同网络服务映射到不同端口和连接的问题。由于应用程序能够在TCP/IP堆栈接收到数据包时立即处理传入数据和连接请求,因此即使在低端系统中也可以实现低响应时间。

UIP不同于其他TCP/IP堆栈,因为它在重传时需要应用程序的帮助。其他TCP/IP堆栈将传输的数据缓冲在内存中,直到已知数据成功地传递到连接的远程端为止。如果需要重新传输数据,堆栈将在不通知应用程序的情况下处理重传。使用这种方法,数据必须在内存中缓冲,同时等待确认,即使如果必须重新传输,应用程序可能能够快速重新生成数据。

为了减少内存使用,UIP利用了应用程序可以重新生成发送的数据并允许应用程序参与重传的事实。UIP不跟踪设备驱动程序发送的数据包内容,UIP要求应用程序主动参与重传。当UIP决定重新传输一个段时,它使用一个标志集调用应用程序,该标志集指示需要重新传输。应用程序检查重传标志,并生成与以前发送的数据相同的数据。从应用程序的角度来看,执行重传与最初发送数据的方式没有什么不同。因此,可以以这样的方式编写应用程序,即将相同的代码用于发送数据和重传数据。另外,需要注意的是,即使实际的重传操作是由应用程序执行的,堆栈也有责任知道何时应该进行重传。因此,应用程序的复杂性不一定增加,因为它在重传中起着积极的作用。

应用事件
应用程序必须实现为C函数,UIP_APPCALL(),每当发生事件时,UIP都会调用。每个事件都有一个相应的测试函数,用于区分不同的事件。这些函数被实现为C宏,计算值为零或非零。请注意,某些事件可以同时发生(例如,新数据可以在确认数据的同时到达)。

连接指针
当应用程序被UIP调用时,全局变量UIP_CON设置为指向UIP_CON结构,用于当前处理的连接,称为“当前连接”。的田野UIP_CON可以使用当前连接的结构,例如区分不同的服务或检查连接到哪个IP地址。一个典型的用途是检查uip_conn->lport(本地TCP端口号),以决定连接应该提供哪些服务。例如,如果uip_conn->lport的值等于80,应用程序可能决定充当HTTP服务器;如果值为23,应用程序可能决定充当Telnet服务器。

接收数据
如果UIP测试函数UIP_newdata()为非零,连接的远程主机已发送新数据.uip_AppData指针指向实际数据。数据的大小是通过uip函数获得的。UIP_datalen()…数据不是由UIP缓冲的,而是在应用程序函数返回后被覆盖,因此应用程序必须直接对传入的数据采取行动,或者自己将传入的数据复制到缓冲区中以供以后处理。

发送数据
在发送数据时,UIP根据可用的缓冲区空间和接收方公布的当前TCP窗口调整应用程序发送的数据的长度。缓冲区空间的大小由内存配置决定。因此,从应用程序发送的所有数据都可能没有到达接收方,并且应用程序可以使用UIP_MSS()函数查看堆栈实际发送的数据。

应用程序使用UIP函数发送数据。UIP_Send()…这个UIP_Send()函数有两个参数:指向要发送的数据的指针和数据的长度。如果应用程序需要RAM空间来生成应该发送的实际数据,则可以为此使用数据包缓冲区(由uip_AppData指针指向)。

应用程序一次只能在连接上发送一个数据块,因此无法调用。UIP_Send()每次应用程序调用不止一次;只发送上次调用的数据。

重传数据
重传由周期性TCP定时器驱动。每次调用周期性计时器时,每个连接的重传定时器都会减少。如果计时器达到零,则应进行重传。由于UIP不跟踪设备驱动程序发送的数据包内容,因此UIP要求应用程序积极参与重传。当UIP决定重新传输一个段时,应用程序函数将使用uip_rexmit()标志集,指示需要重传。

应用程序必须检查uip_rexmit()标记并生成与以前发送的数据相同的数据。从应用程序的角度来看,执行重传与最初发送数据的方式没有什么不同。因此,应用程序的编写方式可以使相同的代码既用于发送数据,又用于重传数据。另外,需要注意的是,即使实际的重传操作是由应用程序执行的,堆栈也有责任知道何时应该进行重传。因此,应用程序的复杂性不一定增加,因为它在重传中起着积极的作用。

合闸连接
应用程序通过调用UIP_CLOSE()在应用程序调用过程中。这将导致连接被彻底关闭。为了指示致命错误,应用程序可能希望中止连接,并通过调用UIP_ABORT()功能。

如果连接已被远程端关闭,则测试函数UIP_CLOSE()是真的。然后,应用程序可以进行任何必要的清理。

报告错误
连接可能发生两个致命错误,要么是连接被远程主机中止,要么是连接多次重新传输最后一个数据并已中止。UIP通过调用应用程序函数来报告这一点。应用程序可以使用这两个测试函数。UIP_ABORT()和UIP_timedout()测试这些错误条件。

轮询
当连接空闲时,UIP每次周期性计时器触发时都会轮询应用程序。应用程序使用测试函数。UIP_ROUP()以检查UIP是否正在对其进行调查。

轮询事件有两个目的。第一种方法是让应用程序周期性地知道连接是空闲的,这允许应用程序关闭已经闲置太久的连接。另一个目的是让应用程序发送已经生成的新数据。应用程序只能在UIP调用时发送数据,因此轮询事件是在其他空闲连接上发送数据的唯一方法。

监听港口
UIP维护一个监听TCP端口的列表。将打开一个新端口,用于侦听UIP_LISK()功能。当连接请求到达侦听端口时,UIP创建一个新连接并调用应用程序函数。测试函数UIP_Connected()如果由于创建了新连接而调用应用程序,则为true。

应用程序可以在UIP_CON结构检查新连接连接到哪个端口。

开口连接
函数可以从UIP内部打开新的连接。UIP_CONNECT()…此函数分配一个新连接,并在连接状态下设置一个标志,该标志将在下次UIP轮询连接时打开到指定IP地址和端口的TCP连接。这个UIP_CONNECT()函数返回指向UIP_CON新连接的结构。如果没有空闲连接槽,则函数返回NULL。

struct uip_conn* uip_connect(uip_ipaddr_t  *ripaddr,uint16_t port)

使用TCP连接到远程主机。

此函数用于启动到指定主机上指定端口的新连接。它分配一个新的连接标识符,将连接设置为SYN_SINT状态,并将重传计时器设置为0。这将导致下一次定期处理此连接时发出TCP SYN段,通常在调用UIP_CONNECT

- **注:**

  只有当通过将UIP_ACTIVE_OPEN定义为1配置了对Active OPEN的支持时,此函数才可用uipopt.h

  由于此函数要求端口号按网络字节顺序排列,所以使用UIP_HTONS()uip_hton()是必要的。
 UIP_HTONS()16位数量从主机字节顺序转换为网络字节顺序.
此宏主要用于将常量从主机字节顺序转换为网络字节顺序。若要将变量转换为网络字节顺序,请使用uip_hton()功能代替。
    附:对应函数
	CCIF uint16_t	uip_htons (uint16_t val)
 	Convert a 16-bit quantity from host byte order to network byte order.16位数量从主机字节顺序转换为网络字节顺序
 

 uip_ipaddr_t ipaddr;

 uip_ipaddr(&ipaddr, 192,168,1,2);
 uip_connect(&ipaddr, UIP_HTONS(80));

- **参数:**

  波波远程主机的IP地址。港按网络字节顺序排列的16位端口号。

- **返回:**

  指向新连接的UIP连接标识符的指针,如果无法分配连接,则为NULL

功能uip_ipaddr()可用于将IP地址打包到UIP用来表示IP地址的两个元素16位数组中。

使用的两个例子如下所示。第一个示例演示如何打开当前连接的远程端的TCP端口8080的连接。如果没有足够的tcp连接槽允许打开新连接,则UIP_CONNECT()函数返回NULL,当前连接由UIP_ABORT().

void connect_example1_app(void) {
   
   if(uip_connect(uip_conn->ripaddr, HTONS(8080)) == NULL) {
   
      uip_abort();
   }
}   

第二个示例演示如何打开到特定IP地址的新连接。在此示例中不进行错误检查。

void connect_example2(void) {
   
   uip_addr_t ipaddr;

   uip_ipaddr(ipaddr, 192,168,0,1);
   uip_connect(ipaddr, HTONS(8080));
}

实例
本节介绍了许多非常简单的UIP应用程序。UIP代码发行版包含几个更复杂的应用程序。

一个非常简单的应用程序
第一个例子展示了一个非常简单的应用程序。应用程序侦听端口1234上的传入连接。建立连接后,应用程序将回复发送给它的所有数据,即“OK”。

此应用程序的实现如下所示。应用程序由调用example1_init()的函数初始化,UIP回调函数称为example1_app()。对于这个应用程序,配置变量UIP_APPCALL应该定义为example1_app()。

void example1_init(void) {
   
   uip_listen(HTONS(1234));
}
void example1_app(void) {
   
   if(uip_newdata() || uip_rexmit()) {
   
      uip_send("ok\n", 3);
   }
}

初始化函数调用UIP函数。UIP_LISK()若要注册侦听端口,请执行以下操作。实际应用程序函数example1_app()使用测试函数UIP_newdata()和uip_rexmit()来确定为什么叫它。如果应用程序是因为远程端发送了数据而被调用的,它会用“ok”进行响应。如果由于网络中数据丢失而调用了应用程序函数,并且必须重新传输,那么它也会发送一个“ok”。注意,这个示例实际上显示了一个完整的UIP应用程序。应用程序不需要处理所有类型的事件,例如UIP_Connected()或UIP_timedout().

更高级的应用程序
第二个示例比前一个示例稍微高级一些,并展示了UIP_CON结构使用。

该应用程序类似于第一个应用程序,因为它侦听传入连接的端口,并通过一个“ok”响应发送给它的数据。最大的区别是这个应用程序输出了一个欢迎的“欢迎!”在建立连接时发出消息。

这个看似很小的操作更改对应用程序的实现方式产生了很大的影响。复杂性增加的原因是,如果数据在网络中丢失,应用程序必须知道要重传哪些数据。如果“欢迎!”消息丢失,应用程序必须重新发送欢迎,如果“ok”消息之一丢失,应用程序必须发送一个新的“ok”。

应用程序知道只要“欢迎!”远程主机尚未确认消息,它可能已被丢弃在网络中。但是,一旦远程主机发送回确认,应用程序就可以确定欢迎已经收到,并且知道任何丢失的数据都必须是“OK”消息。因此,应用程序可以处于两种状态之一:无论是在欢迎发送状态,还是在“欢迎!”已被发送但未被确认,或处于受欢迎状态的“欢迎!”已经被认可了。

当远程主机连接到应用程序时,应用程序发送“欢迎!”信息,并设置它的状态欢迎-发送。当确认欢迎消息时,应用程序将移动到受欢迎的状态。如果应用程序从远程主机接收到任何新数据,它将通过发送“ok”返回来响应。

如果请求应用程序重新传输最后一条消息,它将查看应用程序处于哪个状态。如果应用程序处于欢迎发送状态,则发送“欢迎!”因为它知道以前的欢迎信息还没有被确认。如果应用程序处于欢迎状态,它知道最后一条消息是“ok”消息,并发送这样一条消息。

此应用程序的实现如下所示。应用程序的此配置设置在其实现后遵循。

struct example2_state {
   
   enum {
   WELCOME_SENT, WELCOME_ACKED} state;
};

void example2_init(void) {
   
   uip_listen(HTONS(2345));
}

void example2_app(void) {
   
   struct example2_state *s;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值