如何解释TCP报文的内容

描述

TCP报文格式

TCP协议有着自己的数据包格式,这里把TCP的数据包称为报文段(segment),TCP报文段封装在IP数据报中发送,TCP报文段由TCP首部和TCP数据区组成,首部区域包含了连接建立与断开、数据确认、窗口大小通告、数据发送相关的所有标志和控制信息。如下图:

数据

首部的大小为20~60字节,在没有任何选项的情况下,首部大小为20字节,与不含选项字段的IP报首部大小相同。TCP报文中的数据部分可以为空,例如在一个连接建立或断开时,双方交换的报文段仅有TCP首部;又如当一方没有任何数据需要发送,则它需要使用不包含任何数据的报文段来发送确认信息。

1、源端口号和目的端口号两个字段用于标识发送端和接收端应用进程分别绑定的端口号。这两个值加上IP数据报首部中的源IP地址和目的IP地址就能唯一确定一个TCP连接

2、32位序号字段标识了从TCP发送端到TCP接收端的数据字节编号,它的值为当前报文段中第一个数据的字节序号。在接收方,先计算出数据区数据的长度,然后使用首部中的序号字段,就能计算出报文最后一个字节数据的序号。当建立一个新连接时,握手报文首部中的SYN标志置1,此时,序号字段包含由发送方随机选择的初始序号ISN(Initial Sequence Number)。建立连接的报文(SYN)将占用一个数据编号,因此发送方随后将要发送数据的第一个字节序号为ISN+1

3、32位确认序号只有ACK标志为1时才有效,它包含了本机所期望收到的下一个数据序号,确认常常和反向数据一起捎带发送

4、4位首部长度指出了TCP首部的长度,以4字节为单位。需要这个值是因为选项字段的长度是可变的。由于这个字段有4bit,因此TCP最多有60字节的首部,如果没有任何选项字段,首部长度应该为5(20字节)

5、在TCP首部中有6个标志bit,它们中的多个可同时被设置为1,它们告诉了接收端应该如何解释报文的内容,比如一些报文段携带了确认信息、一些报文段携带了紧急数据、一些报文包含建立或关闭连接的请求。6个标志位的意义如下图:

数据

6、窗口大小字段可看作捎带的另一个例子,窗口通告可以附加在任何报文段中发送。在TCP发送一个报文时,可在窗口字段中填写相应值以通知对方自己的可用缓冲区大小(以字节为单位),报文接收方需要根据这个值来调整发送窗口的大小。这个字段是16bit的,所以通告窗口的最大值为65535字节。窗口字段是实现流量控制的关键字段,当接收方向发送方通知一个大小为0的窗口时,将完全阻止发送方的数据发送

7、16位的紧急指针只有当紧急标志位URG置位时才有效,此时报文中包含了紧急数据,紧急数据始终放在报文段数据开始的地方,而紧急指针定义出了紧急数据在数据区中的结束处,用这个值加上序号字段值就得到了最后一个紧急数据的序号

构造TCP报文段

接下来,我们使用结构体来定义TCP报文,该结构体定义在level-ip的include/tcp.h文件中:

 

struct tcphdr {    uint16_t sport;    uint16_t dport;    uint32_t seq;    uint32_t ack_seq;    uint8_t rsvd : 4;    uint8_t hl : 4;    uint8_t fin : 1,            syn : 1,            rst : 1,            psh : 1,            ack : 1,            urg : 1,            ece : 1,            cwr : 1;    uint16_t win;    uint16_t csum;    uint16_t urp;    uint8_t data[];} __attribute__((packed));

 

这个结构体的成员变量,与我们刚才介绍的TCP报文格式的每个字段是一一对应的,这里不再重复解析。

tcp报文发送接口

