技术

如何使用RedisTemplate访问Redis数据结构 MySQL重要知识点 OAuth2认证授授权流程 分布式锁 服务调用 MQ的介绍 SpringCloud 使用链 Eureka 的点对点通信 介绍Eureka RabbitMQ与其它MQ的对比 Springboot 启动过程分析 Springboot 入门 Linux内存管理 自定义CNI IPAM 扩展Kubernetes 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 Kubernetes安全机制 jvm crash分析 Prometheus 学习 Kubernetes监控 Kubernetes 控制器模型 容器日志采集 容器狂占cpu怎么办? 容器狂打日志怎么办? Kubernetes资源调度-scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects之编排对象 源码分析体会 自动化mock AIOps说的啥 从DevOps中挖掘docker的价值 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes整体结构 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 kubernetes实践 线程排队 jib源码分析之细节 从一个签名框架看待机制和策略 跨主机容器通信 jib源码分析及应用 docker环境下的持续构建 docker环境下的持续发布 一个容器多个进程 kubernetes yaml配置 marathon-client 源码分析 《持续交付36讲》笔记 程序猿应该知道的 mybatis学习 无锁数据结构和算法 《Container-Networking-Docker-Kubernetes》笔记 活用linux 命令 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 swagger PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 《深入剖析kubernetes》笔记 精简代码的利器——lombok 学习 java 语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 JVM4——《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 commons-pipeline 源码分析 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS3——论文学习 Unsafe Spark Stream 学习 linux 文件系统 mysql 批量操作优化 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 forkjoin 泛谈 hbase 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 calico 问题排查 bgp初识 mesos 的一些tips mesos 集成 calico calico AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 compensable-transaction 源码分析 硬件对软件设计的影响 elasticsearch 初步认识 mockito简介及源码分析 线上用docker要解决的问题 《Apache Kafka源码分析》——Producer与Consumer 停止容器 dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 java系并发模型的发展 从一个marathon的问题开始的 docker 环境(主要运行java项目)常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 netty中的future和promise 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 一些tricky的code http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 JVM3——java内存模型 java concurrent 工具类 java exception java io涉及到的一些linux知识 network channel network byte buffer 测试环境docker化实践 通用transport层框架pigeon netty(七)netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 从Go并发编程模型想到的 mesos深入 Macvlan Linux网络源代码学习2 《docker源码分析》小结 对web系统的一些理解 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习 Docker网络五,docker网络的回顾 zookeeper三重奏 数据库的一些知识 Spark 泛谈 commons-chain netty(六)netty回顾 Thrift基本原理与实践(三) Thrift基本原理与实践(二) Thrift基本原理与实践(一) Future 回调 Docker0.1.0源码分析 基于spring boot和Docker搭建微服务 通过Docker Plugin来扩展Docker Engine java gc Docker网络四,基于Centos搭建Docker跨主机网络 google guava的一些理解 Jedis源码分析 Redis概述 Docker回顾 深度学习是个什么鬼 Docker网络三,基于OVS实现Docker跨主机网络 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 netty(四)netty对http协议的实现(废弃) netty(三)netty框架泛谈 向Hadoop学习NIO的使用 以新的角度看数据结构 AQS1——并发相关的硬件与内核支持 使用Ubuntu要做的一些环境准备 Docker网络二,libnetwork systemd 简介 那些有用的sql语句 异构数据库表在线同步 spring aop 实现原理简述——背景知识 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 我们编程的那些潜意识 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 一次代码调试的过程 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 docker volume 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM2——JVM和传统OS对比 git spring rmi和thrift maven/ant/gradle使用 再看tcp mesos简介 缓存系统——具体组件 缓存系统 java nio的多线程扩展 多线程设计模式/《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go 常用的一些库 Netty(一)初步了解 java mina Golang开发环境搭建(Windows下) java nio入门 ibatis自动生成类和文件 Python初学 Goroutine 调度模型猜想 一些编程相关的名词 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Hadoop安装与调试 Kubernetes持久化存储 Kubernetes 其它特性 访问Kubernetes上的服务 Kubernetes副本管理 Kubernetes pod 组件 使用etcd + confd + nginx做动态负载均衡 nginx安装与简单使用 在CoreOS集群上搭建Kubernetes 如何通过fleet unit files 来构建灵活的服务 CoreOS 安装 定制自己的boot2docker.iso CoreOS 使用 Go初学 JVM1——jvm小结 硬币和扑克牌问题 LRU实现 virtualbox 使用 os->c->java 多线程 容器类概述 zabbix 使用 zabbix 安装 Linux中的一些点 关于集群监控 ThreadLocal小结 我对Hadoop的认识 haproxy安装 docker快速入门

