技术

数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群及clusternet学习 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群及clusternet学习 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

再看tcp

2015年07月12日

前言

1978 年在 TCP 协议迭代了 3 个版本后,才被 Jon Postel(IANA 创始人)提出违反了网络分层原则,网络层和传输层耦合在一起很难扩展。于是在 TCP 的第 4 个迭代版本中把协议一分为二,包括网络层 IP 协议和传输层 TCP 协议(这也是今天的 IP 协议被称为 IPv4 的原因,IP层专注于解决跨网络传输消息,TCP 解决任意长度消息的可靠传输)。TCP/IP协议族按照层次由上到下,层层包装。发送协议的主机从上自下将数据按照协议封装,而接收数据的主机则按照协议从得到的数据包解开,最后拿到需要的数据。这种结构非常有栈的味道,所以某些文章也把tcp/ip协议族称为tcp/ip协议栈

TCP/IP不是一个协议,而是一个协议族的统称,里面包括了IP协议、IMCP协议、TCP协议以及我们更加熟悉的http、ftp、pop3协议等等。

程序员如何把控自己的职业把知识结构化。从一个技术关键点开始不断地关联和细化下去,比如:关于TCP协议,首先第一个要记住状态图,怎么建立连接,怎么断连接,状态怎么变迁。TCP没有连接,是靠状态维护连接的(http是无状态的,所以每一次通信 header 都要带上所有信息)。其次,要了解TCP怎么保证可靠性,就是丢包以后怎么重传,重传有哪些技术点。然后,重传会让你联想到拥塞控制,拥塞控制到滑动窗口……。这基本就是TCP的所有东西了,找到关键点,然后顺着这个脉络一点点往下想,通过知识图关联就可以进行顺藤摸瓜。

从抓包看tcp

使用wireshark 抓包工具,从上到下是

  1. 抓到的数据包
  2. 中间是 每个数据包的多层数据,从最外到最内是Frame ==> Ethernet II(可能是apple 的一个特殊协议) ==> Internet Protocol Version 4 ipv4 协议 ==> Transmission Control Protocol tcp 传输控制层协议
  3. 最底部是 数据包的 二进制数据,点击 中间每一个层 后每一层内的字段,底部会高亮对应的 数据。

一个协议由:字段 + 基于字段之上的策略 组成

比如图中的“window size”,是不是看起来很耳熟。

tcp协议字段组成

就像一致性协议一样,可以尝试从容错角度看待 tcp的各项机制。

一说到TCP和UDP,就是

  1. TCP 是面向连接的(一对一的;四元组),UDP 是面向无连接的。
  2. TCP 提供可靠交付,无差错、不丢失、不重复、并且按序到达;UDP 不提供可靠交付,不保证不丢失,不保证按顺序到达。
  3. TCP 是面向字节流的,发送时发的是一个流,缺点是没头没尾,不维护应用报文的边界(要求上层协议比如HTTP/GRPC 自己维护报文的边界);优点是不强制应用必须离散的创建数据块,不限制数据块大小。UDP是面向数据报的,一个一个的发送。
  4. TCP 是可以提供流量控制和拥塞控制的,解决速度不匹配的问题,既防止对端被压垮,也防止网络被压垮。http2 将frame 分为控制frame和数据frame,tcp 没有区分,所以tcp 协议控制字段和通信字段混在一起

这些特性=算法+数据结构,算法由端上的代码体现,数据结构由协议格式体现。

滑动窗口

客户端维护了一个 待发送的数据包的缓冲区,对于缓冲区的数据包,“发送一个数据包,等待该数据包的ack,再发送下一个” 的方式太低效了,所以客户端一次发送多个数据包(批量发送)

  1. client 不等第一个数据包收到ack就发送第二个,那么收到ack包时,如何确定这个ack包是对第一个还是第二个包的确认呢?序列号确认号
  2. 这个“多个”是多少呢? 就是滑动窗口的长度。主机 A 一直向主机 B 发送数据,不考虑主机 B 的接收能力,则可能导致主机 B 的接收缓冲区满了而无法再接收数据,从而导致大量的数据丢包,引发重传机制。而在重传的过程中,若主机 B 的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效率。所以引入了流量控制机制,主机 B 通过告诉主机 A 自己接收缓冲区的剩余大小,来使主机 A 控制发送的数据量。总结来说:所谓流量控制就是控制发送方发送速率,保证接收方来得及接收。

