TCP协议, 常用tcp属性, tcp模拟演示

参考文档
TCP协议 https://tools.ietf.org/html/rfc793
TCP扩展 https://tools.ietf.org/html/rfc1323
wiki资料 https://en.wikipedia.org/wiki/Transmission_Control_Protocol

使用linux的raw socket演示TCP协议 (只是玩具, 仅仅为了演示, 并不严谨)
https://github.com/wzjwhut/raw_socket_tcp

linux下的tcp参数说明
http://man7.org/linux/man-pages/man7/tcp.7.html
https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

先上总结

TCP状态切换

参考 https://tools.ietf.org/html/rfc793#section-3.2

client, 主动connect
server端, listen
收到sync, 回个ack
收到ack
received sync, ack
主动close, 发送 fin.
如果开启了Linger, 则close接口会尽量执行到TIME_WAIT再返回
被动close, 收到 fin, 回个ack
调用close, 发送 fin. 如果忘了调用, 那就惨了
收到ack
收到ACK, 但没收到 fin
收到 fin, 回个ack
等待2个MSL, linux上,MSL=/proc/sys/net/ipv4/tcp_fin_timeout
开启reuseaddr, 可以复用TIME_WAIT 资源
收到 fin, 但没收到对应的ack
终于收到 ack
收到 fin和ack
如果对方死活都不回, 则等超时,单位:秒
/proc/sys/net/ipv4/tcp_fin_timeout
初始状态CLOSED
SYN_SENT
SYN_RCVD
LISTEN
ESTABLISHED
FIN_WAIT_1
CLOSE_WAIT
LAST_ACK
CLOSED
FIN_WAIT_2
TIME_WAIT
CLOSING

RFC的的原图差不多是以下这个样子 (网上找的)
在这里插入图片描述

TCP协议格式

TCP协议的格式就不说了, 到处都是

TCP Header
Offsets Octet0 1 2 3
Octet Bit 0 1 2 3 4 5 6 7 8 910111213141516171819202122232425262728293031
0 0 Source portDestination port
4 32 Sequence number
8 64 Acknowledgment number
12 96 Data offsetReserved
0 0 0
N
S
C
W
R
E
C
E
U
R
G
A
C
K
P
S
H
R
S
T
S
Y
N
F
I
N
Window Size
16 128 ChecksumUrgent pointer (if URG set)
20
...
160
...
Options (if data offset > 5. Padded at the end with "0" bytes if necessary.)
...

基本原理

  1. 发送方: (PUSH消息)
    “我给你发了10封邮件了, 你收到第几封了?”
  2. 接受方: (ACK消息)
    “我收到第6封了”
  3. 发送方:
    “那我把剩下的4封再发一遍”

TCP为每个字节都编了一个序号, 称为sequence number, 它并不是从0计数的, 而是从一个随机值开始计数(我也不知道系统怎么决定个这值)

通过sequence number可以判断哪些字节已经收到过了, 哪些字节还没有收到.
TCP通过flag来决定数据包的作用.

TCP flag:

flag说明
SYNSynchronize, 只用于握手阶段, 双方同步sequence number
PSHPUSH, 立即发送消息. sequence number为本次数据的第1个字节的序号
ACKAcknowledgment, 下次将要接收的数据的sequence number. 一旦tcp握手成功, 每个包都必须设置ACK (RFC规定)
RSTreset, 我这边出错了, 你可以关闭连接了.
FIN正常关闭连接.

以下sequence number, 简称为SEQ

3次握手

先看4次握手. (3次握手可以拆解为4次握手)

A B Sync( SEQ =X ) 将A的SEQ, 同步给B. 取值为X, Ack = X+1 注: 第2,第3步可合并为一步: Sync(SEQ=Y, ACK=X+1) Sync (SEQ = Y) 将B的SEQ, 同步给A. 取值为Y, Ack= Y+1 应答 A下次接收到的 数据第1字节的序号为Y+1 A B

步骤2和步骤3可以合并为1步, 称为3次握手.

发送数据

一旦tcp握手成功, 每个包都必须设置ACK (RFC规定). 以下为了画图简洁, PUSH消息中的ACK略去

A B Push(SEQ=X, PayloadLen = Y) ACK = X + Y 应答 A B

代码演示

