0
点赞
收藏
分享

微信扫一扫

程序猿之路

有点d伤 04-16 18:30 阅读 1

1、再谈端口号

端口号(Port)标识了一个主机上进行通信的不同的应用程序;

在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过 netstat -n查看);

1.1、端口号范围划分

0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。

1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。

1.2、认识知名端口号

有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:

执行下面的命令, 可以看到知名端口号:

cat /etc/services

我们自己写一个程序使用端口号时, 要避开这些知名端口号。

1.3、两个问题

1. 一个进程是否可以bind多个端口号?

2. 一个端口号是否可以被多个进程bind?

这两个问题我们之前谈udp服务器的时候已经说过了,这里再重复一遍:

一个进程可以绑定多个端口号,但是一个端口号不可以被多个进程绑定,如果被多个进程绑定就会让端口号无法决定该将信息发送给哪个进程!

1.4、两个指令

netstat

netstat是一个用来查看网络状态的重要工具。

语法:netstat [选项]

功能:查看网络状态

常用选项:

pidof

在查看服务器的进程id时非常方便。

语法:pidof [进程名]

功能:通过进程名, 查看进程id


2、UDP协议

2.1、UDP协议端格式

2.2、UDP的特点

UDP传输的过程类似于寄信。

2.3、面向数据报

应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;

用UDP传输100个字节的数据:

2.4、UDP的缓冲区

UDP的socket既能读, 也能写, 这个概念叫做全双工。

2.5、UDP使用注意事项

我们注意到, UDP协议首部中有一个16位的最大长度。 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。

 然而64K在当今的互联网环境下, 是一个非常小的数字。

如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装。

2.6、基于UDP的应用层协议

NFS: 网络文件系统

TFTP: 简单文件传输协议

DHCP: 动态主机配置协议

BOOTP: 启动协议(用于无盘设备启动)

DNS: 域名解析协议

当然, 也包括你自己写UDP程序时自定义的应用层协议;


3、TCP协议

TCP全称为 "传输控制协议(Transmission Control Protocol")。人如其名, 要对数据的传输进行一个详细的控制;

3.1、TCP协议段格式

接下来我们来一个个的解释这些字段的含义:

源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;

32位序号/32位确认号: 后面详细讲;(TCP将每个字节的数据都进行了编号,即为序列号。每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。确认序列号的填充的就是收到的报文的序号+1。)

4为首部长度:表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60;

6位标志位:

16位窗口大小: 后面再说(填写的就是自己的接收缓冲区中的剩余空间的大小)

16位校验和: 发送端填充, CRC校验。接收端校验不通过, 则认为数据有问题.。此处的检验和不光包含TCP首部, 也 包含TCP数据部分。 

16位紧急指针: 标识哪部分数据是紧急数据;

40字节头部选项: 暂时忽略;

接下来我们对重要的字段进行详细的解释。

16位窗口大小

我们知道,TCP和UDP最大的区别就是TCP是可靠性保证的传输,那么TCP凭什么保证可靠性呢?最基本的一个特点:确认应答(ACK)机制

这里先简单介绍一下确认应答机制,这个机制就是我们服务端在给客户端发送消息的时候,服务端收到了会给客户端一个确认收到的通知

当然要保证可靠性只靠这个也是不现实的,下面我们看其他问题。

TCP协议规定双方都有自己的发送缓冲区和接受缓冲区,那么如果接收方的接收缓冲区要满了,我们就得让发送方发的慢一点,依据是什么呢?这里就要解释上面没说的16位窗口大小的作用,这个窗口填写的就是自己的接收缓冲区中的剩余空间的大小。根据确认应答机制,服务端在返回确认报文的时候会将自己的接受缓冲区的大小填进报头的16为窗口中,所以,这里也就规定了,client和server基于tcp协议进行通信的时候,发送的是完整的tcp报文!

32位序号/32位确认号

理解完这一点,我们再回到确认应答机制,我们仔细想一想这样的机制是否存在一定的问题,比如我们该如何知道服务器应答的是我们发送的哪一条数据?还有最重要的一点,我们又如何保证我们发送数据的顺序性?对于我们传输层来说,数据的乱序,本身就是不可靠的一种,所以TCP协议也有两个字段来解决这个问题,就是32位序号/32位确认号。

TCP将每个字节的数据都进行了编号(本质就是数组下标),即为序列号。

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。确认序列号的填充的就是收到的报文的序号+1。