发送窗口

接收窗口

在此之后,基于跟接收方的沟通,可以调整滑动窗口的大小。滑动窗口在支持批量发送之外(类似于redis pipeline),又承载了 流控机制/拥塞控制的实现。

深入理解云原生下自适应限流技术原理与应用

序列号和确认号

序列号和确认号针对的是字节而不是报文。

TCP序列号和确认号详解

TCP会话的每一端都包含一个32位(bit)的序列号,该序列号被用来跟踪该端发送的数据量。每一个包中都包含确认号,接收端通过确认号用来通知发送端数据成功接收。从序列号和确认号的角度看,三次握手是这样的(握手握的就是ISN号):

  1. 客户端向服务器发送一个同步数据包SYN请求建立连接,该数据包中,初始序列号(ISN)是客户端随机产生的一个值。
  2. 服务器收到这个SYN后,会向客户端发送一个同步确认ACK(确认号是客户端的初始序列号+1 ),并发送SYN 携带服务端自己的ISN
  3. 客户端收到SYN后,再对服务器进行一个ACK。该数据包中,序列号是上一个同步请求数据包中的确认号值,确认号是服务器的ISN+1。

假设初始序列号是0(不管是客户端请求,还是服务端响应),那么序列号为当前端成功发送的数据位数,确认号为当前端成功接收的数据位数。握手过程中,尽管没有传输有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位(占1bit)。由此,我们就可以知道为什么一些linux命令可以统计流量,为什么说tcp是可靠地?序列号、确认号、checksum即可以保证交互双方正确传输了n字节的数据。序列号来保证所有传输的数据可以按照正常的顺序进行重组,从而保障数据传输的完整。 初始序列号(ISN)随时间而变化的,而且不同的操作系统也会有不同的实现方式,所以每个连接的初始序列号是不同的。TCP连接两端会在建立连接时,交互一些信息,如窗口大小、MSS等,以便为接着的数据传输做准备。

tcp的传输过程是可靠的,那为什么许多较大的下载最终还要校验文件完整性?

  1. TCP 的可靠传输就是保证在传送丢失或者是包校验和出错的时候重传,但 crc 校验只能大概判断一下,并不能保证数据 100% 正确。
  2. 传输层协议只保证传输过程的校验。假如发送方进程在部分数据还没有发送的时候,进程崩溃了,或者断点续传的时候断点计算漏了。这时候数据还没有进入到传输层,整体上也就无法保证了。
  3. 传输过程中我们的包要经过很多复杂的环境,在 HTTP 时代,中间的某个环节的运营商出于利益驱使完全是有能力修改传输的数据的(运营商劫持),当然现在 HTTPS 的广泛应用使得这种情况已经好多了。
  4. tcp 接收方传输层的 ack 确认其实只是确认的接收方的内核正确地收到了。这时候用户进程有没有收到其实不一定。假如用户进程还没来得及接收,进程崩溃了。或者读取内核中的数据时候发生了极低概率的内存翻转等错误,或者是说接收正确,但是写硬盘的时候出错了。 以上这些这些错误都是所谓可靠的 tcp 所无法照顾到的场景。

tcp连接建立与关闭

tcp为了数据通信的可靠性,增加了很多操作(比如数据通信前后,要建立和释放连接),不像udp直接把包发出去就可以。

TCP的“假”连接/状态机

从本质上来讲,所谓的建立连接,其实是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,并用这样的数据结构来保证面向连接的特性。TCP 无法左右中间的任何通路,也没有什么虚拟的连接,中间的通路根本意识不到两端使用了 TCP 还是 UDP。

