网络协议(TCP)

网络协议--TCP协议

TCP是一个巨复杂的协议。关于TCP这个协议的细节,可以去看W.Richard Stevens的《TCP/IP 详解 卷1:协议》。

1.1. IP

IP(Internet Protocol)是互联网的基础协议,其中一个作用就是给互联网中的设备(主机)分配IP地址,以便在互联网中找到某个设备以127开头的IP地址称为本地回环地址,比如127.0.0.1 ,表示本机IP地址,主要作用是方便进行本地测试数据包是互联网中数据传输的基本单位,IP规定了数据如何分段打包,以及单个数据包如何传输到目标主机,以便各种设备之间可以相互交换数据。

那么如何查看IP呢?在windows上是ipconfig,在Linux上是ifconfig,在Linux上还有ip addr命令来查看IP。如果你登录一个被裁剪过的非常小的Linux系统,发现没有ifconfig命令,也没有 ip addr 命令,这个时候可以自行安装net-tools和iproute2这俩个工具,当然这俩个命令大多数的时候是系统自带的。

有时候我们可以通过ping来查看一个ip地址通不通。 ping 是基于ICMP协议工作的。ICMP全称Internet Control Message Protocol,就是互联网控制报文协议。

ping: 查询报文类型的使用。

1.2. 域名

IP地址不方便记忆,而域名(Domain Name),是互联网上主机的名称,每个域名都对应了一个IP地址,而且域名方便记忆,所以人们一般使用域名访问某台主机localhost表示本地主机的名称,对应127.0.0.1

DNS(Domain Name System),即域名系统,存放着互联网上所有域名和IP地址的对应关系,当人们使用域名时,就需要先通过DNS查询域名对应的IP地址,然后再使用IP地址找到目标主机。计算机中的hosts文件是一个本地的 DNS(C:\Windows\System32\drivers\etc目录下),我们可以修改hosts文件手动配置域名和IP地址的对应关系,计算机在查找时会优先查找hosts文件。

1.3. 端口号

一台主机可以提供多种服务(运行多个程序),为了方便外界区分和访问,每个服务都要有一个端口号,通过IP地址和端口号来唯一确定某台主机上的某个服务端口号的范围是 [ 0 , 65535 ] ,其中 [ 0 , 1023 ] 是系统保留端口,提供给系统服务以及其他著名的服务如HTTP(80)。我们自己的程序如果不提供著名服务,就尽量不要占用系统保留端口主机中某个端口号某个时刻只能被一个程序占用,系统采用先申请先得的方式进行分配,程序结束即释放所占用的端口号某个端口号有时候会被其他程序占用,或者程序重复启动时也会报端口号被占用的错误,这时可以在cmd命令行执行netstat -ano 查看占用某个端口号的程序的进程id,然后到任务管理器中找到该进程结束掉即可。

1.4. TCP

传输层里面比较重要的俩个协议,一个是 TCP,一个是UDP。对于不从事底层开发的人员来讲,或者对于开发应用的人来讲,最常用的就是这俩个协议。 TCP(Transmission Control Protocol)传输控制协议,是一个面向连接的,可靠的,基于字节流的传输层通信协议,很多应用层协议都建立在TCP之上,程序也可直接使用TCP进行网络通信。

TCP包头格式:

首先,源端口号和目标端口号是不可少的,如果没有这俩个端口号。数据就不知道发给哪个应用。

接下来是包的序号。为什么要给包编号呢?当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,编号是为了解决乱序问题。

还应该有确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。

TCP是靠谱的协议,但是这不能说明它面临的网络环境好,从IP层面来讲,如果网络状况的确那么差,是没有任何可靠性保证的,而作为IP的上一层TCP也无能为力,唯一能做的就是不断重传。

接下来是一些状态位。列如SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送会引起双方的状态变更。

还有一个重要的就是窗口大小。TCP要做流量控制。通信双方各申明一个窗口,标识自己当前能够处理能力,别发的太快,也别发的太慢。除了做流量控制以外,TCP还会做拥塞控制,即控制发送的速度。

TCP的三次握手:

TCP的连接建立我们常常称为三次握手。也常称之为:“请求-->应答-->应答之应答” 的三个回合。

A:您好,我是A

            B:您好A,我是B

            A:您好B

这里就有个问题:为什么是三次握手而不是2次,4次。。。

我们假设这个通路是非常不可靠的。我们假设这个通路是非常不可靠的,A 要发起一个连接,当发了第一个请求杳无音信的时候,会有很多的可能性,比如第一个请求包丢了,再如没有丢,但是绕了弯路,超时了,还有 B 没有响应,不想和我连接。A 不能确认结果,于是再发,再发。终于,有一个请求包到了 B,但是请求包到了 B 的这个事情,目前 A 还是不知道的,A 还有可能再发。

B 收到了请求包,就知道了 A 的存在,并且知道 A 要和它建立连接。如果 B 不乐意建立连接,则 A 会重试一阵后放弃,连接建立失败,没有问题;如果 B 是乐意建立连接的,则会发送应答包给 A。当然对于 B 来说,这个应答包也是一入网络深似海,不知道能不能到达 A。这个时候 B 自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者 A 已经挂了都有可能。