这里有人就会提一个问题,如果发送的数据一定会有应答,那为什么要分出序号和确认序号,直接合并成一个,客户端用序号决定发送顺序,服务器用序号来回答,不也可以吗?这里就要对之前说的确认应答机制进行一个重新解释,TCP最基本最原始的通信过程,应答的过程并不是单纯的应答,而是捎带应答,也就是服务器可以在给客户端发送数据的同时应答一下之前客户端发送的数据。这样可以提高网络数据传输的效率。

6位标志位

tcp在开始通信的时候建立连接、正常数据通信和断开连接都需要发送tcp报文,所以tcp收到的报文一定是有各种类型的,不同的类型,就决定了服务器不同的动作,6位标志位的作用就是:区分tcp报文的类型。

这里还涉及到另一个保证tcp可靠性的机制,就是超时重传机制。

 

主机 A 发送数据给 B 之后 , 可能因为网络拥堵等原因 , 数据无法到达主机 B;

如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答 , 就会进行重发 ;

但是 , 主机 A 未收到 B 发来的确认应答 , 也可能是因为 ACK 丢失了 ;

因此主机 B 会收到很多重复数据。   那么 TCP 协议需要能够识别出那些包是重复的包 , 并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。

那么超时的时间该如何规定呢?

TCP 为了保证无论在任何环境下都能比较高性能的通信 , 因此会动态计算这个最大超时时间。

3.2、连接管理机制

在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接。

服务端状态转化 :

客户端状态转化 :

首先我们解释一下为什么是三次握手:

对于第一点,三次握手是最小次数是因为,client要验证能收,能发。server也要验证能收,能发,所以三次握手是验证双方通信信道的最小次数。其中前两次是验证client能收,能发,后两次是验证server能收,能发。

第二点, 建立的连接一定会消耗时间和空间,而且谁最后发ACK,谁就先建连接,所以如果最后的ACK丢了,client认为已经建立好了连接,而server不这么认为。换句话说,谁最后发送ACK,谁就先维护连接。所以如果最后是偶数次,也就是最后发ACK是server端,换句话说也就是如果最后的ACK丢了,那么这条连接就维护在server端,可是服务器是一对多的,所以如果很多是client连接server,而且很多的client最后的ACK都连接失败了,那么server就会有大量的临时连接被建立起来,而且还不做事情,还占了操作系统大量资源,那么就会导致操作系统短期之内无法建立或者接收新的内容。虽然最后还是有异常连接,但是不影响,因为最后的异常连接维护在client,是无影响的,不像在server端是一对多的,如果异常连接太多就会导致服务器出问题,有风险。这样维护在client端也是没风险的!因为client端不会维护太多的资源,所以通过风险转移,保证了连接建立。

那么为什么是四次挥手呢?

 当我们在连接断开的时候,我们已经不需要验证双方通信信道是否通常了,因为前面我们一直在通信,所以我们在连接断开连接这个事情上,我们更重要的是在功能上通知对方,换句话说,如果client给server发送给FIN,server收到了,那么server就确认了已经把连接断开的信息给了server,如果server给client发送了ACK,client收到了,那么client就确认了,自身可以释放对应的缓冲区资源,而且server也保证的知道了这条消息,换言之这两次挥手在功能上就已经可以确认了client已经单向的向server断开连接,所以再两次挥手就可以实现server单向的向client断开。所以我们就可以通过四次挥手在功能上完成双方信道的关闭。

理解TIME_WAIT状态

当client收到一次FIN,然后对server进行响应,但是ACK在途中丢了,但是client还是处于TIME_WAIT状态,虽然server不会对client进行响应,但是server因为给client发送了FIN,超时重传的时间没有收到响应,那么就会进行超时重传,也就是说client第二次收到FIN就说明了client对server第一次FIN的响应是丢了的。

        这时可能有人会问:那么如果第二次的FIN也丢了呢?因为client此时处于TIME_WAIT状态,如果第二次的FIN丢了,也就意味着client到server的信道出问题了,server到client的信道也出问题了,那么也就说明了client和server之间的网络出问题了,那么客户端就等TIME_WAIT时间退出就行了,因为此时client没有收到FIN,那么ACK也就不需要重传了。server经过多次尝试也就断开了。

        这时可能又会有人说:这样不还是server经过多次尝试,然后尝试无果最后再断开么?这样就和上面的为什么不会立马进入CLOSED状态相同了么?因为这样做是最好的,而且这种情况虽然可能会发生,但是发生的概率很少。

