[TriCore][官方例程][TC397以太网例程详解] - 16.ECHO 应用 - 初始化

关键词

TC397 官方例程;TC397 以太网例程;TC397 GETH;


简介

本篇为 Aurix TriCore TC397 以太网官方例程分析,重点关注其硬件行为

调试所用的开发板型号:KIT-A2G-TC397-5V-TFT

所使用的例程:Ethernet_1_KIT_TC397_TFT

英飞凌 TriCore 官方例程下载地址

https://github.com/Infineon/AURIX_code_examples


系列文章链接

TC397以太网例程 - 1.概览

TC397以太网例程 - 2.STM 定时器初始化

TC397以太网例程 - 3.LwIP 配置及初始化

TC397以太网例程 - 4.ASCLIN 串口配置

TC397以太网例程 - 5.IP 地址声明

TC397以太网例程 - 6.内存 & 协议等初始化

TC397以太网例程 - 7.netif 网卡配置

TC397以太网例程 - 8.硬件初始化

TC397以太网例程 - 9.基本设置

TC397以太网例程 - 10.默认/用户配置

TC397以太网例程 - 11.RTL8211F 复位

TC397以太网例程 - 12.GETH 初始化

TC397以太网例程 - 13.PHY 初始化

TC397以太网例程 - 14.Tx/Rx 使能

TC397以太网例程 - 15.其他配置

TC397以太网例程 - 16.ECHO 应用 - 初始化

TC397以太网例程 - 17.ECHO 应用 - 轮询定时器

TC397以太网例程 - 18.ECHO 应用 - 接收报文


目录

1.TCP 控制块创建 - tcp_new()

1.1.TCP_PCB 控制块介绍

1.1.1.基本信息

1.1.2.协议相关

1.1.3.附加状态信息

1.1.4.定时器

1.1.5.接收相关

1.1.6.发送相关

1.1.7.回调函数

1.2.全局 TCP_PCB 链表

1.3.创建 TCP_PCB 控制块

2.TCP 控制块绑定 - tcp_bind()

3.设置端口监听 - tcp_listen()

4.设置回调函数 - tcp_accept()

4.1.基本概念 - 会话(Session)

4.2.数据解包 - echoUnpack()

4.3.数据打包 - echoSend()

4.3.1.创建 TCP 报文

4.3.2.将报文加入 unsent 队列

4.4.关闭会话 - echoClose()

4.5.接收回调 - echoRecv()

4.6.发送回调 - echoSent()

4.7.错误回调 - echoError()

4.8.Poll 回调 - echoPoll()


