TCP接收方对于重叠报文的处理

摘要:
2、 为什么这个规则一开始看起来很奇怪?例如,如果我收到一条序列号为[30,50]的消息,然后收到序列号为[40,60]的消息。那么这两条消息都是合法的,这意味着接收方需要同时接受和处理两条消息。这似乎很奇怪。这是在什么情况下发生的?
一、接受方有效负载的判断
在rfc793中说明了对于判断接收到的报文是否有负载的判断在Page 24和Page 25之间,其中的原文说明为
 A segment is judged to occupy a portion of valid receive sequence
  space if
 
    RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
 
  or
 
    RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
 
  The first part of this test checks to see if the beginning of the
  segment falls in the window, the second part of the test checks to see
  if the end of the segment falls in the window; if the segment passes
  either part of the test it contains data in the window.
通俗的说,就是说接收到的报文和接受方的滑动窗口有相交就可以,在内核中,这个检测通过函数
static inline int tcp_sequence(struct tcp_sock *tp, u32 seq, u32 end_seq)
{
return !before(end_seq, tp->rcv_wup) &&
!after(seq, tp->rcv_nxt + tcp_receive_window(tp));
}
完成,这个函数看起来有些复杂,事实上它的确很复杂,但是现在我们只关心其中最为基本的判断,就是判断这个报文是否包含了接受方“可接受的有效负载”这个方面的判断。由于这个函数看起来比较复杂,我们随便从内核中搜索下判断两个区间是否有重叠的函数,模糊搜索overlap,可以看到最为简洁的一个函数linux-2.6.21 et etfilter f_sockopt.c:
static inline int overlap(int min1, int max1, int min2, int max2)
{
return max1 > min2 && min1 < max2;
}
本质上判断两个区间是否相交的实现就是这么判断的,就是每个区间的最大值大于另一个区间的最小值。对于这里的TCP环境,我们就不考虑边界值的情况了,这个边界值有很多细节和讲究,所以这里就暂时不讨论了。
二、为什么会这么规定
这个地方乍一看是比较奇怪的,比方说,如果我先收到了一个序列号为[30,50]的报文,然后再收到一个[40,60]的报文,这个时候两个报文都是合法的,也意味着接受方需要同时接受并处理这两个报文,这个看起来比较奇怪,在什么情况下会出现这种情况呢?最简单的办法就是在内核中修改报文的序列号,强制发送两个这样序列号的报文,这也是我最早验证这个问题使用的方法,但是这么暴力的修改并没有实际意义,既然rfc这么规定,应该是在真实环境中可能出现的才这么规定,如果不能出现,就应该直接禁止掉这种包的接受和处理。所以下面是借助iptables来模拟下真实环境中可能出现的报文重叠现象。
 
tsecer@harry: cat rxcollapse.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <arpa/inet.h> //inet_pton
#include <errno.h> //errno
#include <unistd.h> //sleep
#include <stdlib.h> //exit
#include <netinet/tcp.h>//TCP_NODELAY
 
#define QUIT(err, fmt, msg...) do {fprintf(stderr, "LINE:%d, err %d "fmt" ", __LINE__, err, ##msg); exit(err); }while(0)
int main(int argc, char *argv[])
{
        in_addr_t stloopaddr;
        inet_pton(AF_INET, "127.0.0.1", (void*)&stloopaddr);
        sockaddr_in stlocal = 
                {AF_INET, htons(7777), {INADDR_ANY}};
        sockaddr_in stremote = 
                {AF_INET, htons(22), {stloopaddr}};
        int sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sfd == -1)
        {
                QUIT(errno, "socket");
        }
        if (bind(sfd, (struct sockaddr *)&stlocal, sizeof(stlocal)) == -1)
        {
               QUIT(errno, "bind");
        }
        if (connect(sfd, (struct sockaddr*)&stremote, sizeof(stremote)))
        {
             QUIT(errno, "connect");
        }
 
int iYes=1;
if (setsockopt(sfd, SOL_TCP, TCP_NODELAY, (void*)&iYes, sizeof(iYes)))
{
QUIT(errno, "setsockopt");
}
sleep(3);
char buff1[]= "aaaaaa";
char buff2[]= "bbbbbb";
printf("%d ", write(sfd, (void*)buff1, sizeof(buff1) - 1));
printf("%d ",write(sfd, (void*)buff2, sizeof(buff2) - 1));
        sleep(10000);
}
 
tsecer@harry: g++ rxcollapse.cpp -o rxcollapse
tsecer@harry: cat rxcollapse.sh 
./rxcollapse &
 
sleep 2
 
