TCP/IP协议JAVA TCP/IP Socket详解TCP/IP网络编程

TCP

2017-12-06  本文已影响41人  XLsn0w

端口与进程

TCP 的包是不包含 IP 地址信息的,那是 IP 层上的事,但是有源端口和目的端口。

就是说,端口这一东西,是属于 TCP 知识范畴的。

我们知道两个进程,在计算机内部进行通信,可以有管道、内存共享、信号量、消息队列等方法。

而两个进程如果需要进行通讯最基本的一个前提是能够唯一的标识一个进程,在本地进程通讯中我们可以使用「PID(进程标识符)」来唯一标识一个进程。

但 PID 只在本地唯一,如果把两个进程放到了不同的两台计算机,然后他们要通信的话,PID 就不够用了,这样就需要另外一种手段了。

解决这个问题的方法就是在运输层使用「协议端口号 (protocol port number)」,简称「端口 (port)」.

我们知道 IP 层的 ip 地址可以唯一标识主机,而 TCP 层协议和端口号可以唯一标识主机的一个进程,这样我们可以利用:「ip地址+协议+端口号」唯一标示网络中的一个进程。

在一些场合,也把这种唯一标识的模式称为「套接字 (Socket)」

这就是说,虽然通信的重点是应用进程,但我们只要把要传送的报文交到目的主机的某一个合适的端口,剩下的工作就由 TCP 来完成了。

认识端口

TCP 用一个 16 位端口号来标识一个端口,可允许有 65536 ( 2的16次方) 个不同的端口号,范围在 0 ~ 65535 之间。

端口号根据服务器使用还是客户端使用,以及常见不常见的维度来区分,主要有以下类别:

服务器端使用的端口号

熟知端口号

登记端口号

客户端使用的端口号

下面展开来说说。

端口号的分类

服务器端使用的端口号

熟知端口号

取值范围:0 ~ 1023。

可以在 www.iana.org 查到,服务器机器一接通电源,服务器程序就运行起来,为了让因特网上所有的客户程序都能找到服务器程序,服务器程序所使用的端口就必须是固定的,并且总所众所周知的。

一些常见的端口号:

应用程序FTPTELNETSMTPDNSTFTPHTTPHTTPSSNMP

熟知端口号212325536980443161

登记端口号

取值范围:1024 ~ 49151。

这类端口没有熟知的应用程序使用,但是需要登记,以防重复

客户端使用的端口号

取值范围:49152 ~ 65535。

这类端口仅在客户端进程运行时才动态选择。

又叫 短暂端口号,表示这种端口的存在时间是短暂的,客户进程并不在意操作系统给它分配的是哪一个端口号,因为客户进程之所以必须有一个端口号,是为了让传输层的实体能够找到自己。

PS:在/etc/services文件中可以查看所有知名服务使用的端口。


TCP 是面向字节流的,但传送的数据单元却是报文段。

什么是报文?

例如一个 100kb 的 HTML 文档需要传送到另外一台计算机,并不会整个文档直接传送过去,可能会切割成几个部分,比如四个分别为 25kb 的数据段。

而每个数据段再加上一个 TCP 首部,就组成了 TCP 报文。

一共四个 TCP 报文,发送到另外一个端。

另外一端收到数据包,然后再剔除 TCP 首部,组装起来。

等到四个数据包都收到了,就能还原出来一个完整的 HTML 文档了。

+

在 OSI 的七层协议中,第二层(数据链路层)的数据叫「Frame」,第三层(网络层)上的数据叫「Packet」,第四层(传输层)的数据叫「Segment」。

TCP 报文 (Segment),包括首部和数据部分。

而 TCP 的全部功能都体现在它首部中各字段的作用,只有弄清 TCP 首部各字段的作用才能掌握 TCP 的工作原理。

TCP 报文段首部的前20个字节是固定的,后面有 4N 字节是根据需要而增加的。

下图是把 TCP 报文中的首部放大来看。

TCP 的首部包括以下内容:

源端口 source port

目的端口 destination port

序号 sequence number

确认号 acknowledgment number

数据偏移 offset

保留 reserved

标志位 tcp flags

窗口大小 window size

检验和 checksum

紧急指针 urgent pointer

选项 tcp options

下面展开来描述个字段的意义和作用。

TCP 首部各字段的意义和作用

源端口和目的端口 Port

各占 2 个 字节,共 4 个字节。

用来告知主机该报文段是来自哪里以及传送给哪个应用程序(应用程序绑定了端口)的。

进行 TCP 通讯时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号。