// Cpu0_Main.c
void core0_main (void)
{
    ...
    // ECHO 应用初始化
    echoInit();                                             /* Initialize ECHO application                                  */

ECHO 应用初始化由用户程序定义的 echoInit() 实现,其定义如下:

// Echo.c
/* Function to initialize the ECHO program */
void echoInit(void)
{
    g_echoPcb = tcp_new();                                /* Create a new TCP protocol control block                                                      */

    if (g_echoPcb != NULL)                                /* If the creation was successful...                                                            */
    {
        err_t err = tcp_bind(g_echoPcb, IP_ADDR_ANY, 80); /* ...bind the TCP procotol control block to any local address and local port 80.               */
        
        if (err == ERR_OK)                                /* If the binding was successful...                                                             */
        {
            g_echoPcb = tcp_listen(g_echoPcb);            /* ...set the TCP control block able to accept incoming connections.                            */
            tcp_accept(g_echoPcb, echoAccept);            /* Configure the callback function to be called when a new connection is established.           */

其可以分为如下几个部分:

  • tcp_new():创建 TCP_PCB 控制块(TCP Protocol Control Block);

  • tcp_bind():将 TCP_PCB 和本地地址与端口号绑定;

  • tcp_listen():设置端口监听,将 TCP_PCB 控制块的状态设置为 LISTEN;

  • tcp_accept():设置回调函数;

1.TCP 控制块创建 - tcp_new()

// Echo.c
/* Function to initialize the ECHO program */
void echoInit(void)
{
    g_echoPcb = tcp_new(); /* Create a new TCP protocol control block */

1.1.TCP_PCB 控制块介绍

TCP_PCB 控制块用于存储网络连接相关的状态信息,其主要作用如下:

  • 管理 TCP 连接状态:保存 TCP 连接的当前状态(例如监听、连接已建立、关闭等);

  • 记录网络信息:本地和远端的 IP 地址、端口号等;

  • 控制数据传输:记录发送/接收数据的缓冲区、窗口大小、确认号等,用于控制 TCP 的数据流;

  • 超时与重传控制:管理 TCP 连接的超时、重传和计时器;

  • 资源管理:如内存、缓冲区的分配与释放;

TCP_PCB 中存储本地/远程地址、发送/接收等相关信息,LwIP 中的 TCP_PCB 控制块定义如下:

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
// --------------------------------------------
// 基本信息:IP 地址、网口信息等
/** common PCB members */
  IP_PCB;
  |--> // 宏展开如下:
      /* ip addresses in network byte order */
      ip_addr_t local_ip; // 本地 IP 地址
      ip_addr_t remote_ip; // 远程 IP 地址
      /* Bound netif index */
      u8_t netif_idx;
      /* Socket options */
      u8_t so_options;
      /* Type Of Service */
      u8_t tos;
      /* Time To Live */
      u8_t ttl;
// --------------------------------------------

// --------------------------------------------
// 协议相关:连接状态、优先级、端口号
/** protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb);
  |--> // 宏展开如下:
      struct tcp_pcb *next; /* for the linked list */ // 连接各个 TCP 控制块的链表指针
      void *callback_arg;
      enum tcp_state state; /* TCP state */ // 连接状态
      u8_t prio; // 该控制块的优先级
      /* ports are in host byte order */
      u16_t local_port; // 本地端口号
// --------------------------------------------

  /* ports are in host byte order */
  u16_t remote_port;

// --------------------------------------------
// 附加状态信息
  tcpflags_t flags;
#define TF_ACK_DELAY   0x01U   /* Delayed ACK. */
#define TF_ACK_NOW     0x02U   /* Immediate ACK. */
#define TF_INFR        0x04U   /* In fast recovery. */
#define TF_CLOSEPEND   0x08U   /* If this is set, tcp_close failed to enqueue the FIN (retried in tcp_tmr) */
#define TF_RXCLOSED    0x10U   /* rx closed by tcp_shutdown */
#define TF_FIN         0x20U   /* Connection was closed locally (FIN segment enqueued). */
#define TF_NODELAY     0x40U   /* Disable Nagle algorithm */
#define TF_NAGLEMEMERR 0x80U   /* nagle enabled, memerr, try to output to prevent delayed ACK to happen */
...
#define TF_RTO         0x0800U /* RTO timer has fired, in-flight data moved to unsent and being retransmitted */
...
// --------------------------------------------

  /* the rest of the fields are in host byte order
     as we have to do some math with them */

// --------------------------------------------
// 定时器
  /* Timers */
  u8_t polltmr, pollinterval;
  u8_t last_timer;
  u32_t tmr;
// --------------------------------------------

// --------------------------------------------
// 接收相关
  /* receiver variables */
  u32_t rcv_nxt;   /* next seqno expected */
  tcpwnd_size_t rcv_wnd;   /* receiver window available */
  tcpwnd_size_t rcv_ann_wnd; /* receiver window to announce */
  u32_t rcv_ann_right_edge; /* announced right edge of window */
  ...

  /* Retransmission timer. */
  s16_t rtime; // 重传定时器,当值大于 rto 时重传

  u16_t mss;   /* maximum segment size */

  /* RTT (round trip time) estimation variables */
  u32_t rttest; /* RTT estimate in 500ms ticks */
  u32_t rtseq;  /* sequence number being timed */
  s16_t sa, sv; /* @see "Congestion Avoidance and Control" by Van Jacobson and Karels */

  s16_t rto;    /* retransmission time-out (in ticks of TCP_SLOW_INTERVAL) */
  u8_t nrtx;    /* number of retransmissions */

  /* fast retransmit/recovery */
  u8_t dupacks;
  u32_t lastack; /* Highest acknowledged seqno. */

  /* congestion avoidance/control variables */
  tcpwnd_size_t cwnd;
  tcpwnd_size_t ssthresh;

  /* first byte following last rto byte */
  u32_t rto_end;
// --------------------------------------------

// --------------------------------------------
// 发送相关
  /* sender variables */
  u32_t snd_nxt;   /* next new seqno to be sent */
  u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last
                             window update. */
  u32_t snd_lbb;       /* Sequence number of next byte to be buffered. */
  tcpwnd_size_t snd_wnd;   /* sender window */
  tcpwnd_size_t snd_wnd_max; /* the maximum sender window announced by the remote host */

  tcpwnd_size_t snd_buf;   /* Available buffer space for sending (in bytes). */
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
  u16_t snd_queuelen; /* Number of pbufs currently in the send buffer. */

#if TCP_OVERSIZE
  /* Extra bytes available at the end of the last pbuf in unsent. */
  u16_t unsent_oversize;
#endif /* TCP_OVERSIZE */

  tcpwnd_size_t bytes_acked;

  /* These are ordered by sequence number: */
  struct tcp_seg *unsent;   /* Unsent (queued) segments. */
  struct tcp_seg *unacked;  /* Sent but unacknowledged segments. */
#if TCP_QUEUE_OOSEQ
  struct tcp_seg *ooseq;    /* Received out of sequence segments. */
#endif /* TCP_QUEUE_OOSEQ */

  struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */
// --------------------------------------------

  ...
  struct tcp_pcb_listen* listener;
  ...

// --------------------------------------------
// 回调函数
#if LWIP_CALLBACK_API
  /* Function to be called when more send buffer space is available. */
  tcp_sent_fn sent;
  /* Function to be called when (in-sequence) data has arrived. */
  tcp_recv_fn recv;
  /* Function to be called when a connection has been set up. */
  tcp_connected_fn connected;
  /* Function which is called periodically. */
  tcp_poll_fn poll;
  /* Function to be called whenever a fatal error occurs. */
  tcp_err_fn errf;
#endif /* LWIP_CALLBACK_API */
// --------------------------------------------

  ...
  /* idle time before KEEPALIVE is sent */
  u32_t keep_idle;
  ...

  /* Persist timer counter */
  u8_t persist_cnt;
  /* Persist timer back-off */
  u8_t persist_backoff;
  /* Number of persist probes */
  u8_t persist_probe;

  /* KEEPALIVE counter */
  u8_t keep_cnt_sent;
  ...
};

TCP_PCB 控制块中的成员可大致分为如下几类:

  • 基本信息;

  • 协议相关:连接状态、优先级、端口号等;

  • 附加状态信息;

  • 定时器;

  • 接收相关信息:窗口大小、定时时间等

  • 发送相关信息:窗口大小、待发送信息等待队列、超时重传定时器等;

  • 回调函数:接收/发送回调、Poll 回调、错误回调等;

1.1.1.基本信息

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
// 基本信息:IP 地址、网口信息等
/** common PCB members */
  IP_PCB;
-------------------------------------------------------

// Libraries/Ethernet/lwip/src/include/lwip/ip.h
/** This is the common part of all PCB types. It needs to be at the
    beginning of a PCB type definition. It is located here so that
    changes to this common part are made in one location instead of
    having to change all PCB structs. */
#define IP_PCB                             \
  /* ip addresses in network byte order */ \
  ip_addr_t local_ip;                      \
  ip_addr_t remote_ip;                     \
  /* Bound netif index */                  \
  u8_t netif_idx;                          \
  /* Socket options */                     \
  u8_t so_options;                         \
  /* Type Of Service */                    \
  u8_t tos;                                \
  /* Time To Live */                       \
  u8_t ttl                                 \
  /* link layer address resolution hint */ \
  IP_PCB_NETIFHINT

其中,主要关注如下几个成员:

  • local_ip:本地 IP 地址;

  • remote_ip:远程 IP 地址;

  • tos:服务类型,例如一般服务、最小费用、最高可靠性、最大吞吐量、最小延迟;

1.1.2.协议相关

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
  ...
/** protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb);
-------------------------------------------------------

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/* members common to struct tcp_pcb and struct tcp_listen_pcb */
#define TCP_PCB_COMMON(type) \
  type *next; /* for the linked list */ \
  void *callback_arg; \
  TCP_PCB_EXTARGS \
  enum tcp_state state; /* TCP state */ \
  u8_t prio; \
  /* ports are in host byte order */ \
  u16_t local_port

其中,主要关注如下几个成员:

  • state:TCP 状态,具体包含:

    • 客户端:SYN_SENT、FIN_WAIT1、FIN_WAIT2、CLOSING、TIME_WAIT;

    • 服务器:LISTEN、SYN_RCVD、CLOSE_WAIT、LAST_ACK;

    • 公用:CLOSED、ESTABLISHED;

  • local_port:本地端口;

1.1.3.附加状态信息

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
  ...
  tcpflags_t flags;
#define TF_ACK_DELAY   0x01U   /* Delayed ACK. */
#define TF_ACK_NOW     0x02U   /* Immediate ACK. */
#define TF_INFR        0x04U   /* In fast recovery. */
#define TF_CLOSEPEND   0x08U   /* If this is set, tcp_close failed to enqueue the FIN (retried in tcp_tmr) */
#define TF_RXCLOSED    0x10U   /* rx closed by tcp_shutdown */
#define TF_FIN         0x20U   /* Connection was closed locally (FIN segment enqueued). */
#define TF_NODELAY     0x40U   /* Disable Nagle algorithm */
#define TF_NAGLEMEMERR 0x80U   /* nagle enabled, memerr, try to output to prevent delayed ACK to happen */
...
#define TF_RTO         0x0800U /* RTO timer has fired, in-flight data moved to unsent and being retransmitted */

1.1.4.定时器

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
  ...
  /* Timers */
  u8_t polltmr, pollinterval;
  u8_t last_timer;
  u32_t tmr; // 该 PCB 创建的时间

1.1.5.接收相关

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
  ...
  /* receiver variables */
  u32_t rcv_nxt;   /* next seqno expected */
  tcpwnd_size_t rcv_wnd;   /* receiver window available */
  tcpwnd_size_t rcv_ann_wnd; /* receiver window to announce */
  u32_t rcv_ann_right_edge; /* announced right edge of window */
  ...

  /* Retransmission timer. */
  s16_t rtime; // 重传定时器,当值大于 rto 时重传

  u16_t mss;   /* maximum segment size */

  /* RTT (round trip time) estimation variables */
  u32_t rttest; /* RTT estimate in 500ms ticks */
  u32_t rtseq;  /* sequence number being timed */
  s16_t sa, sv; /* @see "Congestion Avoidance and Control" by Van Jacobson and Karels */

  s16_t rto;    /* retransmission time-out (in ticks of TCP_SLOW_INTERVAL) */
  u8_t nrtx;    /* number of retransmissions */

  /* fast retransmit/recovery */
  u8_t dupacks;
  u32_t lastack; /* Highest acknowledged seqno. */

  /* congestion avoidance/control variables */
  tcpwnd_size_t cwnd;
  tcpwnd_size_t ssthresh;

  /* first byte following last rto byte */
  u32_t rto_end;

1.1.6.发送相关

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
  ...
  /* sender variables */
  u32_t snd_nxt;   /* next new seqno to be sent */
  u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last
                             window update. */
  u32_t snd_lbb;       /* Sequence number of next byte to be buffered. */
  tcpwnd_size_t snd_wnd;   /* sender window */
  tcpwnd_size_t snd_wnd_max; /* the maximum sender window announced by the remote host */

  tcpwnd_size_t snd_buf;   /* Available buffer space for sending (in bytes). */
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
  u16_t snd_queuelen; /* Number of pbufs currently in the send buffer. */

  /* Extra bytes available at the end of the last pbuf in unsent. */
  u16_t unsent_oversize;

  tcpwnd_size_t bytes_acked;

  /* These are ordered by sequence number: */
  struct tcp_seg *unsent;   /* Unsent (queued) segments. */ // 待发送数据队列
  struct tcp_seg *unacked;  /* Sent but unacknowledged segments. */
  struct tcp_seg *ooseq;    /* Received out of sequence segments. */

  struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */

1.1.7.回调函数

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
/** the TCP protocol control block */
struct tcp_pcb {
  ...
#if LWIP_CALLBACK_API
  /* Function to be called when more send buffer space is available. */
  tcp_sent_fn sent;
  /* Function to be called when (in-sequence) data has arrived. */
  tcp_recv_fn recv;
  /* Function to be called when a connection has been set up. */
  tcp_connected_fn connected;
  /* Function which is called periodically. */
  tcp_poll_fn poll;
  /* Function to be called whenever a fatal error occurs. */
  tcp_err_fn errf;
#endif /* LWIP_CALLBACK_API */

1.2.全局 TCP_PCB 链表

LwIP 定义了四种状态的 TCP_PCB 链表,已创建的 TCP_PCB 根据其当前状态,存入对应的链表,如下所示:

// Libraries/Ethernet/lwip/src/core/tcp.c
/* The TCP PCB lists. */

/** List of all TCP PCBs bound but not yet (connected || listening) */
struct tcp_pcb *tcp_bound_pcbs;
/** List of all TCP PCBs in LISTEN state */
union tcp_listen_pcbs_t tcp_listen_pcbs;
/** List of all TCP PCBs that are in a state in which
 * they accept or send data. */
struct tcp_pcb *tcp_active_pcbs;
/** List of all TCP PCBs in TIME-WAIT state */
struct tcp_pcb *tcp_tw_pcbs;
  • tcp_bound_pcbs:调用 tcp_new() 与 tcp_bind() 创建完成的控制块,其状态为 CLOSED;

  • tcp_listen_pcbs:调用 tcp_listen() 设置监听的控制块,其状态为 LISTEN;

1.3.创建 TCP_PCB 控制块

tcp_new() 创建并返回 TCP_PCB 控制块指针,具体的创建工作由 tcp_alloc() 实现,如下所示:

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * Creates a new TCP protocol control block but doesn't place it on
 * any of the TCP PCB lists.
 * The pcb is not put on any list until binding using tcp_bind().
 * If memory is not available for creating the new pcb, NULL is returned.
 *
 * @internal: Maybe there should be a idle TCP PCB list where these
 * PCBs are put on. Port reservation using tcp_bind() is implemented but
 * allocated pcbs that are not bound can't be killed automatically if wanting
 * to allocate a pcb with higher prio (@see tcp_kill_prio())
 *
 * @return a new tcp_pcb that initially is in state CLOSED
 */
struct tcp_pcb *tcp_new(void)
{
  return tcp_alloc(TCP_PRIO_NORMAL); // TCP_PRIO_NORMAL == 64
}
------------------------------------------------------------------

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * Allocate a new tcp_pcb structure.
 *
 * @param prio priority for the new pcb
 * @return a new tcp_pcb that initially is in state CLOSED
 */
struct tcp_pcb *tcp_alloc(u8_t prio)
{
  struct tcp_pcb *pcb;
  ...

  pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB); // 从内存池中分配空间
  ...
  if (pcb != NULL) {
    /* zero out the whole pcb, so there is no need to initialize members to zero */
    memset(pcb, 0, sizeof(struct tcp_pcb));

    pcb->prio = prio;
    pcb->snd_buf = TCP_SND_BUF; // TCP_SND_BUF == 2 * 536
    /* Start with a window that does not need scaling. When window scaling is
       enabled and used, the window is enlarged when both sides agree on scaling. */
    pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND); // 4 * 536
    pcb->ttl = TCP_TTL; // TCP_TTL = 255
    /* As initial send MSS, we use TCP_MSS but limit it to 536.
       The send MSS is updated when an MSS option is received. */
    pcb->mss = INITIAL_MSS; // INITIAL_MSS == 536
    pcb->rto = 3000 / TCP_SLOW_INTERVAL;
    pcb->sv = 3000 / TCP_SLOW_INTERVAL; // TCP_SLOW_INTERVAL == 2 * 250
    pcb->rtime = -1;
    pcb->cwnd = 1;
    pcb->tmr = tcp_ticks;
    pcb->last_timer = tcp_timer_ctr;

    /* RFC 5681 recommends setting ssthresh abritrarily high and gives an example
    of using the largest advertised receive window.  We've seen complications with
    receiving TCPs that use window scaling and/or window auto-tuning where the
    initial advertised window is very small and then grows rapidly once the
    connection is established. To avoid these complications, we set ssthresh to the
    largest effective cwnd (amount of in-flight data) that the sender can have. */
    pcb->ssthresh = TCP_SND_BUF; // TCP_SND_BUF == 2 * 536
    ...
    pcb->recv = tcp_recv_null; // 回调函数

    /* Init KEEPALIVE timer */
    pcb->keep_idle  = TCP_KEEPIDLE_DEFAULT;
    ...
  }
  return pcb;
}

TCP_PCB 创建时绑定 Receive 默认的回调函数:tcp_recv_null(),其定义如下:

// Libraries/Ethernet/lwip/src/core/tcp.c
#if LWIP_CALLBACK_API
/**
 * Default receive callback that is called if the user didn't register
 * a recv callback for the pcb.
 */
err_t tcp_recv_null(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
  ...

  if (p != NULL) {
// -------------------------------
    tcp_recved(pcb, p->tot_len);
// -------------------------------
    pbuf_free(p);
  } else if (err == ERR_OK) {
    return tcp_close(pcb);
  }
  return ERR_OK;
}
#endif /* LWIP_CALLBACK_API */
--------------------------------------------------------------

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * This function should be called by the application when it has
 * processed the data. The purpose is to advertise a larger window
 * when the data has been processed.
 *
 * @param pcb the tcp_pcb for which data is read
 * @param len the amount of bytes that have been read by the application
 */
void tcp_recved(struct tcp_pcb *pcb, u16_t len)
{
  ...
  rcv_wnd = (tcpwnd_size_t)(pcb->rcv_wnd + len);
  ...
    pcb->rcv_wnd = rcv_wnd;
  ...
  wnd_inflation = tcp_update_rcv_ann_wnd(pcb);

  /* If the change in the right edge of window is significant (default
   * watermark is TCP_WND/4), then send an explicit update now.
   * Otherwise wait for a packet to be sent in the normal course of
   * events (or more window to be available later) */
  if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {
    tcp_ack_now(pcb);
    tcp_output(pcb);

2.TCP 控制块绑定 - tcp_bind()

设置默认的 IP 地址、端口号等,然后将其放入全局未建立连接/未监听的控制块链表:

// Echo.c
/* Function to initialize the ECHO program */
void echoInit(void)
{
    ...
    if (g_echoPcb != NULL)                                /* If the creation was successful... */
    {
        err_t err = tcp_bind(g_echoPcb, IP_ADDR_ANY, 80); /* ...bind the TCP procotol control block to any local address and local port 80. */

传入参数:

  • *pcb:tcp_new() 创建的 TCP_PCB 控制块 g_echoPcb;

  • *ipaddr:IP_ADDR_ANY 宏,展开为全局变量 &ip_addr_any,其初始地址为空(0.0.0.0);

  • port:端口号 80;

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * Binds the connection to a local port number and IP address. If the
 * IP address is not given (i.e., ipaddr == IP_ANY_TYPE), the connection is
 * bound to all local IP addresses.
 * If another connection is bound to the same port, the function will
 * return ERR_USE, otherwise ERR_OK is returned.
 *
 * @param pcb the tcp_pcb to bind (no check is done whether this pcb is
 *        already bound!)
 * @param ipaddr the local ip address to bind to (use IPx_ADDR_ANY to bind
 *        to any local address
 * @param port the local port to bind to
 * @return ERR_USE if the port is already in use
 *         ERR_VAL if bind failed because the PCB is not in a valid state
 *         ERR_OK if bound
 */
err_t tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
{
  int i;
  int max_pcb_list = NUM_TCP_PCB_LISTS;
  struct tcp_pcb *cpcb;
  ...

  if (!ip_addr_isany(ipaddr)
  {
    ...
    ip_addr_set(&pcb->local_ip, ipaddr);
        |--> &pcb->local_ip->addr = 0
  }

  pcb->local_port = port; // port = 80

// ---------------------------------------------
  TCP_REG(&tcp_bound_pcbs, pcb); // 将该控制块放入全局链表中
// ---------------------------------------------
-------------------------------------------------

// Libraries/Ethernet/lwip/src/include/lwip/priv/tcp_priv.h
#define TCP_REG(pcbs, npcb)                        \
  do {                                             \
    (npcb)->next = *pcbs;                          \
    *(pcbs) = (npcb);                              \
    tcp_timer_needed();                            \
  } while (0)
-------------------------------------------------

// Libraries/Ethernet/lwip/src/core/tcp.c
/** List of all TCP PCBs bound but not yet (connected || listening) */
struct tcp_pcb *tcp_bound_pcbs;

从上面的流程可以看到,这里仅将默认的 IP 地址设置为 0.0.0.0,端口号设置为 80,然后将 TCP_PCB 放入全局未连接/监听的控制块链表中

3.设置端口监听 - tcp_listen()

// Echo.c
/* Function to initialize the ECHO program */
void echoInit(void)
{
    ...
    if (g_echoPcb != NULL)                     /* If the creation was successful...                                 */
    {
        ...
        if (err == ERR_OK)                     /* If the binding was successful...                                  */
        {
            g_echoPcb = tcp_listen(g_echoPcb); /* ...set the TCP control block able to accept incoming connections. */

tcp_listen() 实际是一个宏,具体调用的方法为 tcp_listen_with_backlog_and_err(),其将连接状态设置为 LISTEN,表示允许接收连接请求,如下所示:

// Libraries/Ethernet/lwip/src/include/lwip/tcp.h
#define  tcp_listen(pcb)  tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
-----------------------------------------------------------------------------------------

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * Set the state of the connection to be LISTEN, which means that it
 * is able to accept incoming connections. The protocol control block
 * is reallocated in order to consume less memory. Setting the
 * connection to LISTEN is an irreversible process.
 * When an incoming connection is accepted, the function specified with
 * the tcp_accept() function will be called. The pcb has to be bound
 * to a local port with the tcp_bind() function.
 * 
 * The tcp_listen() function returns a new connection identifier, and
 * the one passed as an argument to the function will be
 * deallocated. The reason for this behavior is that less memory is
 * needed for a connection that is listening, so tcp_listen() will
 * reclaim the memory needed for the original connection and allocate a
 * new smaller memory block for the listening connection.
 *
 * tcp_listen() may return NULL if no memory was available for the
 * listening connection. If so, the memory associated with the pcb
 * passed as an argument to tcp_listen() will not be deallocated.
 *
 * The backlog limits the number of outstanding connections
 * in the listen queue to the value specified by the backlog argument.
 * To use it, your need to set TCP_LISTEN_BACKLOG=1 in your lwipopts.h.
 * 
 * @param pcb the original tcp_pcb
 * @param backlog the incoming connections queue limit
 * @return tcp_pcb used for listening, consumes less memory.
 *
 * @note The original tcp_pcb is freed. This function therefore has to be
 *       called like this:
 *             tpcb = tcp_listen_with_backlog(tpcb, backlog);
 */
struct tcp_pcb *
tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog)
{
  LWIP_ASSERT_CORE_LOCKED();
  return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);
}
-----------------------------------------------------------------------------------------

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * Set the state of the connection to be LISTEN, which means that it
 * is able to accept incoming connections. The protocol control block
 * is reallocated in order to consume less memory. Setting the
 * connection to LISTEN is an irreversible process.
 *
 * @param pcb the original tcp_pcb
 * @param backlog the incoming connections queue limit
 * @param err when NULL is returned, this contains the error reason
 * @return tcp_pcb used for listening, consumes less memory.
 *
 * @note The original tcp_pcb is freed. This function therefore has to be
 *       called like this:
 *             tpcb = tcp_listen_with_backlog_and_err(tpcb, backlog, &err);
 */
struct tcp_pcb *
tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err)
{
  struct tcp_pcb_listen *lpcb = NULL;
  ...

  lpcb = (struct tcp_pcb_listen *)memp_malloc(MEMP_TCP_PCB_LISTEN);
  ...
  lpcb->callback_arg = pcb->callback_arg;
  lpcb->local_port = pcb->local_port;
// ----------------------------
  lpcb->state = LISTEN;
// ----------------------------
  lpcb->prio = pcb->prio; // 0x40
  lpcb->so_options = pcb->so_options;
  lpcb->netif_idx = NETIF_NO_INDEX;
  lpcb->ttl = pcb->ttl; // 0xFF
  lpcb->tos = pcb->tos; // 0x00
  ...
  ip_addr_copy(lpcb->local_ip, pcb->local_ip);
  if (pcb->local_port != 0) {
    TCP_RMV(&tcp_bound_pcbs, pcb);
  }
  ...
  tcp_free(pcb);
// ----------------------------
#if LWIP_CALLBACK_API
  lpcb->accept = tcp_accept_null;
#endif /* LWIP_CALLBACK_API */
// ----------------------------
  ...
// ----------------------------
  TCP_REG(&tcp_listen_pcbs.pcbs, (struct tcp_pcb *)lpcb);
// ----------------------------
  res = ERR_OK;

done:
  ...
  return (struct tcp_pcb *)lpcb;
}

tcp_listen() 将 TCP_PCB 控制块的状态设置为 LISTEN,并将其放入 tcp_listen_pcbs 链表

同时,绑定默认的 Accept 回调函数为 tcp_recv_null(),如下所示:

// Libraries/Ethernet/lwip/src/core/tcp.c
#if LWIP_CALLBACK_API
/**
 * Default receive callback that is called if the user didn't register
 * a recv callback for the pcb.
 */
err_t
tcp_recv_null(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
  ...

  if (p != NULL) {
// --------------------------------------
    tcp_recved(pcb, p->tot_len);
// --------------------------------------
    pbuf_free(p);
  } else if (err == ERR_OK) {
    return tcp_close(pcb);
  }
  return ERR_OK;
}
#endif /* LWIP_CALLBACK_API */

4.设置回调函数 - tcp_accept()

// Echo.c
/* Function to initialize the ECHO program */
void echoInit(void)
{
    ...
    if (g_echoPcb != NULL)                     /* If the creation was successful... */
    {
        ...
        if (err == ERR_OK)                     /* If the binding was successful...  */
        {
            ...
            tcp_accept(g_echoPcb, echoAccept); /* Configure the callback function to be called when a new connection is established. */

tcp_accept() 绑定用户定义的相关回调函数 echoAccept(),如下所示:

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * Used for specifying the function that should be called when a
 * LISTENing connection has been connected to another host.
 *
 * @param pcb tcp_pcb to set the accept callback
 * @param accept callback function to call for this pcb when LISTENing
 *        connection has been connected to another host
 */
void
tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept) // accept == echoAccept
{
  ...
  if ((pcb != NULL) && (pcb->state == LISTEN)) {
    struct tcp_pcb_listen *lpcb = (struct tcp_pcb_listen *)pcb;
// ----------------------------------------
    lpcb->accept = accept; // 绑定回调函数
// ----------------------------------------
  }
}

tcp_accept() 绑定用户定义的回调函数 echoAccept(),其定义如下所示:

// Echo.c
/* Accept callback: it is called every time a client establish a new connection */
err_t echoAccept(void *arg, tcpPcb *newPcb, err_t err)
{
    ...
    EchoSession *es = (EchoSession*) mem_malloc(sizeof(EchoSession));   /* Allocate memory for the session data                             */

    if (es != NULL)                                   /* If memory allocation was successful the session can be initialized                 */
    {
        es->state = ES_ACCEPTED;                      /* The new session has been accepted                                                  */
        es->pcb = newPcb;                             /* A pointer to the TCP control block to use for this session is saved in the session */
        es->p = NULL;                                 /* The packet buffer for received data is initially empty                             */
        memset(es->storage, 0, STORAGE_SIZE_BYTES);   /* Allocate memory for storing processed received data                                */
        es->nextFreeStoragePos = 0;                   /* The first free position in the received data array is the beginning of the array   */

        tcp_arg(newPcb, es);                          /* Specify that the session shall be passed as argument to every callback function    */
            |--> pcb->callback_arg = arg;
        tcp_recv(newPcb, echoRecv);                   /* Configure the callback function to be called when new data is received             */
            |--> pcb->recv = recv;
        tcp_sent(newPcb, echoSent);                   /* Configure the callback function to be called when new data is sent                 */
            |--> pcb->sent = sent;
        tcp_err(newPcb, echoError);                   /* Configure the callback function to be called when a fatal connection error occurs  */
            |--> pcb->errf = err;
        tcp_poll(newPcb, echoPoll, 0);                /* Configure the callback function to be periodically called by TCP
            |--> pcb->poll = poll;                     * The time interval is specified as multiple of the TCP coarse timer interval, which is
                                                       * called twice a second                                                              */
        ...
        tcp_write(newPcb, g_Logo, strlen(g_Logo), 1); /* Send the Infineon logo to the remote client                                        */

4.1.基本概念 - 会话(Session)

计算机网络中,会话(Session)指两个或多个通信设备之间,或计算机与用户之间临时的、交互式的信息交换过程

会话在某个时间点建立,然后在稍后的某个时间点结束,已建立的会话可能涉及每个方向的多个消息

会话通常是有状态的,这意味着至少有一个通信方需要保存当前的状态信息与历史信息,以便能够进行通信

该例程中,session 的定义如下所示:

// Echo.c
typedef struct    /* Session data structure used for communicating with a single remote client */
{
    u8_t state;  /* The current state for the session */
    tcpPcb *pcb; /* Pointer to the TCP protocol control block used for this session */
    pBuf *p;     /* Pointer to the packet buffers used to store received packets */
    char storage[STORAGE_SIZE_BYTES]; /* Storage for the received strings */
    uint16 nextFreeStoragePos;        /* Position of the next free position in the storage array */
} EchoSession;

其中,部分成员的含义如下所示:

  • state:当前会话的状态,包含如下几种状态:

// Echo.c
enum EchoStates   /* States of the session with the remote user */
{
    ES_NONE = 0,  /* Session not completely initialized */
    ES_ACCEPTED,  /* Session assigned to a remote client and resources are being allocated */
    ES_RECEIVING, /* Session is receiving data sent from remote client */
    ES_CLOSING    /* Session is being closed and assigned resources will be deallocated */
};
  • pcb:当前会话的 TCP_PCB 控制块

// Echo.c
typedef struct tcp_pcb tcpPcb; /* Define a more convenient type */
  • storage[]:用于保存接收到的字符串,数组最大容量 STORAGE_SIZE_BYTES == 256

// Echo.c
#define STORAGE_SIZE_BYTES 256 /* Size in bytes of the space in memory allocated for storing incoming data */

4.2.数据解包 - echoUnpack()

数据包使用 pbuf 存储,可以拷贝或引用数据包的实际数据,其定义如下所示:

// Libraries/Ethernet/lwip/src/include/lwip/pbuf.h
/** Main packet buffer struct */
struct pbuf {
  /** next pbuf in singly linked pbuf chain */
  struct pbuf *next;

// ------------------------------------------------------
  /** pointer to the actual data in the buffer */
  void *payload;
// ------------------------------------------------------

// ------------------------------------------------------
  /**
   * total length of this buffer and all next buffers in chain
   * belonging to the same packet.
   *
   * For non-queue packet chains this is the invariant:
   * p->tot_len == p->len + (p->next? p->next->tot_len: 0)
   */
  u16_t tot_len;
// ------------------------------------------------------

// ------------------------------------------------------
  /** length of this buffer */
  u16_t len;
// ------------------------------------------------------

  /** a bit field indicating pbuf type and allocation sources
      (see PBUF_TYPE_FLAG_*, PBUF_ALLOC_FLAG_* and PBUF_TYPE_ALLOC_SRC_MASK)
    */
  u8_t type_internal;

  /** misc flags */
  u8_t flags;

// ------------------------------------------------------
  /**
   * the reference count always equals the number of pointers
   * that refer to this pbuf. This can be pointers from an application,
   * the stack itself, or pbuf->next pointers from a chain.
   */
  LWIP_PBUF_REF_T ref;
// ------------------------------------------------------

  /** For incoming packets, this contains the input netif's index */
  u8_t if_idx;

  /** In case the user needs to store custom data on a pbuf */
  LWIP_PBUF_CUSTOM_DATA
};

pbuf 以链表形式存储数据包中的实际数据,其中主要关注的成员如下:

  • *payload:指向数据包内容的指针;

  • tot_len:整个链表的长度;

  • len:当前 pbuf 的长度(数据长度);

  • ref:引用计数,为 "0" 时释放该 pbuf;

echoUnpack() 循环从 session 的 pbuf 链表中获取 pbuf 节点,并从节点中拷贝数据,如下图所示:

数据拷贝完成后,便会立即释放该 pbuf 节点,这里需要注意的是,session 中包含了 pbuf 的链表指针,解包的目的是将其指向的 pbuf 中的数据,存入 session 自身的 buffer 中,如下所示:

// Echo.c
/* Unpack function: dequeues data from the package buffer and copies it in the session storage */
void echoUnpack(tcpPcb *tpcb, EchoSession *es)
{
    pBuf *ptr;              /* Local reference to a packet buffer                    */

    // 循环从 session 的 pbuf 链表中获取 pbuf 节点
    while ((es->p != NULL)) /* Process data while there still are packets to process */
    {
// -------------------------------------------------------------------------------------
        // 获取一个 pbuf 节点
        ptr = es->p;        /* Local reference of the packet to process               */
// -------------------------------------------------------------------------------------

        // 检查当前会话的 buffer 是否有足够的空间存放数据(小于 256 bytes)
        if (es->nextFreeStoragePos + ptr->len <= STORAGE_SIZE_BYTES)    /* If there is enough space in the session storage for the new data ... */
        {
// --------------------------------------------------------------------------------------
            // 将数据从 pbuf 节点拷贝至 es 自身的 buffer 中
            memcpy(&es->storage[es->nextFreeStoragePos],  /* ... copy data in the session storage */
                    ptr->payload,
                    ptr->len);
            es->nextFreeStoragePos += ptr->len;           /* Increase the index of the new free position in the session storage array */
// --------------------------------------------------------------------------------------

            u16_t plen = ptr->len;                        /* Number of bytes successfully received and stored */

// --------------------------------------------------------------------------------------
            // 将 es 的 pbuf 指针指向 pbuf 链表的下一个节点
            es->p = ptr->next;                             /* Get the next packet in the chain (if any) */
// --------------------------------------------------------------------------------------

            if (es->p != NULL)                             /* If there is another packet to be processed... */
            {   // pbuf 引用计数加 1,防止下一个节点被释放
                pbuf_ref(es->p);                           /* Inform the LwIP framework that the packet to be processed is linked to the session, */
            }                                              /* and shall not be disposed */

// --------------------------------------------------------------------------------------
            // 释放刚刚已经被取出数据的 pbuf 节点
            u8_t freed;                                    /* Number of bytes successfully freed from memory */
            do
            {
                freed = pbuf_free(ptr);                    /* Free memory used by the packet that was just processed */
                    |--> ref = --(p->ref); // 引用计数减 1
                    |--> if (ref == 0) --> mem_free(p); // 引用计数为 0 时释放内存
            } while (freed == 0);
// --------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------
            // 调整数据收发的窗口
            tcp_recved(tpcb, plen); /* Signal the remote host that more data can be received */
// --------------------------------------------------------------------------------------

每次从 pbuf 节点中取出数据后,都会通知主机,表示可以接收更多数据,同时,主机会调整数据收发时使用的窗口大小,准备接收后续数据,如下所示:

// Libraries/Ethernet/lwip/src/core/tcp.c
/**
 * @ingroup tcp_raw
 * This function should be called by the application when it has
 * processed the data. The purpose is to advertise a larger window
 * when the data has been processed.
 *
 * @param pcb the tcp_pcb for which data is read
 * @param len the amount of bytes that have been read by the application
 */
void
tcp_recved(struct tcp_pcb *pcb, u16_t len)
{
  ...
// ---------------------------------------------------------------------------------
  // 调整数据接收窗口的大小
  rcv_wnd = (tcpwnd_size_t)(pcb->rcv_wnd + len); // 扩大数据接收窗口大小
  if ((rcv_wnd > TCP_WND_MAX(pcb)) || (rcv_wnd < pcb->rcv_wnd)) {
    /* window got too big or tcpwnd_size_t overflow */
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_recved: window got too big or tcpwnd_size_t overflow\n"));
    pcb->rcv_wnd = TCP_WND_MAX(pcb); // 如果超过窗口大小的最大值,则将窗口大小设置为 4 * 536
  } else  {
    pcb->rcv_wnd = rcv_wnd; // 调整 PCB 的接收窗口大小
  }
// ---------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------
  // 通知发送方能发送多大的数据
  wnd_inflation = tcp_update_rcv_ann_wnd(pcb);
      |--> pcb->rcv_ann_wnd = pcb->rcv_wnd;
// ---------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------
  /* If the change in the right edge of window is significant (default
   * watermark is TCP_WND/4), then send an explicit update now.
   * Otherwise wait for a packet to be sent in the normal course of
   * events (or more window to be available later) */
  if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {
    tcp_ack_now(pcb); // 修改通知窗口的大小
        |--> tcp_set_flags(pcb, TF_ACK_NOW)
            |--> (pcb)->flags = (tcpflags_t)((pcb)->flags | (TF_ACK_NOW))
    tcp_output(pcb); // 发送应答报文 ACK
        |--> if (pcb->flags & TF_ACK_NOW) --> tcp_send_empty_ack(pcb);
  }
// ---------------------------------------------------------------------------------

tcp_recved() 调整数据窗口的大小,并设置 TF_ACK_NOW 应答标志,之后通知发送方,表示之前的数据已收到,可以发送下一个报文

4.3.数据打包 - echoSend()

echoSend() 实际调用 tcp_write(),通过已建立连接的 TCP_PCB 控制块向对方发送数据(将数据复制到控制块的 unsent 队列),如下所示:

// Echo.c
/* Send function: enqueues TCP data to be delivered to the remote client */
void echoSend(tcpPcb *tpcb, EchoSession *es)
{
    ...
    err_t wrErr = tcp_write(tpcb, "Board: ", 7, 1);                   /* Enqueue an echo preamble to be sent to the remote client */
    wrErr |= tcp_write(tpcb, es->storage, es->nextFreeStoragePos, 1); /* Enqueue the string stored in the session for sending     */
    ...

tcp_write() 将待发送的数据,复制到 TCP_PCB 控制块的 unsent 队列中,但是不会立即发送数据

该函数不会立即发送数据,如果需要立即发送数据,需要在 tcp_write() 后立即执行 tcp_output()

tcp_write() 分为两个部分,每个部分分为三个阶段:

  • 创建 TCP 报文:

    • 获取待发送报文队列 unsent 中的最后一个报文节点,计算其 pbuf 的剩余可用空间;

    • 在 pcb->unsent 队列末尾新增 pbuf 节点,并将待发送的数据拷贝至该 pbuf 中;

    • 创建新的报文;

  • 将报文加入 unsent 队列:

    • 将数据拷贝至待发送的报文节点中;

    • 将新建的 pbuf 加入 last_unsent 报文的链表中;

    • 将 last_unsent 报文节点插入控制块的 unsent 队列中;

4.3.1.创建 TCP 报文

  • 获取待发送报文队列 unsent 中最后一个报文节点,计算其 pbuf 的剩余可用空间:

// Libraries/Ethernet/lwip/src/core/tcp_out.c
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
{
  ...
  // 计算报文的最大长度
  mss_local = LWIP_MIN(pcb->mss, TCPWND_MIN16(pcb->snd_wnd_max / 2));
  mss_local = mss_local ? mss_local : pcb->mss;

    // 获取 unsent 队列的最后一个节点
    for (last_unsent = pcb->unsent; last_unsent->next != NULL;
         last_unsent = last_unsent->next);

    // 计算节点报文标志位长度
    unsent_optlen = LWIP_TCP_OPT_LENGTH_SEGMENT(last_unsent->flags, pcb);
    // 计算该节点报文的剩余可用空间
    space = mss_local - (last_unsent->len + unsent_optlen);

    // 获取最后一个报文中 pbuf 的剩余可用空间
    oversize = pcb->unsent_oversize;
    if (oversize > 0) { // 如果存在可用的剩余空间
      ...
      seg = last_unsent;
      // 计算存储新报文所需的空间大小
      oversize_used = LWIP_MIN(space, LWIP_MIN(oversize, len));
      // 更新相关空间大小
      pos += oversize_used;
      oversize -= oversize_used;
      space -= oversize_used;
    }

其中,需要区分 space 和 oversize 两个变量,一个报文可以挂载多个 pbuf,space 表示该报文的剩余空间,oversize 表示的是 unsent 队列中,最后一个 pbuf 的剩余空间

  • 在 pcb->unsent 队列末尾新增 pbuf 节点,并将待发送的数据拷贝至该 pbuf 中

这里需要注意,数据拷贝的方式有两种:内存拷贝、内存引用,通过 apiflags 标志位进行判断:

// Libraries/Ethernet/lwip/src/core/tcp_out.c
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
{
  ...
    if ((pos < len) && (space > 0) && (last_unsent->len > 0)) {
      u16_t seglen = LWIP_MIN(space, len - pos);
      seg = last_unsent;

// ---------------------------------------------------------
      /* Create a pbuf with a copy or reference to seglen bytes. We
       * can use PBUF_RAW here since the data appears in the middle of
       * a segment. A header will never be prepended. */
      if (apiflags & TCP_WRITE_FLAG_COPY) { // 使用拷贝内存的方式
        // 申请大小为 seglen 的 pbuf
        if ((concat_p = tcp_pbuf_prealloc(PBUF_RAW, seglen, space, &oversize, pcb, apiflags, 1)) == NULL) {
          ...
        // 将新数据拷贝至该 pbuf 中
        TCP_DATA_COPY2(concat_p->payload, (const u8_t *)arg + pos, seglen, &concat_chksum, &concat_chksum_swapped);
        ...
        queuelen += pbuf_clen(concat_p); // 将 pbuf 加入链表
// ---------------------------------------------------------

// ---------------------------------------------------------
      } else { // 使用内存引用的方式
        /* If the last unsent pbuf is of type PBUF_ROM, try to extend it. */
        struct pbuf *p; // 新建 pbuf
        // 查找 last_unsent 节点中的最后一个 pbuf 
        for (p = last_unsent->p; p->next != NULL; p = p->next);
        // 如果该 pbuf 内存连续,则直接扩展其长度,不用重新分配
        if (((p->type_internal & (PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_DATA_VOLATILE)) == 0) &&
            (const u8_t *)p->payload + p->len == (const u8_t *)arg) {
          LWIP_ASSERT("tcp_write: ROM pbufs cannot be oversized", pos == 0);
          extendlen = seglen;
        } else {
          // 申请一块 PBUF_ROM 类型的内存
          if ((concat_p = pbuf_alloc(PBUF_RAW, seglen, PBUF_ROM)) == NULL) {
          ...
          // 将新 pbuf 的 payload 指向数据地址
          ((struct pbuf_rom *)concat_p)->payload = (const u8_t *)arg + pos;
          queuelen += pbuf_clen(concat_p); // 将 pbuf 加入链表
// ---------------------------------------------------------
  • 创建新的报文(区分内存拷贝 or 内存引用):

// Libraries/Ethernet/lwip/src/core/tcp_out.c
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
{
  ...
  while (pos < len) {
  ...
// ---------------------------------------------------------
    if (apiflags & TCP_WRITE_FLAG_COPY) { // 使用内存拷贝
      /* If copy is set, memory should be allocated and data copied into pbuf */
      // 创建 pbuf,其大小为报文大小 + TCP 首部
      if ((p = tcp_pbuf_prealloc(PBUF_TRANSPORT, seglen + optlen, mss_local, &oversize, pcb, apiflags, queue == NULL)) == NULL) {
      ...
      // 将待发送的数据拷贝至新建的 pbuf 中
      TCP_DATA_COPY2((char *)p->payload + optlen, (const u8_t *)arg + pos, seglen, &chksum, &chksum_swapped);
// ---------------------------------------------------------

// ---------------------------------------------------------
    } else { // 使用内存引用
    ...
    // 创建存放数据的 pbuf
    if ((p2 = pbuf_alloc(PBUF_TRANSPORT, seglen, PBUF_ROM)) == NULL) {
    ...
    // 将数据 pbuf 的 payload 指向数据所处内存
    ((struct pbuf_rom *)p2)->payload = (const u8_t *)arg + pos;
    ...
    // 创建存放 TCP 报文头的 pbuf
    if ((p = pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)) == NULL) {
    ...
    // 连接报文头和数据
    pbuf_cat(p/*header*/, p2/*data*/);
// ---------------------------------------------------------
    }
    ...
// ---------------------------------------------------------
    // 创建新的报文
    if ((seg = tcp_create_segment(pcb, p, 0, pcb->snd_lbb + pos, optflags)) == NULL) {
    
    // 将报文加入报文队列
    if (queue == NULL) {
      queue = seg;
    } else {
      ...
      prev_seg->next = seg;
    }
// ---------------------------------------------------------

4.3.2.将报文加入 unsent 队列

  • 将数据拷贝至待发送的报文节点中:

// Libraries/Ethernet/lwip/src/core/tcp_out.c
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
{
  ...
  /*
   * Phase 1: If data has been added to the preallocated tail of
   * last_unsent, we update the length fields of the pbuf chain.
   */
  if (oversize_used > 0) { // 检查 last_unsent(队列中最后一个报文节点)是否可以继续填充数据
    struct pbuf *p;
    /* Bump tot_len of whole chain, len of tail */
    for (p = last_unsent->p; p; p = p->next) { // 找到该报文节点的最后一个 pbuf
      p->tot_len += oversize_used;
      if (p->next == NULL) { // 将 oversize_used 大小的数据填充入该 pbuf 中
        TCP_DATA_COPY((char *)p->payload + p->len, arg, oversize_used, last_unsent);
        p->len += oversize_used;
      }
    }
    last_unsent->len += oversize_used; // 最后一个报文节点的长度增加
    ...
    last_unsent->oversize_left -= oversize_used; // 最后一个报文节点的剩余空间减少
  }
  pcb->unsent_oversize = oversize; // 更新控制块待发送报文的长度
  • 将新建的 pbuf 加入 last_unsent 报文节点的链表中:

// Libraries/Ethernet/lwip/src/core/tcp_out.c
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
{
  ...
  /*
   * Phase 2: concat_p can be concatenated onto last_unsent->p, unless we
   * determined that the last ROM pbuf can be extended to include the new data.
   */
// ---------------------------------------------------------
  if (concat_p != NULL) { // 使用内存拷贝的方式
    ...
    pbuf_cat(last_unsent->p, concat_p); // 将新建的 pbuf 加入 last_unsent 的 pbuf 链表中
    last_unsent->len += concat_p->tot_len;
// ---------------------------------------------------------

// ---------------------------------------------------------
  } else if (extendlen > 0) { // 使用内存引用的方式
    struct pbuf *p;
    ...
    // 将所有 pbuf 的总长度增加
    for (p = last_unsent->p; p->next != NULL; p = p->next) {
      p->tot_len += extendlen;
    }
    // 由于内存地址连续,因此直接增加长度,通过指针和内存长度获取数据
    p->tot_len += extendlen;
    p->len += extendlen;
    last_unsent->len += extendlen; // 增加报文总长度
  }
// ---------------------------------------------------------
  • 将 last_unsent 报文节点插入控制块的 unsent 队列中:

// Libraries/Ethernet/lwip/src/core/tcp_out.c
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
{
  ...
  /* Phase 3: Append queue to pcb->unsent. Queue may be NULL, but that is harmless */
  if (last_unsent == NULL) {
    pcb->unsent = queue;
  } else {
    last_unsent->next = queue; // 更新 unsent 队列
  }

  /* Finally update the pcb state. */
  pcb->snd_lbb += len;
  pcb->snd_buf -= len;
  pcb->snd_queuelen = queuelen;
  ...

  /* Set the PSH flag in the last segment that we enqueued. */
  if (seg != NULL && seg->tcphdr != NULL && ((apiflags & TCP_WRITE_FLAG_MORE) == 0)) {
    TCPH_SET_FLAG(seg->tcphdr, TCP_PSH); // 将标志位置为 PUSH,即将数据发送给应用层,而不是等待缓冲区填满
  }

  return ERR_OK;

4.4.关闭会话 - echoClose()

关闭会话主要包含三点:

  • 将所有的回调函数置为 NULL;

  • 释放 session 内存(销毁 session);

  • 断开 TCP 连接;

// Echo.c
/* Close function: closes a TCP connection and deallocates session resources */
void echoClose(tcpPcb *tpcb, EchoSession *es)
{
// ----------------------------------------------------------------
    // 将回调函数置为 NULL
    tcp_arg(tpcb, NULL);     /* Set the argument passed to every callback to NULL */
    tcp_sent(tpcb, NULL);    /* Set the data sent callback to NULL                */
    tcp_recv(tpcb, NULL);    /* Set the data received callback to NULL            */
    tcp_err(tpcb, NULL);     /* Set the data error callback to NULL               */
    tcp_poll(tpcb, NULL, 0); /* Set the periodical callback to NULL               */
// ----------------------------------------------------------------

// ----------------------------------------------------------------
    // 销毁 session
    if (es != NULL)             /* If a session still exists...           */
    {
        mem_free(es);           /* ...free memory assigned to the session. */
    }
// ----------------------------------------------------------------

// ----------------------------------------------------------------
    // 断开 TCP 连接
    tcp_close(tpcb);            /* Close the TCP connection */
        |--> tcp_close_shutdown(pcb, 1);
// ----------------------------------------------------------------
}

4.5.接收回调 - echoRecv()

数据接收存在如下几种情况:

  • 待接收的数据为空:此时会检查是否存在尚未处理的数据,如果有则进行处理,否则会直接关闭连接;

  • 存在错误:此时不会改变连接的状态,但是会将会话的 pbuf 销毁;

  • ES_ACCEPTED:将状态设置为 ES_RECEIVING,对数据进行解包及发送;

  • ES_RECEIVING:正在接收数据中,此时又收到新数据,如果原来的数据已经被处理完,则立即处理新数据,否则将新数据放在待处理数据的末尾,随着之前的数据一起处理;

  • 未知状态:将收到的数据标记为已处理,然后直接销毁,不进行处理;

// Echo.c
/* Recv callback: it is called every time data is received through the TCP connection */
err_t echoRecv (void *arg, tcpPcb *tpcb, pBuf *p, err_t err)
{
    err_t retErr;                         /* Allocate memory for function return value */
    EchoSession *es = (EchoSession*) arg; /* Get a pointer to the current session      */

// ----------------------------------------------------------------
    // 未查找到需要接收的数据,表示对方已断开连接
    if (p == NULL)                /* If there is  no enqueued received data after the RECV callback was called, it means the */
    {                             /* remote client closed the connection in the meanwhile                                    */
        es->state = ES_CLOSING;   /* Set the state of this session to CLOSING in order to free its resources                 */
        if (es->p == NULL)        /* If the session does not have any leftover unprocessed data...                           */
        {
            echoClose(tpcb, es);  /* ... close the session and free its resources.                                           */
        }
        else                      /* If the session does have leftover unprocessed data...                                   */
        {
            echoUnpack(tpcb, es); /* ... process unprocessed data ...                                                        */
            echoSend(tpcb, es);   /* ... and send processed data.                                                            */
        }
        retErr = ERR_OK;          /* Signal a successful outcome                                                             */
    }
// ----------------------------------------------------------------

// ----------------------------------------------------------------
    // 存在错误
    else if (err != ERR_OK) /* If there was an error while receiving data...                            */
    {
        /* Cleanup, for unkown reason */
        if (p != NULL)      /* ... if some data was received and stored...                               */
        {
            es->p = NULL;   /* ... invalidate the unprocessed data contained in the session.             */
            pbuf_free(p);   /* Dereference and deallocate last received data which is probably corrupted */
        }
        retErr = err;       /* Propagate the error                                                       */
    }
// ----------------------------------------------------------------

// ----------------------------------------------------------------
    // 当前状态为 ACCEPTED,表示允许数据接收
    else if (es->state == ES_ACCEPTED) /* If the current session is in state ACCEPTED...                     */
    {
        es->state = ES_RECEIVING;      /* ... change its state to RECEIVING.                                 */
        es->p = p;                     /* Set the unprocessed data buffer of the session to the received one */
        echoUnpack(tpcb, es);          /* Perform a first incoming data processing                           */
        echoSend(tpcb, es);            /* Send the first echo to remote client                               */
        retErr = ERR_OK;               /* Signal a successful outcome                                        */
    }
// ----------------------------------------------------------------

// ----------------------------------------------------------------
    // 当前状态为 ES_RECEIVING,表示正在接收数据中,此时又有新数据到达
    else if (es->state == ES_RECEIVING) /* If the current session was already receiving data...                     */
    {
        if (es->p == NULL)              /* ... and no unprocessed data is contained in the session...               */
        {
            es->p = p;                  /* ... set the unprocessed data buffer of the session to the received one   */
            echoUnpack(tpcb, es);       /* Process the incoming data                                                */
            echoSend(tpcb, es);         /* Send an echo to the remote client                                        */
        }
        else                            /* If the session still contains some unprocessed received data...          */
        {
            pBuf *ptr = es->p;          /* ... create a local reference for the received data                       */
            pbuf_chain(ptr, p);         /* Chain the old unprocessed data contained in the session with the new one */
        }
        retErr = ERR_OK;                /* Signal a successful outcome                                              */
    }
// ----------------------------------------------------------------

// ----------------------------------------------------------------
    // 未知状态下不会接收数据,并且会直接将该数据抛弃
    else                              /* If we got new incoming data in an unknown state, discard the data */
    {
        tcp_recved(tpcb, p->tot_len); /* Mark the incoming data as received                                */
        es->p = NULL;                 /* Invalidate the unprocessed data contained in the session          */
        pbuf_free(p);                 /* Dereference and deallocate last received data                     */
        retErr = ERR_OK;              /* Signal a successful outcome                                       */
    }
// ----------------------------------------------------------------

    return retErr; /* Return result */
}

4.6.发送回调 - echoSent()

发送回调直接调用 echoUnpack() 解包数据,然后使用 echoSend() 发送数据,如下所示:

// Echo.c
/* Sent callback: it is called when TCP data has successfully been delivered to the remote host  */
err_t echoSent(void *arg, tcpPcb *tpcb, u16_t len)
{
    LWIP_UNUSED_ARG(len);                 /* Eliminates compiler warning about unused arguments  */

    EchoSession *es = (EchoSession*) arg; /* Get a pointer to the current session                */

    if (es->p != NULL )                   /* If the session has any leftover unprocessed data... */
    {
        echoUnpack(tpcb, es);             /* ... process unprocessed data ...                    */
        echoSend(tpcb, es);               /* ... and send processed data.                        */
    }
    else                                  /* If the session has no leftover unprocessed data...  */
    {
        if (es->state == ES_CLOSING)      /* ... and the session was marked for being closed...  */
        {
            echoClose(tpcb, es);          /* ... close the session and free its resources.       */
        }
    }
    return ERR_OK;                        /* Return successful result                            */
}

4.7.错误回调 - echoError()

产生错误时会直接销毁当前会话,如下所示:

// Echo.c
/* Error callback: it is called if a fatal error has already occurred on the connection */
void echoError(void *arg, err_t err)
{
    LWIP_UNUSED_ARG(err);                 /* Eliminate compiler warning about unused arguments */

    EchoSession *es = (EchoSession*) arg; /* Get a pointer to the current session              */

    if (es != NULL)   /* If a session exists we cannot do anything with it anymore, since the fatal error */
    {                 /* occurred ...                                                                     */
        mem_free(es); /* ... free memory assigned to the session                                           /
    }
}

4.8.Poll 回调 - echoPoll()

Poll 检查当前会话是否存在,如果存在且有数据待处理则进行处理,如果没有待处理的数据,则会判断当前连接是否需要关闭,如下所示:

// Echo.c
/* Poll function: it is called periodically by the TCP stack */
err_t echoPoll(void *arg, tcpPcb *tpcb)
{
    err_t retErr;                         /* Allocate memory for function return value */
    EchoSession *es = (EchoSession*) arg; /* Get a pointer to the current session      */

    if (es != NULL)                       /* If a session exists ...                   */
    {
        if (es->p != NULL ||              /* ... and if the session has any leftover unprocessed data...       */
            es->nextFreeStoragePos != 0)  /* ... or there is still data in the session storage to send back... */
        {
            echoUnpack(tpcb, es);         /* ... process unprocessed data ... */
            echoSend(tpcb, es);           /* ... and send processed data.     */
        }
        else                              /* If the session has no leftover unprocessed data... */
        {
            if (es->state == ES_CLOSING)  /* ... and the session was marked for being closed... */
            {
                echoClose(tpcb, es);      /* ... close the session and free its resources.      */
            }
        }
        retErr = ERR_OK;                  /* Signal a successful outcome                         */
    }
    else                                  /* If no session exists something went wrong, the TCP connection has to be aborted */
    {
        tcp_abort(tpcb);                  /* Abort the TCP connection   */
        retErr = ERR_ABRT;                /* Propagate the abort error  */
    }
    return retErr;                        /* Return result              */
}

下一篇:TC397以太网例程详解] - 17.ECHO 应用 - 轮询定时器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值