LWIP V2.0.3 Whisper

  1. 描述完整的tcp发包过程

  2. 在正式发送数据前,需要先调用netbuf_new来分配一个netbuf结构体.netbuf结构体中包含了储存实际发送数据的pbuf指针p和本地/目标地址信息.
    随后根据实际要发送数据的大小通过netbuf_alloc来对netbuf中的pbuf分配空间(使用pbuf_alloc),此时分配的pbuf,layer为
    PBUF_TRANSPORTtype为PBUF_RAM,表明这是一个ram类型的用于传输层发包的pbuf.

    *pbuf_alloc根据指定的layer,length和type来决定pbuf的分配形式,layer影响的其实是一个offet值,也就是不同的层由于要
    附加不同类型的header,所以导致最终分配的空间,比length要大,例如PBUF_TRANSPORT layer,要附加链路层头(ethernet),ip层头,传输层头,所以实际分配的空间就是length加上这3个头的size.而type影响的是分配使用的内存池,由于playload类型的不同,所以需要从不同的内存池分配合适的空间,来节省内存.

    但这个申请的netbuf也只是用来储存数据, 毕竟收发地址的确定,本质上还是socket/netconn方面的工作,接下来通过API_MSG抛一个数据命令到tcp_thread中准备发送数据,MSG中的携带了要发送的buffer的地址和size (对,又把netbuf解包了)
    实际发送函数 lwip_netconn_do_write
    在调用 lwip_netconn_do_wirte 前,先把netconn状态置为 NETCONN_WRITE ,也就是处于发送状态
    lwip_netconn_do_wirte 在简单netconn类型是否确实是TCP后,开始调用 lwip_netconn_do_writemore 来发送数据
    lwip_netconn_do_writemore首先检查是否启用了发送超时,在开始调用发送时候,记录了发送的时间戳,这里首先检查一下,到执行发送时是否已经超时了,如果超时,则直接报发送失败可以了.
    发送数据时候,先取得真实的发送地址指针(dataptr)和真实的发送数据长度(len),如果发送长度大于 0xffff ,置位
    TCP_WRITE_FLAG_MORE,也就是do_wirte最多一次发送0xffff个字节.
    接下来检查 available(通过检查tcp.pcb的snd_buf值,默认为 TCP_SND_BUF = 2 * 536) , 也就是最大的发送窗口,如果发送窗口小于发送长度,把当次发送长度修改为len,同时置位 TCP_WRITE_FLAG_MORE,可以看到,只要本次do_write的数据不能全部发送出去,都要置位 TCP_WRITE_FLAG_MORE
    接下来调用 tcp_write 把数据送入发送队列,但并不会立刻进行发送.
    如果tcp_write回复成功或者回复ERR_MEM(暂时内存不足),则回调事件 NETCONN_EVT_SENDMINUS ,通知观测者数据已经写入成功 (虽然还没真实发)
    再次检查tcp_write的返回值,如果回复的是成功,接下来调用 tcp_output 把数据发送出去,根据发送结果来设置回调的len参数,也就是向上层报告真实发送出去的数据字节数.
    如果回复的是ERR_MEM,照样调用tcp_output 把数据发送出去,但此时返回给上层的已成功发送字节数为0,也就是说lwip认为内存不足可能是暂存数据过多,尝试把数据先发出去,腾出点内存池来存新数据,但反正本次发送是失败了的.//(所以有时候tcp socket 发送返回0,还真不一样是通讯出错了)
    最后做收尾处理,把netconn的状态置回 NETCONN_NONE ,放出信号量,唤醒阻塞的用户task,操作完成.
    所以可以看到 lwip_netconn_do_writemore 处理的对象是netconn,而tcp协议栈方面的逻辑,除了简单的判断发送窗口是否满足一次发完所有数据外,并没有过多涉及,接下来分析tcp_write和tcp_output的操作,看看数据pass到tcp协议栈后是如果操作的.
    tcp_write:首先计算 mss_local值,它是一半发送窗口(pcb.tcp.snd_wnd_max/2)和pcb.tcp.mss二者中的最小值(tcp.mss值在pcb创建时赋值,默认值是INITIAL_MSS = 536).如果启用了
    如果启用LWIP_NETIF_TX_SINGLE_PBUF特性,对apiflag增加TCP_WRITE_FLAG_COPY置位,这个宏会尽量把多个pbuf合并成一个才发送,适应dma传输或者那些不支持自动多包合并的网卡.

    接下来检查pcb.tcp的unsent队列是否为空.如果不为空,先拿到该链表中最后一包未发送数据(注意unsent链表是个struct tcp_seg 类型链表,里面储存的可是一个tcp包能完整发送的数据大小),(//骚操作来了),接下来检查pcb的
    unsent_oversize字段,这个字段储存的是unsent最后一包segment中最后一包pbuf中剩余的空间大小
    *

    **因为装载playload的pbuf大小不是任意的,所以经常会出现实际分配的pbuf空间比要放的playload size要大的情况,此时把pbuf实际分配size比playload size大的部分称为oversize,当下次有数据要再次写入同一个segment时,先把数据填入其最后一包pbuf中oversize的内存中,这样就把内存利用到尽了.

    如果 unsent_oversize >0 ,那么就把数据拆开,一部分放入到这个unsent->last_segment的last_pbuf里面,另一部分(如果还有哈),检查一下,这个last_segment是否还有储存空间(是否达到了local_mss最大大小),如果还有储存空间,则再把发送数据拆开,把可以填入当前segment的部分,做成一个pbuf,加入到last_segment中.

    到了这一步,可以确定一个事情,我们已经尽量把数据塞到last_segment里面了,但是如果数据还有多余,或者一开始unssent队列就是空的,那么我们接下来只能新创建tcp_segment,也就是另起一个tcp发送数据包来装载数据.
    也就是些新建pbuf,拷贝数据,新建tcp_segment,放入pbuf,并计算好 seqno的过程.
    最后把这个segment存入到unsent链表里面,同时更新pcb的snd_lbb (计算下一个seqno) , snd_buf(发送窗口缩减),以及把当前pcb所持有的pbuf数量记录一下 snd_queuelen.

    接下来就到发送了,tcp_output(struct tcp_pcb *pcb):
    首先检查的是当前pcb的状态,不能是LISTEN_PCB,也不能是 tcp_input_pcb ,也就是在tcp_input过程中(ing)不能发送.然后计算发送窗口 wnd,它是 snd_wnd 和 cwnd之间的最小值,也就是发送窗口和冲突控制窗口之间的最小值

    然后检查,如果没有seg要发送,(pcb->unsent为NULL),或者是发送数据大于发送窗口 (lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd),就直接回复一个空的ACK,完事.

    接下来,调用著名的 ip_route 函数来计算用于发送数据的网卡netif,如果找不到合适的网卡,就GG

    然后判断,如果当前是有数据要发送的,但是窗口过小( lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd &&wnd > 0),并且unacked链为NULL,这种情况就是not fit within window and no in-flight data,这时候就启动persist定时器,然后就退出即可.也就是要等待对端处理完毕,更新window,不然数据确实发不出去(这里lwip也不会帮你针对window分包,据说这样很复杂).
    设置persist_backoff后,在 tcp_slowtmr 低速timer事件中,会周期性的调用 tcp_zero_window_probe(pcb) 来发出window更新探测帧,请求对端更新window

    接下来,如果有数据要发送,并且窗口满足,就可以开始发送流程.
    首先调用 tcp_do_output_nagle ,判断是否应该终止本次发送,以避免过小的数据包发送(如之前分析,tcp_write会尽力填满segment,所以你延迟点发送,确定可以自动合并多次tcp_write的数据包)
    接下来调用 tcp_output_segment 把segment发送到网卡中去.如果发送失败,置位TF_NAGLEMEMERR(不判断失败原因,赖NAGLE??),GG
    如果发送成功,则把segment移动到unacked链表,并且会按照seqno从小到大排列unacked链表.

    所以,可以看到,tcp_wirte 和 tcp_output的操作对象都是segment,tcp_wirte把数据尽力塞满到segment,然后存入unsent队列,tcp_output则实际判断当前segment能否发送,如果发送成功了,就把segment移动到unacked队列,等待ACK回包.
    接下来就看看最后一步,tcp_output_segment是怎样吧数据发送到网卡的

    tcp_output_segment:
    首先检查的就是segment,第一个pbuf的ref值,只有==1时才能发送,
    然后填写 seg->tcphdr->ackno 确定发包里面的ack_no , 设定自身窗口值.
    接下来,根据设置的tcp flag,在playload的最前面的位置开始逐项填入实际的tcp header数据
    计算checksum
    最后调用 ip_output_if 把数据发送到网卡上.

    2. 描述完整的tcp收包过程
    tcpip_input是收包入口,这个函数直接注册到网卡的收包回调函数中,也就是网卡收到数据包后,需要主动调用
    tcpip_input通知lwip数据包到达,tcpip_input会接着调用 tcpip_inpkt(p, inp, ip_input); 来给lwip task发消息,把数据包的处理转移到lwip task中去,保证线程安全.
    所以真正的数据包处理入口函数为 ip4_input/ip6_input
    对输入的数据包,首先检查包头的ip版本号,确保输入函数能正常处理
    检查输入pbuf的size和ip头部的total_size,看看size是否正常
    如果ipheader->totalsize<20 或者 > pbuf len的都全部丢弃,长度错误
    接下来检查IP包的目标地址是否与当前网卡地址匹配,或者这是否一个组播包
    地址不匹配都应该丢弃,但是对于UDP包,如果检测到是DHCP端口的包,则允许
    忽略IP地址校验(According to RFC 1542 section 3.1.1, referred by RFC 2131)
    接下来检查是否有分包,如果这个是IP分包,则使用ip4_reass重新分配pbuf包,也就是增加
    一个本地的分包合并信息.
    接下来先通过raw_input寻找是否有raw pcb注册到这个端口了,如果有就直接把数据交给用户注册
    函数处理(用户自定义的IP层协议处理).
    如果没有,则按照IP头的protocol字段来决定上层处理的协议,并把控制转移给对应的输入函数
    //IP_PROTO_UDP / IP_PROTO_TCP / IP_PROTO_ICMP / IP_PROTO_IGMP
    tcp_input
    首先检查的也是长度和CS,然后把数据字段从网格格式转换到本地格式.
    接下来根据源/本地端口,地址4个参数来确定处理pcb
    首先检查active_pcb,如果检查到,把active_pcb中的活跃pcb移动到队列最前.
    如果active_pcb没有搜索到,则依次检查TIME-WAIT和listen列表.
    找到合适处理的pcb后,先检查其refused_data字段,看看有没上次未处理的数据,有则先处理
    接下来使用 tcp_process , 为该pcb处理tcp状态机,tcp_process仅处理PCB层面的状态.
    最后根据tcp_process的处理结果,做事件回调,通知netconn层做进一步处理.
    pcb中的回调函数是固定的(lwipv2.0.3),在创建pcb时候通过setup_tcp函数做了统一配置,
    所以也就是为了解耦而已.

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值