解释一下第二个理由:有没有可能你在断开连接的时候,历史上还有一些数据没有发送给对方呢?完全有可能,虽然tcp是按序到达的,但实际上网络中被对方先收到的可能就会出现断开连接的报文,比正常的报文先到,这是完全有可能出现的。所以我们等上TIME_WAIT的时间会保证双方通信信道上面的正常数据在网络中尽可能的消散。也就是我们在等的期间,双方的连接依旧维持着,那么曾经发送的历史数据可能在网络中还存在,所以给它一段时间,让它把历史数据消散掉。至于为什么要将历史数据消散,可以理解成为了防止历史上的数据对下次新通信产生影响。

        通过上面所说,我们可以知道,对于我们来说,不是建立连接和断开连接就一定保证可靠性。所以现在再强调一下,我们前面说过,tcp是保证数据通信的可靠性,它是尽量的保证在连接建立之后,连接断开之前,在我们正常通信期间,通过确认应答机制,保证我们能够100%可靠,但是建立连接和断开连接,我们不能做到100%。

TIME_WAIT状态的等待时间

TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime) 的时间后才能回到CLOSED状态。(MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s)

想一想 , 为什么是 TIME_WAIT 的时间是 2MSL?

理解CLOSE_WAIT状态

我们知道,在四次挥手的时候是通过用户层的close(client_fd);close(server_fd);实现的。

那么如果在client段没有关闭这个描述符,server端会存在处于CLOSE_WAIT状态的连接,因为server没有进行第三、四次挥手,这样的问题很大,因为server只有处于CLOSED状态才会释放连接资源,也就意味着有很多处于CLOSE_WAIT状态的话,就表示有大量的连接占着资源,系统的资源就会很少,所以如果后期我们发现server端存在大量的CLOSE_WAIT状态的连接,那么就需要排查一下自己的代码是否有bug,没有即时close(fd);

这里有一个问题:为什么主动断开连接的一方在四次挥手之后不会立马进入CLOSED状态?

在这四次挥手的时候,前三次的:FIN->ACK、FIN丢失了都没事,都会进行超时重传,但是最后一次挥手的ACK丢了就会有很大的问题。

       因为我们前面说了,当client发送完ACK后,client的状态不会立马从TIME_WAIT状态到CLOSED状态,因为如果当client发送完ACK后立马处于CLOSED状态的话,也就意味着client的连接立马关闭,那么万一最后一次挥手的ACK丢了呢?

        server给client发的FIN丢了没事,因为client没有收到,所以会进行超时重传,client对server的响应丢了,而且立马进入了CLOSED就会完蛋。因为对应server来说,不管哪个丢了,都会进行重传,但是因为client进入了CLOSED状态,那么server再怎么重传,client都不会有响应,一旦没有响应,server经过无数次尝试之后,最后也一定会断开连接。但是!在这个server重复的同时,也会短暂的维护一条废弃的连接,这样对于server来讲,也是不友好的,因为我们争取让断开连接的一方,也就是client方来维护这个成本。

        所以只要双方没有断开连接,一切都还有余地。

3.3、滑动窗口

刚才我们讨论了确认应答策略 , 对每一个发送的数据段 , 都要给一个 ACK 确认应答。 收到 ACK 后再发送下一个数据段。

这样做有一个比较大的缺点 , 就是性能较差。 尤其是数据往返的时间较长的时候。

既然这样一发一收的方式性能较低 , 那么我们一次发送多条数据 , 就可以大大的提高性能 ( 其实是将多个段的等待时间重叠在一起了)

什么是滑动窗口呢?

        描述的是,发送方不用等待ACK一次所能发送的数据最大量。这里滑动窗口的大小是与TCP的窗口大小(对方的接收能力)是相关的。

滑动窗口和窗口大小对比

此时看来窗口大小和滑动窗口的大小近似是相等的。

滑动窗口一定会整体右移么?
        先说结论,不一定会!因为我们的发送方的滑动窗口大小是表示可以直接给对方发送的大小,当我们发送过去之后,如果对方的应用层拿了数据,而且是发多少拿多少,那么则会整体右移,但是如果我们发过去了,接收方响应了ACK,但是没有来得及处理,那么也就意味着对方的窗口大小为0了,也就是对方的接受能力为0,那么发送方的滑动窗口大小就滑不动了,此时的滑动窗口就不会在整体右移。

        所以滑动窗口变宽变窄,是与接收方接受能力有关。所以是不一定!!右方还有可能一直不变。

注:滑动窗口实际上就像是数组,在动的就是数组指针。

那么如果出现了丢包 , 如何进行重传 ? 这里分两种情况讨论

情况一 : 数据包已经抵达, ACK被丢了