序号 Sequence Number

占 4 个字节。

TCP 是面向字节流的,在一个 TCP 连接中传输的字节流中的每个字节都按照顺序编号。

例如 100 kb 的 HTML 文档数据,一共 102400 (100 * 1024) 个字节,那么每一个字节就都有了编号,整个文档的编号的范围是 0 ~ 102399。

序号字段值指的是本报文段所发送的数据的第一个字节的序号。

那么 100 的 HTML 文档分割成四个等分之后,

第一个 TCP 报文段包含的是第一个 25kb 的数据,0 ~ 25599 字节, 该报文的序号的值就是:0

第二个 TCP 报文段包含的是第二个 25kb 的数据,25600 ~ 51199 字节,该报文的序号的值就是:25600

......

根据 8 位 = 1 字节,那么 4 个字节可以表示的数值范围:[0, 2^32],一共 2^32 (4294967296) 个序号。

序号增加到最大值的时候,下一个序号又回到了 0.

也就是说 TCP 协议可对 4GB 的数据进行编号,在一般情况下可保证当序号重复使用时,旧序号的数据早已经通过网络到达终点或者丢失了。

确认号 Acknowledgemt Number

占 4 个字节。

表示期望收到对方下一个报文段的序号值

TCP 的可靠性,是建立在「每一个数据报文都需要确认收到」的基础之上的。

就是说,通讯的任何一方在收到对方的一个报文之后,都要发送一个相对应的「确认报文」,来表达确认收到。

那么,确认报文,就会包含确认号

例如,通讯的一方收到了第一个 25kb 的报文,该报文的 序号值=0,那么就需要回复一个确认报文,其中的确认号 = 25600.

数据偏移 Offset

占 0.5 个字节 (4 位)。

这个字段实际上是指出了TCP 报文段的首部长度,它指出了 TCP报文段的数据起始处 距离 TCP报文的起始处 有多远。(注意 数据起始处 和 报文起始处 的意思)

一个数据偏移量 = 4 byte,由于 4 位二进制数能表示的最大十进制数字是 15,因此数据偏移的最大值是 60 byte,这也侧面限制了 TCP 首部的最大长度。

保留 Reserved

占 0.75 个字节 (6 位)。

保留为今后使用,但目前应置为 0。

标志位 TCP Flags

标志位,一共有 6 个,分别占 1 位,共 6 位 。

每一位的值只有 0 和 1,分别表达不同意思。

紧急 URG (Urgent)

当 URG = 1 的时候,表示紧急指针(Urgent Pointer)有效。

它告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送。

URG 要与首部中的 紧急指针 字段配合使用。

确认 ACK (Acknowlegemt)

当 ACK = 1 的时候,确认号(Acknowledgemt Number)有效。

一般称携带 ACK 标志的 TCP 报文段为「确认报文段」。

TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 设置为 1。

推送 PSH (Push)

当 PSH = 1 的时候,表示该报文段高优先级,接收方 TCP 应该尽快推送给接收应用程序,而不用等到整个 TCP 缓存都填满了后再交付。

复位 RST (Reset)

当 RST = 1 的时候,表示 TCP 连接中出现严重错误,需要释放并重新建立连接。

一般称携带 RST 标志的 TCP 报文段为「复位报文段」。

同步 SYN (SYNchronization)

当 SYN = 1 的时候,表明这是一个请求连接报文段。

一般称携带 SYN 标志的 TCP 报文段为「同步报文段」。

在 TCP 三次握手中的第一个报文就是同步报文段,在连接建立时用来同步序号。

对方若同意建立连接,则应在响应的报文段中使 SYN = 1 和 ACK = 1。

终止 FIN (Finis)

当 FIN = 1 时,表示此报文段的发送方的数据已经发送完毕,并要求释放 TCP 连接。

一般称携带 FIN 的报文段为「结束报文段」。

在 TCP 四次挥手释放连接的时候,就会用到该标志。

窗口大小 Window Size

占 2 字节。

该字段明确指出了现在允许对方发送的数据量,它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

窗口大小的值是指,从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。

例如,假如确认号是 701 ,窗口字段是 1000。这就表明,从 701 号算起,发送此报文段的一方还有接收 1000 (字节序号是 701 ~ 1700) 个字节的数据的接收缓存空间。

校验和 TCP Checksum

占 2 个字节。

由发送端填充,接收端对 TCP 报文段执行 CRC 算法,以检验 TCP 报文段在传输过程中是否损坏,如果损坏这丢弃。

