Skip to content

Latest commit

 

History

History
269 lines (183 loc) · 10 KB

【NO.492】tcp协议栈实现,tcp定时器与滑动窗口实现.md

File metadata and controls

269 lines (183 loc) · 10 KB

【NO.492】tcp协议栈实现,tcp定时器与滑动窗口实现

要实现用户态协议栈,必须要搞懂TCP,TCP 11个状态、滑动窗口、拥塞控制、定时器等等。

要使用用户态协议栈,内核提供的epoll就不起作用了,我们需要自己实现用户态的epoll。epoll内部涉及到一个回调的时机,回调的作用是将红黑树中的节点添加进就绪队列,具体在epoll原理里面会具体讲解。搞清楚TCP的11个状态,我们就明白应该在什么时机进行回调了。

1.TCP状态转换图

在前面的[posix与网络协议栈](Build software better, together api和网络协议栈.md)中,已经介绍了tcp的状态转换。可以结合tcp状态转换图一起看。

TCP状态保存在哪里?保存在TCB中,即TCP PCB,协议控制块。里面包含了socket信息,以及sendbuffer,recvbuffer。TCB保存了从listen到time_wait的所有状态。

img

2.用户态TCP协议栈实现

前面实现了UDP协议栈,TCP协议栈实现也是类似的,但是比UDP要复杂很多。

3.TCP头定义

img

seq num初始值是多少,到达最大值(2^32 - 1)后怎么样, 会越界吗?

seq num初始值是一个随机值,之后累加。到达最大值后又从0开始计算,不会越界。

seq num指的是包的数量,还是字节数量?

计算的时候,使用的都是字节数。

TCP的包是什么意思?TCP头为什么没有包长?

TCP前后两个包都有序号,就可以计算出包的长度。

ack num = seq num + 包长。

header length是4bit,最大值是15,单位是4个字节,所以TCP头最大时15*4 = 60字节。没有option的话,TCP头是20字节,header length值就是5。

window size,能够接收数据的最大容量。

urgent pointer, 如果URG位置1,就是告诉对端从这个位置开始的数据,要马上处理。

struct tcphdr {
    unsigned short sport;
    unsigned short dport;
    unsigned int seqnum;
    unsigned int acknum;
    unsigned char hdrlen_resv;
    unsigned char flag; 
    unsigned short window;
    unsigned short checksum;
    unsigned short urgent_pointer;
    unsigned int options[0];
};

4.定义TCP flag

#define TCP_CWR_FLAG        0x80
#define TCP_ECE_FLAG        0x40
#define TCP_URG_FLAG        0x20
#define TCP_ACK_FLAG        0x10
#define TCP_PSH_FLAG        0x08
#define TCP_RST_FLAG        0x04
#define TCP_SYN_FLAG        0x02
#define TCP_FIN_FLAG        0x01

后面5个flag比较重要

ACK 是用来确认的

PSH 告诉对端赶紧通知应用程序把数据包处理了,在数据传输过程都可以设置成PSH。

RST,收到的ack num,或者seq num、widow size非法,或者数据不对了,就给对端回一个RST。三次握手发送第一次后,超时没收到对端的第二次握手,也会发送一个RST。

SYN只是在连接开始的时候,用于告诉对端seq num,也就是发送的第一个包的序号。

FIN,终止。

5.定义TCP包

struct tcppkt {
    struct ethhdr eh; // 14
    struct iphdr ip;  // 20 
    struct tcphdr tcp; // 8
    unsigned char data[0];
};

6.定义TCP状态

typedef enum _tcp_status {    TCP_STATUS_CLOSED,    TCP_STATUS_LISTEN,    TCP_STATUS_SYN_REVD,    TCP_STATUS_SYN_SENT,    TCP_STATUS_ESTABLISHED,    TCP_STATUS_FIN_WAIT_1,    TCP_STATUS_FIN_WAIT_2,    TCP_STATUS_CLOSING,    TCP_STATUS_TIME_WAIT,    TCP_STATUS_CLOSE_WAIT,    TCP_STATUS_LAST_ACK,};

7.定义TCB

typedef enum _tcp_status {
    TCP_STATUS_CLOSED,
    TCP_STATUS_LISTEN,
    TCP_STATUS_SYN_REVD,
    TCP_STATUS_SYN_SENT,
    TCP_STATUS_ESTABLISHED,
    TCP_STATUS_FIN_WAIT_1,
    TCP_STATUS_FIN_WAIT_2,
    TCP_STATUS_CLOSING,
    TCP_STATUS_TIME_WAIT,
    TCP_STATUS_CLOSE_WAIT,
    TCP_STATUS_LAST_ACK,
};

8.实现TCP三次握手

服务端处理好三次握手的状态转换,客户端就能与服务器建立连接。