而且这个时候 B 还能碰到一个诡异的现象就是,A 和 B 原来建立了连接,做了简单通信后,结束了连接。还记得吗?A 建立连接的时候,请求包重复发了几次,有的请求包绕了一大圈又回来了,B 会认为这也是一个正常的的请求的话,因此建立了连接,可以想象,这个连接不会进行下去,也没有个终结的时候,纯属单相思了。因而两次握手肯定不行。

B 发送的应答可能会发送多次,但是只要一次到达 A,A 就认为连接已经建立了,因为对于 A 来讲,他的消息有去有回。A 会给 B 发送应答之应答,而 B 也在等这个消息,才能确认连接的建立,只有等到了这个消息,对于 B 来讲,才算它的消息有去有回。

当然 A 发给 B 的应答之应答也会丢,也会绕路,甚至 B 挂了。按理来说,还应该有个应答之应答之应答,这样下去就没底了。所以四次握手是可以的,四十次都可以,关键四百次也不能保证就真的可靠了。只要双方的消息都有去有回,就基本可以了。

好在大部分情况下,A 和 B 建立了连接之后,A 会马上发送数据的,一旦 A 发送数据,则很多问题都得到了解决。例如 A 发给 B 的应答丢了,当 A 后续发送的数据到达的时候,B 可以认为这个连接已经建立,或者 B 压根就挂了,A 发送的数据,会报错,说 B 不可达,A 就知道 B 出事情了。

当然你可以说 A 比较坏,就是不发数据,建立连接后空着。我们在程序设计的时候,可以要求开启 keepalive 机制,即使没有真实的数据包,也有探活包。

另外,你作为服务端 B 的程序设计者,对于 A 这种长时间不发包的客户端,可以主动关闭,从而空出资源来给其他客户端使用。三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是TCP 包的序号的问题。

A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A,B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢?因为这样往往会出现冲突。

例如,A 连上 B 之后,发送了 1、2、3 三个包,但是发送 3 的时候,中间丢了,或者绕路了,于是重新发送,后来 A 掉线了,重新连上 B 后,序号又从 1 开始,然后发送 2,但是压根没想发送 3,但是上次绕路的那个 3 又回来了,发给了 B,B 自然认为,这就是下一个包,于是发生了错误。

因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4ms 加一,如果计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就死翘翘了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间。

好了,双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样:

一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收。

TCP四次挥手

A:B 啊,我不想玩了。

B:哦,你不想玩了啊,我知道了。

这个时候,还只是 A 不想玩了,也即 A 不会再发送数据,但是 B 能不能在 ACK 的时候,直接关闭呢?当然不可以了,很有可能 A 是发完了最后的数据就准备不玩了,但是 B 还没做完自己的事情,还是可以发送数据的,所以称为半关闭的状态。

这个时候 A 可以选择不再接收数据了,也可以选择最后再接收一段数据,等待 B 也主动关闭。

B:A 啊,好吧,我也不玩了,拜拜。

A:好的,拜拜。

这样整个连接就关闭了。但是这个过程有没有异常情况呢?当然有,上面是和平分手的场面。

A 开始说“不玩了”,B 说“知道了”,这个回合,是没什么问题的,因为在此之前,双方还处于合作的状态,如果 A 说“不玩了”,没有收到回复,则 A 会重新发送“不玩了”。但是这个回合结束之后,就有可能出现异常情况了,因为已经有一方率先撕破脸。

一种情况是,A 说完“不玩了”之后,直接跑路,是会有问题的,因为 B 还没有发起结束,而如果 A 跑路,B 就算发起结束,也得不到回答,B 就不知道该怎么办了。另一种情况是,A 说完“不玩了”,B 直接跑路,也是有问题的,因为 A 不知道 B 是还有事情要处理,还是过一会儿会发送结束。

那怎么解决这些问题呢?TCP 协议专门设计了几个状态来处理这些问题。我们来看断开连接的时候的状态时序图:

断开的时候我们可以看到,当A说“不玩了”,就进入 FIN_WAIT_1 的状态,B 收到“A 不玩”的消息后,发送知道了,就进入 CLOSE_WAIT 的状态。

A 收到“B 说知道了”,就进入 FIN_WAIT_2 的状态,如果这个时候 B 直接跑路,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。

如果 B 没有跑路,发送了“B 也不玩了”的请求到达 A 时,A 发送“知道 B 也不玩了”的 ACK 后,从 FIN_WAIT_2 状态结束,按说 A 可以跑路了,但是最后的这个 ACK 万一 B 收不到呢?则 B 会重新发一个“B 不玩了”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了”会重发的,A 会重新发一个 ACK 并且足够时间到达 B。

A 直接跑路还有一个问题是,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都死翘翘,再空出端口来。

等待的时间设为 2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这个时候 A 再收到这个包之后,A 就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送 RST,B 就知道 A 早就跑了。

TCP状态机

将连接建立和连接断开的俩个时序图综合起来就是著名的TCP的状态机。

总结:

TCP 包头很复杂,但是主要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制;

连接的建立是经过三次握手,断开的时候四次挥手。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值