检验范围包括首部和数据两部分,这也是 TCP 可靠传输的一个重要保障。

紧急指针 Urgent Pointer

占 2 个字节。

仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数。

当 URG = 1 时,发送方 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。

因此,紧急指针指出了紧急数据的末尾在报文段中的位置。

TCP 的整个交流过程可以总结为:先建立连接,然后传输数据,最后释放链接。

三次握手,建立连接

TCP 连接建立要解决的首要问题就是:要使每一方能够确知对方的存在。

三次握手就像,在一个黑暗的森林,你知道前方十点钟方向好像有人。

你喊了一句:Hello?I'am JerryC,Who are you?        ///发送连接

对面回了一句:Hi! I'am David, and nice to meet you!   ///对方反应

然后你回了一句:Nice to meet you too!                        ///自己回应

......(自此,你们才算真正认识了双方,开始了后面省略3000字的谈话)

所以说,两个人需要交朋友(两个端点需要建立连接),至少需要三次的通话(握手)

其实,网络上的传输是没有连接的,TCP 也是一样的。

而 TCP 所谓的「连接」,其实只不过是在通信的双方维护一个「连接状态」,让它看上去好像有连接一样。

连接建立过程

TCP 连接的建立采用客户-服务器方式,

主动发起连接建立的一方叫客户端(Client)

被动等待连接建立的一方叫服务器(Server)

最初的时候,两端都处于CLOSED的状态,然后服务器打开了 TCP 服务,进入LISTEN状态,监听特定端口,等待客户端的 TCP 请求。

第一次握手: 客户端主动打开连接,发送 TCP 报文,进行第一次握手,然后进入SYN_SEND状态,等待服务器发回确认报文。

这时首部的同步位 SYN = 1,同时初始化一个序号 Sequence Number = J。

TCP 规定,SYN 报文段不能携带数据,但会消耗一个序号。

第二次握手: 服务器收到了 SYN 报文,如果同意建立连接,则向客户端发送一个确认报文,然后服务器进入SYN_RCVD状态。

这时首部的 SYN = 1,ACK = 1,而确认号 Acknowledgemt Number = J + 1,同时也为自己初始化一个序号 Sequence Number = K。

这个报文同样不携带数据。

第三次握手

客户端收到了服务器发过来的确认报文,还要向服务器给出确认,然后进入ESTABLISHED状态。

这时首部的 SYN 不再置为 1,而 ACK = 1,确认号 Acknowledgemt Number = K + 1,序号 Sequence Number = J + 1。

第三次握手,一般会携带真正需要传输的数据,当服务器收到该数据报文的时候,就会同样进入ESTABLISHED状态。 此时,TCP 连接已经建立。

对于建立连接的三次握手,主要目的是初始化序号 Sequence Number,并且通信的双方都需要告知对方自己的初始化序号,所以这个过程也叫 SYN。

这个序号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序,因为TCP 会用这个序号来拼接数据。

利用连接设计缺陷实施 TCP Flood 攻击

知道了 TCP 建立一个连接,需要进行三次握手。

但如果你开始思考「三次握手的必要性」的时候,就会知道,其实网络是很复杂的,一个信息在途中丢失的可能性是有的。

如果数据丢失了,那么,就需要重新发送,这时候就要知道数据是否真的送达了。

这就是三次握手的必要性。

但是再向深一层思考,你给我发信息,我收到了,我回复,因为我是君子。

如果是小人,你给我发信息,我就算收到了,我也不回复,你就一直等我着我的回复。

那么很多小人都这样做,你就要一直记住你在等待着小人1号、小人2号、小人3号......直到你的脑容量爆棚,烧坏脑袋。

黑客就是利用这样的设计缺陷,实施 TCP Flood 攻击,属于 DDOS 攻击的一种。

想了解更多 SYN Flood 攻击请看:SYN flood - wiki

四次挥手,释放连接

TCP 有一个特别的概念叫做半关闭,这个概念是说,TCP 的连接是全双工(可以同时发送和接收)的连接,因此在关闭连接的时候,必须关闭传送和接收两个方向上的连接。

客户端给服务器发送一个携带 FIN 的 TCP 结束报文段,然后服务器返回给客户端一个 确认报文段,同时发送一个 结束报文段,当客户端回复一个 确认报文段 之后,连接就结束了。

释放连接过程

在结束之前,通信双方都是处于ESTABLISHED状态,然后其中一方主动断开连接。

下面假如客户端先主动断开连接。

第一次挥手:

客户端向服务器发送结束报文段,然后进入FIN_WAIT_1状态。