流量控制和拥塞控制其实就是根据收到的对端的网络包,调整两端数据结构的状态。TCP 协议的设计理论上认为,这样调整了数据结构的状态,就能进行流量控制和拥塞控制了,其实在通路上是不是真到了,谁也管不着。所谓的可靠,也是两端的数据结构做的事情。不丢失其实是数据结构在“点名”,顺序到达其实是数据结构在“排序”,面向数据流其实是数据结构将零散的包,按照顺序捏成一个流发给应用层。总而言之,“连接”两个字让人误以为功夫在通路,其实功夫在两端

TCP使用了三种基础机制来实现面向连接的服务:

  1. 消息顺序编号:使用序列号进行标记,以便TCP接收服务在向目的应用传递数据之前修正错序的报文排序;
  2. 客户端重发
  3. 服务端顺序ACK。服务端虽然接收数据包是并发的(数据包到达的顺序性无法保证),但数据包的ack是按照编号从小到大逐一确认的。比如服务端已收到了数据包123,又收到了567,服务端会回复ack=3,等到客户端重发4567后收到了4,才回复ack=7。这样只需一个变量,便表达了哪些数据包收到哪些未收到。顺序确认在一致性协议Raft中也有应用。

  1. 对于建立连接来说,都是由客户端发起,所以client 是主动方,server 是被动方。 对于关闭连接来说, client 和 server 都可以发起(通常由客户端发起)
  2. 起初,client 和server 都处于closed状态,连接建立好后,双方都处于established状态,开始传输数据。最后连接关闭, 双方再次回到closed状态。closed/established/time_wait 因为持续时间较久,通过netstat 命令比较容易看到

为什么一定要进行三次握手呢?

tcp握手的目标:同步初始sequence序列号;交换tcp 通讯参数(比如MSS、窗口比例因子、指定校验和算法等)

三次握手和函数调用结合起来理解

  1. 客户端调用connect 的时候,把本地socket状态设置成了TCP_SYN_SENT,选了一个可用端口,接着发出SYN握手请求并启动重传定时器。它会把这个SYN包放入重发队列中,如果收到了关于这个包的确认信息,便将此数据包从队列中删除,如果在计时器超时的时候仍然没有收到确认信息,则需要重新发送该数据包。Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s。客户端和服务端有多种类型的报文段,对于特定报文段,超时重发机制会带来意想不到的后果
  2. 服务端响应 syn 请求的主要工作是 判断接收队列是否满了,满的话可能直接丢弃该请求,否则发出syn+ack,申请request_sock 添加到半连接队列中,同时启动定时器。
  3. 客户端收到服务端的syn+ack 时清除connect 时设置的重传定时器,把当前socket 状态设置为established,开启保活计时器后发出第三次握手的ack 确认。
  4. 服务端响应第三次握手ack 所做的工作是:判断全连接队列是否满了,如果满了修改一下计数器就丢弃了。如果队列不满,把当前半连接对象删除,创建了新sock 后加入全连接队列(所以加入之前要判断是否满),最后将新连接状态设置为 established。
  5. 服务端accept 的工作:从已经建立好的全连接队列中取出 一个返回给用户进程。

《软件架构设计》:无论两次、三次、四次,永远都不知道最后发出去的数据包对方是否收到了,问题无解。那为什么是三次呢?因为三次握手恰好可以保证client 和server 对自己的发送、接收能力做了一次确认

  1. client 发送seq=x,收到了回复的seq=y,ack=x+1 则客户端知道自己的发送、接收没问题
  2. 服务端发送 seq=y,收到了第三次的ack = y+1,可以确认自己的发送、接收也没问题

服务端连接队列满主动丢包

  1. 第一次握手丢包(服务端因半/全连接队列满,丢弃来自客户端的syn包),从客户端视角看跟网络断了没有区别,就是发出去的syn包没有任何反馈,触发客户端重试,连接耗时增加。
  2. 第三次握手丢包(服务端因全连接队列满,丢弃来自客户端的ack包),服务端等到半连接定时器到时后,向客户端重启发起synack,客户端收到后再重新回复第三次握手ack。如果这期间服务端全连接队列一直是满的,那么服务端重试5次就放弃了。所以 尽快在握手成功后通过accept 把新连接取走,不要忙于处理业务逻辑而导致全连接队列满了。

