1 TCP 历史及其设计哲学
TCP/IP 的前身 ARPA:NCP 协议(没有遵循OSI分层概念),隶属美国国防部;主有两个问题1)只是两台机器进行通信,没有IP地址概念2)没有网络容错能力**。如下图所示TCP/IP发展历史,从下图也可以看到IPv4来历。**

早期(TCPv4版本之前)IP协议功能都是融合在TCP协议中的,所以没有IPv1-3;TCPv4和IPv4分层以后分,IP协议专注解决如何跨越不同网络进行传递。由于TCP协议解决任意长度消息传输,而且可以保证是可靠传输,很多基于TCP的应用层协议就不用考虑非常大的消息或者文件如何传输问题,以及丢包、可靠性等问题了。

由于TCP是源于ARPA网,所以TCP协议7个设计理念,很多为了解决ARPA网的问题:
1.尽管失去了网络或网关,互联网通信仍必须继续。也就是要容错能力
2.支持不同类型的通信设备
3.能够连接各种不同种类网络,比如可以连接WIFI、海底光缆网络
2 TCP 解决了哪些问题
2 .1 TCP作用
如下图所示,有三个网络用户所在的网络,中间是一个广域网(骨干网),企业IDC(Internet Data Center)内部网络。客户端浏览器发起一个Http请求,如何选择跨越不同网络是有IP层以及数据链路层解决的,如何构造一条消息或者相应是由应用层决定的,这个消息如何可靠发送是如何保证顺序这个都是由TCP层决定的,TCP会把不定长的HTTP请求切成不同的报文段,如何选择跨越不同网络段是由IP层以及之下层决定的。
#### 2 .2 TCP协议分层
TCP: 面向连接的、可靠的、基于字节流的传输协议。IP: 根据IP地址穿越网络传送数据。报文头部的层层组装与卸载,如下图所示。
TCP协之所以能够有这么多功能是靠TCP头部,用wireshark抓包可以看到:

抓包结果:

2 .3 TCP协议特点
TCP在IP协议之上,解决网络通讯可依赖问题,有如下特点:
- 点对点(不能广播、多播),面向连接的(1对1)
- 双向传递(全双工)
- 字节流,打包成报文段,保证有序接收(即使先收到后一个字节&前一个字节没有收到,也不能扔给应用层处理),重复报文自动丢弃。优点:不强制要求应用层必须离散的创建数据块,不限制数据块大小。缺点:不维护应用层报文的边界,比如http通过\r\n
- 流量缓冲
- 可靠的传输服务(保证可达,丢包会重发)
- 拥塞控制
3 TCP 报文格式
3.1 消息传输的核心要素
参考快递,寄件人和收件人信息
- IP 地址,标识某一台主机
- TCP(UDP)端口,标识某一台主机进程
- HTTP HOST/URI等,映射到具体模块或者服务来处理
物流单号:
- ip序列号,指示某个 ip packet
- tcp 序列号,指示某个 tcp segment
物流系统需求
TTL(time to live),IP可以经过多少跳。
3.2 TCP协议的任务
- 主机内进程寻址(基于端口)
- 创建、管理、终止连接
- 处理并将字节流打包成报文段(如IP报文段)
- 传输数据
- 保证可靠性和传输质量
- 流量控制与拥塞控制
TCP连接是,通过TCP四元组来唯一标识的(源地址、源端口、目标地址、目的端口),理论上对于IPv4 单机最大连接数为2(n32+16+32+16) 。
TCP报文头部的格式如下,常规头部是20字节+option(变长的+可选的),:

- 来源端口(两个字节,16位 )
- 目的端口(两个字节,16位 )
- 序列号(seq ,4个字节,32位)
- 序列号,用户解决网络包乱序问题,保证顺序性
- 如果含有 SYN标识,则此序列号位初始序列号。
- 确认号(ack,4个字节,32位长)期望收到的数据的开始序列号。也即已经收到的seq+1
- 数据偏移 offset(4位长)偏移量
- 用于计算整个TCP报头的长度,因为TCP options字段长度是可变的,所以需要一个方法来计算整个TCP报文头长度
- Offset(Data Offset/Header Length)数据偏移或者头部长度, 长度为4bits,字段每增加1,TCP报头的长度增加4字节,不够用01来填充,Offset最小值为5(默认20字节),最大值为15。所以TCP报文头范围为20-60字节
- 保留(3位长)-置0 目前没有使用,注意这里rfc3540中,很多教科书是基于rfc793文档这个里面会有6位,都没有错。
- 标志位(9位长)这里也同上
- NS—ECN-nonce。ECN显式拥塞通知(Explicit Congestion Notification)是对TCP的扩展,定义于RFC 3540(2003)。ECN允许拥塞控制的端对端通知而避免丢包。ECN为一项可选功能,如果底层网络设施支持,则可能被启用ECN的两个端点使用。在ECN成功协商的情况下,ECN感知路由器可以在IP头中设置一个标记来代替丢弃数据包,以标明阻塞即将发生。数据包的接收端回应发送端的表示,降低其传输速率,就如同在往常中检测到包丢失那样。
- CWR—Congestion Window Reduced,定义于RFC 3168(2001)。
- ECE—ECN-Echo有两种意思,取决于SYN标志的值,定义于RFC 3168(2001)。
- URG—为1表示高优先级数据包,紧急指针字段有效。
- ACK—为1表示确认号字段有效
- PSH—为1表示是带有PUSH标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
- RST—为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求。
- SYN—为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
- FIN—为1表示发送方没有数据要传输了,要求释放连接。
TCP-option 常见的可选项

抓取一下握手报文,可以看到类型是2,长度是4(类型占了一个字节,长度值占了一个字节,最大报文长度占了两个字节,所以一共是4个字节),最大报文长度是1460

TCP option NOP 是用于对齐的,占位的

4 使用tcpdump抓包分析
4.1基本使用方法
- -D 列举出所有网卡设备
- -i 选择网卡设备 , sudo tcpdump -i lo0
- -c 抓取多少报文, sudo tcpdump -i lo0 -c 2
- –time-stamp-precision 指定捕获时间进度,默认为为micro,可选为nano
sudo tcpdump -i lo0 -c 3 --time-stamp-precision nano - -s指定每条报文最大字节数,默认262144字节
查看有哪些网卡设备

这个lo0是环回地址,主要作用是测试本机网络配置,协议栈安装的有没有问题
指定网卡抓包 sudo tcpdump -i lo0
4.2 BPF 过滤器
BPF(Berkeley Packet Filter),在设备驱动级别提供抓包过滤接口,多数抓包工具支持该语法。
expression 表达式:由多个原语组成。
- primitives 原语:是由名称和数字,以及描述它的多个限定词组成。
- qualifiers限定词
- Type:设置数字或者名称所指示的类型,如 host www.baidu.com
- host、port
- net ,设定子网 net 192.168.0.0 mask 255.255.255.0 等价于net 192.168.0.0/24
- portrange 设置端口范围 如portrange 80-100
- Dir:设置网络出入方向 如:dst port 80
- src、dst,src or dst,src and dst 等
- Proto:指定协议类型,例如udp、ip,arp、tcp、icmp等基础协议
- 其他
- 如gateway:指明网关IP地址,等价于ether host ehost and not host host
- broadcast:广播报文,例如ether broadcast 或者 ip broadcast
- multicast:多播报文,例如:ip multicast
- less ,greater:小于或者大于
- Type:设置数字或者名称所指示的类型,如 host www.baidu.com
- qualifiers限定词
- 原语与原语的运算符
- 与:&& 或者and 都可以
- 或:|| 或者or
- 非:!或者 not
- 例如:src or dst port 80 && tcp
案例: sudo tcpdump -i en0 host www.baidu.com and port 443 ,抓取百度网站 443端口

基于协议域过滤,抓SYN 报文
sudo tcpdump -i en0 port 443 and host www.baidu.com and “tcp[13]&2=2”

4.3 文件操作
- -w 输出结果到文件,例如 sudo tcpdump -i en0 -w test
- -C 限制单个文件大小,单位是1M,超出后后缀价数字递增
- -W 指定输出文件最大数量,到达后会重新覆盖写第一个文件
- -G 指定每隔N秒就重新输出至文件,注意w参数应基于strftime参数指定文件名 例如:sudo tcpdump -i en0 -G 3 -w test%M-%S 每隔3秒输出文件
- -r 读取一个抓包文件, sudo tcpdump -r test
- -V将待读取的多个文件写入一个文件中,通过读取该文件同时读取多个文件
4.4 分析详细信息
- -e 显示数据链路层头部
- -q不显示传输层信息
- -v 显示网络层头部更多信息,如,TTL,Id等
- -n 显示ip地址、数字端口代替hostname等
- -S TCP信息以绝对序列号代替相对序列号
- -A以ASCII 方式显示报文,使用HTTP分析
- -x 以16进制方式显示报文内容,不显示数据链路层
- -xx 以16进制方式显示报文内容,显示数据链路层
- -X 以16进制方式和ASCII方式显示报文内容,不显示数据链路层
- -XX 以16进制方式和ASCII方式显示报文内容,显示数据链路层
案例: sudo tcpdump -i en0 port 80 -v
5 三次握手连接
TCP三次握手连接也是保证TCP可靠的基础。先看下三次握手的目的:
- 同步sequence序列号,初始序列号ISN(Initial Sequence Number)
- 交换TCP通讯参数,如MSS、窗口的比例因子,选择确认性,指定校验和算法。

