心灵激情不在,就可能被打败

0%

LwIP的TCP实现之一

本文介绍tcp_new tcp_bind tcp_listen函数的实现

LwIP介绍

目的

LwIP 是 TCP/IP 协议栈的一个实现,它的目的是减少内存使用率和代码大小,使 LwIP 适用于资源受限系统(比如嵌入式系统)。

区别

大部分的 TCP/IP 实现在应用层和底层协议层之间进行了严格的划分。在大部分的操作系统中,底层协议族作为拥有应用层进程通讯入口的操作系统内核的一部分被实现,当应用层发送数据,在被网络代码处理之前,这些数据必须由应用层进程的内存空间复制到内部缓冲区。

例如:BSD Socket需要将发送的数据从应用程序复制TCP/IP协议栈的内部缓存区。

当应用层能够了解底层协议使用的缓冲处理机制时,便可以更加有效的重复使用缓冲区。因此应用层与TCP/IP协议代码使用相同的内存区时,应用层就可以直接读写内部缓冲区,从而避免了内存复制产生的性能损失。LwIP则采用了后面这种方式。

为了减少处理和内存需求, LwIP 使用不需要任何数据复制的经过裁剪的 API。

TCP功能

  • 建立与断开
  • 状态机
  • 输入输出函数
  • 滑动窗口
  • 超时与重传
  • 慢启动与拥塞避免
  • 快速恢复、重传
  • Nagle算法
  • TCP定时器

tcp_new

函数功能

创建一个新的连接标识符(PCB)。 如果没有可用的内存来创建新的pcb,则返回NULL。

函数源码

tcp_new源码:

1
2
3
4
struct tcp_pcb *tcp_new(void)
{
return tcp_alloc(TCP_PRIO_NORMAL);
}

上面函数的返回值为tcp_pcb结构体,该结构体保存一些实现TCP功能的变量,先不详细讨论。下面看下tcp_alloc函数,从名称可以看出该函数是分配TCP结构体的内存。在分析该函数之前,先了解一下LwIP内存分配的两种方式。

tcp_alloc

函数功能

  • 内存堆分配方式

    1
    mem_malloc(memp_t type)

    动态内存堆分配策略原理就是在一个事先定义好大小的内存块中进行分配,其内存分配的策略是采用最快合适( First Fit)方式。mem_init( ) 内存堆的初始化函数,主要是告知内存堆的起止地址,以及初始化空闲列表,由 lwip 初始化时自己调用,该接口为内部私有接口,不对用户层开放。mem_malloc( ) 申请分配内存。

  • 内存池分配方式

    1
    memp_malloc(memp_t type)

    编译的时候与各种类型内存池就会被建立,采用链表方式。

