1.前言 (由于TCP协议复杂,若有解释不对的地方,请毫不客气的提出) 从实用主义的角度出发,学习嵌入式TCPIP可以直接从本章节开始实施,甚至可以直接从HTTP开始学习。我也曾经是一个现实的实用主义者,以为有了AVRNET项目的源码,修改移植之后便可以用于STM32。但是现实总是那么的残酷,对于一个还布熟悉HTML元素,没有任何PHP或者ASP,SOCKET编程知识的我来说,修改AVRNET的任何一行代码都是不可能完成的任务,我几乎不知道修改这行代码造成说明后果。但是通过不断的学习,这些学习包括HTML,CSS,JaveScript,PHP,MySQL,SOCKET等,并不断通过项目积累经验。慢慢的揭开了TCPIP的面纱,而通过TCP让AVR返回Hello是多么令人兴奋的事情。本文将通过分析和整理AVRNET项目源码,实现TCP部分内容,并通过TCP打印Hello和LCD控制实例说明TCP的使用方法,有了TCP的使用基础,那么实现Web服务器将会是小菜一碟。 TCP协议是是一个非常复杂的协议,本文并不会实现TCP的方方面面,毫不避讳的说本文提到的TCP是不完整的TCP,没有TCP坚持定时器和TCP重发功能,更别提滑动窗口功能,TCP的传输速度也低的可怜。但是本文通过最简单的代码实现TCP功能,这些包括TCP建立,TCP数据包发送,TCP关闭连接。 1.1 软件平台硬件平台 Atmega32 + proteus 7.10sp0+WinPcap 编译平台 AVR Studio 6 抓包软件 WireShark 调试工具 网络调试助手 PC机IP:192.168.1.101 AVR IP: 192.168.1.15 AVR TCP默认端口号 3001 1.2 相关资料ENC28J60学习笔记链接 AVRNET学习笔记 ETHERNET和ARP部分 AVRNET学习笔记 IP和ICMP部分 AVRNET学习笔记 UDP部分 AVRNET项目(国外) AVR webserver项目(国外)
2.TCP协议简述 TCP是一个负载的协议,详细的内容请参考以下图书。本小节仅介绍TCP如何建立连接,发送数据和终止连接的技巧,本文也不去讲述TCP首部中的种种字节含义,因为图中都有详细的解释。另外也使用很小的篇幅介绍TCP序号和确认号的使用。 1. TCP IP详解卷1 机械工程出版社出版 2.嵌入式Internet TCP IP基础、实现及应用 北京航空航天出版社 2.1 TCP的建立和终止 在AVRNET项目中暂假定PC机为客户端,AVR为服务器端。在这种情况下,有PC机发起连接,AVR被动的处理整个TCP过程。 1.PC机试图建立连接,发出SYN标志。 2.AVR接收到SYN标志,响应PC机的连接请求,发送SYN和ACK标志 3.PC机收到SYN和ACK标志,发送ACK标志,表示TCP连接建立 4.PC机在建立连接之后,向AVR发送TCP负载数据,并包含PUSH和ACK标志 5.AVR收到负载数据,返回应答数据,并包括FIN、ACK和FUSH标志(FIN意味着AVR试图停止本次TCP连接) 6.PC机对负载数据处理,返回ACK标志 7.PC机也发出FIN标志和ACK标志,同意停止本次TCP连接 8.AVR收到该FIN标志,发出ACK标志,意味着本次TCP通信完全结束。 TCP的建立连接,收发数据和停止连接,配合定时器辅以有限状态机是一个常用的方式,但是实现这些功能需要成千上万行代码,通过分析可以看出。 1.对于FIN标志和SYN标志,无论是服务端还是客户端都需要发送ACK标志 2.AVR共4次收到ACK标志。第一次为上文的第3步,该ACK位的接收表示TCP连接成功,此时负载数据长度为0,AVR对该返回报文不做处理。第二次收到ACK标志,发生在上文的4到5步,和上次收到ACK不同,负载数据一定不为0。第三次收到ACK标志,发生在第6步,此时负载数据长度也为0,AVR对该报文不做任何处理。第4次收到ACK标志发生在第7步,AVR必须应答,但是该报文也含有FIN标志,可以区别于以上几种情况。 AVRNET项目便采用以上的方式,区别TCP数据包,通过IF ELSE实现了TCP状态机。AVRNET项目先处理SYN标志,接着检查所有的ACK标志,若标志也含有FIN标志,则返回ACK,若负载数据长度为0,不做响应,若负载数据大于0,取出数据做出合适的响应。 2.2 TCP序号和确认号 TCP序号和确认号也是一个比较微妙的细节。总结起来有以下几点。 1.TCP的初始序号由发送SYN或发送FIN时决定。 2.TCP建立连接时,PC机发送SYN时设置初始序号,AVR返回SYN和ACK时设置初始序号,这两个初始序号没有任何逻辑上的关系。 3.确认号为上一个数据包的序号加上个数据包的负载长度。 4.SYN标志和FIN特殊,占用个序号,即确认号需在上个数据包的序号基础累加。 AVRNET项目中何时清零序号,何时初始化序号,何时修改确认号,何时修改确认号的确是一个非常麻烦的过程。通过反复的测试,发现及时确认号出现问题程序也能顺利运行。 3.TCP实现 从实用主义角度出发,原理不重要重要的是如何实现实现功能。和UDP不同,TCP生成首部时的步骤较多。由于TCP首部中存在选项,所以需要通过更多的代码确定TCP负载的位置,在这里可以分为在TCP首部中查找负载长度字节,并通过首部中的TCP偏移字节确定长度 3.1 TCP数据发送 TCP发送时的参数很多,大致可以分为以下步骤。 1.确定下一个确认号的具体值。使用时有两种情况,那么累加1,要么累加上一个报文的负载长度。 2.初始化序号,可以在ABRNET作为客户端时使用 3.清除序号,可以在TCP建立连接时使用 4.设置标志,例如FIN SYN等 5.设置其他参数,例如紧急指针,窗口大小 6.填充以太网首部,IP首部 7.加入校验和 - void tcp_send_packet (
- BYTE *rxtx_buffer, //发送缓冲区
- WORD_BYTES dest_port, // 目标端口号
- WORD_BYTES src_port, // 源端口号
- BYTE flags, // TCP标志 FIN SYN ACK
- BYTE max_segment_size, // 初始化序号 接收SYN时使用
- BYTE clear_seqack, // 设置确认号为0 发送SYN时使用
- WORD next_ack_num, // 在上一个数据包序号的基础上累加
- WORD dlength, // 负载长度
- BYTE *dest_mac, // 目标MAC地址
- BYTE *dest_ip ) // 目标IP地址
- {
- BYTE i, tseq;
- WORD_BYTES ck;
-
- // 生成以太网报文头
- eth_generate_header ( rxtx_buffer, (WORD_BYTES){ETH_TYPE_IP_V}, dest_mac );
-
- // 计算数据包 确认号 next_ack_num为累加值
- // 1.确认号因等于上一个数据包的序号加上数据包长度
- // 2.序号等于上一个数据包的确认号
- // 3.FIN和SYN各占一个序号
- // 4.确认号和序号使用大端模式,即高地址存放低位数据
- // 5.确认号修改发生在三种情况,接收到SYN,接收到FIN,接收到负载数据
- if ( next_ack_num )
- {
- for( i = 4 ; i > 0; i-- )
- {
- // 取出上一个数据包的序号,累加next_ack_num
- next_ack_num = rxtx_buffer [ TCP_SEQ_P + i - 1] + next_ack_num;
- // 取出上一个数据包的确认号
- tseq = rxtx_buffer [ TCP_SEQACK_P + i - 1];
- // 复制本次数据包的确认号,即上个数据包的序号+next_ack_num
- rxtx_buffer [ TCP_SEQACK_P + i - 1] = 0xff & next_ack_num;
- // 复制上一个数据包的确认号于本数据包的序号
- rxtx_buffer[ TCP_SEQ_P + i - 1 ] = tseq;
-
- next_ack_num >>= 8;
- }
- }
-
- // 初始化序号
- // 设置最大分片
- // 第一次发送或接收时使用
- if ( max_segment_size )
- {
- // 初始化序号
- rxtx_buffer[ TCP_SEQ_P + 0 ] = 0;
- rxtx_buffer[ TCP_SEQ_P + 1 ] = 0;
- rxtx_buffer[ TCP_SEQ_P + 2 ] = seqnum;
- rxtx_buffer[ TCP_SEQ_P + 3 ] = 0;
- seqnum += 2;
-
- // 初始化 报文段最大长度
- rxtx_buffer[ TCP_OPTIONS_P + 0 ] = 2; // 最大报文长度
- rxtx_buffer[ TCP_OPTIONS_P + 1 ] = 4; // TCP选项长度 TCP选项格式2
- rxtx_buffer[ TCP_OPTIONS_P + 2 ] = HIGH(1408); //
- rxtx_buffer[ TCP_OPTIONS_P + 3 ] = LOW(1408); //
- // 数据偏移,占用高4位,且计算长度为双字
- rxtx_buffer[ TCP_HEADER_LEN_P ] = 0x60;
- dlength += 4;
- }
- else
- {
- // 没有TCP选项时长度为5个双字
- rxtx_buffer[ TCP_HEADER_LEN_P ] = 0x50;
- }
-
- // generate ip header and checksum
- ip_generate_header ( rxtx_buffer, (WORD_BYTES){(sizeof(IP_HEADER) + sizeof(TCP_HEADER) + dlength)}, IP_PROTO_TCP_V, dest_ip );
-
- // 清除序号,一般使用于发送SYN时
- if ( clear_seqack )
- {
- rxtx_buffer[ TCP_SEQACK_P + 0 ] = 0;
- rxtx_buffer[ TCP_SEQACK_P + 1 ] = 0;
- rxtx_buffer[ TCP_SEQACK_P + 2 ] = 0;
- rxtx_buffer[ TCP_SEQACK_P + 3 ] = 0;
- }
-
- // 设置TCP标志
- rxtx_buffer [ TCP_FLAGS_P ] = flags;
-
- // 设置目标端口号
- rxtx_buffer [ TCP_DST_PORT_H_P ] = dest_port.byte.high;
- rxtx_buffer [ TCP_DST_PORT_L_P ] = dest_port.byte.low;
-
- // 设置源端口号
- rxtx_buffer [ TCP_SRC_PORT_H_P ] = src_port.byte.high;
- rxtx_buffer [ TCP_SRC_PORT_L_P ] = src_port.byte.low;
-
- // 设置TCP窗口大小
- rxtx_buffer [ TCP_WINDOWSIZE_H_P ] = HIGH((MAX_RX_BUFFER-sizeof(IP_HEADER)-sizeof(ETH_HEADER)));
- rxtx_buffer [ TCP_WINDOWSIZE_L_P ] = LOW((MAX_RX_BUFFER-sizeof(IP_HEADER)-sizeof(ETH_HEADER)));
-
- // 紧急指针
- rxtx_buffer[ TCP_URGENT_PTR_H_P ] = 0;
- rxtx_buffer[ TCP_URGENT_PTR_L_P ] = 0;
-
- // 计算校验和
- rxtx_buffer[ TCP_CHECKSUM_H_P ] = 0;
- rxtx_buffer[ TCP_CHECKSUM_L_P ] = 0;
- ck.word = software_checksum( &rxtx_buffer[IP_SRC_IP_P], sizeof(TCP_HEADER)+dlength+8, IP_PROTO_TCP_V + sizeof(TCP_HEADER) + dlength );
- rxtx_buffer[ TCP_CHECKSUM_H_P ] = ck.byte.high;
- rxtx_buffer[ TCP_CHECKSUM_L_P ] = ck.byte.low;
-
- // 通过enc28j60发送数据
- enc28j60_packet_send ( rxtx_buffer, sizeof(ETH_HEADER)+sizeof(IP_HEADER)+sizeof(TCP_HEADER)+dlength );
- }
复制代码 3.2 TCP负载长度查询TCP负载长度查询需要根据IP报文中数据包的总大小和TCP报文中的数据偏移量决定,并需要注意TCP的数据偏移量的单位为双字,即4个字节长度。 - WORD tcp_get_dlength ( BYTE *rxtx_buffer )
- {
- int dlength, hlength;
- // 获得IP报文总大小
- dlength = ( rxtx_buffer[ IP_TOTLEN_H_P ] <<8 ) | ( rxtx_buffer[ IP_TOTLEN_L_P ] );
- // 除去IP报文头部大小
- dlength -= sizeof(IP_HEADER);
- // 获得TCP报文数据偏移量 单位为字,需要X4
- hlength = (rxtx_buffer[ TCP_HEADER_LEN_P ]>>4) * 4;
- // 除去TCP报文数据偏移量
- dlength -= hlength;
-
- if ( dlength <= 0 )
- dlength=0;
-
- return ((WORD)dlength);
- }
复制代码 3.3 TCP负载位置查询- BYTE tcp_get_hlength ( BYTE *rxtx_buffer )
- {
- // 获得TCP报文数据偏移量 单位为字,需要X4
- return ((rxtx_buffer[ TCP_HEADER_LEN_P ]>>4) * 4);
- }
复制代码 3.4 TCP负载填充 TCP的负载数据填充和UDP的负载数据填充相似。 - WORD tcp_puts_data_p ( BYTE *rxtx_buffer, PGM_P data, WORD offset )
- {
- BYTE ch;
-
- while( (ch = pgm_read_byte(data++)) )
- {
- rxtx_buffer[ TCP_DATA_P + offset ] = ch;
- offset++;
- }
-
- return offset;
- }
- WORD tcp_puts_data ( BYTE *rxtx_buffer, BYTE *data, WORD offset )
- {
- while( *data )
- {
- rxtx_buffer[ TCP_DATA_P + offset ] = *data++;
- offset++;
- }
-
- return offset;
- }
复制代码 3.5 TCP数据包处理 TCP数据包的处理代码较多,该函数会返回1或0,1代表以太网接收缓冲区的数据被处理,而0代表数据尚未被处理。TCP数据包的处理包含具体的应用实现,该部分出现在服务器端第二次返回ACK之后,具体的代码请结合范例和上文的TCP连接部分。 - BYTE tcp_receive ( BYTE *rxtx_buffer, BYTE *dest_mac, BYTE *dest_ip )
- {
- WORD tcp_reclen, tcp_sendlen , dest_port;
-
- // 获得目标端口号 即客户端端口号
- dest_port = (rxtx_buffer[TCP_SRC_PORT_H_P]<<8)|rxtx_buffer[TCP_SRC_PORT_L_P];
- // 匹配TCP协议类型,匹配端口
- if ( rxtx_buffer [ IP_PROTO_P ] == IP_PROTO_TCP_V && \
- rxtx_buffer [ TCP_DST_PORT_H_P ] == TCP_AVR_PORT_H_V && \
- rxtx_buffer [ TCP_DST_PORT_L_P ] == TCP_AVR_PORT_L_V )
- {
- // 服务器端第1次发送 收到SYN 返回SYN+ACK
- if ( (rxtx_buffer[ TCP_FLAGS_P ] & TCP_FLAG_SYN_V) )
- {
- tcp_send_packet (
- rxtx_buffer, // 发送缓冲区
- (WORD_BYTES){dest_port}, // 目标端口号
- (WORD_BYTES){TCP_AVR_PORT_V}, // 源端口号
- TCP_FLAG_SYN_V|TCP_FLAG_ACK_V, // 标志位 同步位和应答位
- 1, // 初始化序号,只有在接收到SYN时使用
- 0, // 不清除确认号
- 1, // 确认号为上一个数据包的应答编号加1,SYN占用一个序号
- 0, // 负载长度
- dest_mac, // 客户端MAC地址
- dest_ip // 客户端IP地址
- );
-
- return 1;
- }
-
- // 收到ACK报文 多种情况
- if ( (rxtx_buffer [ TCP_FLAGS_P ] & TCP_FLAG_ACK_V) )
- {
- // 获得TCP负载长度
- tcp_reclen = tcp_get_dlength( rxtx_buffer );
-
- if ( tcp_reclen == 0 )
- {
- // 服务器第4次发送,收到FIN,返回ACK
- // 主要是为了区别 建立连接时的最后一个ACK
- if ( (rxtx_buffer[TCP_FLAGS_P] & TCP_FLAG_FIN_V) )
- {
- tcp_send_packet (
- rxtx_buffer, // 发送缓冲区
- (WORD_BYTES){dest_port}, // 目标端口
- (WORD_BYTES){TCP_AVR_PORT_V}, // 源端口
- TCP_FLAG_ACK_V, // 标志位 应答
- 0, // 不操作序号
- 0, // 不清楚确认号
- 1, // FIN占一个序号,在一个数据包的序号的基础上累加1
- 0, // 负载大小
- dest_mac, // 客户端MAC地址
- dest_ip // 客户端IP地址
- );
- }
-
- return 1;
- }
-
- // 服务器第2次发送,返回ACK
- tcp_send_packet (
- rxtx_buffer, // 发送缓冲区
- (WORD_BYTES){dest_port}, // 目标端口号
- (WORD_BYTES){TCP_AVR_PORT_V}, // 源端口号
- TCP_FLAG_ACK_V, // 标志位 应答标志
- 0, // 不操作序号
- 0, // 不清除确认号
- tcp_reclen, // 在上一个数据包的基础上累加http_loadlen
- 0, // 负载长度
- dest_mac, // 客户端MAC地址
- dest_ip ); // 客户端IP地址
-
- // TCP数据包数据,请查看后面的例子
-
- // 服务器第3次发送,发送HTTP响应 发送FIN
- tcp_send_packet (
- rxtx_buffer, // 发送缓冲区
- (WORD_BYTES){dest_port}, // 目标端口
- (WORD_BYTES){TCP_AVR_PORT_V}, // 源端口
- // 标志
- TCP_FLAG_ACK_V | TCP_FLAG_PSH_V | TCP_FLAG_FIN_V,
- 0, // 不操作序号
- 0, // 不清除确认号
- 0, //
- tcp_sendlen, // 负载长度
- dest_mac, // 客户端MAC地址
- dest_ip ); // 客户端IP地址
- // 数据包被处理
- return 1;
- }
- }
-
- // 返回0 代表数据未被处理
- return 0;
- }
复制代码 4.实验 TCP报文的处理位于ARP、 IP、 ICMP和ICMP之后。获得TCP有效负载之后应存在在接收缓冲区中,进行合适的处理并返回结果。实验通过两个例子说明TCP的使用,第一个例子,PC机通过网络调试助手发送xukai871105,AVR返回TCP Hello xukai871105。第二个例子,PC机发送lcd=Hello AVRNET,在AVRNET的LCD显示屏上输出Hello AVRNET。 4.1 程序结构- // 获得新的IP报文
- plen = enc28j60_packet_receive( (BYTE*)&rxtx_buffer, MAX_RXTX_BUFFER );
- if(plen==0) return;
-
- // 保存客服端的MAC地址
- memcpy ( (BYTE*)&client_mac, &rxtx_buffer[ ETH_SRC_MAC_P ], sizeof(MAC_ADDR) );
- // 检查该报文是不是ARP报文
- if ( arp_packet_is_arp( rxtx_buffer, (WORD_BYTES){ARP_OPCODE_REQUEST_V} ) )
- {
- // 向客户端返回ARP报文
- arp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac );
- return;
- }
-
- // 保存客服端的IP地址
- memcpy ( (BYTE*)&client_ip, &rxtx_buffer[ IP_SRC_IP_P ], sizeof(IP_ADDR) );
- // 检查该报文是否为IP报文
- if ( ip_packet_is_ip ( (BYTE*)&rxtx_buffer ) == 0 )
- {
- return;
- }
-
- /* 如果是ICMP报文 向发起方返回数据 */
- if ( icmp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac, (BYTE*)&client_ip ) )
- {
- return;
- }
-
- // 进行UDP处理
- if (udp_receive ( (BYTE *)&rxtx_buffer, (BYTE *)&client_mac, (BYTE *)&client_ip ))
- {
- return;
- }
-
- // 进行TCP处理
- if (tcp_receive ( (BYTE *)&rxtx_buffer, (BYTE *)&client_mac, (BYTE *)&client_ip ))
- {
- return;
- }
复制代码 4.2 打印源IP和源端口4.3 TCP Hello TCP返回Hello的代码和UDP部分相似。在这里不多做解释。 - // 确定TCP负载位置
- WORD tcp_loadpos = tcp_get_hlength( rxtx_buffer) + sizeof(ETH_HEADER) + sizeof(IP_HEADER);
-
- // 复制缓冲区数据
- memcpy(tcp_recbuf,(char*)&rxtx_buffer[tcp_loadpos],tcp_reclen);
-
- //准备返回数据
- strcpy(tcp_sendbuf,"TCP:Hello ");
- strcat(tcp_sendbuf,tcp_recbuf);
- // 填充缓冲区
- tcp_sendlen = tcp_puts_data( rxtx_buffer,(BYTE*)tcp_sendbuf,0);
复制代码图 TCP Hello实验结果 4.4 LCD控制 LCD控制命令和UDP中的LED控制命令的解析过程相似,从TCP输入负载中解析是否包含lcd字符串,若包含则从lcd=之后取出需要显示的字符串,接着清除lcd的所有显示,最后通过lcd打印输出结果。代码和实验结果如下。 - tcp_recbuf[tcp_reclen] = '\0';
- char lcd_buf[32];
- if( !memcmp((char*)&tcp_recbuf,"lcd",3) )
- {
- strcpy(lcd_buf,(char*)&tcp_recbuf[4]);
- // 清屏
- lcd_putc ( '\f' );
- // 通过lcd输出
- lcd_print ( lcd_buf );
- tcp_sendlen = tcp_puts_data( rxtx_buffer,(BYTE*)"lcd command",0);
- }
- else
- {
- tcp_sendlen = tcp_puts_data( rxtx_buffer,(BYTE*)"Unknow command",0);
- }
复制代码图 LCD控制效果 5 总结和展望 即使是一个不完整的TCP其实现过程也是非常复杂的。在web服务器的实现中,正是使用了本文所用到的TCP处理结构,也就是说TCP是HTTP的基础。接着将实现AVRNET项目中最令人兴奋的web服务器功能,并将通过单个静态网页,单个动态网页和多个动态网页实现嵌入式WEB服务器。
图web服务器实现LED控制
工程源码,请各位笑纳,如果觉得程序有任何问题,请毫不客气的提出!
|