iptables -I INPUT -p tcp --tcp-flags ACK ACK -j DROP
netstat -anp | grep -w "22|7777"
sleep 20
 
iptables -D INPUT -p tcp --tcp-flags ACK ACK -j DROP
 
pkill  rxcollapse
tsecer@harry: sh -x rxcollapse.sh 
+ sleep 2
+ ./rxcollapse
+ iptables -I INPUT -p tcp --tcp-flags ACK ACK -j DROP
+ grep -w '22|7777'
+ netstat -anp
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      721/sshd            
tcp        0      0 127.0.0.1:22            127.0.0.1:7777          ESTABLISHED 3630/sshd: [accepte 
tcp       21      0 127.0.0.1:7777          127.0.0.1:22            ESTABLISHED 3628/./rxcollapse   
tcp6       0      0 :::22                   :::*                    LISTEN      721/sshd            
+ sleep 20
6
6
+ iptables -D INPUT -p tcp --tcp-flags ACK ACK -j DROP
+ pkill rxcollapse
rxcollapse.sh: line 11:  3628 Terminated              ./rxcollapse
 
下面是在上面操作的同时通过tcpdump抓包获得的输出内容:
tsecer@harry: 
tsecer@harry: tcpdump -nnvvSi lo -As0  tcp and port 7777
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
07:54:28.302043 IP (tos 0x0, ttl 64, id 47766, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [S], cksum 0xfe30 (incorrect -> 0x3e3f), seq 183558512, win 43690, options [mss 65495,sackOK,TS val 3300326 ecr 0,nop,wscale 7], length 0
E..<..@.@..#.........a..
..p.........0.........
.2[.........
07:54:28.302148 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.22 > 127.0.0.1.7777: Flags [S.], cksum 0xfe30 (incorrect -> 0xb691), seq 2846654934, ack 183558513, win 43690, options [mss 65495,sackOK,TS val 3300327 ecr 3300326,nop,wscale 7], length 0
E..<..@.@.<............a....
..q.....0.........
.2[..2[.....
07:54:28.302240 IP (tos 0x0, ttl 64, id 47767, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [.], cksum 0xfe28 (incorrect -> 0x88d5), seq 183558513, ack 2846654935, win 342, options [nop,nop,TS val 3300327 ecr 3300327], length 0
E..4..@.@..*.........a..
..q.......V.(.....
.2[..2[.
07:54:28.402057 IP (tos 0x0, ttl 64, id 55856, offset 0, flags [DF], proto TCP (6), length 73)
    127.0.0.1.22 > 127.0.0.1.7777: Flags [P.], cksum 0xfe3d (incorrect -> 0xc6ac), seq 2846654935:2846654956, ack 183558513, win 342, options [nop,nop,TS val 3300426 ecr 3300327], length 21
E..I.0@.@.b|...........a....
..q...V.=.....
.2J.2[.SSH-2.0-OpenSSH_6.3
 
07:54:28.402093 IP (tos 0x0, ttl 64, id 47768, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [.], cksum 0xfe28 (incorrect -> 0x87fa), seq 183558513, ack 2846654956, win 342, options [nop,nop,TS val 3300426 ecr 3300426], length 0
E..4..@.@..).........a..
..q.......V.(.....
.2J.2J
07:54:31.308578 IP (tos 0x0, ttl 64, id 47769, offset 0, flags [DF], proto TCP (6), length 58)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe2e (incorrect -> 0x586d), seq 183558513:183558519, ack 2846654956, win 342, options [nop,nop,TS val 3303333 ecr 3300426], length 6
E..:..@.@..".........a..
..q.......V.......
.2g..2Jaaaaaa
07:54:31.308775 IP (tos 0x0, ttl 64, id 47770, offset 0, flags [DF], proto TCP (6), length 58)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe2e (incorrect -> 0x5564), seq 183558519:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303333 ecr 3300426], length 6
E..:..@.@..!.........a..
..w.......V.......
.2g..2Jbbbbbb
07:54:31.318288 IP (tos 0x0, ttl 64, id 47771, offset 0, flags [DF], proto TCP (6), length 58)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe2e (incorrect -> 0x555a), seq 183558519:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303343 ecr 3300426], length 6
E..:..@.@.. .........a..
..w.......V.......
.2g..2Jbbbbbb
07:54:31.521290 IP (tos 0x0, ttl 64, id 47772, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x306b), seq183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303546 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2hz.2Jaaaaaabbbbbb
07:54:31.928855 IP (tos 0x0, ttl 64, id 47773, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x2ed4), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3303953 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2j..2Jaaaaaabbbbbb
07:54:32.743433 IP (tos 0x0, ttl 64, id 47774, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x2ba5), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3304768 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2m@.2Jaaaaaabbbbbb
07:54:34.372490 IP (tos 0x0, ttl 64, id 47775, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x2548), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3306397 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2s..2Jaaaaaabbbbbb
07:54:37.623209 IP (tos 0x0, ttl 64, id 47776, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0x1895), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3309648 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2.P.2Jaaaaaabbbbbb
07:54:44.135414 IP (tos 0x0, ttl 64, id 47777, offset 0, flags [DF], proto TCP (6), length 64)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [P.], cksum 0xfe34 (incorrect -> 0xff24), seq 183558513:183558525, ack 2846654956, win 342, options [nop,nop,TS val 3316160 ecr 3300426], length 12
E..@..@.@............a..
..q.......V.4.....
.2...2Jaaaaaabbbbbb
07:54:50.739325 IP (tos 0x0, ttl 64, id 47778, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [R.], cksum 0xfe28 (incorrect -> 0x30a8), seq 183558525, ack 2846654956, win 342, options [nop,nop,TS val 3322764 ecr 3300426], length 0
E..4..@.@............a..
..}.......V.(.....
.2...2J
07:54:50.739462 IP (tos 0x0, ttl 64, id 55857, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.22 > 127.0.0.1.7777: Flags [.], cksum 0xfe28 (incorrect -> 0x30b8), seq 2846654956, ack 183558513, win 342, options [nop,nop,TS val 3322764 ecr 3300426], length 0
E..4.1@.@.b............a....
..q...V.(.....
.2...2J
07:54:50.740026 IP (tos 0x0, ttl 64, id 30712, offset 0, flags [DF], proto TCP (6), length 40)
    127.0.0.1.7777 > 127.0.0.1.22: Flags [R], cksum 0xa705 (correct), seq 183558513, win 0, length 0
E..(w.@.@............a..
..q....P.......
 
三、模拟的方法的说明
这里模拟的方法就是借助“丢包”来促使发送方在重传的过程中将两个相邻的重传包合并。这里为了简单,没有写自己的服务器端程序,看了下系统打开的侦听端口,发现22端口被sshd打开侦听,所以客户端直接绑定到7777来链接sshd的22端口,当然这个端口并不需要有什么特殊之处,所以不用关注。
在连接的时候,通过TCP_NODELAY禁止nagle算法,也就是要求两个小包"aaaaa"和"bbbbbb"在首次发送的时候是两个独立的包,不用等待第一个包的返回就可以发送第二个小包。
在connect连接到server之后,进程通过sleep暂停一段时间,这个时间用来等待iptable禁掉server的ack包,从而让客户端认为自己发送的两个小包都丢失了,进而进行重传。
两次write之间不要有(太长)间隔,如果第二个包的write调用发生在第一个包的重传之后,此时tcp会进入丢包状态,此时拥塞窗口调整为1,从而第二个包无法通过拥塞检测而无法发送。
 
丢包进入拥塞控制的代码
static void tcp_retransmit_timer(struct sock *sk)--->>>>tcp_enter_loss
tp->snd_cwnd    = 1;
tp->snd_cwnd_cnt   = 0;
tp->snd_cwnd_stamp = tcp_time_stamp;
在发送时的检测
tcp_write_xmit--->>>tcp_cwnd_test
cwnd = tp->snd_cwnd;
if (in_flight < cwnd)
return (cwnd - in_flight);
四、重传报文的合并
tcp_retransmit_skb--->>>tcp_retrans_try_collapse
/* Attempt to collapse two adjacent SKB's during retransmission. */
static void tcp_retrans_try_collapse(struct sock *sk, struct sk_buff *skb, int mss_now)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *next_skb = skb->next;
 
/* The first test we must make is that neither of these two
 * SKB's are still referenced by someone else.
 */
if (!skb_cloned(skb) && !skb_cloned(next_skb)) {
……
/* Update sequence range on original skb. */
TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(next_skb)->end_seq;
 
/* Merge over control information. */
flags |= TCP_SKB_CB(next_skb)->flags; /* This moves PSH/FIN etc. over */
TCP_SKB_CB(skb)->flags = flags;
                ……
}
五 、接受方的处理
从接受方来看,通过了tcp_sequence之后就是通过tcp_data_queue进入队列
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
……
if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* Partial packet, seq < rcv_next < end_seq */
SOCK_DEBUG(sk, "partial packet: rcv_next %X seq %X - %X ",
   tp->rcv_nxt, TCP_SKB_CB(skb)->seq,
   TCP_SKB_CB(skb)->end_seq);
 
tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);
 
/* If window is closed, drop tail of packet. But after
 * remembering D-SACK for its head made in previous line.
 */
if (!tcp_receive_window(tp))
goto out_of_window;
goto queue_and_out;
}                          
……
}
可以看到接收的时候也并没有特别歧视,只是在设置了SO_DEBUG选项的时候打印一个内核日志,然后就照样放到接受队列了。但是这么做总也不是个办法,岂不是接受方会收到莫名其妙的相同的一份拷贝?
六、API层的处理
这个不是常规的tcp socket read的执行流程,但是看起来更简洁,所以就以这个为例进行说明
int tcp_read_sock(struct sock *sk, read_descriptor_t *desc,sk_read_actor_t recv_actor) --->>>
static inline struct sk_buff *tcp_recv_skb(struct sock *sk, u32 seq, u32 *off)
{
struct sk_buff *skb;
u32 offset;
 
skb_queue_walk(&sk->sk_receive_queue, skb) {
offset = seq - TCP_SKB_CB(skb)->seq;
if (skb->h.th->syn)
offset--;
if (offset < skb->len || skb->h.th->fin) {
*off = offset;
return skb;
}
}
return NULL;
}
以刚才的例子[30,50],[40,60]为例,当从30开始读取20个字节之后,在调用tcp_recv_skb时,seq为50,此时返回的off就是第二个报文的50-40=10这个未知开始,或者更直观的说,返回的*off满足 offset = seq - TCP_SKB_CB(skb)->seq;,也就是TCP_SKB_CB(skb)->seq + offset = seq,只截取了前一个报文结束的地方,重叠的地方以起始未知更低的报文为准。

免责声明:文章转载自《TCP接收方对于重叠报文的处理》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇订单系统中并发问题和锁机制的探讨ASP操作Excel技术总结下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

涨姿势了解一下Kafka消费位移可好?

摘要:Kafka中的位移是个极其重要的概念,因为数据一致性、准确性是一个很重要的语义,我们都不希望消息重复消费或者丢失。而位移就是控制消费进度的大佬。本文就详细聊聊kafka消费位移的那些事,包括: 概念剖析 kafka的两种位移 关于位移(Offset),其实在kafka的世界里有两种位移: 分区位移:生产者向分区写入消息,每条消息在分区中的位置信息...

从未如此简单:10分钟带你逆袭Kafka!【转】

【51CTO.com原创稿件】Apache Kafka 是一个快速、可扩展的、高吞吐的、可容错的分布式“发布-订阅”消息系统, 使用 Scala 与 Java 语言编写,能够将消息从一个端点传递到另一个端点。 较之传统的消息中间件(例如 ActiveMQ、RabbitMQ),Kafka 具有高吞吐量、内置分区、支持消息副本和高容错的特性,非常适合大规模消息...

CentOS 7 优化TCP链接

Summary 在优化服务器配置的时候,发现服务器端有大量的TIME_WAIT的连接,这需要优化。 Tomcat 案例 查询tomcat对应端口的tcp链接,发现存在大量TIME_WAIT的链接,还有部分其它状态的连接,总计400+。 TCP连接数以及各个状态的数量 netstat -nat | grep 8980 | wc -l netstat -a...

P2P通信标准协议(一)之STUN

前一段时间在P2P通信原理与实现中介绍了P2P打洞的基本原理和方法,我们可以根据其原理为自己的网络程序设计一套通信规则, 当然如果这套程序只有自己在使用是没什么问题的。可是在现实生活中,我们的程序往往还需要和第三方的协议(如SDP,SIP)进行对接,因此使用标准化 的通用规则来进行P2P链接建立是很有必要的。本文就来介绍一下当前主要应用于P2P通信的几个标...

pythonTCP UDP IPv4 IPv6 客户端和服务端的实现

由于目前工作的需要,需要在IPv4和IPv6两种网络模式下TCP和UDP的连接,要做到客户端发包,服务端收包。 前几天写了代码,但是把UDP的客户端和服务端使用TCP模式的代码了。今天在公司使用该工具的时候,发现了问题,忘记了UDP不需要验证。疏忽,疏忽。不过刚刚接触编程,可以原谅。 现在在家,已经把代码改好了。经测试可以使用。 先运行客户端: pytho...

kafka 消息队列

kafka是使用Java和Scala编写的一个快速可扩展的高吞吐量的分布式消息队列系统。 kafka将数据持久化存储到磁盘上,自带分区和副本机制,因而具有较好的持久化保证。 但是kafka的消息消费没有确认机制,可能因为consumer崩溃导致消息没有完成处理。因此不建议将kafka用于一致性较高的业务场景,kafka经常被用做日志收集和数据仓库之间的缓存...