STM32NET学习笔记——TCP部分

1.前言 
    【由于TCP协议负载,若有说的不对的地方,请及时指出,第一时间改正。本博文2013年2月在某论坛发表,现搬迁至CSDN博客】
    从实用主义的角度出发,学习嵌入式TCPIP可以直接从本章节开始学习,甚至可以直接从HTTP开始学习。我也曾经是一个现实的实用主义者, 以为有了AVRNET项目的源码,修改移植之后便可以用于STM32。但是现实总是那么残酷,对于一个还不熟悉HTML元素,没有任何PHP和SOCKET编程知识的我来说,修改AVRNET的任何一行代码都是不可能完成的任务,我几乎不知道修改这行代码造成哪些变化。但是通过不断地学习情况有所好转,这些学习包括HTML,CSS,JaveScript,PHP,MySQL,SOCKET等。通过这些学习与积累慢慢地揭开了TCPIP的面纱,而通过TCP让STM32返回Hello是多么令人兴奋的事情。
    本文将通过分析和整理AVRNET项目源码并移植到STM32平台中,实现TCP部分内容,通过TCP打印Hello和LED控制实例说明TCP的使用方法。 
    TCP协议是是一个非常复杂的协议,本文并不会实现TCP的方方面面,毫不避讳的说本文所用TCP是不完整的TCP,没有TCP坚持定时器和TCP重发功能,更别提滑动窗口功能,TCP的传输速度也低的可怜。本文试图通过最简单的代码实现最基本的TCP功能,这些包括TCP建立,TCP数据包发送,TCP关闭连接。 
    【 代码仓库】——请以代码仓库中的内容为主。

1.2 相关资料
    【 ENC28J60学习笔记
    【 AVRNET项目(国外) 】

1.3 学习路线建议
    本文仅仅简单的说明TCP使用的方法,通过一个TCP发射的例子积累学习嵌入式网络应用的信心,但是这些知识是远远不够的,你还需要学习以下知识。这些知识将帮助你设计和实现嵌入式网页,设计优美的嵌入式web类型的API。
    【HTML】先学习HTML4,再学习HTML5
    【CSS】先学习CSS2,再学习CSS3
    【Javascript】
    【JSON】强烈建议学习JSON格式和应用方法。
    【JQuery】建议先熟悉Javascript,再学习JQuery。
    【PHP】和PHP工程师不同,更多关注HTTP部分内容。
    【数据库】学习一款数据库,可以是MySQL或SQLite,熟悉基本操作增删改查。
    【Linux】更多关注应用层设计,如果你不介意尝试一下树莓派吧。
    【RESTFul】学习该框架,了解如何设计HTTP API。
    【等等】知识真的是够多的,请您保持耐心。

1.4 其他说明
    【PC IP】192.168.1.101 
    【STM32 IP】192.168.1.15 
    【TCP端口号】3001

2.TCP协议简述 
    本小节仅介绍TCP如何建立连接,发送数据和终止连接的技巧,本文将不详细讲述TCP首部中的各种字节含义,因为你可以找到很多说明的图书。 更多基础内容请参考。
    【1】《TCP IP详解 卷1》 机械工程出版社出版 
    【2】《嵌入式Internet TCP IP基础、实现及应用》 北京航空航天出版社 

2.1 TCP的建立和终止 
    在STM32NET项目中暂假定PC机为客户端,STM32为服务器端。在这种情况下,有PC机发起连接,STM32被动的处理整个TCP过程。 
【1】PC机试图建立连接,发出SYN标志。 
【2】STM32接收到SYN标志,响应PC机的连接请求,发送SYN和ACK标志 
【3】PC机收到SYN和ACK标志,发送ACK标志,表示TCP连接建立 
【4】PC机在建立连接之后,向STM32发送TCP负载数据,并包含PUSH和ACK标志 
【5】STM32收到负载数据,返回应答数据,并包括FIN、ACK和FUSH标志(FIN意味着AVR试图停止本次TCP连接) 
【6】PC机对负载数据处理,返回ACK标志 
【7】PC机也发出FIN标志和ACK标志,同意停止本次TCP连接 
【8】STM32收到该FIN标志,发出ACK标志,意味着本次TCP通信完全结束。 

    TCP的建立连接,收发数据和停止连接,配合定时器辅以有限状态机是一个常用的实现方式,但实现这些功能需要成千上万行代码,通过分析可以看出。 

    【1】对于FIN标志和SYN标志,无论是服务端还是客户端都需要发送ACK标志 
    【2】STM32共4次收到ACK标志。 第一次为上文的第3步,该ACK位的接收表示TCP连接成功,此时负载数据长度为0,STM32对该返回报文不做处理。 第二次收到ACK标志,发生在上文的4到5步,和上次收到ACK不同,负载数据一定不为0。 第三次收到ACK标志,发生在第6步,此时负载数据长度也为0,STM32对该报文不做任何处理。 第四次收到ACK标志发生在第7步,STM32必须应答,但是该报文也含有FIN标志,可以区别于以上几种情况。 
    STM32NET项目便采用以上的方式区别TCP数据包,通过IF ELSE实现了TCP状态机。STM32NET项目先处理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特殊,占用个序号,即确认号需在上个数据包的序号基础累加。 
    STM32NET项目中何时清零序号,何时初始化序号,何时修改确认号,何时修改确认号的确是一个非常麻烦的过程。通过反复的测试,发现即使确认号出现问题程序也能顺利运行。 

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){(IP_HEADER_LEN + TCP_HEADER_LEN + 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-IP_HEADER_LEN-ETH_HEADER_LEN));
    rxtx_buffer [ TCP_WINDOWSIZE_L_P ] = LOW((MAX_RX_BUFFER-IP_HEADER_LEN-ETH_HEADER_LEN));
   
    // 紧急指针
    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], TCP_HEADER_LEN+dlength+8, IP_PROTO_TCP_V + TCP_HEADER_LEN + 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, ETH_HEADER_LEN+IP_HEADER_LEN+TCP_HEADER_LEN+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 -= IP_HEADER_LEN;
    // 获得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); // generate len in bytes;
}