这种情况下 , 部分 ACK 丢了并不要紧 , 因为可以通过后续的 ACK 进行确认 ;

情况二 : 数据包就直接丢了。

这种机制被称为 " 高速重发控制 "( 也叫 " 快重传 ")。

快重传 VS 超时重传

        这里说一下为什么有快重传还要有超时重传,这是因为快重传的条件是发送端主机连续三次收到了同样一个应答,才会触发快重传,那么如果没有触发条件,快重传就是不可以的,所以这里其实超时重传是保底策略,而快重传是提高效率的。

3.4、流量控制

接收端处理数据的速度是有限的。   如果发送端发的太快 , 导致接收端的缓冲区被打满 , 这个时候如果发送端继续发送 , 就会造成丢包, 继而引起丢包重传等等一系列连锁反应。

因此 TCP 支持根据接收端的处理能力 , 来决定发送端的发送速度。   这个机制就叫做 流量控制 (Flow Control) ;

接收端如何把窗口大小告诉发送端呢 ? 回忆我们的 TCP 首部中 , 有一个 16 位窗口字段 , 就是存放了窗口大小信息 ; 那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么 ?

实际上 , TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M ;

3.5、拥塞控制

虽然 TCP 有了滑动窗口这个大杀器 , 能够高效可靠的发送大量的数据 . 但是如果在刚开始阶段就发送大量的数据 , 仍然可能引发问题。

因为网络上有很多的计算机 , 可能当前的网络状态就已经比较拥堵。   在不清楚当前网络状态下 , 贸然发送大量的数据 , 是很有可能引起雪上加霜的。

TCP 引入 慢启动 机制 , 先发少量的数据 , 探探路 , 摸清当前的网络拥堵状态 , 再决定按照多大的速度传输数据 ;

像上面这样的拥塞窗口增长速度 , 是指数级别的。  " 慢启动 " 只是指初使时慢 , 但是增长速度非常快。

少量的丢包 , 我们仅仅是触发超时重传 ; 大量的丢包 , 我们就认为网络拥塞 ;

TCP 通信开始后 , 网络吞吐量会逐渐上升 ; 随着网络发生拥堵 , 吞吐量会立刻下降 ;

拥塞控制 , 归根结底是 TCP 协议想尽可能快的把数据传输给对方 , 但是又要避免给网络造成太大压力的折中方案。

3.6、延迟应答

如果接收数据的主机立刻返回 ACK 应答 , 这时候返回的窗口可能比较小。

一定要记得 , 窗口越大 , 网络吞吐量就越大 , 传输效率就越高 . 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

那么所有的包都可以延迟应答么 ? 肯定也不是 ;

具体的数量和超时时间 , 依操作系统不同也有差异 ; 一般 N 2, 超时时间取 200ms;

3.7、面向字节流

创建一个 TCP socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区 ;

由于缓冲区的存在 , TCP 程序的读和写不需要一一匹配 , 例如:

3.8、粘包问题

那么如何避免粘包问题呢 ? 归根结底就是一句话 , 明确两个包之间的边界。

思考 : 对于 UDP 协议来说 , 是否也存在 " 粘包问题 " 呢?

3.9、TCP异常情况

进程终止 : 进程终止会释放文件描述符 , 仍然可以发送 FIN。 和正常关闭没有什么区别。

机器重启 : 和进程终止的情况相同。

机器掉电 / 网线断开 : 接收端认为连接还在 , 一旦接收端有写入操作 , 接收端发现连接已经不在了 , 就会进行 reset。  即使没有写入操作, TCP 自己也内置了一个保活定时器 , 会定期询问对方是否在。   如果对方不在 , 也会把连接释放。

另外 , 应用层的某些协议 , 也有一些这样的检测机制。 例如 HTTP 长连接中 , 也会定期检测对方的状态。   例如 QQ, QQ断线之后, 也会定期尝试重新连接。

3.10、TCP小结

为什么 TCP 这么复杂 ? 因为要保证可靠性 , 同时又尽可能的提高性能。

可靠性 :

提高性能 :

其他 :

3.11、基于TCP应用层协议

当然 , 也包括你自己写 TCP 程序时自定义的应用层协议 ;

4、TCP/UDP对比

我们说了 TCP 是可靠连接 , 那么是不是 TCP 一定就优于 UDP ? TCP UDP 之间的优点和缺点 , 不能简单 , 绝对的进行比较

归根结底 , TCP UDP 都是程序员的工具 , 什么时机用 , 具体怎么用 , 还是要根据具体的需求场景去判定。

举报

相关推荐

0 条评论