网络“失而复得”

前两次的握手很显然是必须的,主要是最后一次,即客户端收到服务端发来的确认后为什么还要向服务端再发送一次确认呢?这主要是为了防止已失效的请求报文段突然又传送到了服务端而产生连接的误判。考虑如下的情况:客户端发送了一个连接请求报文段/SYN到服务端,但是在某些网络节点上长时间滞留了(可以滞留一个MSL=1分钟),而后客户端又超时重发了一个连接请求报文段该服务端(connect超时配置一般不超过10s),而后正常建立连接,数据传输完毕,并释放了连接。如果这时候第一次发送的请求报文段延迟了一段时间后,又到了服务端,很显然,这本是一个早已失效的报文段。

处理失效SYN 三次握手 两次握手
失效SYN到达服务端 服务端返回ACK 服务端返回ACK,连接建立
服务端ACK到达客户端 客户端不理会 客户端不理会
服务端 超过时间未收到ACK认为连接未建立 等待客户端发送数据,直到超出保活计数器的设定值
而将客户端判定为出了问题,才会关闭这个连接

上述偏表象,更实质的原因是:

  1. TCP 需要 seq 序列号来做可靠重传或接收,而避免连接复用时无法分辨出 seq 是延迟或者是旧链接的 seq,因此需要三次握手来约定确定双方的 ISN(初始 seq 序列号)。
  2. TCP 协议是不限制一个特定的连接(两端 socket 一样)被重复使用的。所以这样就有一个问题:这条连接突然断开重连后,TCP 怎么样识别之前旧连接重发的包?——这就需要独一无二的 ISN(初始序列号)机制。当一个新连接建立时,初始序列号( initial sequence number ISN)生成器会生成一个新的32位的 ISN。这个生成器会用一个32位长的时钟,差不多4µs 增长一次,因此 ISN 会在大约 4.55 小时循环一次。而一个段在网络中并不会比最大分段寿命(Maximum Segment Lifetime (MSL) ,默认使用2分钟)长,MSL 比4.55小时要短,所以我们可以认为 ISN 会是唯一的。发送方与接收方都会有自己的 ISN 来做双方互发通信。
  3. 三次握手(A three way handshake)是必须的, 因为 sequence numbers(序列号)没有绑定到整个网络的全局时钟(全部统一使用一个时钟,就可以确定这个包是不是延迟到的)以及 TCPs 可能有不同的机制来选择 ISN(初始序列号)。接收方接收到第一个 SYN 时,没有办法知道这个 SYN 是否延迟了很久了,除非他有办法记住在这条连接中,最后接收到的那个sequence numbers(然而这不总是可行的)。所以,接收方一定需要跟发送方确认 SYN。PS:防止虚假连接有很多办法,三次握手也可以说是防止服务端资源泄漏(总不能客户端SYN一下,服务端就创建一个socket放到全连接队列),可以认为针对某个具体的问题都有解决办法,综合起来三次握手比较好,就用了三次握手这个方法。

四次分手

关闭连接的操作其实是告诉通信的另一方自己没有需要发送的数据,但是它仍然保持了接收对方数据的能力。 所以拉手3次即可(服务端SYN伴随ACK 一起发了),分手需要4次(服务端ACK 与FIN分开发送)。

为什么 TCP 协议有 TIME_WAIT 状态TIME_WAIT 状态是 TCP 与不确定的网络延迟斗争的结果,而不确定性是 TCP 协议在保证可靠这条路的最大阻碍。

MSL及 配合措施

  1. 进入 TIME_WAIT 的客户端/主动关闭方需要等待 2 MSL 才可以真正关闭连接。
  2. 任何一个IP数据包在网络上逗留的最长时间是MSL,默认120s(Linux 60s),超过这个时间,中间的路由节点会将数据包丢弃