函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
struct tcp_pcb *
tcp_alloc(u8_t prio)
{
struct tcp_pcb *pcb;
u32_t iss;

pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* 若内存空间不够,则函数会释放处于 TIME-WAIT 状态的 TCP */
LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest TIME-WAIT connection\n"));
tcp_kill_timewait();
/* 重新分配tcp_pcb */
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb == NULL) {
/* 若内存空间不够,则函数会释放优先级更低的 PCB(在 PCB 控制块的 prio 字段). */
LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing connection with prio lower than %d\n", prio));
tcp_kill_prio(prio);
pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
if (pcb != NULL) {
/* 调整错误状态:memp_malloc两次失败 */
MEMP_STATS_DEC(err, MEMP_TCP_PCB);
}
}
if (pcb != NULL) {
/* 调整错误状态:timewait PCB已释放 */
MEMP_STATS_DEC(err, MEMP_TCP_PCB);
}
}
if (pcb != NULL) {
/*tcp_pcb结构体信息 */
memset(pcb, 0, sizeof(struct tcp_pcb));
pcb->prio = prio;// 设置 PCB 的优先级为64,优先级在 1~127 之间
pcb->snd_buf = TCP_SND_BUF;//TCP 发送数据缓冲区剩余大小
pcb->snd_queuelen = 0;//发送缓冲中的数据包 pbuf 个数
pcb->rcv_wnd = TCP_WND;//接收窗口大小
pcb->rcv_ann_wnd = TCP_WND;//通告窗口大小
pcb->tos = 0;//IP 报头部 TOS 字段
pcb->ttl = TCP_TTL;//IP 报头部 TTL字段
pcb->mss = (TCP_MSS > 536) ? 536 : TCP_MSS;// 设置最大段大小,不能超过 536 字节(貌似只是初始化)
pcb->rto = 3000 / TCP_SLOW_INTERVAL;//初始超时时间值,为 6s
pcb->sa = 0;//估计出的 RTT 平均值
pcb->sv = 3000 / TCP_SLOW_INTERVAL;//估计出的 RTT 方差
pcb->rtime = -1;//重传定时器,当该值大于 rto 时则重传发生
pcb->cwnd = 1;// 阻塞窗口
iss = tcp_next_iss();//iss 为一个临时变量,保存该连接的初始数据序列号
pcb->snd_wl2 = iss;//上一个窗口更新时收到的ACK序列号
pcb->snd_nxt = iss;//下一个将要发送的序列编号
pcb->lastack = iss;//上一个 ACK 编号
pcb->snd_lbb = iss;//发送队列中最后一个字节的序号
pcb->tmr = tcp_ticks;//tcp_ticks 是一个全局变量,记录了当前协议时钟滴答
pcb->last_timer = tcp_timer_ctr;
pcb->polltmr = 0;

#if LWIP_CALLBACK_API
pcb->recv = tcp_recv_null;//注册默认的接收回调函数
#endif /* LWIP_CALLBACK_API */

#if 1
/* 初始化保活计时器 */
pcb->keep_idle = TCP_KEEPIDLE_DEFAULT;//发送KEEPALIVE之前的空闲时间
/* 开启保活机制 */
pcb->so_options |= SOF_KEEPALIVE;//Socket选项
#if LWIP_TCP_KEEPALIVE
pcb->keep_intvl = TCP_KEEPINTVL_DEFAULT;//保活时间间隔
pcb->keep_cnt = TCP_KEEPCNT_DEFAULT;//每次保活报文个数
#endif /* LWIP_TCP_KEEPALIVE */
#endif
pcb->keep_cnt_sent = 0;//已经发送的保活报文个数
}
return pcb;
}

定时器介绍:LWIP 中包括两个定时器函数:一个函数每 250 ms 调用一次(快速定时器);另一个函数每 500ms 调用一次(慢速定时器)。通过这两个函数实现TCP模块的7个定时器,重传定时器使用 rtime 字段计数,持续定时器使用 persist_cnt 字段计数,其他五个定时器除延迟 ACK 定时器外都使用 rtime 字段计数,延迟 ACK 定时器使用系统 250ms 周期性定时来完成的。上述中,pcb->rto = 3000 / TCP_SLOW_INTERVAL;3000 / 500 = 6(也不太明白为什么是6s而不是3s,可能还会有处理)。

总结

tcp_new()调用tcp_alloc()函数,该函数首先为新的PCB分配内存空间,若内存空间不够,则会释放处于TIME-WAIT状态的PCB或者优先级更低PCB,并为新的PCB分配空间。当内存空间成功分配后,函数会初始化新的PCB的内容。

tcp_bind

函数功能

将PCB绑定到本地IP地址和端口号。IP地址可以指定为IP_ADDR_ANY以便将连接绑定到所有本地IP地址。如果将另一个连接绑定到同一端口,则该函数将返回ERR_USE,否则返回ERR_OK。