启动本机nginx ,启动抓包:sudo tcpdump -i lo0 port 80 -c 3 -S (-S 使用绝对序列号,-c 表示只抓包前3个)。值得思考是,为什么client 初始seq序列号和Server端不一样且为啥都不直接从0开始呢,主要考虑有两点:1)避免TCP连接的相互干扰,对于一个已经终止的TCP连接,网络中仍然有相关报文。所以在上一次连接终止/退出后,如果恰好使用统一端口建立连接报文错误的认为是该次的。2)避免恶意攻击。
为什么ack=seq+1呢,这个主要原因是告诉对方seq+1前序列号已经收到,从seq+1开始继续

可以使用wireshark 看到相应的报文。

6 三次握手过程状态变迁
- CLOSED
- LISTEN (Sever 监听转态)
- SYN-SENT
- SYN-RECEIVED
- ESTABLISHED
mac 下使用 netstat -an -p tcp 查看tcp连接状态


7 三次握手性能优化与安全问题
7.1 底层实现原理
当我们理解三次握手中状态变迁以后,就可以进一步去理解客户端和服务端实现原理。如下图所示会有两个队列,SYN队列(SYN-RECEIVED),当接手到client ACK 报文时候,从SYN队列取出插入到ACCEPT队列中。可见SYN队列长度和ACCEPT队列影响着系统吞吐量。

- 应用层 connect 超时时间调整
- 操作系统内核限制调整
- 服务器端 SYN_RCV 状态
- net.ipv4.tcp_max_syn_backlog:SYN_RCVD 状态连接的最大个数
- net.ipv4.tcp_synack_retries:被动建立连接时,发SYN/ACK的重试次数
- 客户端 SYN_SENT 状态
- net.ipv4.tcp_syn_retries = 6 主动建立连接时,发 SYN 的重试次数
- net.ipv4.ip_local_port_range = 32768 60999 建立连接时的本地端口可用范围
- ACCEPT队列设置
7.2 Fast Open降低时延
名称解释:TCP引入了RTT(Round Trip Time),也就是一个数据包从发出去到回来的时间。
正常三次握手需要2RTT,TCP 提供Fast Open功能,在第一次建立连接,Server会生成一个cookie给client,client 下次在来建立连接时候可以直接带上cookie,只需要一个RTT。
linux 打开TCP fast open ,net.ipv4.tcp_fastopen:系统开启 TFO 功能
- 0:关闭
- 1:作为客户端时可以使用 TFO
- 2:作为服务器时可以使用 TFO
- 3:无论作为客户端还是服务器,都可以使用 TFO
8 实践
Server 代码
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class Server {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket=new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
System.out.println("有客户端连接来了"+socket);
String tmp="hello world";
socket.getOutputStream().write(tmp.getBytes(StandardCharsets.UTF_8));
}
}
}
Client 代码
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8080);
socket.connect(socketAddress, 1000);
byte[] buf = new byte[1024];
while (true) {
InputStream inputStream = socket.getInputStream();
int len = inputStream.read(buf);
if (len == -1) {
System.out.println("close");
break;
}
System.out.println(new String(buf, 0, len));
}
}
}
启动程序,通过wireshark抓包,可用看到三次握手过程,如下图所示,这里seq=0 是相对序列号,可用看到后面展开的详细报文。

第一次握手详细报文,如下图所示,可用看到flag位的SYN=1,TCP头的长度是44字节

发送hello world 这个报文TCP playLoad长度是11,可用通过IP报文总长度-IP头部长度-TCP头部长度=63-20-32=11

通过抓包也可以看到,client收到服务端报文后操作系统层面自动回了ack。

netstat -an -p tcp | grep 8080 可以看到

参考文献
[1]极客时间,陶辉,Web协议详解与抓包实战
[2]https://baike.baidu.com/item/TCP%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F/53100036?fr=aladdin
[3]https://www.rfc-editor.org/rfc/rfc793.txt
[4]https://www.rfc-editor.org/rfc/rfc3540.txt
745

被折叠的 条评论
为什么被折叠?