此报文段 FIN = 1, Sequence Number = M。

第二次挥手:

服务端收到客户端的结束报文段,然后发送确认报文段,进入CLOSE_WAIT状态。

此报文段 ACK = 1, Sequence Number = M + 1。

客户端收到该报文,会进入FIN_WAIT_2状态。

第三次挥手:

同时服务端向客户端发送结束报文段,然后进入LAST_ACK状态。

此报文段 FIN = 1,Sequence Number = N。

第四次挥手:

客户端收到服务端的结束报文段,然后发送确认报文段,进入TIME_WAIT状态,经过 2MSL 之后,自动进入CLOSED状态。

此报文段 ACK = 1, Sequence Number = N + 1。

服务端收到该报文之后,进入CLOSED状态。

关于 TIME_WAIT 过渡到 CLOSED 状态说明

TIME_WAIT进入CLOSED需要经过 2MSL,其中 MSL 就叫做 最长报文段寿命(Maxinum Segment Lifetime),根据 RFC 793 建议该值这是为 2 分钟,也就是说需要经过 4 分钟,才进入CLOSED状态。

状态流转

无论客户端还是服务器,在双方 TCP 通讯的过程中,都会有着一个「状态」的概念,状态会随着 TCP 通讯的不同阶段而变化。

TCP 状态流转图

各种状态表示的意思

CLOSED:表示初始状态

LISTEN:表示服务器端的某个 socket 处于监听状态,可以接受连接

SYN_SENT:在服务端监听后,客户端 socket 执行 CONNECT 连接时,客户端发送 SYN 报文,此时客户端就进入 SYN_SENT 状态,等待服务端确认。

SYN_RCVD:表示服务端接收到了 SYN 报文。

ESTABLISHED:表示连接已经建立了。

FIN_WAIT_1:其中一方请求终止连接,等待对方的 FIN 报文。

FIN_WAIT_2:在FIN_WAIT_2之后, 当对方回应 ACK 报文之后,进入该状态。

TIME_WAIT:表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL 之后即可回到 CLOSED 状态。

CLOSING:一种罕见状态,发生在发送 FIN 报文之后,本应是先收到 ACK 报文,却先收到对方的 FIN 报文,那么就从 FIN_WAIT_1 的状态进入 CLOSING 状态。

CLOSE_WAIT:表示等待关闭,在 ESTABLISHED 过渡到 LAST_ACK 的一个过渡阶段,该阶段需要考虑是否还有数据发送给对方,如果没有,就可以关闭连接,发送 FIN 报文,然后进入 LAST_ACK 状态。

LAST_ACK:被动关闭一方发送 FIN 报文之后,最后等待对方的 ACK 报文所处的状态。

CLOSED:当收到 ACK 保温后,就可以进入 CLOSED 状态了。

可靠性交付的实现

TCP 是一种提供可靠性交付的协议。

也就是说,通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。

但是在网络中相连两端之间的介质,是复杂的,并不确保数据的可靠性交付,那么 TCP 是怎么样解决问题的?

这就需要了解 TCP 的几种技术:

滑动窗口

超时重传

流量控制

拥塞控制

下面来分别讲一下这几种技术的实现原理。

超时重传

重传时机

TCP 报文段在传输的过程中,下面的情况都是有可能发生的:

数据包中途丢失;

数据包顺利到达,但对方发送的 ACK 报文中途丢失;

数据包顺利到达,但对方异常未响应 ACK 或被对方丢弃;

当出现这些异常情况时,TCP 就会超时重传。

TCP 每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到了,但还没有收到确认,就重传这一报文段,这个就叫做「超时重传」。

重传算法

先认识两个概念

RTO ( Retransmission Time-Out ) 重传超时时间

指发送端发送数据后、重传数据前等待接收方收到该数据 ACK 报文的时间。

大白话就是,需要等待多长时间还没收到确认,就重新传一次。

RTO 的设置对于重传非常重要:

设长了,重发就慢,没有效率,性能差;

设短了,重发得就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

RTT ( Round Trip Time ) 连接往返时间

指发送端从发送 TCP 包开始到接收它的 ACK 报文之间所耗费的时间。

而在实际的网络传输中,RTT 的值每次都是随机的,无法事先预预知。

TCP 通过测量来获得连接当前 RTT 的一个估计值,并以该 RTT 估计值为基准来设置当前的 RTO。

这就引入了一类算法的称呼:自适应重传算法(Adaptive Restransmission Algorithm)

这类算法的关键就在于对当前 RTT 的准确估计,以便适时调整 RTO。