tcp数据的发送接口tcp_transmit_skb,会在tcp建立可靠连接和数据发送时被多次调用,如三次握手、write()、read()等。在level-ip中,该接口函数保存在src cp_output.c文件中。如下图:

 

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, uint32_t seq){    struct tcp_sock *tsk = tcp_sk(sk);    struct tcb *tcb = &tsk->tcb;    struct tcphdr *thdr = tcp_hdr(skb);
    /* No options were previously set */    if (thdr->hl == 0) thdr->hl = TCP_DOFFSET;
    skb_push(skb, thdr->hl * 4);
    thdr->sport = sk->sport;    thdr->dport = sk->dport;    thdr->seq = seq;    thdr->ack_seq = tcb->rcv_nxt;    thdr->rsvd = 0;    thdr->win = tcb->rcv_wnd;    thdr->csum = 0;    thdr->urp = 0;
    if (thdr->hl > 5) {        tcp_write_options(tsk, thdr);    }
    tcp_out_dbg(thdr, sk, skb);
    thdr->sport = htons(thdr->sport);    thdr->dport = htons(thdr->dport);    thdr->seq = htonl(thdr->seq);    thdr->ack_seq = htonl(thdr->ack_seq);    thdr->win = htons(thdr->win);    thdr->csum = htons(thdr->csum);    thdr->urp = htons(thdr->urp);    thdr->csum = tcp_v4_checksum(skb, htonl(sk->saddr), htonl(sk->daddr));        return ip_output(sk, skb);}

 

第12行:设置源端口号

第13行:设置目标端口号

第14行:自己发送的数据的起始序号

第15行:通知对方期望接收的下一字节的序号

在IP协议中,会对每个数据报进行编号,而在TCP中没有报文编号的概念,因为它的目标是数据流传输,数据流由连续的字节流组成,尽管在上层可能用各种各样的数据结构和格式来描述数据,但在TCP看来,数据都是字节流。TCP把一个连接中的所有数据字节都进行了编号,当然两个方向上的编号是彼此独立的,编号的初始值由发送数据的一方随机选取,编号取值是0~2^32-1,比如发送方选择的起始编号为20,且将要发送的数据长度为800字节,那么字节编号将覆盖20到819的范围。当所有字节被编上号后,字节流会被分装在若干个TCP报文中发送,此时TCP报文首部的字段能够记录它所运载数据的起始编号以及运载数据的长度。在接收端,可以根据这些信息对报文中数据进行排序和确认。例如,将上述的800字节放在三个报文段中来发送,前两个报文段各装载了300字节的数据,最后一个报文装载了200字节的数据,则三个报文段携带的数据情况如下:

报文段1:起始序号:20,数据长度:300,序号范围:20~319

报文段2:起始序号:320,数据长度:300,序号范围:320~619

报文段3:起始序号:620,数据长度:200,序号范围:620~819

接收方通过确认的机制来告诉发送方数据的接收状况,这是通过向发送方返回一个确认号来完成的,确认号标识出自己期望接收到的下一个字节的编号,例如在上面的例子中,如果接收方接收到了报文段1,则它返回给发送方的确认号为320,表示自己期望收到数据编号为320的数据;确认号是累计的,即如果发送方接收到确认号的值为620,说明接收方已经正确接收了620编号以前的所有数据,接收方可能是在收到两个报文段后才发送的一个确认;还有一种情况,如果接收方只收到报文段1和3,那么它返回的确认号仍然是320,确认号始终表示接收方期望的下一字节数据,尽管可能已有更高编号的数据被收到

第16行:保留位

第17行:设置滑动窗口大小

在数据发送端,所有数据流按照顺序被组织在发送缓存中,什么时候发送数据以及发送的多少是由滑动窗口决定的,使用窗口滑动的概念可以达到很好的流量控制效果和拥塞控制效果。如下图所示,滑动窗口可以看成定义在数据缓冲上的一个窗口,缓冲中存放了从应用程序传递过来的待发送数据,窗口能在缓冲上滑动,滑动窗口状态决定了TCP能发送哪些数据。滑动窗口较大时,  TCP可以在收到确认之前一次性地发送几个报文段,这样,可以使得网络总是处于忙碌状态,提高网络的吞吐率。当发送窗口较小时,能够被发送的报文很少,在极端情况下,当发送窗口为0时,没有任何报文能被发送,减少报文的发送是进行拥塞控制的最直接手段。因此,流量控制和拥塞控制的本质在于对发送窗口的合理调节

