首页资源分类嵌入式系统其他 > UIP协议栈解析

UIP协议栈解析

已有 445176个资源

下载专区

文档信息举报收藏

标    签:UIP协议栈

分    享:

文档简介

基于嵌入式网络通信的uip协议栈使用解析

文档预览

 Uip协议栈学习笔记 Lpb 2017-2-21 关于uip协议栈的移植和uip架构,参加uip-stm32移植.pdf和正点原子关于enc28j60网络实验部分即可,这里关于架构部分略作总结: 1):关于ip、arp包的解包打包和tcp、udp(如果用udp)还有arp老化处理函数是固定的“套路”,这即uip协议栈的主要任务处理运输层和网络层并与其它层建立联系的实现,这只要参考程序实现模板即可,与具体应用程序实现无关。 2):我们需要实现的即是应用程序接口UIP_APPCALL()和UIP_UDP_APPCALL() ,前者是基于tcp的,后者是基于udp的。比如调用UIP_APPCALL()时,则当前uip_buf存的就是当前基于tcp的网络数据包,uip_appdata也是指向当前tcp应用数据,完全不必考虑tcp 和udp共用uip_buf的问题。 3):关于我们实现2)应用程序uip协议栈给我们提供的应用程序接口和宏定义都在uip.h中有详细说明,uip协议栈的优点就是其注释很详细,我们不是研究协议栈的实现,而是使用协议栈,只要看懂uip.h即可。 下面是我们写应用层主要需要使用的一些参数和接口(实际上uip.h都有详细的说明,参看源码才是真理),这里做下总结: uip_buf[UIP_BUFSIZE+2];所有的数据处理都是通过处理他来完成的。uip_buf是网卡(数据链路层)发送和接收数据存放的buf,当然,整个协议栈共用这个buf,也就是说uip_buf包含含有网络帧格式的整个数据。(如果我们需要用到解析mac即用到数据链路层内容应用程序可以使用他,否则没必要使用,解析网络格式反而麻烦了) uip_len 或uip_datalen():表示接收到有效数据的字节长度,注意这里即“纯净”应用数据长度。 uip_newdata() : 为真,则表示收到远程主机发来的新数据(无论tcp udp还是http等)。 void *uip_appdata:指向当前接收到的应用数据或者要发送的应用数据,在发送数据时,只需要使uip_appdata指向我们要发送的数据,然后再调用uip_send()即可实现数据发送。实质上其指向了uip_buf的第一个有效数据(应用数据)的地址 struct uip_conn *uip_conn:指向当前tcp连接的指针,参见这个结构体成员我们基本能够得到tcp和ip协议层的全部信息,比如我们应用程序需要用到对方ip或者在多个tcp连接下区分不同tcp连接就需要用到。其中成员uip_tcp_appstate_t appstate是可以读写的且在实践应用中需要重定义(可以用来存储应用程序特定状态),其他项为只读read-only。 /** * Representation of a uIP TCP connection. * * The uip_conn structure is used for identifying a connection. All * but one field in the structure are to be considered read-only by an * application. The only exception is the appstate field whos purpose * is to let the application store application-specific state (e.g., * file pointers) for the connection. The type of this field is * configured in the "uipopt.h" header file. */ struct uip_conn { 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. 远程tcp端口号*/ u8_t rcv_nxt[4]; /**< The sequence number that we expect to receive next. */ u8_t snd_nxt[4]; /**< 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 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. */ /** The application state. */ uip_tcp_appstate_t appstate; }; struct uip_udp_conn *uip_udp_conn:同上类似,指向当前udp连接的指针,实质上其指向了uip_buf的第一个有效数据(应用数据)的地址,同样包含了远程ip、端口,同样uip_udp_appstate_t appstate用于实现存储应用程序特殊状态, 其他项为只读read-only。 /** * Representation of a uIP UDP connection. */ struct uip_udp_conn { 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; /**< Default time-to-live. */ /** The application state. */ uip_udp_appstate_t appstate; }; 1、接收数据: uip_newdata() : 为真,则表示收到远程主机发来的新数据(无论tcp udp还是http等),配合uip_appdata指向实际数据,实际数据长度由uip_len 或uip_datalen()获得。 2、发送数据: TCP发送应用数据:void uip_send(const void *data, int len);指针指向要发送的数据,len即要发送数据的长度。 UDP发送应用数据:uip_udp_send(len),查看这个宏#define uip_udp_send(len) uip_send((char *)uip_appdata, len)可知其实质也是通过给uip_appdata指针指向要发送的数据,再send出去而已。 上述两个函数在一个时间里应用程序只能在连接中发送一块数据,因此不可以在每个应用程序中启用调用其超过1次,因其调用后会改变某些全局变量,因此在调用函数返回前它不能被调用。 3、重发数据:uip_rexmit() 宏为真,表示发送的网络数据丢失,即未正确发出。则应用程序要重新发送上一次发出的数据,重发仍为上述发送,也就是通过2)中的函数发送 4、关闭连接:uip_close() //tcp udp 关闭当前连接,这会导致连接干净的关闭。 另外,还有中止连接操作uip_abort()用来当错误发生时指出致命错误。 另外还有比如uip_closed()若为真表示连接被远端关闭,我们应用程序可以做一些必要的清理工作。 5、报告错误:有两个致命错误可以发生在连接中一种是连接由远程主机中止,另一种是连续重复发送上一次数据而被中止 uip_aborted() 为真表示被远程主机中止连接 uip_timedout() 为真表示由于连续重复发送上一次数据而被中止当前连接 6、轮询:uip_poll() uip协议栈在连接空闲时会周期性的轮询应用程序,我们看下面注释也可知道我们应用程序应使用uip_poll()来检查当前应用程序是否被轮询过,为真即轮询过,空闲状态我们也可用来发送数据而不必考虑远程主机是否在发数据 *Is non-zero if the reason the application is invoked is that the * current connection has been idle for a while and should be * polled. * * The polling event can be used for sending data without having to * wait for the remote host to send data. #define uip_poll() (uip_flags & UIP_POLL) 7、监听端口 void uip_listen(u16_t port);用于tcp连接服务器端 ,如果一个新的连接产生,则应用程序被调用,测试函数uip_connected()为真 8、uip_connect()用于创建一个新的tcp连接,参数为远程连接的ip地址和端口号 9、数据流控制: 函数uip_stop()和uip_restart(),uIP提供存取TCP数据流的控制途径。设想一个应用程序下载数据到一个慢速设备,例如磁盘驱动器。如果磁盘驱动器的作业队列满了,应用程序不会准备从服务器接收更多的数据,直到队列排出空位。函数uip_stop()可以用于维护流控制和停止远程主机发送数据。当应用程序准备好接收更多数据,函数uip_restart()用于告知远程终端再次发送数据。函数uip_stopped()可以用于检查当前连接是否停止。  网络协议 1.ARP地址解析协议:ARP协议映射了IP地址和以太网MAC物理地址,它在以太网上的TCP/IP操作是需要的。ARP在uIP里实现的是包含一个IP到MAC地址的映射。当一个IP包要在以太网上发出,查询ARP表,去找出包要发送去的MAC地址。如果在表里找不到IP地址,ARP请求包就会发出。请求包在网络里广播和请求给出IP地址的MAC地址。主机通过发出一个ARP回应,响应请求IP地址。当uIP给出一个ARP回应,更新ARP表。 为了节省储存器,一个IP地址的ARP请求覆盖发出的请求输出IP包。它是假定上层将重新发送那些被覆盖了的数据。 每十秒表更新一次,旧的条目会被丢弃。默认的ARP表条目生存时间是20分钟。 2.IP网际协议:uIP的IP层代码有两个职责:验证输入包的IP头的正确性和ICMP 和TCP协议之间多路复用。 IP层代码是非常简单的,由9条语句组成。事实上,uIP的IP层极大地简化了,它没有实现碎片和重组。 3.ICMP因特网信息控制协议:在uIP里,只有一种ICMP信息实现了: ICMP回响信息。ICMP回响信息常常用于ping程序里的检查主机是否在线。在uIP里,ICMP回响处理在一个非常简单的方式。ICMP类型字段的改变是从 \echo"类型到 \echo reply"类型,从而 ICMP调整校验和。其次,IP地址里的IP头交换,包发回到原先的发送者。 4.TCP传输控制协议:为了减少储存器的使用,uIP里的TCP没有实现发送和接收数据的调整窗口。输入的TCP段不会通过uIP缓存,但必须立即由应用程序处理。注意这不能避免应用程序自己缓冲数据。输出数据时,uIP不能在每个连接有超过一个未解决的TCP段。 连接状态 在uIP,每个TCP连接的完全态包含当地和远端的TCP端口编号,远程主机的IP地址,重发时间值,上一段重发的编号,和连接的段的最大尺寸。除此之外,每个连接也可以保持一些应用状态。三个序列号是,期望接收的下一个字节的序列号,上一发送段第一字节的序列号,下一发送字节的序列号。连接的状态由uip_conn结构表现,可以在图 5 看到。一个uip_conn结构数组用于在uIP里保持所有的连接。数组的大小等于同时的最大数量的连接,它在编译时间里设置。 struct uip_conn { u8_t tcpstateflags; /* TCP状态和标志. */ u16_t lport, rport; /*当地和远端端口. */ u16_t ripaddr[2]; /*同等远端的IP地址. */ u8_t rcv_nxt[4]; /* 我们期待接收的下一个序列号. */ u8_t snd_nxt[4]; /*上一个发送的序列号. */ u8_t ack_nxt[4]; /* 通过从远端的下一个应答去应答序列号. */ u8_t timer; /* 重发时间r. */ u8_t nrtx; /*计算特殊段的重发数量. */ u8_t mss; /* 连接的最大段大小. */ u8_t appstate[UIP_APPSTATE_SIZE]; }; 图 5: uip_conn 结构 输入处理 TCP输入处理和检验TCP校验和一起开始。如果校验和是对的,在当前活动的TCP连接之间,源、目的端口号和IP地址复用包。没有活动的连接符合输入包时,如果包不是一个监听端口的连接请求,包丢弃。如果包是一个关闭端口的请求,uIP 发一个 RST包回应。 如果发现了一个监听端口,uip_conn结构数组扫描任何一个非活动连接。如果发现一个,数组由新连接的端口号和IP地址填充。 如果连接请求携带一个TCP MSS (最大段大小)选择,它会分析,再次检查当前最大段大小MSS去决定当前连接的MSS,前后者的最小值会被选择。最后,一个回应包发去确应开启连接。 应该将输入包送去一个已经活跃的连接,包的序列号和从远端主机来的期望的下一个序列号一起被检查 (uip_conn结构里的rcv_nxt 变量显示于图 5)。如果序列号不是期望得到的下一个,包会被丢掉和发一个ACK去指出期望得到的下一个序列号。紧接着,检查输入包里的确应号,看看是否确应连接的所有输出数据。它做了后,应用程序会知道这个事实的。 当序列号和确应号被检测过,依靠当前TCP状态,包将会被不同地处理。如果连接在SYN-RCVD状态和输入包确应先前发送的SYNACK包,连接将会输入ESTABLISHED状态,调用应用函数去通知已经完全连接。在连接的建立状态,如果有新数据由远端主机发送或者远端主机确应之前发送的数据,就调用应用函数。 当应用函数返回,TCP检查应用程序是否还有数据要发。如果有,一个TCP/IP包会形成在包缓存里。 输出处理 输出处理过程比输入处理直接和简单得多。 基本上,所有TCP和IP头字段由uip_conn 结构里的值充满,计算TCP和IP的校验和。当uip_process()函数返回,包通过网络设备驱动发出去。 重发 当uIP通过periodic_timer被调用时,重发就进入运作。连接里有些特殊的数据(也就是数据发出了去但仍没有确应的)通过UIP_OUTSTANDING位在uip_conn 结构里的TCP状态标志变量标记。那个连接,时间变量减少。当时间到达零,上一段必须重发和调用应用函数去做真正的重发。如果一个特殊段重发编号超出一个可设置的界限,连接会结束和发一个RST段到远端连接结束,调用应用函数去通知它连接超时。 重置TCP TCP规格要求如果TCP头里的序列号和确应号在当前连接的接收窗口失去了,有RST (复位)标志设置的包必须断开连接。为了减少代码的大小,uIP不严格遵守这个规定。相反,如果一个有RST标志设置的包在连接里到达,连接会消灭那些不重要的序列号和确应号值。这个行为将会在将来的uIP版本修订 配置uIP: uIP的设置隐藏在一个叫uipopt.h的单独头文件里。这个文档不仅包含了那些项目特性的设置选项 (例如uIP网点的IP地址和同时发生连接的最大值),而且有结构和C编译器的特殊选项。文档是独立的和有注释说明的。 应用例子: 应用例子 这节提供一些简单的uIP应用例子 一个简单的应用例子 第一个例子非常简单。应用程序监听输入连接的端口1234。当一个连接建立了,应用程序通过说“OK”回应所有发送给它的数据。 图 6 显示了应用程序的实现。应用程序调用example1_init()初始化,uIP的回叫函数是example1_app(),在这个例子里,可设置的变量UIP_APPCALL应该要定义在example1_app。 初始化函数调用uIP函数uip_listen()去注册一个监听端口。实际的应用函数example1_app()使用测试函数uip _newdata()和uip_rexmit()去确定为什么调用它。如果调用应用程序是因为最远端发了数据给它,它回应一个"ok"。如果调用应用函数是因为数据在网络里丢失和需要重发,它也发送一个 "ok"。 注意,这个例子显示了一个完全的uIP应用。应用程序不需要处理所有类型事件例 如: uip _connected()或uip_timedout()。 void example1_init(void) { uip_listen(1234); } void example1_app(void) { if(uip_newdata() || uip_rexmit()) { uip_send("ok\n", 3); } } 图 6: 一个非常简单的应用程序 一个更高级的应用 第二个例子只是稍微比第一个高级一点,显示了程序中状态段怎样在uip_conn结构中使用。 这个应用程序和第一有些相似,它监听一个输入连接的端口和回应发送给它的数据一个"ok"。最大的不同是当连接建立时,这个程序打印输出一个欢迎信息"Welcome!"。 程序怎样实现,表面上是操作上的小改动产生效果上很大的不同。复杂性增加的原因是如果数据在网络里丢失,程序必须知道那个数据需要重发。如果"Welcome!"信息丢失了,程序必须重发欢迎信息,如果其中一个"ok"信息丢失,程序必须发一个新的"ok"。 程序知道只要"Welcome!"信息没有被远程主机确应,它就可能在网络里丢失了。但一旦远程主机已发了一个确应回来,程序可以应为欢迎信息已经被接收,知道一些丢失的数据是一个"ok"信息。 因此程序可以在两个之中的一个状态:在WELCOME-SENT状态,"Welcome!"已经发出但没有被确应,或者WELCOME-ACKED状态,"Welcome!"已被确应。 当远程主机连接到应用程序时,程序发一个"Welcome!"信息和发它的状态去 WELCOME-SENT。当欢迎信息被确应了,程序进入WELCOME-ACKED状态。如果程序从远程主机收到任何新数据,它回应一个"ok" 。 如果应用程序被请求重发上一条信息,它看程序在那个状态。如果程序在WELCOME-SENT状态,它知道先前的欢迎信息没有被确应,它再发一个"Welcome!"信息。如果程序在 WELCOME-ACKED状态,它知道上一条信息是 "ok" ,只发一条信息。 应用的实现可以在图 7看到。图 8显示了应用的设置 struct example2_state { enum {WELCOME_SENT, WELCOME_ACKED} state; }; void example2_init(void) { uip_listen(2345); } void example2_app(void) { struct example2_state *s; s = (struct example2_state *)uip_conn->appstate; if(uip_connected()) { s->state = WELCOME_SENT; uip_send("Welcome!\n", 9); return; } if(uip_acked() && s->state == WELCOME_SENT) { s->state = WELCOME_ACKED; } if(uip_newdata()) { uip_send("ok\n", 3); } if(uip_rexmit()) { switch(s->state) { case WELCOME_SENT: uip_send("Welcome!\n", 9); break; case WELCOME_ACKED: uip_send("ok\n", 3); break; } } } 图 7: 一个高级应用 #define UIP_APPCALL example2_app #define UIP_APPSTATE_SIZE sizeof(struct example2_state) 图 8: 应用程序设置 区分应用程序 如果系统要运行多个应用程序,区分它们的其中一个技术是使用连接的远程终端或当地终端的TCP端口编号。图 9 显示了怎样将上面的例子结合在一个应用程序里。 void example3_init(void) { example1_init(); example2_init(); } void example3_app(void) { switch(uip_conn->lport) { case htons(1234): example1_app(); break; case htons(2345): example2_app(); break; } } 图 9: 两个应用程序组合和使用不同的当地端口 接收大量数据 这个例子显示了一个简单的程序连接主机,发一个文件的HTTP请求和从慢速设备如磁盘驱动器下载文件。这个显示了怎样使用uIP的流控制函数。程序显示在图 10。 当连接已经建立,一个HTTP请求发去服务器。这是唯一的发送数据,应用程序知道是否需要重发数据,那么请求要重发。所以要合理的将两个事件结合在例子里。 当应用程序从远程主机接收数据,它通过使用函数device_enqueue()发这些数据去设备。要注意的是这个例子假设这个函数复制数据去它自己的缓存。因为在uip_appdata缓存的数据将会被下一个输入数据覆盖。 如果等待设备的队列满了,应用程序通过调用uIP函数uip_stop()停止来之远程主机的数据。应用程序确保它不接收任何数据,直到调用uip_restart()。应用程序轮询事件,用于检查队列是否不在满,如果没满,通过uip_restart()数据流重新开始。 void example4_init(void) { u16_t ipaddr[2]; uip_ipaddr(ipaddr, 192,168,0,1); uip_conn(ipaddr, 80); } void example4_app(void) { if(uip_connected() || uip_rexmit()) { uip_send("GET /file HTTP/1.0\r\nServer:192.186.0.1\r\n\r\n",48); return; } if(uip_newdata()) { device_enqueue(uip_appdata, uip_datalen()); if(device_queue_full()) { uip_stop(); } } if(uip_poll() && uip_stopped()) { if(!device_queue_full()) { uip_restart(); } } } 图 10: 一个接收大量数据的应用程序 一个简单的网络服务器 这个例子显示了一个非常简单的文件服务器程序和使用端口编号去决定发送那个文档。如果文档适当格式化一下,这个简单的程序可以作为一个有静态网页的网络服务器。图 11 显示了实现方法。 应用程序的状态包括一个指向要发送数据的指针和剩下发送数据的大小。当一个远程主机连接去应用程序,当地端口编号用于决定那个文档要发送。第一块数据使用uip_send()发送。由于有监管,至多的最大段字节数据发送了。 应用程序由肯定应答驱动。当数据被确应,新数据可以发送。如果没有数据要发送了,连接使用uip_close()关闭。 struct example5_state { char *adaptor; unsigned int dataleft; }; void example5_init(void) { uip_listen(80); uip_listen(81); } void example5_app(void) { struct example5_state *s; s = (struct example5_state)uip_conn->appstate; if(uip_connected()) { switch(uip_conn->lport) { case htons(80): s->dataptr = data_port_80; s->dataleft = datalen_port_80; break; case htons(81): s->dataptr = data_port_81; s->dataleft = datalen_port_81; break; } uip_send(s->dataptr , uip_mss() < s->dataleft? uip_mss(): s->dataleft); return; } if(uip_acked()) { if(s->dataleft < uip_mss()) { uip_close(); return; } s->dataptr += uip_mss(); s->dataleft -= uip_mss(); uip_send(s->dataptr , uip_mss() < s->dataleft? uip_mss(): s->dataleft); } } 图 11: 一个简单的文件服务器

Top_arrow
回到顶部
EEWORLD下载中心所有资源均来自网友分享,如有侵权,请发送举报邮件到客服邮箱bbs_service@eeworld.com.cn 或通过站内短信息或QQ:273568022联系管理员 高进,我们会尽快处理。