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 Octet 0 1 2 3
Octet Bit  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 Source port Destination port
4 32 Sequence number
8 64 Acknowledgment number
12 96 Data offset Reserved
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 Checksum Urgent 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 说明
SYN Synchronize, 只用于握手阶段, 双方同步sequence number
PSH PUSH, 立即发送消息. sequence number为本次数据的第1个字节的序号
ACK Acknowledgment, 下次将要接收的数据的sequence number. 一旦tcp握手成功, 每个包都必须设置ACK (RFC规定)
RST reset, 我这边出错了, 你可以关闭连接了.
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

猜你喜欢

转载自blog.csdn.net/wzj_whut/article/details/86693870