关闭时为什么要TIME_WAIT 2MSL?换个表述,主动方为什么要等待2MSL 不能直接进入 CLOSED 状态?TIME_WAIT 仅在主动断开连接的一方出现(另一方是CLOSE_WAIT),被动断开连接的一方发完FIN后会直接进入 CLOSED 状态。PS:连接是由状态体现的,是两个人的事儿,特定的数据包和计时器都可以更改通信两端的状态

  1. 等待足够长的时间以确定被动关闭连接的一方收到 FIN 对应的 ACK 消息。以客户端主动关闭为例,只要客户端等待 2 MSL 的时间,有两种可能
    1. 服务端正常收到了 ACK 消息并关闭当前 TCP 连接;
    2. 服务端自发送FIN后等了2ML没有收到 ACK 消息,会重新发送 FIN 关闭连接并等待新的 ACK 消息,客户端自发送ACK 2MSL 内会收到 服务端重发的FIN,继而回复服务端ACK。==> 客户端2MSL 内又收到了FIN 即表示自己发送的ACK 未送达,没收到即表示ACK送达了
  2. 如果客户端等待的时间不够长,当服务端还没有收到 ACK 消息时,客户端就重新与服务端建立 TCP 连接就会造成以下问题 — 服务端因为没有收到 ACK 消息,所以仍然认为当前连接是合法的,客户端重新发送 SYN 消息请求握手时会收到服务端的 RST 消息,连接建立的过程就会被终止。假如不回复RST 拒绝 连接,可能连接关闭之后客户端再重开一个新的连接,新老连接的四元组<源地址,源端口,目的地址,目的端口>是一样的。老连接关闭后,仍可能有数据包在网络上“闲逛”,但是序列号是老的,这个过期的消息却可能被服务端正常接收,这就会带来比较严重的问题。

等待2*MSL造成一个问题:在2MSL时间内,该地址上的连接(客户端地址,端口和服务器的端口地址)不能被使用。在 Linux 上,客户端的可以使用端口号 32,768 ~ 61,000,总共 28,232 个端口号与远程服务器建立连接,但是如果主机在MSL时间内创建的 TCP 连接数超过 28,232,那么再创建新的 TCP 连接就会发生错误。这也是为什么client要建连接池的原因之一。

一个有关tcp的非常有意思的问题

backlog

tcp 连接机制 的缺陷 常见Dos攻击原理及防护(死亡之Ping、Smurf、Teardown、LandAttack、SYN Flood) 故意让服务端 维持一堆半连接,直到超过 backlog

再聊 TCP backlogbacklog 参数跟 listen 函数有关,listen 函数的定义如下:int listen(int sockfd, int backlog);listen 最主要的工作就是申请和初始化接收队列,包括全连接队列和半连接队列。其中全连接队列是一个链表,而半连接队列由于需要快速的查找,所以使用的是一个哈希表。全/半两个队列是三次握手中很重要的两个数据结构,有了它们服务器才能正常响应来自客户端的三次握手。所以服务器端都需要 listen 一下才行。为什么服务端程序都需要先 listen 一下?

To understand the backlog argument, we must realize that for a given listening socket, the kernel maintains two queues :

  1. An incomplete connection queue, which contains an entry for each SYN that has arrived from a client for which the server is awaiting completion of the TCP three-way handshake. These sockets are in the SYN_RCVD state .
  2. A completed connection queue, which contains an entry for each client with whom the TCP three-way handshake has completed. These sockets are in the ESTABLISHED state. 「全连接队列」包含了服务端所有完成了三次握手,但是还未被应用调用 accept 取走的连接队列。此时的 socket 处于 ESTABLISHED 状态。每次应用调用 accept() 函数会移除队列头的连接。如果队列为空,accept() 通常会阻塞。全连接队列也被称为 Accept 队列。

你可以把这个过程想象生产者、消费者模型。内核是一个负责三次握手的生产者,握手完的连接会放入一个队列。我们的应用程序是一个消费者,取走队列中的连接进行下一步的处理。这种生产者消费者的模式,在生产过快、消费过慢的情况下就会出现队列积压。

