参考文档
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
RFC的的原图差不多是以下这个样子 (网上找的)
TCP协议格式
TCP协议的格式就不说了, 到处都是
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.) ... |
基本原理
- 发送方: (PUSH消息)
“我给你发了10封邮件了, 你收到第几封了?” - 接受方: (ACK消息)
“我收到第6封了” - 发送方:
“那我把剩下的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次握手)
步骤2和步骤3可以合并为1步, 称为3次握手
.
发送数据
一旦tcp握手成功, 每个包都必须设置ACK (RFC规定). 以下为了画图简洁, PUSH消息中的ACK略去
代码演示
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次握手关闭连接
半连接half-open
一端的连接关闭了, 但另一端由于没有收到Reset或Fin消息, 导致tcp的状态不处于Closed.
常见的情况:
- 一端直接崩溃了, 另外一端不知情. 这种情况可以心跳解决
- 某端应用层代码忘了释放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