第18行:校验和设置为0

第19行:紧急指针设置为0,

TCP采用了缓冲机制来保证协议的高效性,在数据发送时,TCP内核将延迟小分组数据的交付,它将等待足够长的时间,以期待接收更多的应用数据,最后再一起发送;在接收数据时,TCP首先是先将数据放在接收缓冲中,只有在应用程序准备就绪或者TCP协议认为时机恰当的时候,数据才会被交付给应用程序。上述缓冲机制是出于对网络性能提升的考虑,但是它可能会妨碍某些应用程序的使用。例如,两个应用程序进行交互式通信,一端应用程序打算把用户键入的字符发送给另一端应用程序,并且期望对方立即响应。在这种情况下,TCP的数据收集与延迟发送、缓冲递交会给用户带来很不好的体验。为了满足交互式应用场合的需求,TCP可以采用下面的方式解决这个问题。发送方应用程序向TCP传递数据时,请求推送(push)操作,这时,TCP协议不会等待发送缓冲区被填满,而是直接将报文发送出去。同时,被推送出去的报文首部中推送位(PSH)将被置1,接收端在收到这类报文时,会尽快将数据递交给应用程序,而不必缓冲更多的数据再递交

tcp接收接口

tcp数据接收接口为tcp_in()函数。该函数在ip数据帧读取接口ip_rcv()函数中调用。该函数保存在srcip_input.c文件中,我们来了解一下这个函数,如下图:

 

void tcp_in(struct sk_buff *skb){    struct sock *sk;    struct iphdr *iph;    struct tcphdr *th;
    iph = ip_hdr(skb);    th = (struct tcphdr*) iph->data;
    tcp_init_segment(th, iph, skb);        sk = inet_lookup(skb, th->sport, th->dport);
    if (sk == NULL) {        print_err("No TCP socket for sport %d dport %d
",                  th->sport, th->dport);        free_skb(skb);        return;    }    socket_wr_acquire(sk->sock);
    tcp_in_dbg(th, sk, skb);    /* if (tcp_checksum(iph, th) != 0) { */    /*     goto discard; */    /* } */    tcp_input_state(sk, th, skb);
    socket_release(sk->sock);}

 

第7行:从sk_buff中读取ip首部信息

第8行:从ip数据包的data区域中读取tcp数据信息

第10行:进行关键字段的大小端变换和计算应答序号等

第12行:根据通信端口,查找管理该次tcp连接的结构体sock

第20行:获取该结构体的读写锁

第26行:数据递交给tcp_input_state()函数处理,该函数会进行tcp通信状态的变化,以及获取把整理好的有序报文递交给应用程序。

总结

在对可靠性要求很高的场合下,使用TCP提供的传输性能是很合适的,TCP将两个进程间传递的数据看作数据流的形式,两个先后发出的TCP报文虽然在网络中也是互不相关的传输,但是它们携带的数据之间却具有关联关系,因为TCP给传输的每个字节数据一个唯一的编号。在接收端,所有数据将按照编号被顺序组织起来,当所有数据接收成功后,TCP才把数据递交给应用层。应用层不必担心报文的乱序、重复、丢失等问题,TCP采用正面确认以及超时重传等机制保证数据流能全部正确到达。

但是,我们这篇文章还没涉及到这部分的内容,但是我们已经找到分析的突破口,那就是上面所提到的tcp_input_state()函数,以后我们将在该函数里面剖析tcp种种可靠性机制。  

      审核编辑:彭静
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分