函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
err_t tcp_bind(struct tcp_pcb *pcb, ip_addr_t *ipaddr, u16_t port)
{
int i;
int max_pcb_list = NUM_TCP_PCB_LISTS;
struct tcp_pcb *cpcb;

LWIP_ERROR("tcp_bind: can bind in state CLOSED", pcb->state == CLOSED, return ERR_VAL);

#if SO_REUSE
/* 除非设置了REUSEADDR标志,否则我们还必须检查处于TIME-WAIT状态的pcb。我们不转储TIME_WAIT pcb;它们仍然可以通过使用本地和远程IP地址和端口来区分的传入数据包进行匹配。*/
if (ip_get_option(pcb, SOF_REUSEADDR)) { //pcb->so_options字段的值
max_pcb_list = NUM_TCP_PCB_LISTS_NO_TIME_WAIT;
}
#endif /* SO_REUSE */

if (port == 0) {
port = tcp_new_port();//分配新的端口号
if (port == 0) {
return ERR_BUF;
}
}

/* 检查链表IP地址和端口号是否已经被使用 */
for (i = 0; i < max_pcb_list; i++) {
for(cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {
if (cpcb->local_port == port) {
#if SO_REUSE
/ *如果两个PCB都设置了REUSEADDR,则忽略检查端口 * /
if (!ip_get_option(pcb, SOF_REUSEADDR) ||
!ip_get_option(cpcb, SOF_REUSEADDR))
#endif
{
if (ip_addr_isany(&(cpcb->local_ip)) ||
ip_addr_isany(ipaddr) ||
ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
return ERR_USE;
}
}
}
}
}

if (!ip_addr_isany(ipaddr)) {
pcb->local_ip = *ipaddr;
}
pcb->local_port = port;
TCP_REG(&tcp_bound_pcbs, pcb);
LWIP_DEBUGF(TCP_DEBUG, ("tcp_bind: bind to port %"U16_F"\n", port));
return ERR_OK;
}

链表类型:处于侦听状态的链表tcp_listen_pcbs;处于稳定状态的链表 tcp_active_pcbs;已经绑定完毕的 PCB 链表tcp_bound_pcbs;处于 TIME-WAIT 状态的 PCB 链表tcp_tw_pcbs

总结

tcp_bind()函数将两个参数的值赋值给PCB中local_ip和local_port 的字段。但这里有个前提,就是这个<IP 地址、端口>对没有被使用,因此函数需要先遍历各个PCB链表,以保证这个<IP 地址、端口>对没有被其他PCB使用。

TCP的状态机

TCP状态机

状态 描述
CLOSED 关闭状态,没有连接活动或正在进行
LISTEN 监听状态,服务器正在等待连接进入
SYN RCVD 收到一个连接请求,尚未确认
SYN SENT 经发出连接请求,等待确认
ESTABLISHED 连接建立,正常数据传输状态
FIN WAIT 1 (主动关闭)已经发送关闭请求,等待确认
FIN WAIT 2 (主动关闭)收到对方关闭确认,等待对方关闭请求
TIMED WAIT 完成双向关闭,等待所有分组死掉
CLOSING 双方同时尝试关闭,等待对方确认
CLOSE WAIT (被动关闭)收到对方关闭请求,已经确认
LAST ACK (被动关闭)等待最后一个关闭确认,并等待所有分组死掉

这个就不多做解释了,网上许多博客都有,这里只是记录方便以后查阅。博客地址:tcp状态介绍最详细–没有之一

TCP连接与断开状态

TCP连接与断开
客户端的状态正常流程:

1
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED

服务器的状态正常流程:

1
CLOSED->LISTEN->SYN_RCVD->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED

TCP其他状态迁移

  • LISTEN->SYN_SENT,服务器打开连接。
  • SYN_SENT->SYN_RCVD,服务器和客户端在SYN_SENT状态下如果收到SYN数据报,则都需要发送SYN的ACK数据报并把自己的状态调整到SYN收到状态,准备进入ESTABLISHED
  • SYN_SENT->CLOSED,在发送超时的情况下,会返回到CLOSED状态。
  • SYN_RCVD->LISTEN,如果受到RST包,会返回到LISTEN状态。
  • SYN_RCVD->FIN_WAIT_1,这个迁移是说,可以不用到ESTABLISHED状态,而可以直接跳转到FIN_WAIT_1状态并等待关闭。

附录

上面一共说了四种 PCB 链表,现在看看它们各自用来链接了处于哪种状态的 PCB 控制块。 tcp_bound_pcbs 链表用来连接新创建的控制块,可以认为新建的控制块处于 closed 状态。tcp_listen_pcbs 链表用来连接处于 LISTEN 状态的控制块, tcp_tw_pcbs 链表用来连接处于TIME_WAIT 状态的控制块,tcp_active_pcbs 链表用来连接处于 TCP 状态转换图中其他所有状态的控制块。

tcp_listen

函数功能

命令PCB开始监听客户端的连接。当一个客户端连接被接受时,tcp_accept()函数将被调用。PCB必须使用tcp_bind()函数绑定到本地端口。tcp_listen()函数返回一个新的连接标识符,并且作为参数传递给函数的那个PCB将被释放。这么做的原因是监听连接需要更少的内存,因此tcp_listen()将回收原始连接所需的内存并分配一个用于侦听连接的新的较小的内存块。如果没有可用的内存,tcp_listen()会返回NULL。如果是这样,则传递给tcp_listen()作为参数的PCB对象将不会被释放。

函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define  tcp_listen(pcb) tcp_listen_with_backlog(pcb,TCP_DEFAULT_LISTEN_BACKLOG)

struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog)
{
struct tcp_pcb_listen *lpcb;

LWIP_UNUSED_ARG(backlog);
LWIP_ERROR("tcp_listen: pcb already connected", pcb->state == CLOSED, return NULL);

/* 是否已经监听 */
if (pcb->state == LISTEN) {
return pcb;
}
#if SO_REUSE
if (ip_get_option(pcb, SOF_REUSEADDR)) {
/* 由于SOF_REUSEADDR允许重新使用本地地址,因此我们必须确保此端口对于每个本地IP仅使用一次 */
for(lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next) {
if (lpcb->local_port == pcb->local_port) {
if (ip_addr_cmp(&lpcb->local_ip, &pcb->local_ip)) {
/* this address/port is already used */
return NULL;
}
}
}
}
#endif /* SO_REUSE */
/* 使用内存池分配内存 */
lpcb = (struct tcp_pcb_listen *)memp_malloc(MEMP_TCP_PCB_LISTEN);
if (lpcb == NULL) {
return NULL;
}
lpcb->callback_arg = pcb->callback_arg;//传递给回调函数的参数
lpcb->local_port = pcb->local_port;
lpcb->state = LISTEN;
lpcb->prio = pcb->prio;
lpcb->so_options = pcb->so_options;
ip_set_option(lpcb, SOF_ACCEPTCONN);//修改pcb->so_options的值
lpcb->ttl = pcb->ttl;
lpcb->tos = pcb->tos;
ip_addr_copy(lpcb->local_ip, pcb->local_ip);
if (pcb->local_port != 0) {
TCP_RMV(&tcp_bound_pcbs, pcb);//删除tcp_bound_pcbs链表上的节点
}
memp_free(MEMP_TCP_PCB, pcb);
#if LWIP_CALLBACK_API
lpcb->accept = tcp_accept_null;
#endif /* LWIP_CALLBACK_API */
#if TCP_LISTEN_BACKLOG //控制同时建立的连接请求数量(包括未完成和已完成的)
lpcb->accepts_pending = 0;
lpcb->backlog = (backlog ? backlog : 1);
#endif /* TCP_LISTEN_BACKLOG */
TCP_REG(&tcp_listen_pcbs.pcbs, (struct tcp_pcb *)lpcb);//链表中加入节点
return (struct tcp_pcb *)lpcb;
}

总结

tcp_listen()先申请一个tcp_pcb_listen的结构,然后将PCB参数中的有用字段拷贝进来,然后将这个PCB的结构挂接到链表tcp_listen_pcbs上。

结语

本文讲了tcp_new tcp_bind tcp_listen函数的实现,下篇讲述LwIP中TCP的输入输出函数。