如果某服务挂了,那么内核会帮忙收尾,根据情况或走 RST 或走 FIN,访问者就知道连接关了。但如果主机挂了,或者中间网络设备挂了,客户端没有超时配置,就只能 tcp keepalive 来判断死链接,按照默认内核配置语言两个多小时。

为什么可靠

协议就是一系列约定,屏蔽底层各种错乱,得到一个完整有序的数据包序列:

如何看待谷歌 Google 打算用 QUIC 协议替代 TCP/UDP?

TCP 发展与上下游

TCP 与操作系统的关系

tcp/ip 只是一系列的协议,tcp真正的实现靠的是操作系统,进而抽象到语言层 有一个socket api作为入口,进行字节流交互。除linux 实现外,lwIP 是由瑞典计算机科学研究院(SICS)的 Adam Dunkels 开发的小型开源 TCP/IP 协议栈,它是一个用 C 语言实现的软件组件。

//golang
(c *conn) Read(b []byte) (int, error)   
// c
int recv(int sockfd, void *buf, int len, int flags);
// java
InputStream in = socket.getInputStream();
int InputStream.read(byte b[], int off, int len);

编程接口

从基于 IP 协议的网络视角来看,数据并不是源源不断的流(stream),而是一个个大小有明确限制的 IP 数据包。

package net
type IPAddr struct {
    IP   IP
    Zone string // IPv6 scoped addressing zone
}
func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
func ListenIP(network string, laddr *IPAddr) (*IPConn, error)

func (c *IPConn) Read(b []byte) (int, error)
func (c *IPConn) ReadFrom(b []byte) (int, Addr, error)
func (c *IPConn) ReadFromIP(b []byte) (int, *IPAddr, error)
func (c *IPConn) Write(b []byte) (int, error)
func (c *IPConn) WriteTo(b []byte, addr Addr) (int, error)
func (c *IPConn) WriteToIP(b []byte, addr *IPAddr) (int, error)
func (c *IPConn) Close() error

为什么需要有多套传输层的协议(TCP 和 UDP)呢?还是因为应用需求是多样的。底层的 IP 协议不保证数据是否到达目标,也不保证数据到达的次序。出于编程便捷性的考虑,TCP 协议就产生了。但是 TCP 协议对传输协议的可靠性保证,对某些应用场景来说并不是一个好特性。最典型的就是音视频的传输。在网络比较差的情况下,我们往往希望丢掉一些帧,但是由于 TCP 重传机制的存在,可能会反而加剧了网络拥塞的情况。这种情况下,UDP 协议就比较理想,它在 IP 协议基础上的额外开销非常小,基本上可以认为除了引入端口(port)外并没有额外做什么,非常适合音视频的传输需求。

package net
type TCPAddr struct {
    IP   IP
    Port int
    Zone string // IPv6 scoped addressing zone
}
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)

func (c *TCPConn) Read(b []byte) (int, error)
func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Close() error

func (l *TCPListener) Accept() (Conn, error)
func (l *TCPListener) AcceptTCP() (*TCPConn, error)
func (l *TCPListener) Close() error

引用

TCP/IP 的七个设计理念

  1. internet communication must continue despite loss of networks or gateways. 要能容错。
  2. the network must support multiple types of communications service. 支持不同类型的通讯设备。
  3. the internet architecture must accommodate a variety of networks. 支持连接不同种类的网络比如wifi、光纤等
  4. the internet architecture must permit distributed management of its resources.
  5. the internet architecture must be cost effective.
  6. the internet architecture must permit host attachment with a low level of effort.
  7. the resources used in the internet architecture must be accountable.

那些你不知道的TCP冷门知识!

  1. TCP中并不是所有的RST都有效
  2. Linux内核究竟有多少TCP端口可用。其中 ip_local_port_range 范围内的可以被系统随机分配,其他需要指定绑定使用,同一个端口只要TCP连接四元组不完全相同可以无限复用。

一个同事深挖的客户端端口占满的问题

TCP面向连接中的“连接”究竟是什么,可靠与不可靠

TCP连接的建立和释放