http://blog.chinaunix.net/uid-9863638-id-1996367.htmllwip源码分析6——tcp层
2010-1-16
TCP层:
看了这么久,终于到tcp层了,用凌总的话来说就是进入主战场了,兴奋啊,哈哈。
回顾一下tcp的特性,就当重温comer的第一卷:
1. 滑动窗口的概念:
Tcp是通过正面确认和重传技术来保证可靠性的,但是如果简单的使用正面确认的技术将极大的浪费带宽,因为它在收到前一个分组的确认信息前必须推迟下一个分组的传送。
由此诞生了一种更为复杂的技术,在保证可靠性的同时能充分利用带宽——滑动窗口。(其实就是批量传输的概念,当滑动窗口的长度为 1 时就是简单的正面确认)。![](http://blog.chinaunix.net/photo/93784_090824114708.jpg)
滑动窗口把整个序列分成三部分:左边的是发送了并且被确认的分组,窗口右边是还没发送的分组,窗口内部是待确认的分组,窗口内部又分成已经发送待确认的,和未发送但将立即发送。
因为tcp是双工的,上面仅仅讲了发送,接受也有相应的滑动窗口,不过相对简单点。可参加协议卷2:p648.
1. 这几天被其他的事情耽误了,继续。
2010-1-20 tcp的理论知识还是相对不够啊,继续补理论知识。
Tcp报文头部的代码比特位(6bit,也有文档称之为flag标志位),有6种:
A.URG urgent pointer 紧急指针
B. ADK acknowledge number 确认号
C. PSH push 推送操作
D.RST reset connection 复位连接
E. SYN synchronize sequence number 同步序列号
F. FIN end of data 发送方字节流已发完
这里要注意几点,当建立一个新的连接时,SYN标志才会变1.而由于发送ACK无需任何代价,因为32bit的确认序列和ACK标志一样,总是TCP首部的一部分,因此在建立连接之后,这个字段(ACK和确认序列号)总是被设置
2. 2010-1-21
之前总是看comer的那本用tcp/ip进行网际互联,这本书初学者看很好,但是想深入一点的话就觉得太简单了点,拿了fp的tcp/ip详解 卷1:协议,觉得就讲的很好,所以建议对tcp/ip有一定基础的还是看tcp/ip详解比较好,而且lwip的代码就是根据这本书来写的,在tcp这部分的代码中有tcp_pcb的state,lwip中的state的状态机都是和卷1中182一模一样。
看了书中关于状态机的这张表,就能很好的理解lwip中tcp_input->tcp_process中的代码了。
3. Nagle算法
Nagle算法是以他的发明人John Nagle的名字命名的,它用于自动连接许多的小缓冲器消息;这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率。Nagle算法于1984年定义为福特航空和通信公司IP/TCP拥塞控制方法,这是福特经营的最早的专用TCP/IP网络减少拥塞控制,从那以后这一方法得到了广泛应用。Nagle的文档里定义了处理他所谓的小包问题的方法,这种问题指的是应用程序一次产生一字节数据,这样会导致网络由于太多的包而过载(一个常见的情况是发送端的"愚笨窗口综合症(Silly Windw Syndrome)")。从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的标题数据。这种情况转变成了4000%的消耗,这样的情况对于轻负载的网络来说还是可以接受的,但是重负载的福特网络就受不了了,它没有必要在经过节点和网关的时候重发,导致包丢失和妨碍传输速度。吞吐量可能会妨碍甚至在一定程度上会导致连接失败。Nagle的算法通常会在TCP程序里添加两行代码,在未确认数据发送的时候让发送器把数据送到缓存里。任何数据随后继续直到得到明显的数据确认或者直到攒到了一定数量的数据了再发包。尽管Nagle的算法解决的问题只是局限于福特网络,然而同样的问题也可能出现在ARPANet。这种方法在包括因特网在内的整个网络里得到了推广,成为了默认的执行方式,尽管在高互动环境下有些时候是不必要的,例如在客户/服务器情形下。在这种情况下,nagling可以通过使用TCP_NODELAY 插座选项关闭。
Lwip对nagle算法的解释如下:尽量将用户的数据合成一个数据包发送。只有在以下情况下,会立即发送:
a. 在本连接上没有已经发送了,但未被确认的数据。
b. 用户定义了nodelay标志位或者infr标志位(快速重传),
c. Pcb连接上存在超过1个的未发送数据
d. Pcb连接的未发送数据的长度超过mss(就是TCP数据包每次能够传输的最大数据分段)
首先我们来看一下讲解lwip第一节中的流程图:
可以很明显的看到tcp在整个lwip中占的部分是非常大的,在较老的版本中几乎占到了50%左右的代码量,在1.3.2中增加了不少其他的控制协议,但是即使这样大概也有30%左右。先来看一下lwip的文件组织目录:
/core/tcp.c:tcp协议的普通通用函数,比如制造数据结构以及tcp timer的一些函数;
/core/tcp_in.c:tcp层的输入处理,从上图中我们也可以看到具体的流程为:(ip_input() ->)
tcp_input() -> * tcp_process() -> tcp_receive() (-> application).
/core/tcp_out.c:tcp层的输出处理,具体的流程为:tcp_write()->tcp_enqueue()->tcp_output->ip_output_if()
2010-1-18
Tcp这部分好复杂啊,看了两天的框架还是很乱,硬着头皮继续了。暂时可能会写的比较乱,以后再慢慢整理吧。
理解tcp_process下面这张状态图非常重要:
状态:描述
CLOSED:无连接是活动的或正在进行
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用说它已经完成
FIN_WAIT2:另一边已同意释放
ITMED_WAIT:等待所有分组死掉
CLOSING:两边同时尝试关闭
TIME_WAIT:另一边已初始化一个释放
LAST_ACK:等待所有分组死掉
看了tcpip详解后终于有点眉目了,我是从tcp_input入手的,但是刚好这部分代码是tcp中最复杂的一部分,像详解卷二中花了近100页来讲述但是对tcp_output啥都没提,可见一斑了。
2010-1-22
Tcp_input函数进来的时候做一些对数据首部的处理之后会去寻找一个合适的接口(和目的相同的ip和端口),和udp不同的是,tcp的pcb不像udp那么简单只有一个列表,tcp协议中建立了三个pcb的列表,分别为active,time_wait,listen
Active是最最常见的pcb链表,这个链表中的所有pcb已经处于能够正常接收和发送数据的状态了。言下之意下面两种状态都不是在正常接收或发送数据的状态。
Time_wait状态时指tcp关闭一个连接之后会进入的状态,它在这个状态停留的时间为最大报文段生存时间(MSL)的两倍,用来区分新旧连接。
关于time_wait队列的pcb的处理在tcp_timewait_input函数中解释了,在代码中队接受包的类型分成下面几类:
A.TCP_RST 直接返回
B. Tcp_syn:这里为什么同步序列号是在范围内反而是一个error没搞明白?
C. TCP_FIN: 重新启动定时器
对于listen状态的pcb,即等待syn被动打开,在服务器端用的比较多。可以参见上面的状态机图,listen状态只有收到syn才会进入SYN_rcv状态,因此相应代码被分为两种状态:
A.FLAGS == TCP_ACK 发送tcp_rst。
B. FLAGS == TCP_SYN 被动打开成功,则先需要建立一个新的pcb(调用tcp_alloc函数),为这个新建立的pcb填充成员。将这个pcb加入到active队列中。调用tcp_enqueue回应带有SYN,ACK标志的数据包。
对于tcp_input函数的接下来的处于只是针对active的pcb的,因为两外两种状态的pcb在他们本身阶段已经返回了。
Tcp_process基本就是对状态机图的代码诠释,具体参考我对lwip1.3.2的代码注释。这里面有一点需要注意,比如在SYN_RCVD的case中,这里面不仅仅调用了pcb自带的accept函数,还调用了tcp层自带的接收函数tcp_receive。就在我们第一篇综述中的那张图所示,上层和协议层的交互有两种方式,一种是应用程序可以在协议层进程中实现(即利用lwip自己提供的一套内部回调函数来通信),还有一种是应用程序和协议层通过邮箱等通信方式,因此代码中实现了两种方式。
TCP_EVENT_ACCEPT(pcb, ERR_OK, err);
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_process中调用tcp_receive函数。
2010-1-28
前端时间在忙基础软件重大专项的预算,忙了好几天,累死我了,tcp这部分的进度耽搁,继续。这次主要讲解tcp函数的发送部分。
我们的流程是tcp_write->tcp_enqueue->tcp_output.
1. tcp_write函数
这个函数很简单,首先判断此时的pcb是否进入了可发送数据的状态,这些状态有ESTABLISHED, CLOSE_WAIT, SYN_SENT, SYN_RECD,(还有一些在状态机里面见到的状态,如LISTEN,FIN_WAIT1,FIN_WAIT2,CLOSED,CLOSING等则要么处于连接还未建立,要么是连接已关闭的状态,顶多再能发个ack包,不能再发数据了)如果是处于这些有效状态,则调用tcp_enqueue函数。
2. tcp_enqueue函数
tcp_enqueue通过把上层需要发送的数据包放在队列中,最后一起调用tcp_output来提高效率。看一下参数:
tcp_enqueue(struct tcp_pcb *pcb, void *arg, u16_t len, u8_t flags, u8_t apiflags, u8_t optflags)
pcb:当前连接的pcb,arg:需要发送的数据,len:数据长度,flags:tcp首部的标志位。apiflags:TCP_WRITE_FLAG_COPY(说明空间需要分配并且拷贝至pbuf,否则则说明数据来自静态的存储器,比如rom,不需要拷贝)TCP_WRITE_FLAG_MORE。
看代码流程:
A.首先保存一些变量,比如此次传输数据的长度,要传输的数据的地址
B. 将所要传输的数据切割成一个个tcp_seg结构体,最大长度不超过一个mss,如果小于就是一个tcp_seg。很奇怪,第一个seg是存放在queue指针的队列上,其余的是存放在useg指针的队列上,并且useg始终指向最后一个seg结构体。
C. 完成了数据切割,我们现在需要把这些seg加入到pcb->unsend队列中。但是这段代码很奇怪,不知道是我看错了还是有bug?但理论上不会有这么明显的bug啊。
3. tcp_output函数
在tcp_enqueue函数中已经把我们要发送的数据放在了pcb的unsend链表上了。接下来我们就是要将这些数据发送出去。看流程:
A. 首先过滤flags标志位为立即响应并且没有携带数据的调用,直接tcp_send_empty_ack(pcb);(因为不是每个对tcp_output的调用都是经过tcp_enqueue)
B. 利用while循环对unsend链表上的seg一个个发送或跳过。
a) 跳过:对每个seg包会首先用nagle算法做一下检测,如果满足nagle条件,则会跳过这个数据包不做任何操作,留置合并一起发送。具体的条件可参考tcp概念部分的文章。
b) 发送:对于一般的seg包,则先置ack标志位,然后调用tcp_output_segment(seg, pcb);发送。
4. 关于tcp_output中的tcp_send_empty_ack(pcb);
a) 这里面的操作比较简单,主要是建立pbuf,然后建立tcp的首部与pbuf的联系,tcphdr = p->payload,然后填充tcphdr,传递给ip层。(调用ip_output函数)
5. 关于tcp_output中的tcp_output_segment(seg, pcb);
a) 同tcp_send_empty_ack(pcb);类似,不是很复杂。