TCP还有最后一点东西需要扫尾。现在我们要跳出tcp_process函数,继续回到tcp_input中,前面说过,tcp_input接收IP层递交上来的数据包,并根据数据包查找相应TCP控制块,并根据相关控制块所处的状态调用函数tcp_timewait_input、tcp_listen_input或tcp_process进行处理。如果是调用的前两个函数,则tcp_input在这两个函数返回后就结束了,但若调用的是tcp_process函数,则函数返回后,tcp_input还要进行许多相应的处理。
要继续往下讲就得看看一个很重要的全局变量recv_flags,前面说TCP全局变量的时候也简单说到过。这个变量与控制块中的flags字段相似,都是用来描述当前TCP控制块的所处状态的。flags字段可以设置的各个标志位及其意义如下宏定义所示:
#define TF_ACK_DELAY (u8_t)0x01U // 延迟回复ACK包
#define TF_ACK_NOW (u8_t)0x02U // 立即发送ACK包
#define TF_INFR (u8_t)0x04U // 处于快速重传状态
#define TF_FIN (u8_t)0x20U // 本地上层应用关闭连接
#define TF_NODELAY (u8_t)0x40U // 禁止Nagle算法禁止
#define TF_NAGLEMEMERR (u8_t)0x80U // 发送缓存空间不足
上面的各个字段基本都已经涉及过了,再来看看全局变量recv_flags可以设置的各个标志位及其意义,如下宏定义所示:
#define TF_RESET (u8_t)0x08U // 接收到RESET包
#define TF_CLOSED (u8_t)0x10U // 在LAST_ACK状态收到ACK包,连接成功关闭
#define TF_GOT_FIN (u8_t)0x20U // 接收到FIN包
为什么要用两个字段来描述相应TCP控制块的状态呢,不是很明了?个人理解有两个原因:一是由于控制块中的flags字段本身是8位的,若以每位描述一种状态,则不足以描述上面的9种状态;二是上面的9种描述状态很明显可以分为两类,第一类的6种与TCP的数据包处理密切相关,第二类的3种与TCP的状态转换密切相关。
在tcp_input每次调用tcp_process之前,recv_flags都会被初始化为0,在tcp_process的处理中,相关控制块在完成状态转换后,该全局变量与状态转换相关的位则会被置位,在函数返回到tcp_input后,tcp_input还会根据相应设置好的recv_flags值对控制块做后续处理。
if (recv_flags & TF_RESET) {
// TF_RESET标志表示接收到了对端的RESET包
TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_RST); // 若注册了回调函数
// 则调用该函数通知上层
tcp_pcb_remove(&tcp_active_pcbs, pcb); // 将控制块从链表中删除
memp_free(MEMP_TCP_PCB, pcb); // 释放控制块内存空间
} else if (recv_flags & TF_CLOSED) { // TF_CLOSED表示服务器成功关闭连接
tcp_pcb_remove(&tcp_active_pcbs, pcb); // 将控制块从链表中删除
memp_free(MEMP_TCP_PCB, pcb); // 释放控制块内存空间
} else {
err = ERR_OK;
if (pcb->acked > 0) { // 如果收到的数据包确认了unacked队列中的数据
TCP_EVENT_SENT(pcb, pcb->acked, err); //则可调用自定义的函数发送数据包
}
if (recv_data != NULL) { // 若成功的接收了数据包中的数据
if(flags & TCP_PSH) { //全局变量 flags保存的是TCP头部中的标志字段
recv_data->flags |= PBUF_FLAG_PUSH; // 将数据包pbuf字段
} // 设置PUSH标志
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err); // 调用自定义的函数接收数据
if (err != ERR_OK) { //若上层接收数据失败
pcb->refused_data = recv_data; //用控制块refused_data字段暂存数据
}
} // if (recv_data != NULL)
if (recv_flags & TF_GOT_FIN) { // 如果接收到FIN包
TCP_EVENT_RECV(pcb, NULL, ERR_OK, err); // 调用自定义的函数接收数据
}
if (err == ERR_OK) { //若处理正常则
tcp_output(pcb); // 试图往外发数据包
}
} //else
有两个地方需要说一下,首先是函数tcp_pcb_remove,源码如下所示,代码里面比较重要的两个函数是TCP_RMV和tcp_pcb_purge,这里就不再仔细说明了。
void tcp_pcb_remove(struct tcp_pcb **pcblist, struct tcp_pcb *pcb)
{
TCP_RMV(pcblist, pcb); // 从某链表上移除PCB控制块
tcp_pcb_purge(pcb); // 清空控制块的数据缓冲队列,释放内存空间
if (pcb->state != TIME_WAIT && //如果该控制块上有被延迟的ACK,则立即发送
pcb->state != LISTEN &&
pcb->flags & TF_ACK_DELAY) {
pcb->flags |= TF_ACK_NOW;
tcp_output(pcb);
}
pcb->state = CLOSED; // 置状态
}
还有个需要注意的地方是回调函数的调用:如上面TCP_EVENT_XXX所示。在实际应用程序中,我们可以通过回调函数的方式与LWIP内核交互,在初始化一个PCB控制块的时候,可以设定控制块中相应函数指针字段的初始值,包括sent、recv、connected、accept、poll、errf等。在内核处理中,会在TCP_EVENT_XXX处调用我们预先注册的函数,从而完成应用程序与协议栈之间的交互。关于应用程序与协议栈间接口的问题,又是一个庞大的工程,也是我们以后会继续讨论的重点。
到这里,TCP部分就基本讲完了,当然TCP层中还有一些东西没有讲到,如tcp_write等函数。TCP层学习的关键是了解整个TCP层运行的机制,在这个基础上去阅读源代码,应该不会存在什么的问题的。从《TCP建立与断开》到《TCP终结与小结》,可以看出TCP的篇幅实在是太多了,实际源代码也是如此,从代码量上看,TCP部分占了整个协议栈代码量的一半左右。注意,一般讲TCP协议时都会谈到UDP,但在这里我还不想涉及UDP协议,原因是在一般嵌入式产品中,都需要提供有效可靠的网络服务,而UDP的本质特点让其无法满足这一要求。所以,如果真是要将LWIP用于我们的产品中,则使用的基本是TCP协议,在后续的讲解中,我们会看到应用程序怎样利用LWIP建立一个Web服务器,这使得我们可以远程的通过http访问我们的设备了。接下来要说的就是协议栈与应用程序间的接口问题了。
要继续往下讲就得看看一个很重要的全局变量recv_flags,前面说TCP全局变量的时候也简单说到过。这个变量与控制块中的flags字段相似,都是用来描述当前TCP控制块的所处状态的。flags字段可以设置的各个标志位及其意义如下宏定义所示:
#define TF_ACK_DELAY (u8_t)0x01U // 延迟回复ACK包
#define TF_ACK_NOW (u8_t)0x02U // 立即发送ACK包
#define TF_INFR (u8_t)0x04U // 处于快速重传状态
#define TF_FIN (u8_t)0x20U // 本地上层应用关闭连接
#define TF_NODELAY (u8_t)0x40U // 禁止Nagle算法禁止
#define TF_NAGLEMEMERR (u8_t)0x80U // 发送缓存空间不足
上面的各个字段基本都已经涉及过了,再来看看全局变量recv_flags可以设置的各个标志位及其意义,如下宏定义所示:
#define TF_RESET (u8_t)0x08U // 接收到RESET包
#define TF_CLOSED (u8_t)0x10U // 在LAST_ACK状态收到ACK包,连接成功关闭
#define TF_GOT_FIN (u8_t)0x20U // 接收到FIN包
为什么要用两个字段来描述相应TCP控制块的状态呢,不是很明了?个人理解有两个原因:一是由于控制块中的flags字段本身是8位的,若以每位描述一种状态,则不足以描述上面的9种状态;二是上面的9种描述状态很明显可以分为两类,第一类的6种与TCP的数据包处理密切相关,第二类的3种与TCP的状态转换密切相关。
在tcp_input每次调用tcp_process之前,recv_flags都会被初始化为0,在tcp_process的处理中,相关控制块在完成状态转换后,该全局变量与状态转换相关的位则会被置位,在函数返回到tcp_input后,tcp_input还会根据相应设置好的recv_flags值对控制块做后续处理。
if (recv_flags & TF_RESET) {
// TF_RESET标志表示接收到了对端的RESET包
TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_RST); // 若注册了回调函数
// 则调用该函数通知上层
tcp_pcb_remove(&tcp_active_pcbs, pcb); // 将控制块从链表中删除
memp_free(MEMP_TCP_PCB, pcb); // 释放控制块内存空间
} else if (recv_flags & TF_CLOSED) { // TF_CLOSED表示服务器成功关闭连接
tcp_pcb_remove(&tcp_active_pcbs, pcb); // 将控制块从链表中删除
memp_free(MEMP_TCP_PCB, pcb); // 释放控制块内存空间
} else {
err = ERR_OK;
if (pcb->acked > 0) { // 如果收到的数据包确认了unacked队列中的数据
TCP_EVENT_SENT(pcb, pcb->acked, err); //则可调用自定义的函数发送数据包
}
if (recv_data != NULL) { // 若成功的接收了数据包中的数据
if(flags & TCP_PSH) { //全局变量 flags保存的是TCP头部中的标志字段
recv_data->flags |= PBUF_FLAG_PUSH; // 将数据包pbuf字段
} // 设置PUSH标志
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err); // 调用自定义的函数接收数据
if (err != ERR_OK) { //若上层接收数据失败
pcb->refused_data = recv_data; //用控制块refused_data字段暂存数据
}
} // if (recv_data != NULL)
if (recv_flags & TF_GOT_FIN) { // 如果接收到FIN包
TCP_EVENT_RECV(pcb, NULL, ERR_OK, err); // 调用自定义的函数接收数据
}
if (err == ERR_OK) { //若处理正常则
tcp_output(pcb); // 试图往外发数据包
}
} //else
有两个地方需要说一下,首先是函数tcp_pcb_remove,源码如下所示,代码里面比较重要的两个函数是TCP_RMV和tcp_pcb_purge,这里就不再仔细说明了。
void tcp_pcb_remove(struct tcp_pcb **pcblist, struct tcp_pcb *pcb)
{
TCP_RMV(pcblist, pcb); // 从某链表上移除PCB控制块
tcp_pcb_purge(pcb); // 清空控制块的数据缓冲队列,释放内存空间
if (pcb->state != TIME_WAIT && //如果该控制块上有被延迟的ACK,则立即发送
pcb->state != LISTEN &&
pcb->flags & TF_ACK_DELAY) {
pcb->flags |= TF_ACK_NOW;
tcp_output(pcb);
}
pcb->state = CLOSED; // 置状态
}
还有个需要注意的地方是回调函数的调用:如上面TCP_EVENT_XXX所示。在实际应用程序中,我们可以通过回调函数的方式与LWIP内核交互,在初始化一个PCB控制块的时候,可以设定控制块中相应函数指针字段的初始值,包括sent、recv、connected、accept、poll、errf等。在内核处理中,会在TCP_EVENT_XXX处调用我们预先注册的函数,从而完成应用程序与协议栈之间的交互。关于应用程序与协议栈间接口的问题,又是一个庞大的工程,也是我们以后会继续讨论的重点。
到这里,TCP部分就基本讲完了,当然TCP层中还有一些东西没有讲到,如tcp_write等函数。TCP层学习的关键是了解整个TCP层运行的机制,在这个基础上去阅读源代码,应该不会存在什么的问题的。从《TCP建立与断开》到《TCP终结与小结》,可以看出TCP的篇幅实在是太多了,实际源代码也是如此,从代码量上看,TCP部分占了整个协议栈代码量的一半左右。注意,一般讲TCP协议时都会谈到UDP,但在这里我还不想涉及UDP协议,原因是在一般嵌入式产品中,都需要提供有效可靠的网络服务,而UDP的本质特点让其无法满足这一要求。所以,如果真是要将LWIP用于我们的产品中,则使用的基本是TCP协议,在后续的讲解中,我们会看到应用程序怎样利用LWIP建立一个Web服务器,这使得我们可以远程的通过http访问我们的设备了。接下来要说的就是协议栈与应用程序间的接口问题了。