TCP/IP传输层协议实现 - TCP连接的建立与终止(lwip)
Posted arm7star
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP传输层协议实现 - TCP连接的建立与终止(lwip)相关的知识,希望对你有一定的参考价值。
1、lwip tcp相关数据结构
1.1、tcp报文格式
《TCP-IP详解卷 1:协议》TCP包首部结构如下:
1.2、lwip tcp数据结构
tcp相关数据结构如下,tcp_pcb_listen为服务器的监听tcp链表,各监听的tcp通过next指针连接成一个链表,tcp_pcb为tcp控制块(tcp_pcb_listen监听仅需要端口、ip地址等信息,tcp_pcb为通信过程的tcp控制块,需要包含tcp通信协议的数据等,tcp_pcb包含了tcp_pcb_listen所有的信息,因为监听仅需要少部分信息,为了节省内存,因此从tcp_pcb提取了部分必要监听数据组成tcp_pcb_listen),tcp_seg为tcp报文段链表(tcp报文段有大小限制mss,大的数据被拆分成一个个小的报文段连接在tcp_seg链表里面),tcp_hdr为tcp首部(1.1节中TCP首部的实现),pbuf存储数据(数据连接在pbuf链表里面,tot_len为pbuf链表的数据长度,len为该pbuf的数据长度)。
2、tcp的状态变迁图
《TCP-IP详解卷 1:协议》TCP的状态变迁图如下,红色箭头线为服务器监听、接受连接过程,蓝色箭头线为客户连接服务器过程。
3、tcp绑定和监听
3.1、tcp服务器绑定(tcp_bind)
3.1.1、tcp_pcb链表
tcp服务器端除了有tcp server程序,也有tcp client程序,对于tcp server,监听的tcp信息在tcp_bound_pcbs、tcp_listen_pcbs里面,对于tcp client,使用的ip、端口等信息存储在tcp_active_pcbs、tcp_tw_pcbs里面。
tcp_bound_pcbs | 存储了绑定的ip地址、端口信息,tcp绑定之后如果不调用listen监听的话,绑定的ip及端口仍不能接受连接,仅占用了ip及端口 |
tcp_listen_pcbs | 为正在监听的tcp_pcb,绑定之后调用listen将监听的tcp控制块加入到tcp_listen_pcbs链表 |
tcp_active_pcbs | 为accept后的tcp控制块,保存的是正在通信的tcp_pcb |
tcp_tw_pcbs | 为正在关闭的tcp控制块(TIME_WAIT状态,等待超时彻底删除tcp_pcb) |
绑定过程需要查找ip及端口地址是否被占用,在下图中从上到下查找各个链表。
3.1.2、tcp端口的创建(tcp_new_port)
tcp绑定时如果端口为0,则由tcp协议动态创建一个端口,lwip调用tcp_new_port函数创建一个端口。tcp_new_port有一个静态局部变量port,用于记录查找到的可用端口,tcp_new_port第一次从TCP_LOCAL_PORT_RANGE_START开始查找,下次从port开始查找(在下一次调用tcp_new_port创建新的端口前,如果port前的端口没有被释放或者有部分被释放,那么port前有很多端口不被占用,在port后查找到可用端口到可能性及效率远高于在port前查找,因此每次都从port开始往后查找可用端口)。
tcp_new_port检查tcp_active_pcbs、tcp_tw_pcbs、tcp_listen_pcbs是否占用新的端口,都没有占用则该端口可用。(没有检查tcp_bound_pcbs的端口,tcp_bind函数检查tcp_new_port的端口是否被tcp_bound_pcbs占用;另外有些tcp server绑定的是0.0.0.0,所以lwip端口占用检查并没有检查ip,多个网卡时,不同的ip理论上可以绑定同样的端口)
tcp_new_port函数代码实现如下:
static u16_t
tcp_new_port(void)
{
struct tcp_pcb *pcb;
#ifndef TCP_LOCAL_PORT_RANGE_START
#define TCP_LOCAL_PORT_RANGE_START 4096 // 开始端口(动态端口仅从TCP_LOCAL_PORT_RANGE_START~TCP_LOCAL_PORT_RANGE_END之间分配)
#define TCP_LOCAL_PORT_RANGE_END 0x7fff
#endif
static u16_t port = TCP_LOCAL_PORT_RANGE_START; // 开始查找的端口
again:
if (++port > TCP_LOCAL_PORT_RANGE_END) { // 已经到了最大端口,重新从TCP_LOCAL_PORT_RANGE_START开始查找
port = TCP_LOCAL_PORT_RANGE_START;
}
for(pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) { // 检查该端口是否在已连接的tcp_active_pcbs中被占用
if (pcb->local_port == port) { // 端口被占用
goto again; // 检查下一个端口
}
}
for(pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) { // 检查该端口是否在正在关闭的连接tcp_tw_pcbs中被占用
if (pcb->local_port == port) { // 端口被占用
goto again; // 检查下一个端口
}
}
for(pcb = (struct tcp_pcb *)tcp_listen_pcbs.pcbs; pcb != NULL; pcb = pcb->next) { // 检查该端口是否在监听的tcp_listen_pcbs中被占用
if (pcb->local_port == port) {
goto again;
}
}
return port;
}
3.1.3、ip及端口绑定(lwip_bind)
lwip_bind发送消息到tcp/ip线程调用tcp_bind绑定,tcp/ip线程处理很多数据结构,相关数据结构的处理都在一个线程里面处理,因此避免并发操作,也避免应用层多线程之间的互斥。
tcp_bind主要检查ip及端口是否被占用(tcp_new_port仅检查了端口),ip地址除了192.168.1.1这类地址外还有0.0.0.0的ANY地址,192.168.1.1与0.0.0.0不能同时监听,TIME-WAIT状态的地址可以复用,因此TIME-WAIT状态的tcp_pcb仅检查ip地址是否相等。收到tcp报文时,先是检查是否匹配tcp_tw_pcbs(地址直接比较,不检查ANY地址),只要tcp_bind的地址不是tcp_tw_pcbs tcp_pcb的地址,那么tcp处理就不会匹配tcp_tw_pcbs的tcp_pcb,而是转tcp_bind的tcp_pcb处理。
tcp_bind检查ip地址及端口不冲突后设置tcp_pcb的端口及地址。
tcp_bind函数代码实现如下:
err_t
tcp_bind(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port)
{
struct tcp_pcb *cpcb;
LWIP_ERROR("tcp_bind: can only bind in state CLOSED", pcb->state == CLOSED, return ERR_ISCONN);
if (port == 0) {
port = tcp_new_port(); // 创建动态端口
}
/* Check if the address already is in use. */
/* Check the listen pcbs. */
for(cpcb = (struct tcp_pcb *)tcp_listen_pcbs.pcbs;
cpcb != NULL; cpcb = cpcb->next) {
if (cpcb->local_port == port) { // 与listen的端口相同
if (ip_addr_isany(&(cpcb->local_ip)) || // listen的端口是0.0.0.0(0.0.0.0相当于服务器上的所有ip地址,tcp_bind参数需要监听的ip地址,如果tcp_bind前已经有listen,那么不能绑定到该ip及端口)
ip_addr_isany(ipaddr) || // 如果当前需要绑定的ip地址是0.0.0.0,而且已有进程监听非0.0.0.0地址,当前需要绑定的ip地址0.0.0.0包含了已监听的ip地址,因此不能再绑定到该ip及端口
ip_addr_cmp(&(cpcb->local_ip), ipaddr)) { // ip地址相同,不能绑定
return ERR_USE;
}
}
}
/* Check the connected pcbs. */
for(cpcb = tcp_active_pcbs;
cpcb != NULL; cpcb = cpcb->next) {
if (cpcb->local_port == port) {
if (ip_addr_isany(&(cpcb->local_ip)) ||
ip_addr_isany(ipaddr) ||
ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
return ERR_USE;
}
}
}
/* Check the bound, not yet connected pcbs. */
for(cpcb = tcp_bound_pcbs; cpcb != NULL; cpcb = cpcb->next) {
if (cpcb->local_port == port) {
if (ip_addr_isany(&(cpcb->local_ip)) ||
ip_addr_isany(ipaddr) ||
ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
return ERR_USE;
}
}
}
/* @todo: until SO_REUSEADDR is implemented (see task #6995 on savannah),
* we have to check the pcbs in TIME-WAIT state, also: */
for(cpcb = tcp_tw_pcbs; cpcb != NULL; cpcb = cpcb->next) {
if (cpcb->local_port == port) {
if (ip_addr_cmp(&(cpcb->local_ip), ipaddr)) { // TIME-WAIT状态地址复用,tcp_tw_pcbs仅检查ip地址是否相同
return ERR_USE;
}
}
}
if (!ip_addr_isany(ipaddr)) {
pcb->local_ip = *ipaddr; // 绑定非0.0.0.0的ip地址
}
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;
}
3.2、tcp服务器监听(lwip_listen)
与linux一样,应用层是通过socket与协议栈通信,lwip_socket关联到。(socket在bind前创建,本文跳过socket创建代码)
lwip_listen主要发送消息到tcp/ip线程,tcp/ip线程创建一个tcp_pcb_listen加入到tcp_listen_pcbs链表。
lwip_listen函数代码实现如下:
int
lwip_listen(int s, int backlog)
{
struct lwip_socket *sock;
err_t err;
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_listen(%d, backlog=%d)\\n", s, backlog));
sock = get_socket(s); // 通过socket描述符获取lwip_socket
if (!sock)
return -1;
/* limit the "backlog" parameter to fit in an u8_t */
if (backlog < 0) {
backlog = 0;
}
if (backlog > 0xff) {
backlog = 0xff;
}
err = netconn_listen_with_backlog(sock->conn, backlog); // 调用netconn_listen_with_backlog发送消息到tcp/ip线程(backlog为监听状态未被accept的连接个数,超过backlog将不被接受)
if (err != ERR_OK) {
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_listen(%d) failed, err=%d\\n", s, err));
sock_set_errno(sock, err_to_errno(err));
return -1;
}
sock_set_errno(sock, 0);
return 0;
}
netconn_listen_with_backlog组装listen的消息及处理函数发送给tcp/ip线程处理并等待listen完成。
netconn_listen_with_backlog函数代码实现如下:
err_t
netconn_listen_with_backlog(struct netconn *conn, u8_t backlog)
{
struct api_msg msg;
/* This does no harm. If TCP_LISTEN_BACKLOG is off, backlog is unused. */
LWIP_UNUSED_ARG(backlog);
LWIP_ERROR("netconn_listen: invalid conn", (conn != NULL), return ERR_ARG;);
msg.function = do_listen; // listen回调函数
msg.msg.conn = conn; // 需要监听的netconn
#if TCP_LISTEN_BACKLOG
msg.msg.msg.lb.backlog = backlog; // backlog参数
#endif /* TCP_LISTEN_BACKLOG */
TCPIP_APIMSG(&msg); // 发送消息到tcp/ip线程并等待conn op_completed完成
return conn->err;
}
tcp/ip线程收到listen消息后,调用消息里面的回调函数do_listen进行监听。do_listen创建tcp_pcb_listen并创建netconn相关的邮箱(用于应用层与协议中之间的消息收发)
do_listen函数代码实现如下:
void
do_listen(struct api_msg_msg *msg)
{
#if LWIP_TCP
if (!ERR_IS_FATAL(msg->conn->err)) {
if (msg->conn->pcb.tcp != NULL) {
if (msg->conn->type == NETCONN_TCP) { // listen仅针对tcp协议有效
if (msg->conn->pcb.tcp->state == CLOSED) { // netconn还没有本监听(不允许重复调用listen)
#if TCP_LISTEN_BACKLOG
struct tcp_pcb* lpcb = tcp_listen_with_backlog(msg->conn->pcb.tcp, msg->msg.lb.backlog); // 调用tcp_listen_with_backlog创建初始化一个tcp_pcb_listen并加入到tcp_listen_pcbs
#else /* TCP_LISTEN_BACKLOG */
struct tcp_pcb* lpcb = tcp_listen(msg->conn->pcb.tcp);
#endif /* TCP_LISTEN_BACKLOG */
if (lpcb == NULL) {
msg->conn->err = ERR_MEM;
} else {
/* delete the recvmbox and allocate the acceptmbox */
if (msg->conn->recvmbox != SYS_MBOX_NULL) {
/** @todo: should we drain the recvmbox here? */
sys_mbox_free(msg->conn->recvmbox); // 删除recvmbox(listen状态不需要recvmbox,recvmbox仅用于接收数据)
msg->conn->recvmbox = SYS_MBOX_NULL;
}
if (msg->conn->acceptmbox == SYS_MBOX_NULL) {
if ((msg->conn->acceptmbox = sys_mbox_new(DEFAULT_ACCEPTMBOX_SIZE)) == SYS_MBOX_NULL) { // 创建acceptmbox,用于接收accept消息
msg->conn->err = ERR_MEM;
}
}
if (msg->conn->err == ERR_OK) {
msg->conn->state = NETCONN_LISTEN;
msg->conn->pcb.tcp = lpcb;
tcp_arg(msg->conn->pcb.tcp, msg->conn);
tcp_accept(msg->conn->pcb.tcp, accept_function);
}
}
} else {
msg->conn->err = ERR_CONN;
}
}
}
}
#endif /* LWIP_TCP */
TCPIP_APIMSG_ACK(msg);
}
tcp_listen_with_backlog函数创建tcp_pcb_listen、设置监听的地址端口等信息,从tcp_bound_pcbs删除listen的tcp_pcb等。
监听完成后,状态转移到LISTEN,该地址端口就可以接受客户的连接了。
4、tcp的三次握手
《TCP-IP详解卷 1:协议》连接建立与终止的时间系列如下,报文段1到报文段3为3次握手过程。
4.1、客户连接(tcp_connect)
tcp握手由客户发起(第一次握手),客户调用lwip_connect发起连接,最终由tcp_connect实现。
tcp_connect初始化tcp_pcb序号、收发窗口、最大报文段等,状态转移到SYN_SENT,构造SYN报文(即报文段1),调用tcp_output发送报文,即第一次握手。
tcp_connect函数代码实现如下:
err_t
tcp_connect(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port,
err_t (* connected)(void *arg, struct tcp_pcb *tpcb, err_t err))
{
err_t ret;
u32_t iss;
LWIP_ERROR("tcp_connect: can only connected from state CLOSED", pcb->state == CLOSED, return ERR_ISCONN);
LWIP_DEBUGF(TCP_DEBUG, ("tcp_connect to port %"U16_F"\\n", port));
if (ipaddr != NULL) {
pcb->remote_ip = *ipaddr;
} else {
return ERR_VAL;
}jia
pcb->remote_port = port;
if (pcb->local_port == 0) {
pcb->local_port = tcp_new_port();
}
iss = tcp_next_iss(); // 获取initial sequence number,根据上一次的initial sequence number加上tcp_ticks生成一个随机值,随机的initial sequence number一定程度避免攻击(如果攻击者知道initial sequence number,那么可以伪造一个RST的报文发送到网络上,就会导致客户连接不上正确的服务器)
pcb->rcv_nxt = 0; // 还没获取到服务器的initial sequence number
pcb->snd_nxt = iss; // 第一帧的sequence number
pcb->lastack = iss - 1; // 已获取应答的sequence number
pcb->snd_lbb = iss - 1; // 已发送的sequence number
pcb->rcv_wnd = TCP_WND; // 初始化接收窗口大小
pcb->rcv_ann_wnd = TCP_WND; // 初始化通告窗口大小(通告对方本地接收窗口大小)
pcb->rcv_ann_right_edge = pcb->rcv_nxt; // 通告窗口右边沿
pcb->snd_wnd = TCP_WND; // 初始化发送窗口大小
/* 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 = (TCP_MSS > 536) ? 536 : TCP_MSS; // 初始化最大报文段大小(没有考虑网卡最大报文段)
#if TCP_CALCULATE_EFF_SEND_MSS
pcb->mss = tcp_eff_send_mss(pcb->mss, ipaddr); // 初始化最大报文段大小(查找输出路由,从网卡及默认mss最大报文大小获取较小值作为最大报文段大小)
#endif /* TCP_CALCULATE_EFF_SEND_MSS */
pcb->cwnd = 1; // 拥塞窗口大小设置为1(慢启动)
pcb->ssthresh = pcb->mss * 10; // 慢启动门限ssthresh设置为10个最大报文段
pcb->state = SYN_SENT; // 状态转移到SYN_SENT
#if LWIP_CALLBACK_API
pcb->connected = connected;
#endif /* LWIP_CALLBACK_API */
TCP_RMV(&tcp_bound_pcbs, pcb); // 从tcp_bound_pcbs删除tcp_pcb
TCP_REG(&tcp_active_pcbs, pcb); // 将tcp_pcb加入到tcp_active_pcbs
snmp_inc_tcpactiveopens();
ret = tcp_enqueue(pcb, NULL, 0, TCP_SYN, 0, TF_SEG_OPTS_MSS
#if LWIP_TCP_TIMESTAMPS
| TF_SEG_OPTS_TS
#endif
); // 构造一个SYN报文并加入到pcb的发送队列
if (ret == ERR_OK) {
tcp_output(pcb); // 调用tcp_output发送pcb发送队列里面的报文
}
return ret;
}
4.2、服务器接受连接(accept)
ip网络层接收到报文后,检查校验解析报文后,对于tcp协议报文,调用tcp_input进行处理。tcp与ip一样先对报文进行校验,校验完成后再处理数据。
校验checksum。
#if CHECKSUM_CHECK_TCP
/* Verify TCP checksum. */
if (inet_chksum_pseudo(p, (struct ip_addr *)&(iphdr->src),
(struct ip_addr *)&(iphdr->dest),
IP_PROTO_TCP, p->tot_len) != 0) {
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packet discarded due to failing checksum 0x%04"X16_F"\\n",
inet_chksum_pseudo(p, (struct ip_addr *)&(iphdr->src), (struct ip_addr *)&(iphdr->dest),
IP_PROTO_TCP, p->tot_len)));
#if TCP_DEBUG
tcp_debug_print(tcphdr);
#endif /* TCP_DEBUG */
TCP_STATS_INC(tcp.chkerr);
TCP_STATS_INC(tcp.drop);
snmp_inc_tcpinerrs();
pbuf_free(p);
return;
}
#endif
tcp头部解析(源/目的端口、序号、通告窗口等获取)。
/* Convert fields in TCP header to host byte order. */
tcphdr->src = ntohs(tcphdr->src); // 源端口
tcphdr->dest = ntohs(tcphdr->dest); // 目的端口
seqno = tcphdr->seqno = ntohl(tcphdr->seqno); // 报文sequence number
ackno = tcphdr->ackno = ntohl(tcphdr->ackno); // 报文ack sequence number(连接默认为0)
tcphdr->wnd = ntohs(tcphdr->wnd); // 通告窗口大小(发送端发送时,用rcv_ann_wnd填充wnd,所以报文获取到的是对端的通告窗口大小)
报文flags及长度获取(SYN/FIN不带数据但是占一个数据长度)。
flags = TCPH_FLAGS(tcphdr); // 报文flags获取
tcplen = p->tot_len + ((flags & (TCP_FIN | TCP_SYN)) ? 1 : 0); // FIN/SYN报文虽然不带数据,但是占据一个sequence number,报文长度需要加1
tcp_input先检查tcp_active_pcbs、tcp_tw_pcbs是否有对应的tcp_pcb,已连接、正在关闭的地址及端口不允许再次连接(即不允许调用两次connect),都不存在然后检查tcp_listen_pcbs,对匹配的tcp_pcb_listen调用tcp_listen_input处理报文。
prev = NULL;
for(lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next) { // 遍历监听的tcp_listen_pcbs
if ((ip_addr_isany(&(lpcb->local_ip)) || // 监听的是0.0.0.0,不需要检查对端连接的目的ip地址(ip网络层已经检查过报文目的ip地址是本机地址)
ip_addr_cmp(&(lpcb->local_ip), &(iphdr->dest))) && // 如果监听的不是0.0.0.0,则检查是否是listen的ip地址
lpcb->local_port == tcphdr->dest) { // 检查监听的端口是否是客户连接的目的端口
/* Move this PCB to the front of the list so that subsequent
lookups will be faster (we exploit locality in TCP segment
arrivals). */
if (prev != NULL) { // 如果prev不为空(匹配的tcp_pcb不在tcp_listen_pcbs表头,将tcp_pcb移动到表头,下次可能也是连接该tcp_pcb,因此可以提高遍历tcp_listen_pcbs的速度)
((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;
}
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packed for LISTENing connection.\\n"));
tcp_listen_input(lpcb); // 调用tcp_listen_input处理连接请求
pbuf_free(p);
return;
}
prev = (struct tcp_pcb *)lpcb;
}
tcp_listen_input报文处理。
检查报文flags,如果报文带ACK标记,则发送RST报文,重置客户连接。
if (flags & TCP_ACK) { // 连接的第一个报文应该是SYN,服务器端还没发送过任何报文,没有需要ACK的数据,不应该会收到ACK报文,发送RST报文重置客户连接
/* 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(ackno + 1, seqno + tcplen,
&(iphdr->dest), &(iphdr->src),
tcphdr->dest, tcphdr->src);
} else if (flags & TCP_SYN) {
SYN报文处理,如果等待被应用层接收的连接大于等于backlog则不处理,直接返回错误。
} else if (flags & TCP_SYN) { // SYN报文
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection request %"U16_F" -> %"U16_F".\\n", tcphdr->src, tcphdr->dest));
#if TCP_LISTEN_BACKLOG
if (pcb->accepts_pending >= pcb->backlog) { // 挂起等待应用层accept的连接数accepts_pending大于等于应用层设置的最大backlog数,则不再接收连接,等应用层处理连接后再接收(此次没有发送任何应答,所以客户端超时后可以重试连接)
LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: listen backlog exceeded for port %"U16_F"\\n", tcphdr->dest));
return ERR_ABRT;
}
#endif /* TCP_LISTEN_BACKLOG */
新建一个tcp_pcb用于保存客户端与服务器的连接信息(accept返回一个新的socket,该socket对应一个tcp_pcb),tcp_alloc做了与tcp_connect类似的工作,初始化服务器端序号、收发窗口等。
npcb = tcp_alloc(pcb->prio);
/* If a new PCB could not be created (probably due to lack of memory),
we don't do anything, but rely on the sender will retransmit the
SYN at a time when we have more memory available. */
if (npcb == NULL) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\\n"));
TCP_STATS_INC(tcp.memerr);
return ERR_MEM;
}
根据客户端报文发送过来的通告窗口、最大报文段大小、客户端sequence number等,更新tcp_pcb的信息,进入SYN_RCVD状态,等待客户端的第三次握手。(tcp_alloc仅设置了服务器默认的一些信息,与对端相关的信息保存在报文里面,根据报文更新)
#if TCP_LISTEN_BACKLOG
pcb->accepts_pending++; // 等待应用层accept的连接个数accepts_pending加1
#endif /* TCP_LISTEN_BACKLOG */
/* Set up the new PCB. */
ip_addr_set(&(npcb->local_ip), &(iphdr->dest)); // 从客户报文获取本机连接地址
npcb->local_port = pcb->local_port; // 设置连接的端口
ip_addr_set(&(npcb->remote_ip), &(iphdr->src)); // 设置远端客户ip地址
npcb->remote_port = tcphdr->src; // 从报文获取客户的端口
npcb->state = SYN_RCVD; // 状态变更为SYN_RCVD
npcb->rcv_nxt = seqno + 1; // 下一个期望收到的seqno(客户端下一个报文的sequence number)
npcb->rcv_ann_right_edge = npcb->rcv_nxt;
npcb->snd_wnd = tcphdr->wnd; // 发送窗口大小设置为对方的通告窗口大小
npcb->ssthresh = npcb->snd_wnd; // 慢启动门限设置为发送窗口大小
npcb->snd_wl1 = seqno - 1;/* initialise to seqno-1 to force window update */
npcb->callback_arg = pcb->callback_arg; // callback_arg包含应用层的netconn相关信息,通过netconn与应用层通信
#if LWIP_CALLBACK_API
npcb->accept = pcb->accept; // accept回调函数(唤醒应用层阻塞的accept,需要等第三次握手)
#endif /* LWIP_CALLBACK_API */
/* inherit socket options */
npcb->so_options = pcb->so_options & (SOF_DEBUG|SOF_DONTROUTE|SOF_KEEPALIVE|SOF_OOBINLINE|SOF_LINGER); // socket选项(阻塞、非阻塞、保活等标记)
/* Register the new PCB so that we can begin receiving segments
for it. */
TCP_REG(&tcp_active_pcbs, npcb); // 新的tcp_pcb加入tcp_active_pcbs链表(第三次握手的消息匹配tcp_active_pcbs,不在调用tcp_listen_input)
/* Parse any options in the SYN. */
tcp_parseopt(npcb); // 根据客户端报文的选项信息设置npcb(最大报文大小等待)
#if TCP_CALCULATE_EFF_SEND_MSS
npcb->mss = tcp_eff_send_mss(npcb->mss, &(npcb->remote_ip)); // 查找远端输出网卡,根据网卡最大报文更新tcp_pcb的mss(协议默认的mss以及客户端服务器协商后的最大报文可能大于输出网卡的mss)
#endif /* TCP_CALCULATE_EFF_SEND_MSS */
SYN|ACK报文应答(第二次握手,ACK应答客户端的connect报文)
/* Send a SYN|ACK together with the MSS option. */
rc = tcp_enqueue(npcb, NULL, 0, TCP_SYN | TCP_ACK, 0, TF_SEG_OPTS_MSS
#if LWIP_TCP_TIMESTAMPS
/* and maybe include the TIMESTAMP option */
| (npcb->flags & TF_TIMESTAMP ? TF_SEG_OPTS_TS : 0)
#endif
); // 第二次握手,组装一个SYN/ACK报文,添加到tcp_pcb发送队列
if (rc != ERR_OK) {
tcp_abandon(npcb, 0);
return rc;
}
return tcp_output(npcb); // 调用tcp_output发送tcp_pcb发送队列里面的报文
4.3、客户端connect返回
客户端第一次握手调用tcp_connect时,已经将tcp_pcb添加到tcp_active_pcbs链表,服务器端获取到第一次握手报文后,发送SYN|ACK报文,客户端调用tcp_input处理第二次握手报文。
tcp_input对于tcp_active_pcbs匹配过程与tcp_listen_pcbs类似,tcp_active_pcbs不判断ANY地址,连接之后除本地间用0.0.0.0地址通信,不存在ANY地址,虽然listen可以监听0.0.0.0,但是连接后使用客户的目的地址填充服务器tcp_pcb的源地址,不在一个主机的客户机不可能连接远端0.0.0.0的地址。另外tcp_input也会把匹配的tcp_pcb移动到链表最前面,提供下次查找tcp_pcb的效率。
inseg、recv_data、recv_flags初始化。(inseg、recv_data、recv_flags用于记录当前报文的信息,接收过程的信息,很多函数用的了这些信息,因此用全局变量存储这些信息)
/* Set up a tcp_seg structure. */
inseg.next = NULL; // inseg下一段报文设置为NULL
inseg.len = p->tot_len; // 本报文总长度
inseg.dataptr = p->payload; // 报文的数据
inseg.p = p; // 指向tcp报文
inseg.tcphdr = tcphdr; // tcp报文头部
recv_data = NULL; // recv_data设置为NULL
recv_flags = 0; // recv_flags设置为0
应用层refused_data数据再次发送。(与握手无关,连接过程不存在数据收发)
/* If there is data which was previously "refused" by upper layer */
if (pcb->refused_data != NULL) { // 收到PUSH标志后,push数据到应用层失败,需要再次push
/* Notify again application with data previously received. */
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: notify kept packet\\n"));
TCP_EVENT_RECV(pcb, pcb->refused_data, ERR_OK, err); // TCP_EVENT_RECV发送数据到应用层的邮箱
if (err == ERR_OK) {
pcb->refused_data = NULL; // 发送成功设置refused_data为NULL
} else { // refused_data还没被应用层接收,暂时不处理新的报文(如果客户端有数据发送,则通过tcp的超时重传发送被服务器忽略的报文)
/* drop incoming packets, because pcb is "full" */
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: drop incoming packets, because pcb is \\"full\\"\\n"));
TCP_STATS_INC(tcp.drop);
snmp_inc_tcpinerrs();
pbuf_free(p);
return;
}
}
调用tcp_process处理输入报文。
tcp_input_pcb = pcb;
err = tcp_process(pcb);
tcp_input_pcb = NULL;
tcp_process输入报文处理。
重置KEEPALIVE定时器及发送次数,根据收到的报文更新本地tcp_pcb信息。
/* Update the PCB (in)activity timer. */
pcb->tmr = tcp_ticks; // TCP_KEEPALIVE时间设置为当前时间,在tcp_ticks之后keep_intvl时间后才需要发送KEEPALIVE报文
pcb->keep_cnt_sent = 0; // TCP_KEEPALIVE发送次数设置为0(收到了远端报文,说明远端是ALIVE,需要重新开始KEEPALIVE计数
tcp_parseopt(pcb); // 根据收到的tcp报文选项更新pcb信息(中间网络可能发生变化,最大报文等可能变化,需要根据报文选项信息更新本地的数据)
SYN_SENT状态处理。
服务器端二次握手报文SYN|ACK处理。
发送缓存、接收序号、发送窗口等设置,并进入ESTABLISHED状态。(snd_wl1设置小于下一个接收报文的seqno,下次接收到的报文的seqno大于snd_wl1,触发协议栈更新snd_wnd发送窗口相关信息)
if ((flags & TCP_ACK) && (flags & TCP_SYN)
&& ackno == ntohl(pcb->unacked->tcphdr->seqno) + 1) {
pcb->snd_buf++; // 发送缓存加1(SYN虽然没有发送数据,但是占一个字节)
pcb->rcv_nxt = seqno + 1; // 期望服务器端发送的下一个报文的序号rcv_nxt(connect时没有获取任务服务器的数据,默认为0)
pcb->rcv_ann_right_edge = pcb->rcv_nxt;
pcb->lastack = ackno; // 服务器已应答的报文序号
pcb->snd_wnd = tcphdr->wnd; // 使用服务器端的通告窗口大小更新本地的发送窗口大小
pcb->snd_wl1 = seqno - 1; /* initialise to seqno - 1 to force window update */ // 强制更新窗口(下次收到数据报文时,snd_wl1小于seqno,会更新snd_wnd窗口相关数据,三次握手过程发送的数据小,可以正常通过网络,发送窗口值不够准确,等服务器有数据发送过来时,以服务器发送数据报文中的通告窗口为准)
pcb->state = ESTABLISHED; // 进入ESTABLISHED状态
根据服务器SYN|ACK第二次握手报文进入的网卡更新mss,更新慢启动门限ssthresh,更新拥塞窗口,发送队列、unacked队列更新,没有unacked报文则停止超时重传定时器,否则直接设置定时器超时(unacked队列可以马上发送,不需要等待原来的超时时间)。发送ACK第三次握手协议,通知应用层连接已经建立。
#if TCP_CALCULATE_EFF_SEND_MSS
pcb->mss = tcp_eff_send_mss(pcb->mss, &(pcb->remote_ip)); // 更新mss(connect时,根据发送网卡设置了mss,存在发送出去的报文应答时不是从一个网卡发送过来的情况,ARP表里面记录了服务器报文进入的网卡)
#endif /* TCP_CALCULATE_EFF_SEND_MSS */
/* Set ssthresh again after changing pcb->mss (already set in tcp_connect
* but for the default value of pcb->mss) */
pcb->ssthresh = pcb->mss * 10; // 重新更新ssthresh(mss根据服务器及路由等已经更新,需要同步更新ssthresh)
pcb->cwnd = ((pcb->cwnd == 1) ? (pcb->mss * 2) : pcb->mss); // 拥塞窗口设置为两个mss报文大小(connect时,设置为一个mss大小 )
LWIP_ASSERT("pcb->snd_queuelen > 0", (pcb->snd_queuelen > 0));
--pcb->snd_queuelen; // 第一次握手的报文已经被应答(snd_queuelen减1)
LWIP_DEBUGF(TCP_QLEN_DEBUG, ("tcp_process: SYN-SENT --queuelen %"U16_F"\\n", (u16_t)pcb->snd_queuelen));
rseg = pcb->unacked; // rseg指向unacked的第一个报文(第一次握手报文)
pcb->unacked = rseg->next; // unacked指向下一个待应答报文(对于非阻塞的connect,在没有完全建立连接的SYN_SENT、SYN_RCVD应用层都可以发送数据,此时数据缓存在snd_buf缓存队列里面,如果发送的数据大于可用的snd_buf,那么tcp协议会触发把unsent的报文添加到unacked队列里面去,unacked可以理解为正在发送的报文,unsent可以理解为等待发送的队列,发送窗口及拥塞控制等控制unacked队列里面有多少报文可以在网络上发送,snd_buf控制应用层有多少unsent报文可以缓存)
/* If there's nothing left to acknowledge, stop the retransmit
timer, otherwise reset it to start again */
if(pcb->unacked == NULL)
pcb->rtime = -1; // 重传传定时器设置为-1,关闭重传定时器
else {
pcb->rtime = 0; // 如果有unacked数据,那么设置rtime为0(lwip定时器周期性检查是否有定时器超时,比较time是否小于当前时间,小于则超时,rtime设置为0,那么下次定时器检查点rtime肯定超时了)
pcb->nrtx = 0; // 重传次数设置为0
}
tcp_seg_free(rseg); // 释放已经应答的报文
/* Call the user specified function to call when sucessfully
* connected. */
TCP_EVENT_CONNECTED(pcb, ERR_OK, err); // 通知应用层连接已经建立
tcp_ack_now(pcb); // 发送第三次握手ACK到服务器
自此,客户端的握手已经完成,客户端已经可以发送数据了。
4.4、服务器端accept
listen状态下,服务器端处理第一次握手、发送第二次握手报文后,创建了一个tcp_pcb并加入到tcp_active_pcbs里面,然后进入SYN_RCVD状态,等待客户端的第三次握手报文。服务器端处理第三次握手报文走的路径与客户端处理第二次握手报文路径一致,都是调用tcp_process处理。
SYN_RCVD状态更新拥塞窗口及最大报文等(序号等信息在第一次握手已经获取到),通知应用层accept。
case SYN_RCVD:
if (flags & TCP_ACK) { // 收到第三次握手ACK报文
/* expected ACK number? */
if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)) { // 第二次握手时,报文只要一个数据seqno就是pcb->lastack+1,pcb->snd_nxt就是seqno + 1,判断等价于“TCP_SEQ_BETWEEN(ackno, seqno, seqno + 1))“实际就是”ackno == seqno“
u16_t old_cwnd;
pcb->state = ESTABLISHED; // 进入ESTABLISHED状态
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection established %"U16_F" -> %"U16_F".\\n", inseg.tcphdr->src, inseg.tcphdr->dest));
#if LWIP_CALLBACK_API
LWIP_ASSERT("pcb->accept != NULL", pcb->accept != NULL);
#endif
/* Call the accept function. */
TCP_EVENT_ACCEPT(pcb, ERR_OK, err); // 通知应用层accept
if (err != ERR_OK) {
/* If the accept function returns with an error, we abort
* the connection. */
tcp_abort(pcb);
return ERR_ABRT;
}
old_cwnd = pcb->cwnd; // 记录就的拥塞窗口大小
/* If there was any data contained within this ACK,
* we'd better pass it on to the application as well. */
tcp_receive(pcb); // 如果第三次握手附加数据,调用tcp_receive处理报文数据(附加数据可能有客户端的mss等)
pcb->cwnd = ((old_cwnd == 1) ? (pcb->mss * 2) : pcb->mss); // 拥塞窗口更新为两个mss大小
if (recv_flags & TF_GOT_FIN) { // 连接后马上断开,进入CLOSE_WAIT状态
tcp_ack_now(pcb);
pcb->state = CLOSE_WAIT;
}
}
/* incorrect ACK number */
else {
/* send RST */
tcp_rst(ackno, seqno + tcplen, &(iphdr->dest), &(iphdr->src),
tcphdr->dest, tcphdr->src);
}
} else if ((flags & TCP_SYN) && (seqno == pcb->rcv_nxt - 1)) {
/* Looks like another copy of the SYN - retransmit our SYN-ACK */
tcp_rexmit(pcb);
}
break;
自此服务器端的accept已经可以获取一个新的socket了,三次握手完成。
5、tcp的四次挥手
5.1、客户端调用tcp_close关闭连接(进入FIN_WAIT_1状态)
调用tcp_close前tcp_pcb的recv回调函数指针设置为NULL(应用层不再接收数据,所有数据直接被丢弃,协议层的处理仍按正常流程走,认为应用层已经接收到数据),调用tcp_close发送FIN报文,进入FIN_WAIT_1状态。(第一次挥手)
case ESTABLISHED:
err = tcp_send_ctrl(pcb, TCP_FIN); // 发送FIN报文
if (err == ERR_OK) {
snmp_inc_tcpestabresets();
pcb->state = FIN_WAIT_1; // 进入FIN_WAIT_1状态
}
break;
半关闭过程如下:
5.2、服务器收到FIN报文(进入CLOSE_WAIT状态)
服务器收到FIN报文交由tcp_process函数处理,接收到的数据发送给应用层,然后发送一个空的EOF数据给应用层(类似文件的EOF,应用层知道接收数据已经完成,不需要再从协议栈读数据),发送客户端FIN报文的ACK给客户端(第二次挥手),进入CLOSE_WAIT状态。(CLOSE_WAIT状态的数据仍然可以发送,只是会被对方直接丢弃,可以理解CLOSE_WAIT状态为等待应用层发送完数据,等待应用层调用tcp_close;如果服务器unacked的数据太多,导致FIN的ACK报文发送延迟很长时间,那么客户端的FIN会不断重传,直到超过重传次数后直接删除连接;客户端收到FIN的ACK会进入FIN_WAIT_2状态,如果长时间没有调用tcp_close,客户端超时后,一样会删除连接,下次收到服务器的报文将发送RST报文重置服务器的连接)
case ESTABLISHED:
tcp_receive(pcb);
if (recv_flags & TF_GOT_FIN) { /* passive close */ // 收到FIN被动关闭
tcp_ack_now(pcb); // 发送ACK给客户端,如果unsent队列有数据并且可以发送(拥塞窗口足够),tcp_ack_now调用tcp_output时仅将可以发送的unsent报文添加到unacked队列里面(tcp_output对于非SYN_SENT状态的报文都会加ACK标记,unacked队列里面的数据已经在网络发送,unacked报文可能对端已经接收还没应答而已,因此只能将ACK标记添加到unsent报文里面,unsent报文的ACK序号才是收到FIN时的序号),如果unsent为空或者unsent超出拥塞窗口大小,那么tcp_output构造一个不带数据的ACK报文发送出去,报文的序号在unsent报文之后...... FIN_WAIT_1接收到报文后,如果ackno为snd_nxt(FIN报文的seqno加1,FIN报文被应答),那么客户端FIN_WAIT_1可以转为FIN_WAIT_2状态,FIN_WAIT_2状态超时会删除连接,因此半关闭状态服务器端不能无限制发送数据,FIN_WAIT_2超时后连接被删除,服务器发送报文会被重置(RST)
pcb->state = CLOSE_WAIT; // 进入CLOSE_WAIT状态
}
break;
5.3、客户端收到ACK应答(进入FIN_WAIT_2状态)
非同时关闭情况下,如果FIN报文没有被接收确认(调用tcp_close时,tcp发送队列里面还有其他数据,FIN_WAIT_1状态收到的ACK有可能是对队列中的其他数据的应答,FIN报文仍在unsent缓存队列或unacked发送队列,或者FIN之前的报文还没有确认,对端需要等FIN之前的所有报文收到后才应答FIN报文),则收到报文后不做处理,等待本地数据发送完成、FIN被对端确认;如果本地报文及FIN报文被对端确认接收,FIN报文被应答,那么直接进入FIN_WAIT_2状态。可以理解FIN_WAIT_1状态为等待FIN报文被确认(FIN之前的报文也被对端接收),可以理解FIN_WAIT_2状态为等待对端的FIN报文(对端FIN之前的报文也被本地接收)。
case FIN_WAIT_1:
tcp_receive(pcb); // 接收数据
if (recv_flags & TF_GOT_FIN) {
if ((flags & TCP_ACK) && (ackno == pcb->snd_nxt)) { // 收到FIN的ACK,本地所有数据及FIN都已应答,也收到对端的FIN报文(对端FIN之前的报文都已经接收到),自此可以确定本地没有数据要发送,对端也没有数据需要接收,可以进入TIME_WAIT状态,对后续收到到报文不需要解析直接应答即可;《TCP-IP详解卷 1:协议》里面只有收FIN|ACK的报文,并没有同时发FIN|ACK的报文,tcp收到FIN报文时都是调用tcp_ack_now立即应答,tcp_ack_now直接调用tcp_output构造ACK报文发送,ACK没有超时重传机制,FIN_WAIT_1状态并不一定能收到FIN的ACK应答,客户端在FIN_WAIT_1等待FIN应答过程,假如服务器调用tcp_close发送FIN报文,调用tcp_output发送FIN报文时,对于非SYN_SENT状态的tcp_output都会默认设置ACK标记(对客户端FIN报文的应答),因此客户端会收到FIN|ACK的报文
LWIP_DEBUGF(TCP_DEBUG,
("TCP connection closed %"U16_F" -> %"U16_F".\\n", inseg.tcphdr->src, inseg.tcphdr->dest));
tcp_ack_now(pcb); // 发送ACK应答对端的FIN报文
tcp_pcb_purge(pcb);
TCP_RMV(&tcp_active_pcbs, pcb); // 从tcp_active_pcbs删除tcp_pcb
pcb->state = TIME_WAIT; // 进入TIME_WAIT状态(此状态下收到的报文都调用tcp_timewait_input处理,所有数据都丢弃,不发送给应用层,直接确认当前收到的报文,rcv_nxt设置为当前报文的下一个字节
TCP_REG(&tcp_tw_pcbs, pcb); // 添加tcp_pcb到tcp_tw_pcbs
} else { // 同时关闭(客户端、服务器同时调用tcp_close,都在FIN_WAIT_1状态收到对方的FIN报文)
tcp_ack_now(pcb); // 发送FIN报文的ACK
pcb->state = CLOSING; // 进入CLOSING状态(CLOSING状态收到ACK后进入TIME_WAIT状态)
}
} else if ((flags & TCP_ACK) && (ackno == pcb->snd_nxt)) { // 非同时关闭,FIN报文被确认,服务器端还没调用tcp_close(发送队列、未发送队列或者应用层还有数据需要发送,甚至服务器端就没有数据要发送也不调用tcp_close)
pcb->state = FIN_WAIT_2; // 进入FIN_WAIT_2状态
}
break;
5.4、服务器调用tcp_close关闭连接(进入LAST_ACK状态)
与客户端调用tcp_close一样,服务器端也发送FIN报文(第三次挥手),但是服务器端进入的是LAST_ACK状态,LAST_ACK状态下应用层也不接收数据,与客户端一样等待FIN报文被确认。(FIN报文发送出去后不一定所有发送的报文都被确认,tcp协议一次发送几个报文,tcp应答报文也不是立马应答,而是等待一段时间,如果收到多个报文,那么多个报文一起应答,减少ACK报文发送的数量,因此FIN报文可能和其他数据报文一起发送出去,但是FIN报文可能先到,等其他报文被接收后,对端才发送对FIN报文的应答,因此LAST_ACK状态等待FIN报文的应答)
case CLOSE_WAIT:
err = tcp_send_ctrl(pcb, TCP_FIN); // 被动关闭,CLOSE_WAIT状态调用tcp_close,发送FIN报文
if (err == ERR_OK) {
snmp_inc_tcpestabresets();
pcb->state = LAST_ACK; // 进入LAST_ACK状态
}
break;
5.5、客户端收到FIN报文(进入TIME_WAIT状态)
客户端收到服务器的FIN报文,发送ACK(第四次挥手),将tcp_pcb加入到tcp_tw_pcbs,并进入TIME_WAIT状态,等待网络上的其他报文被接收,TIME_WAIT状态调用tcp_timewait_input处理报文,一律应答收到报文的最后一个字节。(此时服务器端的FIN及之前的报文都已经接收到了,网络中可能存在超时重发的延迟报文,这部分报文已经被确认了,不需要再处理,直接确认已接收该报文及该报文前的数据是没有任何问题的;另外收到服务器的FIN之后,调用tcp_ack_now发送的ACK应答报文并没有超时重传机制,如果ACK报文被丢了,那么服务器端会重发FIN报文,甚至会重发延迟ACK的报文(收到的报文没有立即应答,而是和FIN的应答一起发送),因此tcp_timewait_input对这些报文一律直接应答即可)
case FIN_WAIT_2:
tcp_receive(pcb);
if (recv_flags & TF_GOT_FIN) { // 收到FIN报文(此时本地没有待发送的数据也没有未被确认的数据,另外收到服务器的FIN报文则表明服务器端的所有数据也接收到了)
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection closed %"U16_F" -> %"U16_F".\\n", inseg.tcphdr->src, inseg.tcphdr->dest));
tcp_ack_now(pcb); // 发送ACK应答
tcp_pcb_purge(pcb);
TCP_RMV(&tcp_active_pcbs, pcb); // 从tcp_active_pcbs删除tcp_pcb
pcb->state = TIME_WAIT; // 进入TIME_WAIT状态
TCP_REG(&tcp_tw_pcbs, pcb); // tcp_pcb加入到tcp_tw_pcbs
}
break;
5.6、服务器客户端进入CLOSED状态
客户端的TIME_WAIT状态仅对收到的报文进行确认,TIME_WAIT并不会处理报文数据,TIME_WAIT到CLOSED状态由定时器触发,TIME_WAIT超时后,可以认为这个时间段内所有网络上重发的报文都已经传送完了,tcp_pcb进入CLOSED状态,地址及端口可以被重新使用;
服务器端的LAST_ACK也是一样,在客户端发送FIN到服务器发送FIN这段时间内,客户端的数据不管是重发还是什么数据,理论上早都已经到达了,因此服务器端不用等待客户端重发数据,收到最后一个报文的ACK就可以直接关闭;另外LAST_ACK也有一个定时器,如果在超时时间内没有收到ACK,那么也直接关闭。
tcp的好几个状态应该都有定时器,这些状态不可能无限等待下去,有些发送报文的,本身就有超时重发定时器,没有数据发送的状态,由tcp的定时器对这些状态的时间进行检查,如果这些状态超时,同样会关闭连接。
tcp连接终止状态转移图如下:
(红线主动关闭,蓝线被动关闭)
以上是关于TCP/IP传输层协议实现 - TCP连接的建立与终止(lwip)的主要内容,如果未能解决你的问题,请参考以下文章