标签


再看tcp

2015年07月12日

前言

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

TCP/IP协议族按照层次由上到下,层层包装。发送协议的主机从上自下将数据按照协议封装,而接收数据的主机则按照协议从得到的数据包解开,最后拿到需要的数据。这种结构非常有栈的味道,所以某些文章也把tcp/ip协议族称为tcp/ip协议栈

TCP的“假”连接/状态机

《软件架构设计》在客户端与服务器之间并不存在一条可靠的“物理管道”,只是在逻辑层面,通过一定的机制,让TCP 之上的应用层就像有一个可靠地连接一样。具体的说, 每条连接用 client ip,client port,server ip,server port 唯一确定,在代码中是一个个的socket,仅在逻辑层面连接是存在的,要经历建立、数据传输、关闭阶段,要完整的维护三个阶段中连接的每种可能的状态。

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状态

tcp协议字段组成

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

Alt text

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

序列号和确认号

TCP序列号和确认号详解

TCP会话的每一端都包含一个32位(bit)的序列号,该序列号被用来跟踪该端发送的数据量。每一个包中都包含确认号,在接收端则通过确认号用来通知发送端数据成功接收。

从序列号和确认号的角度看,三次握手是这样的:

  1. 客户端向服务器发送一个同步数据包请求建立连接,该数据包中,初始序列号(ISN)是客户端随机产生的一个值。

  2. 服务器收到这个同步请求数据包后,会对客户端进行一个同步确认ACK(确认号是客户端的初始序列号+1 )。这个数据包中,序列号是服务器随机产生的一个值。

  3. 客户端收到这个同步确认数据包后,再对服务器进行一个确认。该数据包中,序列号是上一个同步请求数据包中的确认号值,确认号是服务器的初始序列号+1。

假设初始序列号是0(不管是客户端请求,还是服务端响应),那么序列号为当前端成功发送的数据位数,确认号为当前端成功接收的数据位数。握手过程中,尽管没有传输有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位(占1bit)。

由此,我们就可以知道为什么一些linux命令可以统计流量,为什么说tcp是可靠地?序列号、确认号、checksum即可以保证交互双方正确传输了n字节的数据。序列号来保证所有传输的数据可以按照正常的顺序进行重组,从而保障数据传输的完整。

tcp连接建立与释放

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

三次握手(从程序层面看,大致对应socket.connect函数)

  1. 客户端向服务器发送连接请求,
  2. 服务端向客户端发送确认
  3. 客户端收到服务端确认后,向服务端发送确认

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

前两次的握手很显然是必须的,主要是最后一次,即客户端收到服务端发来的确认后为什么还要向服务端再发送一次确认呢?这主要是为了防止已失效的请求报文段突然又传送到了服务端而产生连接的误判。

理论上:在TCP传送一个数据包时,它会把这个数据包放入重发队列中,同时启动计时器,如果收到了关于这个包的确认信息,便将此数据包从队列中删除,如果在计时器超时的时候仍然没有收到确认信息,则需要重新发送该数据包。

考虑如下的情况:客户端发送了一个连接请求报文段到服务端,但是在某些网络节点上长时间滞留了,而后客户端又超时重发了一个连接请求报文段该服务端,而后正常建立连接,数据传输完毕,并释放了连接。如果这时候第一次发送的请求报文段延迟了一段时间后,又到了服务端,很显然,这本是一个早已失效的报文段,但是服务端收到后会误以为客户端又发出了一次连接请求,于是向客户端发出确认报文段,并同意建立连接。