3.4 TCP负载填充
    TCP的负载数据填充和UDP的负载数据填充相似。
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, // 在上一个数据包的基础上累加tcp_reclen
                             0, // 负载长度
                             dest_mac, // 客户端MAC地址
                             dest_ip ); // 客户端IP地址
           
            // 确定TCP负载位置
            WORD tcp_loadpos = tcp_get_hlength( rxtx_buffer) + ETH_HEADER_LEN + IP_HEADER_LEN;
            // 复制缓冲区数据
            memcpy(tcp_recbuf,(char*)&rxtx_buffer[tcp_loadpos],tcp_reclen);
           
            // 范例1 TCP:Hello
            //准备返回数据
            strcpy(tcp_sendbuf,"TCP:Hello ");
            strcat(tcp_sendbuf,tcp_recbuf);
            // 填充缓冲区
            tcp_sendlen = tcp_puts_data( rxtx_buffer,(BYTE*)tcp_sendbuf,0);
           
            // 服务器第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的使用。

4.1 程序结构 

void server_process ( void )
{
    MAC_ADDR client_mac;
    IP_ADDR client_ip;
    WORD plen;
   
    // 获得新的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 TCP Hello 
    接收到TCP数据包之后,在负载数据之前加入Hello字符串,如果输入为xukai871105,则返回Hello xukai871105。通过网络调试助手查看返回结果。使用strcpy函数把Hello复制到tcp_sendbuf数组中,接着使用strcat把tcp_recbuf中的字符串连接到tcp_sendbuf之后,最后调用tcp_send_packet填充到发送缓冲区中。
#if TCP_ECHO
            // 范例1 TCP:Hello
            //准备返回数据
            strcpy(tcp_sendbuf,"TCP:Hello ");
            strcat(tcp_sendbuf,tcp_recbuf);
            // 填充缓冲区
            tcp_sendlen = tcp_puts_data(rxtx_buffer,(BYTE*)tcp_sendbuf,0);
#endif
图1 TCP Hello实验结果 
4.3 LED控制 
    验证TCP发送和接收之后,可以通过定义一组指令实现LED的控制。
    led,x,y
    x表示LED编号,取值范围为1或2
    y表示LED状态,1为打开,2为关闭

    具体代码如下:
#if TCP_LEDCTRL
            int match_count = 0;
            int led_index = 0;
            int led_status = 0;
            // 匹配led,x,y
            match_count = sscanf(tcp_recbuf,"led,%d,%d", &led_index, &led_status);
            if(match_count == 2)
            {
                switch(led_index)
                {
                case 1:
                    led_status?BSP_LEDOn(1):BSP_LEDOff(1);
                    tcp_sendlen = tcp_puts_data(rxtx_buffer, (BYTE*)"LED1 Control OK\r\n", 0);
                    // dlength = udp_puts_data(rxtx_buffer, (BYTE*)"LED1 Control OK\r\n", 0);
                    break;
                case 2:
                    led_status?BSP_LEDOn(2):BSP_LEDOff(2);
                    tcp_sendlen = tcp_puts_data(rxtx_buffer, (BYTE*)"LED2 Control OK\r\n", 0);
                    break;
                default:
                    tcp_sendlen = tcp_puts_data(rxtx_buffer, (BYTE*)"Invalid LED Index\r\n", 0);
                    break;
                }
            }
            else
            {
                tcp_sendlen = tcp_puts_data(rxtx_buffer, (BYTE*)"unknow command\r\n", 0);
            }
#endif

图2 TCP LED控制

5 总结和展望 
    本文为STM32NET学习笔记的最后一篇,以后将不再更新。按照最原始的计划还有WEB部分的内容,但是若使用这样的框架尝试,那么整个系统会变得非常复杂且不可控制。如果想深入嵌入式网络,请查阅LwIP相关知识,或者尝试一下树莓派等linux开发板。

  • 6
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值