如题,一次测试中发现,lwip的TCP连接控制块明明设置为26,设备作为服务器,电脑作为客户端去连接,但当连接19个之后发现再连接TCP时,机器自动发送RST数据包,通过追踪发现是因为资源分配不足,tcp_alloc失败,后面看到一篇文章,是因为lwip的内存堆使用太小,本人使用的是rt-thread,apm32f407,最后发现是因为程序使用了110KB,总内存是128,所以给rtt只有18KB,内存堆过小。后面将一片空间分配给CCM后解决。
#pragma location = 0x10000000
struct selfstruct_t s_data[40*1024];
RST被调用路径为:
tcp_input->tcp_listen_input->tcp_alloc->tcp_kill_timewait->tcp_abort->tcp_abandon->tcp_rst
后面有测试发现只能连接21个,连接22个时自动发送了分手报文。经过排查发现sockets连接定义为52,导致sockets不足,修改宏定义:
/* MEMP_NUM_NETCONN: the number of struct netconns. */
#define MEMP_NUM_NETCONN 64 //52
分手报文发送被调用的路径为:
lwip_accept->alloc_socket 失败 执行 netconn_delete->
err_t
netconn_delete(struct netconn *conn)
{
struct api_msg msg;
msg.function = do_delconn;
msg.msg.conn = conn;
tcpip_apimsg(&msg);
netconn_free(conn);
return ERR_OK;
}
之后再tcpip_thread中->do_delconn->do_close_internal->tcp_close->tcp_close_shutdown->tcp_send_fin->tcp_enqueue_flags.
题外话:
RST 标志是通过 tcp_rst 函数发送的。这个函数声明为:
/**
* Send a TCP RESET packet (empty segment with RST flag set) either to
* abort a connection or to show that there is no matching local connection
* for a received segment.
*
* Called by tcp_abort() (to abort a local connection), tcp_input() (if no
* matching local pcb was found), tcp_listen_input() (if incoming segment
* has ACK flag set) and tcp_process() (received segment in the wrong state)
*
* Since a RST segment is in most cases not sent for an active connection,
* tcp_rst() has a number of arguments that are taken from a tcp_pcb for
* most other segment output functions.
*
* @param pcb TCP pcb (may be NULL if no pcb is available)
* @param seqno the sequence number to use for the outgoing segment
* @param ackno the acknowledge number to use for the outgoing segment
* @param local_ip the local IP address to send the segment from
* @param remote_ip the remote IP address to send the segment to
* @param local_port the local TCP port to send the segment from
* @param remote_port the remote TCP port to send the segment to
*/
void
tcp_rst(const struct tcp_pcb *pcb,
u32_t seqno,
u32_t ackno,
const ip_addr_t *local_ip,
const ip_addr_t *remote_ip,
u16_t local_port,
u16_t remote_port)
从注释得知,tcp_rst 函数发送 TCP RESET 数据包(带有 RST 标志的空帧),用于中止连接或者向对方表明你指定的数据接收者查无此人(接收到的数据帧没有匹配的本地控制块 pcb)。
tcp_rst 函数主要由以下函数调用:
tcp_abort 函数:中止一个本地连接
tcp_input 函数:没有找到匹配的本地控制块 pcb
tcp_listen_input 函数:接收到的帧设置了 ACK 标志
tcp_process 函数:接收到包含错误状态的帧
tcp_input 函数中
1.本地连接接收端已经关闭,但仍收到数据,调用 tcp_abort 函数,发送 RST 标志,通知远程主机并非所有数据都被处理。简化后的代码如下所示:
void
tcp_input(struct pbuf *p, struct netif *inp)
{
// 经过一系列检测,没有错误
/* 在本地找到有效的控制块 pcb */
if (pcb != NULL) {
err = tcp_process(pcb);
/* 报文中包含有效数据 */
if (recv_data != NULL) {
if (pcb->flags & TF_RXCLOSED) {
/* received data although already closed -> abort (send RST) to
notify the remote host that not all data has been processed */
pbuf_free(recv_data);
tcp_abort(pcb);
goto aborted;
}
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
}
}
return;
}
这个代码也能看出,如果报文中包含有效数据,指向数据的 pbuf 缓存由应用程序释放,其它情况都是由内核释放。
2.根据接收到的数据包内容(IP、端口号)查找本地控制块 pcb ,发现匹配的 pcb 处于 TIME_WAIT 状态,则调用 tcp_timewait_input 函数,在这个函数中若满足:报文段包含握手 SYN 标志且报文段编号合法,则调用 tcp_rst 函数发送 RST 标志。
void
tcp_input(struct pbuf *p, struct netif *inp)
{
// 经过一系列检测,没有错误
// 在 tcp_active_pcbs 链表中没有找到匹配的控制块 pcb
if (pcb == NULL) {
/*在 tcp_tw_pcbs 链表中查找匹配的控制块 pcb*/
for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) {
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"));
tcp_timewait_input(pcb);
pbuf_free(p);
return;
}
}
}
}
3.根据接收到的数据包内容(IP、端口号)查找本地控制块 pcb ,若发现“查无此人”,则向数据发送者发送 RST 标志,以复位连接。
void
tcp_input(struct pbuf *p, struct netif *inp)
{
// 经过一系列检测,没有错误
// 在 tcp_active_pcbs 链表中【没有】找到匹配的控制块 pcb
// 在 tcp_tw_pcbs 链表中【没有】找到匹配的控制块 pcb
// 在 tcp_listen_pcbs.listen_pcbs 链表中【没有】找到匹配的控制块 pcb
/* If no matching PCB was found, send a TCP RST (reset) to the
sender. */
LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_input: no PCB match found, resetting.\n"));
if (!(TCPH_FLAGS(tcphdr) & TCP_RST)) {
tcp_rst(NULL, ackno, seqno + tcplen, ip_current_dest_addr(),
ip_current_src_addr(), tcphdr->dest, tcphdr->src);
}
pbuf_free(p);
return;
}
这在客户端程序中很有用。
假如你的设备使用 lwIP 协议栈(裸机),作客户端,使用出错重连机制连接上位机。如果上位机程序关闭,就会出现以下过程:
设备发送 SYN 建立连接 -> 上位机操作系统找不到匹配的控制块 pcb (因为上位机程序关闭了)-> 上位机发送 RST 标志复位连接 -> 设备触发出错重连 -> 设备发送 SYN 建立连接 -> …
出现死循环连接!!
这一过程可能会非常快,当设备很多时,会在局域网中形成 SYN 风暴。
这个知识还可以用于判断连接失败原因。
比如你已经开启了上位机程序,但发现连不上,抓包发现每次连接,上位机都回复 RST 标志,最可能的原因是你连接的端口号错了。
tcp_process 函数中
1.处于 SYN_RCVD(LISTEN 状态的服务器,接收到 SYN 握手标志后,进入 SYN_RCVD )状态的连接。
收到正确的 ACK 标志后,回调 accept 函数,表示有新的连接建立。若 accept 回调函数返回值不是 ERR_OK ,则调用 tcp_abort()函数,发送 RST 标志。
如果收到的 ACK 序号不合法,则调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:
/**
* Implements the TCP state machine. Called by tcp_input. In some
* states tcp_receive() is called to receive data. The tcp_seg
* argument will be freed by the caller (tcp_input()) unless the
* recv_data pointer in the pcb is set.
*
* @param pcb the tcp_pcb for which a segment arrived
*
* @note the segment which arrived is saved in global variables, therefore only the pcb
* involved is passed as a parameter to this function
*/
static err_t
tcp_process(struct tcp_pcb *pcb)
{
/* Do different things depending on the TCP state. */
switch (pcb->state) {
case SYN_RCVD:
if (flags & TCP_ACK) {
/* expected ACK number? */
if (TCP_SEQ_BETWEEN(ackno, pcb->lastack + 1, pcb->snd_nxt)) {
pcb->state = ESTABLISHED;
tcp_backlog_accepted(pcb);
/* Call the accept function. */
TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);
if (err != ERR_OK) {
/* If the accept function returns with an error, we abort
* the connection. */
/* Already aborted? */
if (err != ERR_ABRT) {
tcp_abort(pcb); // <--这里
}
return ERR_ABRT;
}
} else {
/* incorrect ACK number, send RST */
tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(), // <--这里
ip_current_src_addr(), tcphdr->dest, tcphdr->src);
}
}
break;
}
return ERR_OK;
}
2.处于 SYN_SEND( CLOSED 状态的客户端,发送 SYN 握手标志后,进入 SYN_SEND )状态的连接,期待收到 SYN + ACK 标志,如果收到的报文中只有 ACK 标志,则调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:
static err_t
tcp_process(struct tcp_pcb *pcb)
{
/* Do different things depending on the TCP state. */
switch (pcb->state) {
case SYN_SENT:
/* received SYN ACK with expected sequence number? */
if ((flags & TCP_ACK) && (flags & TCP_SYN)
&& (ackno == pcb->lastack + 1)) {
//处理正确的报文
}
/* received ACK? possibly a half-open connection */
else if (flags & TCP_ACK) {
/* send a RST to bring the other side in a non-synchronized state. */
tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(), // <-- 这里
ip_current_src_addr(), tcphdr->dest, tcphdr->src);
/* Resend SYN immediately (don't wait for rto timeout) to establish
connection faster, but do not send more SYNs than we otherwise would
have, or we might get caught in a loop on loopback interfaces. */
if (pcb->nrtx < TCP_SYNMAXRTX) {
pcb->rtime = 0;
tcp_rexmit_rto(pcb);
}
}
break;
}
return ERR_OK;
}
tcp_rst 函数下面的代码也显示了另外一个小知识,在这种情况下,协议栈会立即重发一个 SYN 握手标志。这样做可以更快的建立连接。
tcp_close 函数中
发现还有数据没被应用层处理,或者接收窗口值不正确,则调用调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:
if ((pcb->state == ESTABLISHED) || (pcb->state == CLOSE_WAIT)) {
if ((pcb->refused_data != NULL) || (pcb->rcv_wnd != TCP_WND_MAX(pcb))) {
/* don't call tcp_abort here: we must not deallocate the pcb since
that might not be expected when calling tcp_close */
tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip, // <-- 这里
pcb->local_port, pcb->remote_port);
tcp_pcb_purge(pcb);
TCP_RMV_ACTIVE(pcb);
/* Deallocate the pcb since we already sent a RST for it */
if (tcp_input_pcb == pcb) {
/* prevent using a deallocated pcb: free it from tcp_input later */
tcp_trigger_input_pcb_close();
} else {
tcp_free(pcb);
}
return ERR_OK;
}
}
tcp_slowtmr 函数中
如果使能保活定时器,并且保活超时(默认超时时间:2 小时 + 9*75 秒),则调用 tcp_rst 函数,发送 RST 标志。
简化后的代码为:
/**
* Called every 500 ms and implements the retransmission timer and the timer that
* removes PCBs that have been in TIME-WAIT for enough time. It also increments
* various timers such as the inactivity timer in each PCB.
*
* Automatically called from tcp_tmr().
*/
void
tcp_slowtmr(void)
{
/* Check if KEEPALIVE should be sent */
if (ip_get_option(pcb, SOF_KEEPALIVE) &&
((pcb->state == ESTABLISHED) ||
(pcb->state == CLOSE_WAIT))) {
if ((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + TCP_KEEPCNT_DEFAULT * TCP_KEEPINTVL_DEFAULT) / TCP_SLOW_INTERVAL) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: KEEPALIVE timeout. Aborting connection to "));
++pcb_remove;
++pcb_reset;
} else if ((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + pcb->keep_cnt_sent * TCP_KEEPINTVL_DEFAULT )
/ TCP_SLOW_INTERVAL) {
err = tcp_keepalive(pcb);
if (err == ERR_OK) {
pcb->keep_cnt_sent++;
}
}
}
/* If the PCB should be removed, do it. */
if (pcb_remove) {
if (pcb_reset) {
tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,
pcb->local_port, pcb->remote_port);
}
}
}
}
与保活相关的时间定义在 tcp_priv.h 中:
#ifndef TCP_KEEPIDLE_DEFAULT
#define TCP_KEEPIDLE_DEFAULT 7200000UL /* Default KEEPALIVE timer in milliseconds */
#endif
#ifndef TCP_KEEPINTVL_DEFAULT
#define TCP_KEEPINTVL_DEFAULT 75000UL /* Default Time between KEEPALIVE probes in milliseconds */
#endif
#ifndef TCP_KEEPCNT_DEFAULT
#define TCP_KEEPCNT_DEFAULT 9U /* Default Counter for KEEPALIVE probes */
tcp_listen_input 函数中
1.处于监听状态的连接只接收 SYN 标志,如果收到了 ACK 标志,则调用 tcp_rst 函数,发送 RST 标志。简化后的代码为:
static void
tcp_listen_input(struct tcp_pcb_listen *pcb)
{
if (flags & TCP_ACK) {
/* For incoming segments with the ACK flag set, respond with a RST. */
LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_listen_input: ACK in LISTEN, sending reset\n"));
tcp_rst((const struct tcp_pcb *)pcb, ackno, seqno + tcplen, ip_current_dest_addr(),
ip_current_src_addr(), tcphdr->dest, tcphdr->src);
}
return;
}
2.在 tcp_listen_input 函数中,接收到 SYN 标志后,就会调用 tcp_alloc 函数申请 TCP_PCB 控制块。
tcp_alloc 函数设计原则是尽一切可能返回一个有效的 TCP_PCB 控制块,因此,当 TCP_PCB 不足时,函数可能 “杀死”(kill)正在使用的连接,以释放 TCP_PCB 控制块!
具体就是:
先调用 tcp_kill_timewait 函数,试图找到 TIME_WAIT 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志,以便通知远端释放连接;
如果第 1 步失败了,则调用 tcp_kill_state 函数,试图找到 LAST_ACK 和 CLOSING 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abandon(pcb, 0) 函数 “杀” 掉这个连接,注意这个函数并不会发送 RST 标志,处于这两种状态的连接都是等到对方发送的 ACK 就会结束连接,不会有数据丢失;
如果第 2 步也失败了,则调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接!如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志。
TCP 连接具有优先级。
优先级由一个 u8_t 整数指定,数值越小,优先级越低。
优先级可以在 TCP_PRIO_MIN 和 TCP_PRIO_MAX 之间选择,默认是 TCP_PRIO_NORMAL :
#define TCP_PRIO_MIN 1
#define TCP_PRIO_NORMAL 64
#define TCP_PRIO_MAX 127
这个信息也很重要,它告诉我们低优先级的 TCP 连接可以被高优先级连接给抢占掉!!
如果一个 TCP 连接很重要,那么你应该手动提高它的优先级。方法是在 accept 回调函数中,使用 tcp_setprio(pcb, new_prio) 函数更改 TCP 连接的优先级:
static err_t xxxx_protocol_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
if(pcb == NULL)
return ERR_OK;
tcp_setprio (pcb, TCP_PRIO_MAX); //<--这里
tcp_recv(pcb, xxxx_protocol_recv);
tcp_err(pcb, xxxx_protocol_err);
pcb->so_options |= SOF_KEEPALIVE; //增加保活机制
tcp_link_accept(pcb->remote_ip.addr, pcb->remote_port); //这是我的私有函数, 用于跟踪链接上线
return(ERR_OK);
}
另外一个需要注意的地方,上面我们说“调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接”,这是针对 lwIP 2.0.0 及以上版本说的,lwIP 1.4.1 和这个不同,应表述为:
对于lwIP 1.4.1 版本,调用 tcp_kill_prio(prio) 函数,试图找到小于等于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接。
一个是“小于”,一个是“小于等于”。