http://b.cnc.qzone.qq.com/cgi-bin/blognew/blog_output_data?uin=40317647&blogid=1228464038&imgdm=imgcache.qq.com&bdm=b.cnc.qzone.qq.com&mode=1&numperpage=15&blogseed=0.05020814931591866&property=GoRE×tamp=1255757003
摘要:为了实现51单片机接入internet,开发基于51单片机的TCP/IP具有重要意义。为此开发了zlIP,它是针对51单片机的特点使用KeilC51编程语言编写的TCP/IP,具有代码量小和兼容BSD套接字(socket)用户接口等特点。zlIP1.0版注重于运行速度,zlIP2.0版注重于用户接口的易用性,以从不同的角度试验在51单片机上实现TCP/IP的特点。通过比较两个版本的优缺点和吸收国内外其它TCP/IP的优点,分析了在单片机上实现TCP/IP的速度、程序大小、内存大小、编译器等特点,并针对这些特点总结和提出多种技巧和方法,并对这些技巧、方法的优缺点进行了分析。最后讲述了几点关键技术:设计清晰的TCP/IP和应用层的接口、采用前后台和多线程程序结构的比较,内存管理方法和防止多余的内存拷贝,实现数据包整序重发和窗口控制等。
关键词:TCP/IP;单片机;zlIP
1 引言
随着网络应用的不断扩大,将各类电子设备接入Internet的需求越来越大。电子设备入网有多种解决方案:例如使用嵌入式系统,如使用ARM+Linux;一些实现TCP/IP的芯片也已经可以获得,例如Analog Devices推出的Internet Modem(1);在51系列单片机运行TCP/IP协议栈等。前两种方案具有良好的性能,而在单片机上实现TCP/IP的方案具有很低的价格,在某些对网络速度要求不高的领域,有广阔的应用前景。
2 TCP/IP在单片机上实现的特点
2.1 速度慢
我们先来了解51单片机网络传输的极限速率。TCP/IP发送过程中主要的运算量集中在三个部分:应用程序将数据拷贝到RAM、计算TCP校验和、将RAM中的数据包拷贝到网络设备的发送缓冲区。对每一个字节数据,两次拷贝大致共使用12×2=24个指令周期;计算TCP校验和使用16个指令周期。采用12M的晶振,最高网络传输速度为25K字节/秒。实际上要比这个速度慢,zlIP第一版速度只有11.752K字节/秒。
为了提高速度可以采用快速的单片机比如Winbond公司的77E58或者AVR单片机,当然还可以提高晶振频率。除此之外还有:使用KeilC时,尽量避免使用Reentrant函数,Reentrant类型的函数比一般函数速度要慢很多,但是某些时候为了程序结构的需要必须使用Reentrant,这就需要在速度和结构之间作一个选择;指针使用“指定存储类型”的指针(memory-specific pointer)(2);精简协议栈去除运算量大但是用处不大的功能,目前zlIP中TCP定时重发时间是固定的,也没有拥塞窗口控制,也没有IP层路由算法;防止数据包的不必要的拷贝;优化计算校验和和内存拷贝函数。
2.2 程序存储空间和外部RAM空间不大
通常TCP/IP协议栈需要大量的RAM来存储需要被应答的TCP包,如果规定时间内没有被应答则重发这个TCP包,被应答以后释放这个TCP包。
为了减小RAM使用量,能否不存储需要被应答的TCP包?(3)。当数据包需要重新发送时,如果能够重新产生数据包所需的数据,那时就可以不存储。例如存在于EEPROM中的html网页。但是这种方法存在以下两个缺点:一,TCPIP和应用层接口变得复杂。当需要重发时,必须需要应用层重新产生数据,实际上将TCP负责的重发机制转移到了应用层。应用层程序编写变得复杂。二,对数据无法重新产生的应用不适用。例如语音采集。
2.3编译器
TCP/IP一般采用C语言或者混合汇编来写。以KeilC516.0编译器为例。与X8086编辑的代码不同,使用KeilC要注意函数重入、指针、函数指针这三个问题。使用可重入函数和一般指针(generic pointer)使得程序代码增大,运行速度变慢。使用函数指针时,要么需要手动重建调用树(Call tree),要么将通过函数指针调用的函数都设置为可重入函数。所以尽量少用重入函数、函数指针和一般指针。
3 zlIP的特点和实现技巧
3.1 特点
其它的TCP/IP有lwIP、uIP、ucIP、tinyTCP等。其中lwIP、uIP、tinyTCP已经成功地移植到了单片机。lwIP是专门为微处理器设计的TCP/IP协议栈,lwIP的功能很全面,但是相对来说代码较大,有人做过移植lwip+ucOSII代码量为60K(4)。uIP侧重于减小代码量(选择AVR为目标器件时,代码为5K左右)和减小RAM使用量(100字节左右)。uIP采用了不保存需要应答的数据包的RAM使用方案,没有和BSD的套接字接口兼容,应用层接口较复杂。zlIP介于uIP和lwIP之间,它针对单片机设计,有中等代码量和RAM使用量,使用套接字的应用层接口,所有的外部变量都使用了xdata类型,全部指针都为明确存储类型的指针,需要重入的函数已经声明为reentant,使用KeilC的小模式下编译。使用12M晶振、KeilC编译器、89C52单片下测试的技术参数如下:
2.0版主要功能有:支持套接字形式的多个TCP连接。支持多个网络设备。支持通过网关发送数据包和数据包转发功能。响应ping命令。支持TCP包的整序、重发和窗口控制流量控制。
3.2 zlIP实现TCP/IP的技巧和方法
3.2.1设计套接字接口
zlIP接口函数基本和BSD的套接字接口相同。提供的用户接口函数有:
*TCPSocket()。函数原型:socket * TCPSocket(IP_ADDR ScrIP)。功能:申请一个套接字。ScrIP是这个套接字的本地IP地址。返回socket类型指针,如果申请失败返回NULL。
*TCPConnect()。函数原型:BOOL TCPConnect(socket * pTCB, IP_ADDR DestIP, WORD DestPort,void (* recv)(void * buf,WORD size),void (* close)(socket * pSocket))。功能:向IP地址为DestIP的服务器的DestPort端口发起连接。参数recv和close用于设置当接收到数据包和对方要求关闭TCP连接时应该调用的回调函数指针。连接成功返回TRUE,否则返回FALSE。
*TCPSend()。函数原型:BOOL TCPSend(socket * pTCB,void *buf,WORD DataSize)。功能:发送数据。发送数据的TCP连接是套接字指针pTCB对应的连接,发送的数据的起始地址为buf,大小为DataSize。发送成功返回TRUE,否则返回FALSE。
*TCPListen()。函数原型:BOOL TCPListen(socket *pTCB,WORD ScrPort,void (* accept)(socket *pNewTCB)) 。功能:使用套接字pTCB在ScrPort端口监听。参数accept是当有客户端向这个监听端口连接成功时调用的回调函数指针。
*TCPClose()。函数原型:void TCPClose(socket *pTCB)。功能:我方主动关闭连接时调用TCPClose函数,它将要求关闭套接字pTCB对应的连接。TCPClose返回以后这个TCP连接可能保持,因为另一方还没有发起关闭请求。
*TCPAbort()。函数原型:void TCPAbort(socket *pTCB)。功能:当使用完这个套接字以后,调用TCPAbort,将这个套接字释放,还给系统。
TCP/IP协议运行中,接收数据包到达、另一方发起关闭连接、另一方向我方发起连接这些事件发生以后如何通知应用层?下面以收到数据包为例提供几种思路:(1)TCP/IP模块设置一个变量bRecv表征是否有数据包到达,应用层必须反复的查询这个变量,如果为TRUE,则调用一个接收函数接收这个数据包。但是这种方法增加了应用层程序的复杂性。(2)固定的回调函数。当TCP层接收到一个数据包后调用OnReceive(pTCB,buf,size)函数。用户必须在应用层定义一个函数名为OnReceive的函数。然后在OnReceive函数中处理接收的数据。(3)回调函数指针。每个套接字保存函数指针recv,接收到数据时TCP调用recv指向的函数。这样每个套接字可以独立定义接收函数,并且函数名可以任意。zlIP使用了第三种思路。它的回调函数指针有:
*recv。原型为:void (* recv)(void * buf,WORD size)。TCP接收到数据包时将调用这个函数。接收的数据的起始地址为buf,大小为size。
*close。原型为:void (* close)(socket * pSocket)。TCP发现对方想关闭连接时调用这个函数。pSocket指出了是哪个连接。
accept。原型为:void (* accept)(socket *pNewTCB)。TCP发现另一方成功连接到我方某个端口时调用这个函数。pNewTCB是将要接管这个TCP连接的套接字指针。在accept()函数中还要设置pNewTCB的回调函数指针recv和close。
3.2.2 zlIP的输入输出流程简介
和其它的多数TCP/IP协议一样,zlIP采用了协议分层的结构。分为应用层、TCP层、IP层和网络设备接口层。图1描述了zlIP输入和输出数据包的流程以及需要调用的函数。输出时,TCP层先查看unsend队列,发现非空,将数据包插入队列;发现为空,则查看对方窗口是否够大能够接收这个数据包,然后填写TCP头部信息。IP层需要选择一个网络设备接口,选择的方法是:目的IP和该接口的子网掩码相与是否等于子网掩码。然后调用这个接口的Output函数来发送。zlIP提供了NetIfAdd()函数,可以动态添加网络设备接口。输入时,Timer()函数调用每个接口的Input函数。IP层判断IP版本、IP校验和、判断是否应该转发数据包,然后根据IP头部的protocol字段将包传给相应的高层处理。TCP层,需要判断TCP校验和,然后在现有的套接字中查找,判断是否有套接字可以接收这个数据包,判断TCP序号是否为希望的,然后更新这个连接的状态(包括释放被应答的数据包和TCP状态机的转化等),然后调用该套接字的回调函数recv。需要强调一下,如果接收的TCP的序号不在我方滑动窗口内,那么应该马上发送一个TCP应答包,因为这很可能是我方发送的应答包丢失了,我方接收的数据包是对方重发的TCP包。
3.2.3 单片机上实现TCP/IP的两种程序结构
从图1可以看到,右方有一个Timer()函数。它的一个功能是调用TCPTimer(),TCPTimer用于处理TCP数据包的重发等功能。另一个功能是调用每个接口的Input()函数接收到达的数据包。Timer()函数必须在短时间(一般20ms)内被调用一次,否则接收数据包和TCP定时等功能将停止。Timer()函数的调用有两种方式查询方式和中断方式,Timer()函数的不同调用方式决定了两种程序结构。
(1)前后台程序结构(5)
查询方式的调用对应前后台程序结构。实现方法是:设置一变量bTimerOut,在定时中断中将bTimerOut设置为真,应用层在程序流程中反复查询bTimerOut是否为真,真则调用Timer(),然后置bTimerOut为假。程序主流程必须是类似图2的形式:程序主流程是一个大循环,在循环中处理发送数据包等应用层协议同时查询bTimeOut。
缺点:由于Timer()必须在短时间内被反复调用,这就要求大循环循环一次的时间要在20ms以内。这给应用程序的编写带来了限制,例如有时程序可能需要在大循环中等待键盘按下,但是这里这种长时间的等待是不允许的。
(2)多线程程序结构
另一种方案是使用多线程。Timer()函数会自动地每隔20ms被调用一次。实现多线程有两种方法:① 在单片机的定时中断中调用Timer函数;② 使用操作系统。
缺点:多线程程序结构解决了前后台程序的缺点。应用程序再也不用套用固定的程序格式。但是,这是有代价的。使用多线程,这就意味着某些函数可能被重入,这些函数必须定义为reentrant类型,从而降低了运行速度。
多线程结构还要注意网络设备驱动函数被重入的问题。以NE2K的以太网卡驱动为例,拷贝数据包到网卡缓存前要先设置寄存器(例如起始地址),然后开始拷贝。如果设置完寄存器以后中断发生,并且驱动函数被重入,那么寄存器的设置被修改,中断返回以后拷贝将出错。可以使用禁止中断、全局标志位、信号量等方法防止重入。
3.2.4内存管理方法和无多余数据包拷贝的实现
TCP/IP的内存的管理方法这里介绍两种:分页方法和链表方法。
(1)分页方法(6):内存划分为多个128字节大小的小页和少量1536字节大小的大页。一个页分配给一个数据包。用一个数组memFlag记录各个内存页是否已经被分配。分配内存的时候只要查找数组membFlag,以获得一个空闲的内存页。为了提高查找的效率,可以将每次查找的起始页设为上次找到的空闲页的下一个页。释放时,将memFlag相应的元素置为FALSE。在协议层之间传送数据包只要传送页的序号就可以了。这种内存管理方法,分配和释放内存的速度较快。但是由于页的大小固定,不能和数据包大小相适应,造成内存的浪费。
(2)链表方法:链表方法根据数据包大小分配相应大小的内存块。如图3所示,链表将内存块链接起来,used字段表示该内存块是否正在使用,pSstart和pEend表示数据部分有效数据的开始地址和结束地址。分配时,搜索内存链表找到一个没有分配的比所需空间大的内存块,截取所需的大小。该内存块被截取以后可能还有较多剩余,这时将剩余部分从原内存块中分离出来,成为一个新的内存块,并插入链表。释放时,将used置为假,如果pNext或者pPre指向的内存块也是空闲的,将其和自己合并,以防止内存分片(7)。在协议层之间传送数据包只要传送内存块的起始地址就可以了。这种内存管理方法空间浪费小但是运算量相对较大。
无数据包拷贝是指除了获得数据到RAM和数据包到网络设备发送缓存这两次拷贝外没有数据包的拷贝。这节省了拷贝时间。介绍两种实现方法:
(1)链表方式:例如当应用层将DataSize大小的应用层数据交给TCP层发送,一般的做法是申请一个DataSize+TCPHeadSize大小的内存然后填写TCP头部,并将数据包拷贝到TCP的载荷中。使用链表方式:TCP层只申请TCPHeadSize大小的内存,然后将这个TCPHead用链表连接到应用层数据。这种方式缺点是:同一个数据包的内存不连续,这加大了计算校验和内存释放的复杂度,运算量大。
(2)预留空间方式:应用层为DataSize大小的数据包申请内存的时候,实际申请的是DataSize+AllHeadSize,其中AllHeadSize表示所有协议头部大小总和。拷贝应用层数据时在其前面留出AllHeadSize大小的空余空间。内存块头部的pStart指示了程序所在层的有效数据的开始,例如在应用层时指向应用层数据包的开始地址。应用层将这个数据包传给TCP层以后,TCP层只要在pStart-TCPHeadSize开始的内存空间加一个TCP头部即可。这种方式运算量很小,但是应用层必须事先知道其底层的协议头大小之和,违反了下层协议和上层无关的要求。
3.2.5如何实现整序、重发和窗口控制
zlIP使用了队列缓存的方式来实现。这里队列的一个元素指向一个数据包,队列的最大长度没有限制。对于整序,使用ooSeq队列(7),如果发现接收的TCP包序号并不是希望的,但是序号在接收窗口内,此时我们不能立刻接收这个包也不应丢弃,先将这个包放入ooSeq队列。每当,一个希望的TCP包被接收以后,再查看ooSeq队列现在是否有TCP包成为了希望的数据包,如果有则将其取出并处理。对于重发,使用unacked队列,每一个需要被应答的TCP数据包发送以后都要放入unacked队列,等到被应答以后才从队列中删除。TCP重发定时只针对unacked队列第一个TCP包,如果定时超出,重新发送,重发次数超出规定值,则报错。对于窗口控制,使用unsend队列,如果发现对方的窗口过小无法接收这个数据包,则只发送部分数据,将多余部分放入unsend队列,等待对方发来TCP包通知新的窗口大小时,再次判断是否可以发送了。如果在unsend队列不为空的情况下,我方应用层传来需要发送的数据包都应插入unsend队列。我方的TCP窗口的大小就是剩余内存空间的大小。
3.2.6 捎带应答的实现
捎带应答指的是,当对方一个需要应答的TCP包到达时,我方不马上给予应答,而是等待一个较短的时间。如果在这段时间内,我方有数据发送,则会捎带给予了应答,这减少了包的发送数量。
摘要:为了实现51单片机接入internet,开发基于51单片机的TCP/IP具有重要意义。为此开发了zlIP,它是针对51单片机的特点使用KeilC51编程语言编写的TCP/IP,具有代码量小和兼容BSD套接字(socket)用户接口等特点。zlIP1.0版注重于运行速度,zlIP2.0版注重于用户接口的易用性,以从不同的角度试验在51单片机上实现TCP/IP的特点。通过比较两个版本的优缺点和吸收国内外其它TCP/IP的优点,分析了在单片机上实现TCP/IP的速度、程序大小、内存大小、编译器等特点,并针对这些特点总结和提出多种技巧和方法,并对这些技巧、方法的优缺点进行了分析。最后讲述了几点关键技术:设计清晰的TCP/IP和应用层的接口、采用前后台和多线程程序结构的比较,内存管理方法和防止多余的内存拷贝,实现数据包整序重发和窗口控制等。
关键词:TCP/IP;单片机;zlIP
1 引言
随着网络应用的不断扩大,将各类电子设备接入Internet的需求越来越大。电子设备入网有多种解决方案:例如使用嵌入式系统,如使用ARM+Linux;一些实现TCP/IP的芯片也已经可以获得,例如Analog Devices推出的Internet Modem(1);在51系列单片机运行TCP/IP协议栈等。前两种方案具有良好的性能,而在单片机上实现TCP/IP的方案具有很低的价格,在某些对网络速度要求不高的领域,有广阔的应用前景。
2 TCP/IP在单片机上实现的特点
2.1 速度慢
我们先来了解51单片机网络传输的极限速率。TCP/IP发送过程中主要的运算量集中在三个部分:应用程序将数据拷贝到RAM、计算TCP校验和、将RAM中的数据包拷贝到网络设备的发送缓冲区。对每一个字节数据,两次拷贝大致共使用12×2=24个指令周期;计算TCP校验和使用16个指令周期。采用12M的晶振,最高网络传输速度为25K字节/秒。实际上要比这个速度慢,zlIP第一版速度只有11.752K字节/秒。
为了提高速度可以采用快速的单片机比如Winbond公司的77E58或者AVR单片机,当然还可以提高晶振频率。除此之外还有:使用KeilC时,尽量避免使用Reentrant函数,Reentrant类型的函数比一般函数速度要慢很多,但是某些时候为了程序结构的需要必须使用Reentrant,这就需要在速度和结构之间作一个选择;指针使用“指定存储类型”的指针(memory-specific pointer)(2);精简协议栈去除运算量大但是用处不大的功能,目前zlIP中TCP定时重发时间是固定的,也没有拥塞窗口控制,也没有IP层路由算法;防止数据包的不必要的拷贝;优化计算校验和和内存拷贝函数。
2.2 程序存储空间和外部RAM空间不大
通常TCP/IP协议栈需要大量的RAM来存储需要被应答的TCP包,如果规定时间内没有被应答则重发这个TCP包,被应答以后释放这个TCP包。
为了减小RAM使用量,能否不存储需要被应答的TCP包?(3)。当数据包需要重新发送时,如果能够重新产生数据包所需的数据,那时就可以不存储。例如存在于EEPROM中的html网页。但是这种方法存在以下两个缺点:一,TCPIP和应用层接口变得复杂。当需要重发时,必须需要应用层重新产生数据,实际上将TCP负责的重发机制转移到了应用层。应用层程序编写变得复杂。二,对数据无法重新产生的应用不适用。例如语音采集。
2.3编译器
TCP/IP一般采用C语言或者混合汇编来写。以KeilC516.0编译器为例。与X8086编辑的代码不同,使用KeilC要注意函数重入、指针、函数指针这三个问题。使用可重入函数和一般指针(generic pointer)使得程序代码增大,运行速度变慢。使用函数指针时,要么需要手动重建调用树(Call tree),要么将通过函数指针调用的函数都设置为可重入函数。所以尽量少用重入函数、函数指针和一般指针。
3 zlIP的特点和实现技巧
3.1 特点
其它的TCP/IP有lwIP、uIP、ucIP、tinyTCP等。其中lwIP、uIP、tinyTCP已经成功地移植到了单片机。lwIP是专门为微处理器设计的TCP/IP协议栈,lwIP的功能很全面,但是相对来说代码较大,有人做过移植lwip+ucOSII代码量为60K(4)。uIP侧重于减小代码量(选择AVR为目标器件时,代码为5K左右)和减小RAM使用量(100字节左右)。uIP采用了不保存需要应答的数据包的RAM使用方案,没有和BSD的套接字接口兼容,应用层接口较复杂。zlIP介于uIP和lwIP之间,它针对单片机设计,有中等代码量和RAM使用量,使用套接字的应用层接口,所有的外部变量都使用了xdata类型,全部指针都为明确存储类型的指针,需要重入的函数已经声明为reentant,使用KeilC的小模式下编译。使用12M晶振、KeilC编译器、89C52单片下测试的技术参数如下:
2.0版主要功能有:支持套接字形式的多个TCP连接。支持多个网络设备。支持通过网关发送数据包和数据包转发功能。响应ping命令。支持TCP包的整序、重发和窗口控制流量控制。
3.2 zlIP实现TCP/IP的技巧和方法
3.2.1设计套接字接口
zlIP接口函数基本和BSD的套接字接口相同。提供的用户接口函数有:
*TCPSocket()。函数原型:socket * TCPSocket(IP_ADDR ScrIP)。功能:申请一个套接字。ScrIP是这个套接字的本地IP地址。返回socket类型指针,如果申请失败返回NULL。
*TCPConnect()。函数原型:BOOL TCPConnect(socket * pTCB, IP_ADDR DestIP, WORD DestPort,void (* recv)(void * buf,WORD size),void (* close)(socket * pSocket))。功能:向IP地址为DestIP的服务器的DestPort端口发起连接。参数recv和close用于设置当接收到数据包和对方要求关闭TCP连接时应该调用的回调函数指针。连接成功返回TRUE,否则返回FALSE。
*TCPSend()。函数原型:BOOL TCPSend(socket * pTCB,void *buf,WORD DataSize)。功能:发送数据。发送数据的TCP连接是套接字指针pTCB对应的连接,发送的数据的起始地址为buf,大小为DataSize。发送成功返回TRUE,否则返回FALSE。
*TCPListen()。函数原型:BOOL TCPListen(socket *pTCB,WORD ScrPort,void (* accept)(socket *pNewTCB)) 。功能:使用套接字pTCB在ScrPort端口监听。参数accept是当有客户端向这个监听端口连接成功时调用的回调函数指针。
*TCPClose()。函数原型:void TCPClose(socket *pTCB)。功能:我方主动关闭连接时调用TCPClose函数,它将要求关闭套接字pTCB对应的连接。TCPClose返回以后这个TCP连接可能保持,因为另一方还没有发起关闭请求。
*TCPAbort()。函数原型:void TCPAbort(socket *pTCB)。功能:当使用完这个套接字以后,调用TCPAbort,将这个套接字释放,还给系统。
TCP/IP协议运行中,接收数据包到达、另一方发起关闭连接、另一方向我方发起连接这些事件发生以后如何通知应用层?下面以收到数据包为例提供几种思路:(1)TCP/IP模块设置一个变量bRecv表征是否有数据包到达,应用层必须反复的查询这个变量,如果为TRUE,则调用一个接收函数接收这个数据包。但是这种方法增加了应用层程序的复杂性。(2)固定的回调函数。当TCP层接收到一个数据包后调用OnReceive(pTCB,buf,size)函数。用户必须在应用层定义一个函数名为OnReceive的函数。然后在OnReceive函数中处理接收的数据。(3)回调函数指针。每个套接字保存函数指针recv,接收到数据时TCP调用recv指向的函数。这样每个套接字可以独立定义接收函数,并且函数名可以任意。zlIP使用了第三种思路。它的回调函数指针有:
*recv。原型为:void (* recv)(void * buf,WORD size)。TCP接收到数据包时将调用这个函数。接收的数据的起始地址为buf,大小为size。
*close。原型为:void (* close)(socket * pSocket)。TCP发现对方想关闭连接时调用这个函数。pSocket指出了是哪个连接。
accept。原型为:void (* accept)(socket *pNewTCB)。TCP发现另一方成功连接到我方某个端口时调用这个函数。pNewTCB是将要接管这个TCP连接的套接字指针。在accept()函数中还要设置pNewTCB的回调函数指针recv和close。
3.2.2 zlIP的输入输出流程简介
和其它的多数TCP/IP协议一样,zlIP采用了协议分层的结构。分为应用层、TCP层、IP层和网络设备接口层。图1描述了zlIP输入和输出数据包的流程以及需要调用的函数。输出时,TCP层先查看unsend队列,发现非空,将数据包插入队列;发现为空,则查看对方窗口是否够大能够接收这个数据包,然后填写TCP头部信息。IP层需要选择一个网络设备接口,选择的方法是:目的IP和该接口的子网掩码相与是否等于子网掩码。然后调用这个接口的Output函数来发送。zlIP提供了NetIfAdd()函数,可以动态添加网络设备接口。输入时,Timer()函数调用每个接口的Input函数。IP层判断IP版本、IP校验和、判断是否应该转发数据包,然后根据IP头部的protocol字段将包传给相应的高层处理。TCP层,需要判断TCP校验和,然后在现有的套接字中查找,判断是否有套接字可以接收这个数据包,判断TCP序号是否为希望的,然后更新这个连接的状态(包括释放被应答的数据包和TCP状态机的转化等),然后调用该套接字的回调函数recv。需要强调一下,如果接收的TCP的序号不在我方滑动窗口内,那么应该马上发送一个TCP应答包,因为这很可能是我方发送的应答包丢失了,我方接收的数据包是对方重发的TCP包。
3.2.3 单片机上实现TCP/IP的两种程序结构
从图1可以看到,右方有一个Timer()函数。它的一个功能是调用TCPTimer(),TCPTimer用于处理TCP数据包的重发等功能。另一个功能是调用每个接口的Input()函数接收到达的数据包。Timer()函数必须在短时间(一般20ms)内被调用一次,否则接收数据包和TCP定时等功能将停止。Timer()函数的调用有两种方式查询方式和中断方式,Timer()函数的不同调用方式决定了两种程序结构。
(1)前后台程序结构(5)
查询方式的调用对应前后台程序结构。实现方法是:设置一变量bTimerOut,在定时中断中将bTimerOut设置为真,应用层在程序流程中反复查询bTimerOut是否为真,真则调用Timer(),然后置bTimerOut为假。程序主流程必须是类似图2的形式:程序主流程是一个大循环,在循环中处理发送数据包等应用层协议同时查询bTimeOut。
缺点:由于Timer()必须在短时间内被反复调用,这就要求大循环循环一次的时间要在20ms以内。这给应用程序的编写带来了限制,例如有时程序可能需要在大循环中等待键盘按下,但是这里这种长时间的等待是不允许的。
(2)多线程程序结构
另一种方案是使用多线程。Timer()函数会自动地每隔20ms被调用一次。实现多线程有两种方法:① 在单片机的定时中断中调用Timer函数;② 使用操作系统。
缺点:多线程程序结构解决了前后台程序的缺点。应用程序再也不用套用固定的程序格式。但是,这是有代价的。使用多线程,这就意味着某些函数可能被重入,这些函数必须定义为reentrant类型,从而降低了运行速度。
多线程结构还要注意网络设备驱动函数被重入的问题。以NE2K的以太网卡驱动为例,拷贝数据包到网卡缓存前要先设置寄存器(例如起始地址),然后开始拷贝。如果设置完寄存器以后中断发生,并且驱动函数被重入,那么寄存器的设置被修改,中断返回以后拷贝将出错。可以使用禁止中断、全局标志位、信号量等方法防止重入。
3.2.4内存管理方法和无多余数据包拷贝的实现
TCP/IP的内存的管理方法这里介绍两种:分页方法和链表方法。
(1)分页方法(6):内存划分为多个128字节大小的小页和少量1536字节大小的大页。一个页分配给一个数据包。用一个数组memFlag记录各个内存页是否已经被分配。分配内存的时候只要查找数组membFlag,以获得一个空闲的内存页。为了提高查找的效率,可以将每次查找的起始页设为上次找到的空闲页的下一个页。释放时,将memFlag相应的元素置为FALSE。在协议层之间传送数据包只要传送页的序号就可以了。这种内存管理方法,分配和释放内存的速度较快。但是由于页的大小固定,不能和数据包大小相适应,造成内存的浪费。
(2)链表方法:链表方法根据数据包大小分配相应大小的内存块。如图3所示,链表将内存块链接起来,used字段表示该内存块是否正在使用,pSstart和pEend表示数据部分有效数据的开始地址和结束地址。分配时,搜索内存链表找到一个没有分配的比所需空间大的内存块,截取所需的大小。该内存块被截取以后可能还有较多剩余,这时将剩余部分从原内存块中分离出来,成为一个新的内存块,并插入链表。释放时,将used置为假,如果pNext或者pPre指向的内存块也是空闲的,将其和自己合并,以防止内存分片(7)。在协议层之间传送数据包只要传送内存块的起始地址就可以了。这种内存管理方法空间浪费小但是运算量相对较大。
无数据包拷贝是指除了获得数据到RAM和数据包到网络设备发送缓存这两次拷贝外没有数据包的拷贝。这节省了拷贝时间。介绍两种实现方法:
(1)链表方式:例如当应用层将DataSize大小的应用层数据交给TCP层发送,一般的做法是申请一个DataSize+TCPHeadSize大小的内存然后填写TCP头部,并将数据包拷贝到TCP的载荷中。使用链表方式:TCP层只申请TCPHeadSize大小的内存,然后将这个TCPHead用链表连接到应用层数据。这种方式缺点是:同一个数据包的内存不连续,这加大了计算校验和内存释放的复杂度,运算量大。
(2)预留空间方式:应用层为DataSize大小的数据包申请内存的时候,实际申请的是DataSize+AllHeadSize,其中AllHeadSize表示所有协议头部大小总和。拷贝应用层数据时在其前面留出AllHeadSize大小的空余空间。内存块头部的pStart指示了程序所在层的有效数据的开始,例如在应用层时指向应用层数据包的开始地址。应用层将这个数据包传给TCP层以后,TCP层只要在pStart-TCPHeadSize开始的内存空间加一个TCP头部即可。这种方式运算量很小,但是应用层必须事先知道其底层的协议头大小之和,违反了下层协议和上层无关的要求。
3.2.5如何实现整序、重发和窗口控制
zlIP使用了队列缓存的方式来实现。这里队列的一个元素指向一个数据包,队列的最大长度没有限制。对于整序,使用ooSeq队列(7),如果发现接收的TCP包序号并不是希望的,但是序号在接收窗口内,此时我们不能立刻接收这个包也不应丢弃,先将这个包放入ooSeq队列。每当,一个希望的TCP包被接收以后,再查看ooSeq队列现在是否有TCP包成为了希望的数据包,如果有则将其取出并处理。对于重发,使用unacked队列,每一个需要被应答的TCP数据包发送以后都要放入unacked队列,等到被应答以后才从队列中删除。TCP重发定时只针对unacked队列第一个TCP包,如果定时超出,重新发送,重发次数超出规定值,则报错。对于窗口控制,使用unsend队列,如果发现对方的窗口过小无法接收这个数据包,则只发送部分数据,将多余部分放入unsend队列,等待对方发来TCP包通知新的窗口大小时,再次判断是否可以发送了。如果在unsend队列不为空的情况下,我方应用层传来需要发送的数据包都应插入unsend队列。我方的TCP窗口的大小就是剩余内存空间的大小。
3.2.6 捎带应答的实现
捎带应答指的是,当对方一个需要应答的TCP包到达时,我方不马上给予应答,而是等待一个较短的时间。如果在这段时间内,我方有数据发送,则会捎带给予了应答,这减少了包的发送数量。
作者:jackxiang@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:http://jackxiang.com/post/2078/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!
评论列表