LWIPtcp_input()函数分析

Posted Evan_ZGYF丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LWIPtcp_input()函数分析相关的知识,希望对你有一定的参考价值。

收录于:

【LWIP】LWIP协议|相关知识汇总|LWIP学习笔记


相关链接:

【LWIP】Ip4_input函数分析

——|【LWIP】pbuf_realloc函数分析

【LWIP】tcp_input()函数分析

——|【LWIP】tcp_timewait_input函数解析

——|【LWIP】tcp_listen_input函数分析

【LWIP】tcp_receive函数分析

【LWIP】udp_input函数分析


调用流程:

    数据包首先调用ethernet_input()函数到达数据链路层,去掉以太网头部;
    根据以太网头部类型判断:如果是ARP报文传给调用arp_input()交给ARP协议处理,如果是IP报文就调用ip_input()进入IP层处理;
    ip_input()函数中比较数据报文的目的IP地址,如果与某个网络接口的IP地址相同,则接收这个报文,依照IP头部的协议字段,调用各自协议的输入处理函数;
    如果是TCP类型,调用tcp_input()。


函数简析:

    tcp_input接收IP层递交上来的数据包,并根据数据包查找相应TCP控制块,并根据相关控制块所处的状态调用函数tcp_timewait_inputtcp_listen_inputtcp_process进行处理。
    如果是调用的前两个函数,则tcp_input在这两个函数返回后就结束了,但若调用的是tcp_process函数,则函数返回后,tcp_input还要进行许多相应的处理。


具体分析:

    (在源码中有详细注释)   

   1.检查报文会否有效(pbuf大小是否小于TCP报头20字节,不包括选项);
    2.判断是否是多播/单播报文,丢弃;
    3.是否开启TCP校验(宏配置),若开启调用ip_chksum_pseudo函数进行TCP校验和验证;
    4.获取TCP首部长度(包括选项部分);
    5.将p指针移向pbuf的有效数据部分(除去TCP报文头和选项部分,若选项有一部分在第二个pbuf中,代码上操作较复杂);
    6.网络字节序转主机字节序;
    7.遍历tcp_active_pcbs链表,是否匹配(若匹配有操作将这个pcb移到链表前端,下次找到更快,可以借鉴);
    8.若不在tcp_active_pcbs链表,遍历tcp_tw_pcbs和tcp_listen_pcbs链表,是否匹配,
       若在tcp_tw_pcbs链表中匹配,调用tcp_timewait_input函数处理,若在tcp_listen_pcbs链表中匹配,调用tcp_listen_input函数处理,
       在遍历tcp_listen_pcbs链表中还会判断本地ip是否设置任意IP地址,操作不同;
    9.接上面第7.点,若在tcp_active_pcbs链表匹配,经过处理后进入tcp_process函数;
    10.若是进入tcp_timewait_input函数,tcp_listen_input函数,tcp_input在这两个函数返回后就结束了,若是进入tcp_process函数,返回后还要做很多处理,
       判断返回值类型(收到复位报文双方连接成功关闭收到对方的FIN请求),做对应操作,
    11.  若在3张链表里都未找到匹配的pcb,则调用tcp_rst函数向源主机发送一个TCP复位数据包


源码:

void
tcp_input(struct pbuf *p, struct netif *inp)

  struct tcp_pcb *pcb, *prev;
  struct tcp_pcb_listen *lpcb;
#if SO_REUSE
  struct tcp_pcb *lpcb_prev = NULL;
  struct tcp_pcb_listen *lpcb_any = NULL;
#endif /* SO_REUSE */
  u8_t hdrlen_bytes;
  err_t err;

  LWIP_UNUSED_ARG(inp);

  PERF_START;

  TCP_STATS_INC(tcp.recv);
  MIB2_STATS_INC(mib2.tcpinsegs);

  /*获取TCP头,这边使用了一个全局变量tcphdr*/
  tcphdr = (struct tcp_hdr *)p->payload;