假设不采用三次握手,这时服务端只要发送了确认,新的连接就建立了,但由于客户端没有发出建立连接的请求,因此不会理会服务端的确认,也不会向服务端发送数据,而服务端却认为新的连接已经建立了,并在一直等待客户端发送数据,这样服务端就会一直等待下去,直到超出保活计数器的设定值,而将客户端判定为出了问题,才会关闭这个连接。这样就浪费了很多服务器的资源。而如果采用三次握手,客户端就不会向服务端发出确认,服务端由于收不到确认,就知道客户端没有要求建立连接,从而不建立该连接。

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

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

与基于拜占庭节点的复杂的一致性协议相比,笔者猜测,tcp 得以简单一点的因素包括但不限于

  1. 点对点两两通信
  2. 基于可信节点,“不可信”来自于网络通道的不可靠,但也只是丢包、延迟和重复,没有篡改。

关闭时为什么要TIME_WAIT?

一个连接由<client ip,client port,server ip,server port> 唯一标识,连接关闭之后再重开应该是一个新的连接,但用四元组无法区分新老连接。老连接关闭后,仍可能有数据包在网络上“闲逛”,新连接打开时可能会收到 老连接的 数据包。为此:

  1. TCP/IP 定义了一个MSL,任何一个IP数据包在网络上逗留的最长时间是MSL,默认120s,超过这个时间,中间的路由节点会将数据包丢弃
  2. 一个连接保持TIME_WAIT 2*MSL 时间,再进入closed状态,就可以避免老连接上闲逛的数据包串到新的连接上

综上,一个连接不是想关就能关的,关闭后要等2*MSL 才能重开,这就造成一个问题:如果频繁的创建连接,最后可能导致大量的连接处于TIME_WAIT状态,最终耗光所有的连接资源。这也是为什么client要建连接池。

“聪明的”tcp/ip

Sliding Window:TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

拥塞控制:TCP通过Sliding Window来做流控(Flow Control),但是TCP觉得这还不够,因为Sliding Window需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。

如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络。

拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。

backlog

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

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

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.Berkeley-derived implementations add a fudge factor to the backlog: It is multiplied by 1.5

When a SYN arrives from a client, TCP creates a new entry on the incomplete queue and then responds with the second segment of the three-way handshake: the server’s SYN with an ACK of the client’s SYN (Section 2.6). This entry will remain on the incomplete queue until the third segment of the three-way handshake arrives (the client’s ACK of the server’s SYN), or until the entry times out. (Berkeley-derived implementations have a timeout of 75 seconds for these incomplete entries.)

If the queues are full when a client SYN arrives, TCP ignores the arriving SYN (pp. 930–931 of TCPv2); it does not send an RST. This is because the condition is considered temporary, and the client TCP will retransmit its SYN, hopefully finding room on the queue in the near future. If the server TCP immediately responded with an RST, the client’s connect would return an error, forcing the application to handle this condition instead of letting TCP’s normal retransmission take over. Also, the client could not differentiate between an RST in response to a SYN meaning “there is no server at this port” versus “there is a server at this port but its queues are full.”

端口

什么是端口号(port)?

注意,这个号码是用在TCP,UDP上的一个逻辑号码,并不是一个硬件端口,我们平时说把某某端口封掉了,也只是在IP层次把带有这个号码的IP包给过滤掉了而已。

Socket 是一个编程接口,(linux tcp/ip协议栈的实现通常有一个socket层),包括几个最基本的函数接口。比如create、listen、accept、connect、read和write等等。Socket可以支持不同的传输层协议(TCP或UDP ),Socket跟TCP/IP 并没有必然的联系,socket的出现只是可以更方便的使用TCP/IP 协议栈而已。

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

TCP 不是唯一

Tcp 有一些痼疾诸如队头阻塞、重传效率低等,因此Google 基于UDP 提出了一个QUIC(quick udp internet connection),在重传效率、减少RTT次数、连接迁移(以客户端生成的64位标识而不是4元组来表示一个连接,更适合移动客户端频繁建立连接的场景)等方面做了一些工作。

引用

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

TCP连接的建立和释放