传输层
负责数据能够从发送端到输出端,解决进程与进程间通信的问题
0.端口号
标识了一个主机上进行通信的不同的应用程序;
一个进程可以同时绑定多个端口,一个端口号不能同时绑定多进程
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一条网络上唯一的通信链路(可以通过netstat -n查看);
早期规定端口号是一个无符号,二字节的数字,故:0—65535。
- 0 — 1023: 知名端口号, 他们的端口号都是固定的。
- 1024 — 65535: 操作系统动态分配的端口号,由操作系统从这个范围分配。
知名端口号:
- ssh协议, 使用22端口
- http协议, 使用80端口
- https协议, 使用443端口
- dns协议,使用53端口
- mysql协议,使用3306端口
- Redis协议,使用6379端口
1.UDP(User Datagram Protocol)
基本只实现了进程和进程间通信
UDP的三个特点:
- 无连接:知道对端的IP和端口号就直接进行传输, 不需要建立连接;
- 不可靠:没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;可以保证数据不被修改,无法保证数据不会丢失和数据顺序不会错乱(网络层不同的寻路,可能导致到达的顺序不固定)。
- 面向数据报:不能够灵活的控制读写数据的次数和数量。应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并。
UDP协议段格式
- 16位源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
- 16位UDP长度: 表示整个数据报(UDP首部+UDP数据)的最大长度;
- 校验和:属于Hash算法,发送端Hash(有效数据)=校验和1,接收端Hash(有效数据)=校验和2,判断是否相等。如果校验和出错,就会直接丢弃;
UDP没有真正意义上的发送缓冲区
调用socket.send(packet)会直接交给内核(OS),由内核(OS)将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区。
但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了, 再到达的UDP数据就会被丢弃;
- 因为UDP(UDP首部+UDP数据)最大数据为16位,所以UDP能传输的最大数据长度是64K。
- 如果需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装。
2.TCP(Transmission Control Protocol)
实现可靠的进程和进程间通信
TCP的三个特点:
- 有连接:TCP协议需要一条通信通道,管理一组和通道相关的数据。(定义一个Connection类,一个connection对象,描述一条唯一的通信通道)
- 可靠性:通过实现一些列机制,保证可靠性。如:确认应答 + 超时重传,流量控制 + 拥塞控制,捎带应答,延迟应答,快重传等等。
- 面向字节流:创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区;由于缓冲区的存在,TCP程序的读写不需要匹配。
可靠性包括:1.数据不会被篡改;2.尽最大能力将发送的数据传递给接收方;3.保证顺序。
TCP协议端格式
(1). 源/目的端口号:表示数据是从哪个进程来, 到哪个进程去;
(2). 32位序号(Sequence Number,SN):本次要发送数据的第一个字节的编号。例如:发送{7,8,9,10},填充7。
(3). 32位确认号(Acknowledge Sequence Number,ASN): 给出下一个要接收的编号。例如:发送{7,8,9,10},填充11。
(4). 4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60
(5). 6位标志位:
URG: 紧急指针是否有效;
ACK: 确认字符,表示发来的数据是否确认接收无误。
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走;
RST:对方要求重新建立连接;把携带RST标识的称为复位报文段;
SYN: 请求建立连接;把携带SYN标识的称为同步报文段;
FIN:通知对方, 本端要关闭了, 把携带FIN标识的称为结束报文段。
(6). 16位窗口大小: 接收端填充;接收端将自己可以接收的缓冲区大小放入TCP首部中的 “窗口大小” 字段,通过ACK段通知发送端;
(7). 16位校验和: 发送端填充;CRC校验TCP数据(首部+数据),接收端校验不通过, 则认为数据有问题。
(8). 16位紧急指针:标识哪部分数据是紧急数据; 40字节头部选项: 暂时忽略;
确认应答机制(ACK)
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次从哪里开始发。
超时重传机制
1.接收方没有收到
- 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
2.应答ACK丢失
- 去重数据:接收方会收到很多重复数据,TCP协议需要能够识别出哪些包是重复的包,并且把重复的丢弃掉。此时可以利用前面提到的序列号,就可以很容易做到去重的效果。
- 超时时间:一般选择往返时间,但这个时间的长短,随着网络环境的不同是有差异的。 TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
滑动窗口
对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。(左图所示)
那么我们一次发送多条数据, 就可以大大的提高性能,其实是将多个段的等待时间重叠在一起了(右图所示)
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000 个字节(四个段).
- 发送前四个段的时候,不需要等待任何ACK,直接发送;
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
- 窗口越大, 则网络的吞吐率就越高;
如果出现丢包,如何进行重传?
情况一:数据包已抵达,ACK被丢了(不要紧,可以通过后续ACK进行确认)
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认;
情况二:数据包丢了(快重传)
当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 “我想要的是 1001” 一样;如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 - 2000 重新发送;这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;这种机制被称为 “高速重发控制”(也叫 “快重传”)。
流量控制
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制(FlowControl);
- 接收端将自己可以接收的缓冲区大小放入TCP 首部中的 “窗口大小” 字段,通过ACK端通知发送端;
- 窗口大小字段越大,说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后,就会减慢自己的发送速度;
- 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
拥塞控制
由于网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,不能贸然发送大量的数据。因此,TCP引入慢启动机制,先发少量的数据,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。从此引入拥塞窗口概念。
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
- 每次发送数据包的时候,将拥塞窗口和接收窗口大小做比较, 取较小的值作为实际发送的窗口(指数级增长);
- 为了不增长的那么快,引入一个叫做慢启动的阈值;当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长;
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
少量的丢包,仅触发超时重传;大量的丢包,认为是网络拥塞;
当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
拥塞控制归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
延迟应答
如果接收端接收到数据立刻返回ACK应答,此时返回的窗口可能比较小,因此需要延迟应答。由于窗口越大,网络吞吐量越大,传输效率越高,所以延迟应答本质是为了保证网络不拥塞的情况下尽量提高传输效率。
所有包都可以延迟应答吗?答:肯定不是。
- 数量限制:每隔N个包就应答一次(N一般取2)
- 时间限制:超过最大延迟时间就应答一次(一般取200ms)
捎带应答
捎带应答是指在同一个TCP包中即发送数据又发送确认应答的一种机制。接受数据后如果立刻返回数据,就无法实现捎带应答,所以捎带应答是在延迟应答的基础上进行的。由此,网络的利用率会提高,计算机的负荷也会减轻。
三次挥手过程中可以捎带应答吗?答:前两次不可以,第三次可以。
面向字节流
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时 机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接,既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读 数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
TCP异常情况
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN,正常走四次挥手流程。
- 机器重启:和进程终止的情况相同。
- 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。
3.连接管理——三次握手
三次握手:建立连接过程中,一共需要来回三次数据交互
主动发送方(客户端)执行主动打开,被动接收方(服务器)执行被动打开。
开始时:客户端处于CLOSED状态;服务器执行ServerSocket ss = new ServerSocket()从CLOSED状态转换为LISTEN状态。
- 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端状态从:CLOSED → SYN_SENT。
在SYN报文段中,标志位:SYN=1,ACK=0;初始序号seq=x,确认号ack = 0;该报文段不能携带数据,但要消耗掉一个序号。
- 第二次握手:服务器收到客户端的SYN 报文之后,会以自己的 SYN 报文作为应答,并且指定自己的初始化序列号 ISN。同时会把客户端的 ISN + 1作为确认号ack的值,表示自己已经收到了客户端的 SYN,此时服务器状态从:LISTEN → SYN_RCVD的状态。
在SYN + ACK报文段中,标志位:SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y;该报文段不能携带数据,但要消耗掉一个序号。
- 第三次握手:客户端收到 SYN报文之后,会发送一个 ACK 报文,把服务器的 ISN + 1 作为 ack 的值,表示已经收到了服务端的 SYN + ACK 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED状态,此时,双方已建立起了连接。
在ACK报文段中,标志位:SYN = 0,ACK = 1,确认号ack = y+1,序号seq = x+1;该报文段可以携带数据,不携带数据则不消耗序号。
应用层代码
客户端:Socket s = new Socket(host, port),直接从CLOSED → ESTABLISHED;
服务器:ServerSocket ss = new ServerSocket(),从CLOSED → LISTEN;Socket s = ss.accept(),从SYN_RCVD → ESTABLISHED。
为什么需要三次握手,而不是两次也不是四次?
- 第一次握手:客户端发送网络包,服务端收到了。
服务器得出结论:客户端的发送能力、服务端的接收能力是正常的。 - 第二次握手:服务端发包,客户端收到了。
客户端得出结论:服务器的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。 - 第三次握手:客户端发包,服务端收到了。
服务器得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
所以,两次握手并不能完成建立连接,四次握手会造成资源的浪费。
4.连接管理——四次挥手
四次挥手:终止连接过程中,一共需要来回四次数据交互
为了避免服务器出现大量TIME_WAIT,一般主动关闭有客户端发起。
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端状态从ESTABLISHED → FIN_WAIT1。
即发出FIN报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务器的确认。
- 第二次挥手:服务器收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK报文的序列号值,表明已经收到客户端的报文了,此时服务器状态从ESTABLISHED → CLOSE_WAIT 。
即服务器收到FIN报文段后发出ACK报文段(ACK=1,确认号ack=u+1,序号seq=v),服务器进入CLOSE_WAIT(关闭等待)状态。此时TCP处于半关闭状态。客户端收到服务器的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务器发出FIN报文段。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端状态从CLOSE_WAIT → LAST_ACK。
即服务器没有需要向客户端发出的数据,服务器发出FIN报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务器进入LAST_ACK(最后确认)状态,等待客户端的确认。
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务器的序列号值 +1 作为自己 ACK报文的序列号值,此时客户端处于 TIME_WAIT 状态,需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED状态。服务器收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务器的FIN报文段后,对此发出ACK报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过2MSL后,客户端才进入CLOSED状态。
收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。
为什么要进行四次挥手?
在关闭连接时,当服务端收到FIN报文后,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文。因此不能一起发送。故需要四次挥手。
为什么要等待2MSL?
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
当TCP执行一个主动关闭,并发送最后一个ACK,该连接必须在TIME_WAIT状态停留2倍的MSL时间。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。TIME_WAIT状态就是用来重发可能丢失的ACK报文。
5.TCP状态转换图
- 虚线表示服务端的状态变化情况;
- 实线表示客户端的状态变化情况;
- CLOSED是一个假想的起始点, 不是真实状态。
6.TCP与UDP的对比
TCP | UDP | |
---|---|---|
可靠性 | 可靠 | 不可靠 |
连接性 | 有连接 | 无连接 |
报文 | 面向字节流 | 面向数据报 |
效率 | 传输效率低 | 传输效率高 |
缓冲区 | 无发送缓冲区,有接受缓冲区 | 有发送缓冲区,有接受缓冲区 |
报头 | 简单 | 复杂 |
应用层协议 | HTTP/HTTPS/SSH/SMTP | DNS/TFTP/DHCP/BOOTP |