#if TCP_INPUT_DEBUG
  tcp_debug_print(tcphdr);
#endif

  /* 判断数据包长度是否小于TCP报头长度
   * (检查报文是否有效) */
  if (p->len < TCP_HLEN) 
    /* 若报文无效,丢弃 */
    LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: short packet (%"U16_F" bytes) discarded\\n", p->tot_len));
    TCP_STATS_INC(tcp.lenerr);
    goto dropped;
  

  /* 若是多播包/广播包,丢弃
   * (不处理多播/广播报文)*/
  if (ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif()) ||
      ip_addr_ismulticast(ip_current_dest_addr())) 
    TCP_STATS_INC(tcp.proterr);
    goto dropped;
  

  /* (通过宏配置是否开启TCP校验) */
#if CHECKSUM_CHECK_TCP
  IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_CHECK_TCP) 
    /* TCP校验和验证 */
    u16_t chksum = ip_chksum_pseudo(p, IP_PROTO_TCP, p->tot_len,
                               ip_current_src_addr(), ip_current_dest_addr());
    if (chksum != 0) 
        LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packet discarded due to failing checksum 0x%04"X16_F"\\n",
          chksum));
      tcp_debug_print(tcphdr);
      TCP_STATS_INC(tcp.chkerr);
      goto dropped;
    
  