关于自适应重传算法,经历过多次的迭代和修正。

从 1981 年的RFC793提及的经典算法,到 1987 年 Karn 提出的 Karn/Partridge 算法,再到后来的 1988 年的 Jacobson / Karels 算法。

最后的这个算法在被用在今天的 TCP 协议中(Linux的源代码在:tcp_rtt_estimator)。

自适应重传算法的发展读者有兴趣可以参考其他资料,在这里我拎一个现在在用的算法出来讲讲,随意感受一下。

Jacobson / Karels 算法

1988年,有人推出来了一个新的算法,这个算法叫 Jacobson / Karels Algorithm(参看RFC6298)。

其计算公式:

SRTT = SRTT + α ( RTT – SRTT ) —— 计算平滑 RTT

DevRTT = ( 1-β )DevRTT + β( | RTT - SRTT | ) ——计算平滑 RTT 和真实的差距(加权移动平均)

RTO= µSRTT + ∂DevRTT

其中:

α、β、μ、∂是可以调整的参数,在 RFC6298 中给出了对应的参考值,而在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4;

SRTT 是 Smoothed RTT 的意思,是 RTT 的平滑计算值,即根据每次测量的 RTT 和旧的 RTT 进行运算,得出新的 RTT。SRTT 的值,会在每一次测量到 RTT 之后进行更新;

DevRTT 是 Deviation RTT 的意思,根据每次测量的 RTT 和旧的 SRTT 值进行运算,得出新的 DevRTT;

由算法可以知道 RTO 的值会根据每次测量的 RTT 值变化而变化,基本要点是 TCP 监视每个连接的性能,由每一个 TCP 的连接情况推算出合适的 RTO 值,根据不同的网络情况,自动修改 RTO 值,以适应负责的网络变化。

拥塞控制

慢启动(Slow Start) 与 拥塞避免(Congestion Avoidance)

[ ] TODO

快速重传(Fast Retransmit) 与 快速恢复(Fast Recovery)

[ ] TODO

滑动窗口 Sliding Window

滑动窗口协议比较复杂,也是 TCP 协议的精髓所在。

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

滑动窗口分为「接收窗口」和「发送窗口」

因为 TCP 协议是全双工的,会话的双方都可以同时接收和发送,那么就需要各自维护一个「发送窗口」和「接收窗口」。

发送窗口

大小取决于对端通告的接受窗口。

只有收到对端对于本端发送窗口内字节的 ACK 确认,才会移动发送窗口的左边界。

下图是发送窗口的示意图:

对于发送窗口,在缓存内的数据有四种状态:

#1 已发送,并得到接收方 ACK 确认;

#2 已发送,但还未收到接收方 ACK;

#3 未发送,但接收方允许发送,接收方还有空间

#4 未发送,且接收方不允许发送,接收方没有空间

如果下一刻,收到了接收方对于 32-36 字节序的数据包的 ACK 确认,那么发送方的窗口就会发生「滑动」。

并且发送下一个 46-51 字节序的数据包。

滑动窗口的概念,描述了 TCP 的数据是怎么发送,以及怎么接收的。

TCP 的滑动窗口是动态的,我们可以想象成小学常见的一个数学题,一个水池,体积 V,每小时进水量 V1, 出水量 V2。

当水池满了就不允许再注入了,如果有个液压系统控制水池大小,那么就可以控制水的注入速率和量了。

应用程序可以根据自身的处理能力变化,通过 API 来控制本端 TCP 接收窗口的大小,来进行流量控制。

接收窗口

大小取决于应用、系统、硬件的限制。

下图是接收窗口的示意图(找不到图,唯有自己画了):

相对于发送窗口,接受窗口在缓存内的数据只有三种状态:

已接收已确认;

未接收,准备接收;

未接收,并未准备接收;

下一刻接收到来自发送端的 32-36 数据包,然后回送 ACK 确认报,并且移动接收窗口。

另外接收端相对于发送端还有不同的一点,只有前面所有的段都确认的情况下才会移动左边界,

在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认,以此确保对端会对这些数据重传。

假如 32-36 字节不是一个报文段的,而是每个字节一个报文段的话,那么就会分成了 5 个报文段。

在实际的网络环境中,不能确保是按序收到的,其中会有一些早达到,一些迟到达。

如图中的 34、35 字节序,先收到了,接收窗口也不会移动。

因为有可能 32、33 字节序会出现丢包或者超时,这时就需要发送端重发报文段了。

上一篇下一篇

猜你喜欢

热点阅读