int main() {
    struct nm_pkthdr h;
    struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
    if (nmr == NULL) return -1;
    struct pollfd pfd = {0};
    pfd.fd = nmr->fd;
    pfd.events = POLLIN;
    struct ntcb tcb;
    while (1) {
        int ret = poll(&pfd, 1, -1);
        if (ret < 0) continue;
        if (pfd.revents & POLLIN) {
            unsigned char *stream = nm_nextpkt(nmr, &h);
            struct ethhdr *eh = (struct ethhdr *)stream;
            if (ntohs(eh->h_proto) ==  PROTO_IP) {
                struct udppkt *tcp = (struct udppkt *)stream;
                if (tcp->ip.type == PROTO_TCP) {
                    struct tcppkt *tcp = (struct tcppkt *)stream;
                    unsigned int sip = tcp->ip.sip;
                    unsigned int dip = tcp->ip.dip;
                    unsigned short sport = tcp->tcp.sport;
                    unsigned short dport = tcp->tcp.dport;
                    tcb = search_tcb();
                    if (tcb->status == TCP_STATUS_LISTEN) { //
                        if (tcp->tcp.flag & TCP_SYN_FLAG) {
                            client_tcb = create_tcb();
                            client_tcb->status = TCP_STATUS_SYN_REVD;
                            // 将sip,sport,smac与dip,dport,dmac互换
                            // send syn, ack pkt
                            // seqnum, ack 
                        } 
                    } else if (tcb->status == TCP_STATUS_SYN_REVD) {
                        if (tcp->tcp.flag & TCP_ACK_FLAG) {
                            client_tcb->status = TCP_STATUS_ESTABLISHED;
                        }
                    }
                }
            }
        }
    }
}

9.数据发送过程

MSS(Maximum Segment Size,最大报文长度),是TCP协议定义的一个选项,MSS选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。

MTU,是对数据链路层的限制。

客户端到服务器

  1. 发送1M的文件
  2. sendbuffer = 2k
  3. mss = 512
  4. mtu = 1500
while (1) {
    poll(fd)
    send(fd, buffer, 1k, 0);
}

如果客户端sendbuff = 2k, mss = 512。要分4个包发送

客户端能否发出去这4个包呢?

不一定,取决于服务器的接收窗口window size大小。如果window size是1024。客户端如果发送两个包,每个包大小512,则如果服务器的应用程序没有取,那么回的ack包里面的window size就会是0,那么客户端的sendbuffer里面就会剩下1k数据发送不了。就会等服务器数据处理完了再发送。

如果每发送一个包,就等待ack,这种效率太慢。我们需要能够同时发送多个包,就是慢启动的过程。

10.慢启动的过程

第一次发送 1 * mss

第二次发送 2 * mss

第三次发送 4 * mss

img

慢启动的过程,发送1mss,2mss,4*mss,…

如何判断数据包超出网络负载?

通过判断超时。超时时间又怎么算呢?

拥塞避免,从客户端往服务器发送数据包,网络上的数据越来越多,造成网络拥塞,致使服务器没有办法正确接收数据。

如何判断数据包超出网络负载?

rtt, round trip time, 数据包往返一次的时间。

进入电梯这种弱网的环境下,rtt突然变大,叫做抖动。

当前rtt计算方法

rtt = 0.1 * rtt(new) + 0.9 * rtt(old), 是一个消抖的过程。

用于判断当前这一次有没有超时,一旦出现超时,判断在发送包的数量上是否需要减一减,超出网络负载。

如果服务器的window size是0,没有接收的空间了,客户端就不能再发送了。如果服务器处理完数据,有空间了,客户端怎么能知道服务器有空间了呢?

服务器端window是0,等到服务端将数据处理完,window不为0的时候,客户端怎么能知道服务器已经有接收空间了呢?

  1. 服务器主动告诉客户端。– 不好的地方,如果通知包在网络中丢失了怎么办?
  2. 客户端定时查询 – TCP是这种做法,当收到对端window为0,定时发送探测包, 就是探测定时器。

客户端定时查询更好。

11.滑动窗口

滑动窗口也是以mss作为单位的。

滑动窗口。

在接收的过程中间,准备好指针。一根指针对应已经发送确认的,另一根指针对应允许接收的最大位置。两根指针之间的长度表示window size。

回ack的表示前面的数据都已经收到,都可以调用recv进行处理;未发送ack先不用管,表示数据还没有组织好,还不能调用recv处理。

img

window大小和recvbuff的关系?

window size和recvbuff是有关系的,但是是两个概念。

看起来window size = recvbuff / 2, 这个没有找到具体的说明。

12.定时器

重传定时器、探测定时器(坚持定时器)、keepalive、TIME_WAIT定时器、延迟ack定时器

重传定时器,发送端发送一个包后,启动重传定时器,RTT超时重传,如果在规定时间内收到ack包,则撤销定时器;

探测定时器,如果对端window size 是0,则启动探测定时器;

TCP已经有keepalive,应用层为什么还要提供心跳包?

TCP keepalive也是心跳包,超时主动回收TCB,应用层感知不到。应用层心跳包可控制性更强。

TIME_WAIT定时器,time_wait时间是2msl,防止4次挥手的最后一次ack丢失。

延迟ack定时器,接收端收到TCP包,启动200ms定时器,后面再次收到数据,则重置定时器,超时后发送ack。

原文链接:https://zhuanlan.zhihu.com/p/503077683

作者:Hu先生的Linux