#endif /* CHECKSUM_CHECK_TCP */

  /* 获取TCP首部长度
   * (若无其他选项,最小为20字节) */
  hdrlen_bytes = TCPH_HDRLEN(tcphdr) * 4;
  /* 若获取到的TCP报头小于20字节或者TCP报头长度大于报文总长度,为无效报文,丢弃 */
  if ((hdrlen_bytes < TCP_HLEN) || (hdrlen_bytes > p->tot_len)) 
    LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: invalid header length (%"U16_F")\\n", (u16_t)hdrlen_bytes));
    TCP_STATS_INC(tcp.lenerr);
    goto dropped;
  

  /* 将有效载荷指针移动到pbuf中,这样它就指向了 TCP数据而不是TCP报头。 */
  tcphdr_optlen = hdrlen_bytes - TCP_HLEN;    //tcphdr_optlen  = TCP报头选项长度 (TCP报头总长度 - TCP标准报头20字节)
  tcphdr_opt2 = NULL;                         //tcphdr_opt2   指向NULL
  /* 判断TCP报头是否在一个pbuf中 */
  if (p->len >= hdrlen_bytes) 
    /* 若TCP报头在第一个pbuf中 */
    tcphdr_opt1len = tcphdr_optlen;           //tcphdr_opt1len = TCP报头选项长度
    pbuf_header(p, -(s16_t)hdrlen_bytes);     //将指针移动到pbuf数据中
   else 
    u16_t opt2len;
    /* 若TCP报头在多个pbuf中 */
    LWIP_ASSERT("p->next != NULL", p->next != NULL);

    /* 去除TCP标准报头(不会失败) */
    pbuf_header(p, -TCP_HLEN);

    /* 去掉第一个pbuf的长度 */
    tcphdr_opt1len = p->len;
    opt2len = tcphdr_optlen - tcphdr_opt1len;

    /* options continue in the next pbuf: set p to zero length and hide the
        options in the next pbuf (adjusting p->tot_len) */
    pbuf_header(p, -(s16_t)tcphdr_opt1len);

    /* 检查TCP报头选项部分是否在第二个pbuf中 */
    if (opt2len > p->next->len) 
      /* 丢弃过短的报文 */
      LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: options overflow second pbuf (%"U16_F" bytes)\\n", p->next->len));
      TCP_STATS_INC(tcp.lenerr);
      goto dropped;
    

    /* 记住指向TCP报头选项的第二部分的指针
     * (有部分选项在第二个pbuf中,记录TCP报头选项的开始部分) */
    tcphdr_opt2 = (u8_t*)p->next->payload;

    /* 将第二个pbuf的指针指向pbuf 的数据部分 */
    pbuf_header(p->next, -(s16_t)opt2len);
    p->tot_len -= opt2len;

    LWIP_ASSERT("p->len == 0", p->len == 0);
    LWIP_ASSERT("p->tot_len == p->next->tot_len", p->tot_len == p->next->tot_len);
  

  /*将TCP报头中的数据由网络字节序转换为主机字节序 */
  tcphdr->src = lwip_ntohs(tcphdr->src);
  tcphdr->dest = lwip_ntohs(tcphdr->dest);
  seqno = tcphdr->seqno = lwip_ntohl(tcphdr->seqno);
  ackno = tcphdr->ackno = lwip_ntohl(tcphdr->ackno);
  tcphdr->wnd = lwip_ntohs(tcphdr->wnd);

  /* 6位标志位 */
  flags = TCPH_FLAGS(tcphdr);
  /* TCP数据包中数据的总长度,对于有FIN或SYN标志的数据包,该长度要加1 */
  tcplen = p->tot_len + ((flags & (TCP_FIN | TCP_SYN)) ? 1 : 0);

  /* Demultiplex an incoming segment. First, we check if it is destined
   * for an active connection.
   * (以下就是对接收到的数据包进行分类处理,也就是寻找合适的接口,根据IP,port) */

  prev = NULL;

  /* 遍历tcp_active_pcbs链表 (活动状态下的链表)*/
  for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) 
    LWIP_ASSERT("tcp_input: active pcb->state != CLOSED", pcb->state != CLOSED);
    LWIP_ASSERT("tcp_input: active pcb->state != TIME-WAIT", pcb->state != TIME_WAIT);
    LWIP_ASSERT("tcp_input: active pcb->state != LISTEN", pcb->state != LISTEN);
    /* 若端口号匹配,并且IP地址匹配 (就把这个移到pcb链表的前面,下次查找更快) 可以借鉴*/
    if (pcb->remote_port == tcphdr->src &&
        pcb->local_port == tcphdr->dest &&
        ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr()) &&
        ip_addr_cmp(&pcb->local_ip, ip_current_dest_addr())) 
      /* 把这个PCB移到列表的前面,这样后面的 查找速度更快(我们在TCP段中使用了位置 移位) */
      LWIP_ASSERT("tcp_input: pcb->next != pcb (before cache)", pcb->next != pcb);
      if (prev != NULL) 
        prev->next = pcb->next;
        pcb->next = tcp_active_pcbs;
        tcp_active_pcbs = pcb;
       else 
        TCP_STATS_INC(tcp.cachehit);
      
      LWIP_ASSERT("tcp_input: pcb->next != pcb (after cache)", pcb->next != pcb);
      break;
    
    prev = pcb;
  

  /* 若不在tcp_active_pcbs链表中,遍历tcp_tw_pcbs和tcp_listen_pcbs链表 (等待状态下的链表) */
  if (pcb == NULL) 
    /* 如果它没有连接到一个活动连接,我们检查连接 时候的状态。*/
    for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) 
      LWIP_ASSERT("tcp_input: TIME-WAIT pcb->state == TIME-WAIT", pcb->state == TIME_WAIT);
      /* 若端口&IP匹配 */
      if (pcb->remote_port == tcphdr->src &&
          pcb->local_port == tcphdr->dest &&
          ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr()) &&
          ip_addr_cmp(&pcb->local_ip, ip_current_dest_addr())) 
        /* 我们不太关心把PCB移到前面 因为我们不太可能接受这个列表 有许多段时间等待连接
         * (在此处不需要把该链表移到前面,因为需要等待连接) */
        LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packed for TIME_WAITing connection.\\n"));
        /* 等待状态下的pcb进入tcp_timewait_input */
        tcp_timewait_input(pcb);
        pbuf_free(p);
        return;
      
    

    /* 最后,如果我们仍然没有匹配,我们会检查所有的PCBs 正在收听即将到来的连接。(监听状态下的链表) */
    prev = NULL;
    for (lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next) 
      /* 匹配端口号,否则是指向本地的端口 */
      if (lpcb->local_port == tcphdr->dest) 
              /* 本地IP是否设置的任意IP地址(这边是类型标志?) */
        if (IP_IS_ANY_TYPE_VAL(lpcb->local_ip)) 
          /* 找到任何类型(ipv4-ipv6)匹配 */
