咱们在平时的开发过程当中,或多或少都会涉猎到网络传输这块。
这篇文章,主要是整理一下 TCP 的一些知识要点,做为一名开发者来讲,尽管有那么多的基础设施(框架、组件)帮咱们屏蔽了这些细节。当我仍然认为了解它的一些基本原理必有些裨益,尤为是当你在分布式环境上遇到一些棘手问题时,一些原理性的知识可能会让你快速找到答案。面试
TCP 是传输层的协议,全称是叫作 Transmission Control Protocol,这个协议在 IETF RFC 793 进行了定义。
在互联网产生以前,咱们的电脑都是相互独立的,每台机器都有着本身的操做系统并保持着本身的运行。
因而,为了将这些电脑链接起来,并可以基于一种"通道"的形式进行数据、资源的传输及交互,IETF 制定了 TCP 协议。缓存
那么,IETF又是什么? 这是一个使人尊敬的技术组织,叫 Internet Engineering Task Force,即互联网工程任务组。
这是一个成立于1985年的开放性组织,如今咱们所提到的 HTTP、TCP、IP 这些重要的网络协议,都是出自于该组织。
能够这么说,IETF 是互联网的始做俑者,没有它就没有如今繁荣的互联网了。tomcat
值得一提的是,IETF并不是权贵组织,它是一个"来自民间" 的自组织、自管理的团队,很是崇尚于自由平等的精神。安全
整个互联网的底层机制是由一套标准网络协议组成的,为了更方便于理解,人们便定义了所谓的“网络分层模型"。
在学习计算机网络课程的时候,都会提到两种网络模型,以下:服务器
在之前,因为术语众多,有许多人常常被OSI、ISO所迷惑..cookie
从上面的图中能够看出,TCP/IP 基本上是OSI 模型的简化版,固然也更加容易理解。
在网络层如下,物理层、数据链路层所涉及的一些技术手段及概念都相对晦涩难懂,就好比光缆、中继器、交换机等须要一些专业背景才能掌握通透。
对于大多数的软件应用来讲,将网络层如下的部分统称为“网络接口层" 无疑是更加简单的。网络
所以,OSI 模型尽管很是完善且全面,但已经被 TCP/IP 模型所淘汰,在互联网应用盛行的今天不多被说起。并发
图-TCP/IP 网络模型框架
TCP 是整个 TCP/IP 协议族中最重要的传输层协议,它定义了一种面向链接的、可靠的、基于流的传输方式。
HTTP 是基于 TCP 的,因此说 TCP 是整个互联网的协议其一并不为过。
同时,咱们在使用 HTTP 协议实现应用系统间的交互时,也常常免不了会与 TCP 打上交道。所以有必要了解一些基本机制。
首先,TCP 是基于链接的,也就是在进行数据传输以前,客户端与服务端(或者说是通讯的双方)须要先创建一个可信的链接。
在数据传输结束后,再经过一种协定的方式断开链接,由通讯的双方释放资源。这里涉及到的,就是常说的"三次握手"、"四次挥手"
其次,TCP 是可靠的,它定义了一种数据包的"超时重传机制",简单说,就是每个数据包在发送出去后的都会等待一个响应。
若是指定时间内没有收到响应,由发送方进行必定次数的重传来保证数据的可靠传输。
最后,TCP 是基于流的,这是指在传输数据时应用层不须要关注数据包的边界,TCP在数据传输时会自动根据网络环境将数据进行缓冲、分组、合并。
这点跟基于报文的协议(UDP)是大相径庭的。固然,基于流的传输也保证了数据收发的有序性,所以每一个数据包都附带上一个属于当前链接的序列号。
全双工是通信上的术语,通常在软件开发领域提到的并很少。
这是指数据同时在两个方向上传输,TCP 是基于全双工的可信传输协议。
固然 UDP 也能够实现全双工的传输,但 TCP 只能实现点对点的传输,没法支持广播或者多播(分组)。
黑板:半双工的区别在于,同一时间只能有一个方向的传输
透视一个协议的最原始的方法就是看它的数据包,一个TCP 的报文格式以下:
这里面的字段就包括了:
源端口
代表发送端所使用的端口号,用于目标主机回应。
目的端口
代表要链接的目标主机的端口号。
序号
代表发送的数据包的顺序,通常为上次发送包中的顺序号+1。
若该数据包是整个TCP链接中的第一个包(SYN包),则该值是随机生成的。
确认号
代表本端TCP已经接收到的数据,其值表示期待对端发送的下一个字节的序号。
实际上告诉对方,在这个序号减1之前的字节已正确接收。
若该数据包是整个TCP链接中的第一个包(SYN包),则确认号通常为0。
数据偏移
表示以32位(4字节)为单位的TCP分组头的总长度(首部长度),用于肯定用户数据区的起始位置。
在没有可变内容的状况下,TCP头部的大小为20字节,对应该值为5。
标志位
紧急标志位(URG):开启时代表此数据包处于紧急状态应该优先处理
确认标志位(ACK):开启时代表确认号有效,不然忽略确认号
推送标志位(PSH):开启时代表应该尽快交付给应用进程,而没必要等到缓存区填满才推送,好比 telnet 的场景
复位标志位(RST):开启时代表TCP链接出现链接出现错误,数据包非法拒绝链接
同步标志位(SYN):开启时代表链接创建的标志
终止标志位(FIN):开启时代表释放一个链接
窗口大小
代表指望接受到的数据包字节数,用于拥塞控制。
校验和
实现对TCP报文头以及数据区进行校验。
紧急指针
在紧急状态下(URG打开),指出窗口中紧急数据的位置(末端)。
选项(可变)
用于支持一些特殊的变量,好比最大分组长度(MSS)。
填充
用于保证可变选项为32 bit的整数倍。
黑板:通常状况下TCP 头部为20字节,加上20字节的 IP头部,一个数据包至少包含40字节的头部
链是指链路,这个是物理层的概念,好比光缆光纤,或是无线的电磁波。
但这里所说的链路实际上是网络链接的意思,即IP 上层的概念。
那么,一个TCP 正常的通信流程,会包含建链(创建链接)、传输数据、拆链(关闭链接),以下图所示:
(图来自网络)
据上图所示,在进行 TCP 进行数据传输时,都不可避免的会通过这两个阶段:
下面,重点说明下建链与拆链的过程
在创建TCP链接时,须要通过三次交互,也成为三次握手(HandShake)。
一、客户端发起链接请求,发送 SYN包(SYN=i)到服务器,并进入到SYN-SEND状态,等待服务器确认
二、服务器收到SYN包后,必须确认客户的 SYN(ack=i+1),同时本身也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入SYN-RECV状态
三、客户端收到服务器的SYN+ACK包,向服务器发送确认报ACK(ack=k+1),此后客户端和服务器进入ESTABLISHED状态,双方能够开始传送数据。
在谈论三次握手的时候,有几个问题是须要关注的:
问题1. 为何是三次握手
这个问题在技术面试时屡试不爽,原话是能不能两次,或者是四次握手呢?
答案就是,TCP 是可靠的传输,在创建链接时就应该通过两端的确认过程,如上面的流程,
只有在三次握手的状况下,客户端和服务端都通过了一次真正(SYN+ACK)的确认过程。这样的链接便认为是可信的。
此外,若是仅仅只是两次握手,一旦网络不稳定形成 SYN 包重传则会直接致使重复创建链接,浪费资源。
问题2. 什么是syn flood攻击
syn flood 是一种经典的 ddos攻击手段,这里面用到了TCP 三次握手存在的漏洞。
在上面的图中,能够看到当服务端接收到 SYN 后进入 SYN-RECV 状态,此时的链接称为半链接,同时会被服务端写入一个 半链接队列。
想象一下,若是攻击者在短期内不断的向服务端发送大量的 SYN 包而不响应,那么服务器的 半链接队列很快会被写满,从而致使没法工做。
实现 syn flood 的手段,能够经过伪造源 IP 的方式,这样服务器的响应就永远到达不了客户端(握手没法完成);
固然,经过设定客户端防火墙规则也能够达到一样的目的。
对 syn flood 实现拦截是比较困难的,能够经过启用 syn_cookies 的方式实现缓解,但这一般不是最佳方案。
最好的办法是经过专业的防火墙来解决,基本上全部的云计算大T 都具有这个能力。
关于 syn flood 能够看看这篇文章
问题3. 半链接队列和全链接队列如何调优
这里提到了一个"半链接队列"(syns queue),与其对应的还有一个 "全链接队列"(accept queue)
前者用于暂存未创建彻底的链接,后者是链接在成功创建后进入的一个队列。
半链接队列默认大小能够经过内核参数调整:
echo 4096 > /proc/sys/net/ipv4/tcp_max_syn_backlog
黑板:tcp_max_syn_backlog 在 syn_cookies 开启时是无效的,这两个选项存在冲突
对于全链接队列,若是服务器未能及时经过 accept 调用将其中的链接取走,会致使队列溢出(链接失效)
全链接队列的大小的内核调优方式:
echo 4096 > /proc/sys/net/core/somaxconn
那么,是否是只有内核调优这种方法能影响这两个参数呢?答案是否认的。
实际上,在应用层调用 socket listen 时也支持设置一个 backlog参数,这几个之间的关系以下:
半链接队列长度 = min(backlog,内核 net.core.somaxconn,内核 tcp_max_syn_backlog) 全链接队列长度 = min(backlog,内核 net.core.somaxconn)
黑板:通常的应用服务器如 netty、tomcat 都支持设置 backlog 参数,可是在真正进行调优时还须要配合考虑内核参数的配置。
在释放链接时,因为TCP是全双工的,所以最后要由两端分别进行关闭,这个流程以下:
一、客户端发送一个FIN,用来关闭客户端到服务器的数据传送,客户端进入FIN_WAIT_1状态。
二、服务器收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务器进入CLOSE_WAIT状态,而客户端进入FIN_WAIT2状态。
三、服务器发送一个FIN,用来关闭服务器到客户端的数据传送,服务器进入LAST_ACK状态。
四、客户端收到FIN后,客户端进入TIME_WAIT状态,接着发送一个ACK给服务器,确认序号为收到序号+1,服务器进入CLOSED状态,完成释放。
关闭链接有主动关闭和被动关闭一说,这里为了简化理解,咱们以客户端做为主动关闭方,服务器为被动关闭方。
四次挥手须要关注的问题:
问题1. 为何是四次挥手
发送FIN的一方就是主动关闭(客户端),而另外一方则为被动关闭(服务器)。
当一方发送了FIN,则表示在这一方再也不会有数据的发送。
其中当被动关闭方受到对方的FIN时,此时每每可能还有数据须要发送过去,所以没法当即发送FIN(也就是没法将FIN与ACK合并发送),
而是在等待本身的数据发送完毕后再单独发送FIN,所以整个过程须要四次交互。
问题2. 什么是半关闭
客户端在收到第一个FIN的ACK响应后,会进入FIN_WAIT2 状态时,此时服务器处于 CLOSE_WAIT状态,这种状态就称之为半关闭。
从半关闭到全关闭,须要等待第二次FIN的确认才算结束。此时,客户端要等到服务器的FIN才能进入TIME_WAIT,
若是对方迟迟不发送FIN呢,则会等待一段时间后超时,这个能够经过内核参数tcp_fin_timeout控制,默认是60s。
问题3. 为何服务器会有大量 closewait
半关闭的状态下的服务器链接会处于 closewait 状态,直到服务器发送了FIN。
那么在应用层则是调用socket.close()会执行FIN的发送,若是服务器出现大量CLOSE_WAIT状态的链接,那么有可能的缘由:
问题4. timewait 会带来什么问题
当客户端收到了对方的FIN时,会进入TIME_WAIT状态,此时会保持一段时间再进入CLOSE状态。
这么作的缘由主要仍是为了可靠的关闭链接。在将TCP 进行可靠性设计之时就考虑了许多网络的不稳定性的因素,好比:
发送给对方的ACK 可能会没法及时收到,此时对方可能重传FIN过来,若是提早进入CLOSE则会返回RST而不是ACK,就会影响关闭流程。
所以 TIME_WAIT 状态默认会持续一段时间,直到确认不会再有重传的数据包以后再安全的关闭。
黑板:这里timewait的持续时间默认是 2*MSL(总共1分钟),这个MSL叫Max Segment Lifetime,也就是关于一个数据包在网络中传输的最大生命周期的预设。
MSL默认是30s,固然这个值在如今已经能够大幅度缩减。可见在当时在设计之初,网络情况有多么的糟糕。
那么timewait会带来什么问题?
若是频繁的主动关闭链接,可能会产生大量 timewait,因为timewait 的链接占用了一个句柄及少许内存(4K),那么就有可能会影响其余链接的创建,好比:
出现 too many open files 异常..
该如何解决:
黑板:HTTP 协议里头发现了timewait的问题,因而在 HTTP 1.1 中定义了 KeepAlive 用来支持链接的重用。
问题5. RST 是什么,为何会出现
RST 是一个特殊的标记,用来表示当前应该当即终止链接。如下这些状况都会产生RST:
RST 机制有时候也会被利用,作一些端口的扫描,以下:
-> 端口开启,可接受SYN
-> 端口关闭,响应RST
原文只是想总结下 TCP 参数调优的几个细节,没想到TCP 牵扯出来的东西实在太多,光是一个简单的握手、挥手流程就存在这么多的细节和坑。 能够说为了保证数据传输的可靠性,早期的设计者确实考虑了太多的东西。固然,这也为上层的应用实现铺平了道路。 鉴于篇幅缘由,只作了TCP 建链、拆链方面的介绍。关于数据的传输的一些细节,将在下篇文章梳理及分享。