int rawtcp_send(rawtcp_t* tcp_state, const char* buffer, size_t buffer_len)
{
    printf("rawtcp_send\n");
    int ret = -1;
    size_t total_bytes_to_be_sent = buffer_len;
    tcp_flags_t flags = { 0 };
	flags.psh = 1;
    flags.ack = 1;

	while (total_bytes_to_be_sent > 0)
	{
		packet_t* packet = create_packet();
		/**如果数据很大,  拆包, 一段一段的发送 */
        packet->payload_len = total_bytes_to_be_sent > tcp_state->max_segment_size ?
                    tcp_state->max_segment_size : (uint16_t)total_bytes_to_be_sent;
		memcpy(packet->offset[DATA_OFFSET], buffer, packet->payload_len);
        build_packet_headers(tcp_state, packet, packet->payload_len, &flags);

        int trycount = 0;
        uint32_t rrt = 0; 
        ret = -1;
        do{
            /* 期望对方回复 */
            uint32_t expected_ack_seq = tcp_state->last_acked_seq_num + packet->payload_len;
            if ((ret = send_packet(tcp_state, &packet->payload,
                    ((struct iphdr*) packet->offset[IP_OFFSET])->tot_len)) < 0)
            {
                printf("Send error!! Exiting..\n");
                break;
            }
            usleep(10*1000 + rrt);
            rrt += (rrt==0)?(600*1000):rrt;
            receive_data(tcp_state);
            if(tcp_state->last_acked_seq_num == expected_ack_seq){
                printf("send segment success\n");
                ret = 0;
                break;
            }else{
                printf("not invalid seq");
            }
        }while(trycount++<5);
		destroy_packet(packet);
        if(ret == -1){
            goto EXIT;
        }
		total_bytes_to_be_sent -= packet->payload_len;
	}
EXIT:
    return ret;
}

正常关闭连接

4次/3次握手关闭连接

A B Fin(SEQ=X, ACK=Y) ACK=X+1 Fin(SEQ=Y+1, ACK=X+1) 注: 第2,第3步有时也可合并为一步: Fin(SEQ=Y+1, ACK=X+1) ACK=Y+1 A B

半连接half-open

一端的连接关闭了, 但另一端由于没有收到Reset或Fin消息, 导致tcp的状态不处于Closed.
常见的情况:

  1. 一端直接崩溃了, 另外一端不知情. 这种情况可以心跳解决
  2. 某端应用层代码忘了释放socket, 导致底层的TCP一直处于CLOSE_WAIT

retransmission

其中对方无响应, 本端会尝试重发. 例如下面的抓包, 分别隔了0.6, 1.2, 2.4, 4.8, 9.6秒重发数据
重发的次数和间隔时间是由系统参数设置. 达到最大重试次数之后, 会发送一个RST(reset)包, 中断连接.

6831	14:04:27.570328	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460
6847	14:04:28.170357	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460
6887	14:04:29.370259	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460
7000	14:04:31.770203	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460
7162	14:04:36.576104	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460
7773	14:04:46.172930	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460
8917	14:05:05.368123	192.168.3.212	115.28.94.100	TCP	1514	[TCP Retransmission] 49388 → 11234 [PSH, ACK] Seq=3945 Ack=1 Win=65700 Len=1460

windows的相关设置参数
https://support.microsoft.com/en-us/help/170359/how-to-modify-the-tcp-ip-maximum-retransmission-time-out

linux的设置

cat /proc/sys/net/ipv4/tcp_retries1
3
cat /proc/sys/net/ipv4/tcp_retries2
15

tcp会尝试3次重发, 之后会执行Dead Gateway Detection, 最多尝试15次
https://tools.ietf.org/html/rfc1122#page-90
https://tools.ietf.org/html/rfc1122#page-51

keepalive

可以发送1个空内容的PSH消息, 如果收到了ACK, 则认为active. (实际应用中, 并没有什么卵用)
单位为秒.

cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

这三个参数的意思是, 系统每7200秒进行1次keepalive探测, 如果探测未成功, 则隔75秒之后再试, 最多尝试9次.
也可以使用sysctrl命令查看参数

sysctl net.ipv4.tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_intvl
sysctl net.ipv4.tcp_keepalive_probes

由于这三个参数都是系统参数, 应用层无法修改. 导致无法实际应用 .
实际做法是, 由应用层设置SO_TIMEOUT, 或者其它定时器, 服务端使用最小堆定时器, 如果若干时间内, 没有收到应用层的数据或心跳消息, 则断开连接.

orphaned

应用层调用socket或http的close类似接口, 来释放socket资源. 但是系统的tcp并不是立刻就能释放, 因为底层还要发送fin, 接收ack. 因此, 如果应用层已经正常释放socket资源, 但是底层还在处理, 这样的socket就称为orphaned socket

REUSEADDR

TCP状态切换图可以看出, tcp从TIME_WAIT切换到Closed状态, 需要一定的时间
开启REUSEADDR后, 可以将新建的socket绑定到某个TIME_WAIT对应的地址和端口上.

Linger

影响close的行为.
如果开启了Linger, 并且值不为0, 假设为X, 那么应用层的close可能会最多卡顿X秒, 因为它会尝试优雅的关闭连接, 即等待对方的Fin和Ack消息, 一旦开启了Linger, 必然会有卡顿的现象.
通常都是禁用linger

相关的系统参数有

# 系统等待对方的Fin消息的超时时间
sysctl net.ipv4.tcp_fin_timeout
  • 9
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值