#if SO_REUSE
          lpcb_any = lpcb;
          lpcb_prev = prev;
#else /* SO_REUSE */
          break;
#endif /* SO_REUSE */
         else if (IP_ADDR_PCB_VERSION_MATCH_EXACT(lpcb, ip_current_dest_addr())) 
                /* 不是任意类型的IP地址,匹配IP */
          if (ip_addr_cmp(&lpcb->local_ip, ip_current_dest_addr())) 
            /* 找到精确匹配,break */
            break;
           else if (ip_addr_isany(&lpcb->local_ip)) 
            /* 本地IP是否设置的任意IP地址,找到任意IP地址 */
#if SO_REUSE
            lpcb_any = lpcb;
            lpcb_prev = prev;
#else /* SO_REUSE */
            break;
#endif /* SO_REUSE */
          
        
      
      prev = (struct tcp_pcb *)lpcb;
    
#if SO_REUSE
    /* 首先尝试任意的本地IP
     * (本地IP地址为任意IP的pcb) */
    if (lpcb == NULL) 
      /* 如果没有找到特定的本地IP,则只传递给任何一个 */
      lpcb = lpcb_any;
      prev = lpcb_prev;
    
#endif /* SO_REUSE */
    /* 如果是精确匹配的本地IP地址 */
    if (lpcb != NULL) 
      /* 把这个PCB移到列表的前面,这样后面的 查找速度更快. */
      if (prev != NULL) 
        ((struct tcp_pcb_listen *)prev)->next = lpcb->next;
              /* our successor is the remainder of the listening list */
        lpcb->next = tcp_listen_pcbs.listen_pcbs;
              /* put this listening pcb at the head of the listening list */
        tcp_listen_pcbs.listen_pcbs = lpcb;
       else 
        TCP_STATS_INC(tcp.cachehit);
      

      LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packed for LISTENing connection.\\n"));
      /* 监听链表下的pcb进入tcp_listen_input */
      tcp_listen_input(lpcb);
      pbuf_free(p);
      return;
    
  

#if TCP_INPUT_DEBUG
  LWIP_DEBUGF(TCP_INPUT_DEBUG, ("+-+-+-+-+-+-+-+-+-+-+-+-+-+- tcp_input: flags "));
  tcp_debug_print_flags(TCPH_FLAGS(tcphdr));
  LWIP_DEBUGF(TCP_INPUT_DEBUG, ("-+-+-+-+-+-+-+-+-+-+-+-+-+-+\\n"));
#endif /* TCP_INPUT_DEBUG */

  /* 若在tcp_active_pcbs链表中找到了,则经过处理后进入tcp_process */
  if (pcb != NULL) 
    /* The incoming segment belongs to a connection. */
#if TCP_INPUT_DEBUG
    tcp_debug_print_state(pcb->state);
#endif /* TCP_INPUT_DEBUG */

    /* 建立一个tcpseg结构 */
    inseg.next = NULL;              // 关闭报文段队列功能
    inseg.len = p->tot_len;         // 设置该报文段的数据长度
    inseg.p = p;                    // 设置报文段数据链表头指针
    inseg.tcphdr = tcphdr;          // 报文段的TCP头

    recv_data = NULL;               // 数据接收结果被保存在该全局变量,然后往上层提交
    recv_flags = 0;                 // tcp_process执行完后的结果(控制块的状态变迁)将会被保存在该全局变量,首先在这里被清0
    recv_acked = 0;

    if (flags & TCP_PSH) 
      p->flags |= PBUF_FLAG_PUSH;
    

    /* If there is data which was previously "refused" by upper layer
     * (tcp_pcb的refused_data指针上是否还记录有尚未往上层递交的数据)*/
    if (pcb->refused_data != NULL) 
            /* 有的话回调用户recv函数接收未递交的数据
             * 判断处理recv函数的处理结果,成功refused_data指针清空,继续往下执行tcp_process
             * 失败意味着tcp_pcb都被占用满,丢弃接收包不再处理,直接返回*/
      if ((tcp_process_refused_data(pcb) == ERR_ABRT) ||
        ((pcb->refused_data != NULL) && (tcplen > 0))) 
        /* pcb has been aborted or refused data is still refused and the new
           segment contains data */
              /* 若接收窗口声明为0,调动tcp_send_empty_ack,下次看 */
        if (pcb->rcv_ann_wnd == 0) 
          /* this is a zero-window probe, we respond to it with current RCV.NXT
          and drop the data segment */
          tcp_send_empty_ack(pcb);
        
        TCP_STATS_INC(tcp.drop);
        MIB2_STATS_INC(mib2.tcpinerrs);
        goto aborted;
      
    
    tcp_input_pcb = pcb;       // 记录处理当前报文的控制块
    /* 这里就是进入tcp_process处理接收包环节了(解析下次看),该函数实现了TCP状态转换功能 */
    err = tcp_process(pcb);
    /* (若返回值为ERR_ABRT,说明控制块已经被完全删除(tcp_abort()),什么也不需要做)
     * 返回值不为ERR_ABRT时,判断报文处理的3种结果 */
    if (err != ERR_ABRT) 
      /* 接收到对方的复位报文 (第一种情况) */
      if (recv_flags & TF_RESET) 
        /* 回调用户的errf函数 */
        TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_RST);
        /* 删除控制块 */
        tcp_pcb_remove(&tcp_active_pcbs, pcb);
        /* 释放控制块空间 */
        memp_free(MEMP_TCP_PCB, pcb);
       else 
        err = ERR_OK;
        /* If the application has registered a "sent" function to be
           called when new send buffer space is available, we call it
           now.
           (如果应用程序已经注册了一个“发送”函数当新的发送缓冲区空间可用时,我们调用它现在)*/
        if (recv_acked > 0) 
          u16_t acked16;
#if LWIP_WND_SCALE
          /* 被重新调用的是u32t,但是发送的回调只需要u16t, 所以我们可能要多次调用它。 */
          u32_t acked = recv_acked;
          while (acked > 0) 
            acked16 = (u16_t)LWIP_MIN(acked, 0xffffu);
            acked -= acked16;
#else
          
            acked16 = recv_acked;
#endif
            TCP_EVENT_SENT(pcb, (u16_t)acked16, err);
            if (err == ERR_ABRT) 
              goto aborted;
            
          
          recv_acked = 0;
        
        /* 双方连接成功断开(第二种情况) */
        if (recv_flags & TF_CLOSED) 
          /* 连接已经关闭,我们将重新分配 PCB */
          if (!(pcb->flags & TF_RXCLOSED)) 
            /* Connection closed although the application has only shut down the
               tx side: call the PCB's err callback and indicate the closure to
               ensure the application doesn't continue using the PCB.
               (连接关闭尽管应用程序只关闭了 tx端:调用PCB的错误回调,并指示关闭 确保应用程序不会继续使用PCB。) */
            TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_CLSD);
          
          tcp_pcb_remove(&tcp_active_pcbs, pcb);
          memp_free(MEMP_TCP_PCB, pcb);
          goto aborted;
        
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
        while (recv_data != NULL) 
          struct pbuf *rest = NULL;
          pbuf_split_64k(recv_data, &rest);
#else /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
        if (recv_data != NULL) 
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */

          LWIP_ASSERT("pcb->refused_data == NULL", pcb->refused_data == NULL);
          if (pcb->flags & TF_RXCLOSED) 
            /* 如果本地TCP控制块已经处于TF_RXCLOSED状态,则后续接收到的数据都作废 */
            pbuf_free(recv_data);
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
            if (rest != NULL) 
              pbuf_free(rest);
            
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
            tcp_abort(pcb);
            goto aborted;
          

          /* 通知应用程序已接收到数据。 */
          TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
          if (err == ERR_ABRT) 
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
            if (rest != NULL) 
              pbuf_free(rest);
            
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
            goto aborted;
          

          /* 如果上层无法接收到这些数据,就存储它 */
          if (err != ERR_OK) 
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
            if (rest != NULL) 
              pbuf_cat(recv_data, rest);
            
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
            pcb->refused_data = recv_data;
            LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \\"full\\"\\n"));
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
            break;
           else 
            /* Upper layer received the data, go on with the rest if > 64K */
            recv_data = rest;
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
          
        

        /* 如果收到对方的FIN请求 */
        if (recv_flags & TF_GOT_FIN) 
          if (pcb->refused_data != NULL) 
            /* 如果我们拒绝了数据,就推迟这个时间。 */
            pcb->refused_data->flags |= PBUF_FLAG_TCP_FIN;
           else 
            /* 纠正接收窗口  */
            if (pcb->rcv_wnd != TCP_WND_MAX(pcb)) 
              pcb->rcv_wnd++;
            
            /* 用一个NULL指针回调用户的recv函数,通过这种方式用户程序可以知道对方的关闭请求 */
            TCP_EVENT_CLOSED(pcb, err);
            if (err == ERR_ABRT) 
              goto aborted;
            
          
        

        tcp_input_pcb = NULL;          //当前报文到此处理完毕,清空当前报文的控制块
        /* Try to send something out. */
        tcp_output(pcb);               //输出报文
#if TCP_INPUT_DEBUG
#if TCP_DEBUG
        tcp_debug_print_state(pcb->state);
#endif /* TCP_DEBUG */
#endif /* TCP_INPUT_DEBUG */
      
    
    /* Jump target if pcb has been aborted in a callback (by calling tcp_abort()).
       Below this line, 'pcb' may not be dereferenced! */
aborted:
    tcp_input_pcb = NULL;
    recv_data = NULL;

    /* give up our reference to inseg.p */
    if (inseg.p != NULL)
    
      pbuf_free(inseg.p);
      inseg.p = NULL;
    
   else 

    /* 如果在3张链表里都未找到匹配的pcb,则调用tcp_rst向源主机发送一个TCP复位数据包 */
    LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_input: no PCB match found, resetting.\\n"));
    if (!(TCPH_FLAGS(tcphdr) & TCP_RST)) 
      TCP_STATS_INC(tcp.proterr);
      TCP_STATS_INC(tcp.drop);
      tcp_rst(ackno, seqno + tcplen, ip_current_dest_addr(),
        ip_current_src_addr(), tcphdr->dest, tcphdr->src);
    
    pbuf_free(p);
  

  LWIP_ASSERT("tcp_input: tcp_pcbs_sane()", tcp_pcbs_sane());
  PERF_STOP("tcp_input");
  return;
dropped:
  TCP_STATS_INC(tcp.drop);
  MIB2_STATS_INC(mib2.tcpinerrs);
  pbuf_free(p);

 

以上是关于LWIPtcp_input()函数分析的主要内容,如果未能解决你的问题,请参考以下文章

TCP/IP协议栈--IP首部选项字段的分析

HTTP协议图--HTTP 报文首部之首部字段(重点分析)

IP数据报首部格式

IP数据报首部格式

报文分析1以太网首部

分析一帧基于UDP的TFTP协议帧