1.概述
1.1 传输层的服务和协议
(1)传输层为允许在不同主机(Host)上的
进程提供了一种逻辑通信机制
(2)端系统(如手机、电脑)运行传输层协议
发送方:将来自应用层的消息进行封装并向下提交给 网络层接收方:将接收到的Segment进行组装并向上提交给应用层(3)传输层可以为应用提供多种协议,如UDP、TCP
逻辑通信机制:传输层提供的一种抽象服务,它使得不同主机上的应用程序能够直接进行数据传输,而无需关注底层网络的实现细节
1.2 传输层&网络层关系
网络层:提供主机之间的逻辑通信机制;传输层:提供应用程序之间的逻辑通信机制- 传输层位于网络层之上,依赖于网络层提供的服务,并对网络层服务进行(可能的)增强
1.3 传输层协议
用户数据报协议(UDP):可靠、按序的交付服务
- 拥塞控制
- 流量控制
- 连接建立
传输控制协议(TCP):不可靠的交付服务
- 基于
尽力而为的网络层,没有做(可靠性方面的)扩展
两种协议均不保证:
- 延迟
- 带宽
1.4 套接字(Socket)
概念:套接字作为应用层和传输层之间的接口,充当了应用进程与网路协议栈(如TCP/IP协议栈)之间的桥梁
作用:
- 1.提供端点通信:套接字为每个通信进程提供了一个唯一的标识符(IP地址+端口号),这使得不同主机上的应用进程能够相互识别并进行通信
- 2.封装传输层功能:应用进程通过套接字提供的接口进行数据的发送和接收(send和receive函数);对于TCP套接字,套接字接口提供了来连接管理的功能(connect、accept、close等函数)
- 3.简化网络编程:套接字将底层网络协议的复杂性进行疯转,应用程序不需要了解TCP/IP协议栈的具体实现细节,只需要调用套结字提供的功能就可以进行网络通信
下面是基于Java中的套接字实现的TCP回显服务器。Java中的Socket是基于操作系统提供的套接字实现的,并进行了进一步的封装
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.*;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 38917
* Date: 2025-03-18
* Time: 17:29
*/
public class TCPEchoSever {
//
private final ServerSocket socket;
//线程池
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(4,8,1,
TimeUnit.MINUTES, new ArrayBlockingQueue<>(1024),Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
//
public TCPEchoSever(int port) throws IOException {
socket = new ServerSocket(port);
}
//启动服务器
protected void start() throws IOException {
System.out.println("服务器启动");
while (true){
//将服务器和客户端连接
//accept()有阻塞效果,等待客户端建立联系
Socket clientSocket = socket.accept();
//每与一个客户端建立连接,都创建一个线程来执行客户端的请求
executor.execute(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
//
public void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 服务器上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
//inputStream从网卡读数据
try(InputStream inputStream = clientSocket.getInputStream();
//OutputStream往网卡写数据
OutputStream outputStream = clientSocket.getOutputStream()) {
//从网卡读数据
//byte[] array = new byte[1024];int ret = inputStream.read(array);
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scanner = new Scanner(inputStream);
while (true){
//读取完毕,当客户端下线的时候产生
//在用户输入之前,hasNext()有阻塞效果
//当客户端断开连接时,scanner.hasNext()返回false并中断循环
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
//1.读取请求并解析
//用户传过来的请求必须带有空白符,没有的话就会阻塞
String request = scanner.next();
//2.计算响应
String response = process(request);
//3.返回响应
//outputStream.write(response.getBytes(),0,response.getBytes().length);//这个方式不方便添加空白符
//通过PrintWriter来封装outputStream
//添加\n
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
//打印日志
System.out.printf("[%s:%d] request:%s,response:%s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
//计算响应
protected String process(String request) {
return request;
}
//
public static void main(String[] args) throws IOException {
TCPEchoSever sever = new TCPEchoSever(9090);
sever.start();
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 38917
* Date: 2025-03-18
* Time: 17:30
*/
public class TCPEchoClient {
private final Socket socket;
//
public TCPEchoClient(String severIp,int port) throws IOException {
socket = new Socket(severIp,port);
}
//
public void start(){
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//读取控制台
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNetWork = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true){
System.out.print("->");
//在用户输入之前,hasNext()有阻塞效果
if (!scannerConsole.hasNext()){
break;
}
//1.从控制台输入请求
String request = scannerConsole.next();
//2.发送请求
//让请求的结尾有\n
printWriter.println(request);
//刷新缓冲区
printWriter.flush();
//3.从服务器读取响应
String response = scannerNetWork.next();
//4.将响应打印到控制台
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//
public static void main(String[] args) throws IOException {
TCPEchoClient client = new TCPEchoClient("127.0.0.1",9090);
client.start();
}
}
2.复用&分用
复用:在发送端,传输层将从不同应用进程接收到的数据合并成一个数据流,然后向下提交给网络层的过程。具体过程如下:
- 1.接收多个应用进程的数据:在一台主机上,可能有多个应用进程同时进行网络通信,例如浏览器、邮件客户端、文件传输等
- 2.分配端口号:每个应用进程都会被分配一个唯一的端口号,用于标识数据来源
- 3.封装数据:传输层将来自不同应用进程的数据分割成适当大小的数据段(Segment),并在每个数据段的头部添加源端口号和目的端口号等信息
- 4.合并数据流:这些数据段被合并成一个数据流,传递给网络层进行进一步传输
分用(解复用):在接收端,传输层将来自网络层的数据根据端口号分解,并将其正确地向上提交给对应的应用进程的过程。具体过程如下:
- 1.接收数据流:网络层将接收到的数据传递给传输层
- 2.解析端口号:传输层解析每个数据段的头部信息,提取目的端口号
- 3.分配数据:根据目的端口号,传输层将数据段分配给相应的应用进程
3.UDP
3.1 协议格式

Datagram是
用户数据报协议(UDP)的数据传输单元,它将来自应用层的数据封装成数据报并添加UDP头部信息,然后向下提交给网络层
UDP报头由
源目的端口号,UDP长度,UDP校验和组成。每个部分都是16位比特位,所以UDP报头一共占8字节
- 1.源端口号:发送方进程绑定的端口号
- 2.目的端口号:接收方进程绑定的端口号
- 3.UDP长度:一个UDP数据报所能携带的数据(加上报头)最大是2^16字节,即64KB
- 4.校验和:用于校验数据传输过程中是否发生了改变
3.2 特点解析
1.无连接:UDP的套接字在传输数据之前不需要建立连接,发送端直接键该数据发送给接收端,无需进行三次握手2.不可靠传输:不保证数据的可靠传输,也不提供错误检测和纠正机制,数据达到接收端时可能会丢失、重复或乱序达到,需要应用程序自行处理3.无序传输:不保证数据的顺序,接收端可能会接收乱序的数据,应用程序需要自行排序4.无流量控制:不提供流量控制机制,发送端可以以任意速率发送数据,这可能导致网络拥塞和数据丢失5.无拥塞控制:不会根据网络状况调整发送速率,在网络拥塞时可能会加剧拥塞问题6.面向数据报:数据传输以数据报为单位- 7.
全双工:通信双方都能同时收发数据
Question:传输层中已经有了TCP协议,UDP存在的意义是什么?
Answer:
- 1.低延迟:由于没有建立连接和确认应答等机制,UDP的延迟较低
- 2.低开销:UDP头部仅8字节,TCP头部20字节
- 3.简单性:UDP协议远远比TCP协议简单,易于实现和维护
- 4.适合实时应用:适合对实时性要求高、对可靠性要求低的应用,如视频、直播、DNS等
4.TCP
4.1 协议格式

Segment是
传输控制协议(TCP)中的数据传输单元,它将来自应用层的数据分割成多个段(segment),并在每个段中添加TCP头部信息,然后向下提交给网络层
- 1.首部长度:表示TCP头部长度。以4字节为一个单位,用4个二进制比特位表示,表示的范围是((0~15) * 4)字节。因为TCP头部前20字节是固定的,所以TCP头部的长度范围是20 ~ 60字节
- 2.保留:为未来新增功能预留空间(4个比特位)
- 3.选项:扩展TCP功能,最大长度是40字节
- 4.填充:保证TCP头部的长度是4的整数倍
- 5.标志位:URG,ACK,PSH,RST,SYN,FIN(后面再详细介绍)
4.2 特点解析
4.2.1 确认应答/(确认)序列号
- 接收方接收到数据后,向发送方返回一个确认信号(ack),告诉发送方数据被成功接收。ACK报文段只是作为确认使用的,一般来说不携带应用层数据(载荷),也就是说只有报头部分。但有可能和其他的数据进行合并,这个是后话

- 另外,TCP将每个字节的数据都进行了编号,叫做序列号,保证数据到达接收方时不会混乱
- 其次,每个ACK报文也有一个确认序列号,例如ACK(1001),其一是告诉发送方1000之前的数据我以及收到了;其二是让发送方从1001开始发送

Question:当后发的数据(1001~2000)先到达服务器时,ACK是否会返回ACK(2001)?
Answer:其实服务器不会返回ACK(2001),因为数据(1~1000)还没到达,也就是说ACK(1001)都还没返回给客户端,凭什么返回ACK(2001)。
那么此时又引出一个问题,既然ACK(1001)没返回,客户端为什么可以发送数据(1001 ~2000)?
这是因为TCP协议发送数据时,不是一条一条发送的,这个到后面讲滑动窗口时再细讲
4.2.2 超时重传
TCP虽然号称可靠传输,但实际上数据不可能100%传输到对端,这光靠代码是无法解决的

遇到上述情况时,会触发TCP的超时重传机制
发送方等待一段时间后,没有收到服务器返回的ACK,那么就会默认该数据丢包了,会再次发送该数据,如果依然没有收到ACK,会再次重发,但每次重发的时间间隔会拉长,达到一定次数后就不再重重发。
Question:如果数据成功达到了,但是ACK丢包了,怎么办?
Answer:虽然客户端会重发数据,但是服务器缓冲区会对接收的数据进行检查,相同序列号的数据不会接收
4.2.3 连接管理/状态转换
1.三次握手
- SYN:客户端发起连接请求
- ACK:服务器表示你的请求我收到了,但到底要不要连接,ACK无法决定;SYN:服务器决定与服务器建立连接(一般来说服务器不会拒绝连接请求,除非服务器繁忙,所以ACK和SYN一般是同步返回给客户端的)
- ACK:客户端告诉服务器,你的ACK+SYN我也收到了
Question:两次挥手能不能建立连接?四次挥手又那不能?
Answer:两次不行,四次多余
我举例说明,此时张三和李四开黑,因为不在同一个地方,所以要开麦交流,那么在开黑之前需要确定双方的麦克风和听筒都没问题

至于四次握手没必要我上面以及已经说过了,服务器的ACK和SYN是可以合并发送的,能一次发送就不两次
2.三次握手的状态转换
- 1.服务器和客户端都没启动
- 2.服务器启动,客户端未启动
> - 3.服务器和客户端都启动
3.四次挥手&状态转换

- FIN:客户端调用close(也可能是服务器)并发送断连请求
- ACK:服务器收到FIN后返回ACK告诉客户端:我收到了你的断连请求,并进入CLOSE_WAIT状态
- FIN:服务器进入CLOSE_WAIT状态后需要处理完之前的数据,再调用close并向客户端发送FIN确认断连。然后进入LAST_ACK状态,等待客户端发送最后的ACK
- ACK:客户端收到FIN后进入TIME_WAIT状态,同时发送ACK
注意:当服务器收到ACK之后,就进入CLOSED状态彻底关闭连接; 而客户端会等待一段时间后才进入CLOSED状态,为了确保服务器收到ACK
Question:服务器的ACK和FIN能不能合并?
Answer:不能,因为ACK和FIN发送的时间大概率不同步,服务器需要处理完之前的数据才能发送FIN;如果正好处理完毕,ACK和FIN也有可能同步发送。但是一般来说ACK和FIN是不同步的,所以一般叫做四次挥手
4.2.4 滑动窗口(效率机制)
确认应答/超时重传/连接管理都是安全机制,但也会降低传输效率。滑动窗口就是在保证可靠传输的基础上,尽可能地提高传输效率。 根据确认应答机制,客户端每发送一个请求都需要收到服务器的确认应答报文后才会传输发送下一条请求
1.原理介绍
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)

窗口大小:无需等待确认应答还可以继续发送请求的最大值,例如上图可以连续发送4个请求而不需要ACK,那么窗口大小就是4

发送缓冲区:既然是一次性发送多个请求,那么操作系统会维护一个发送缓冲区来统计哪些数据没有被应答2.丢包处理
Question1:如果ACK丢包了,怎么办? 答案:ACK丢包问题不大。例如,服务器返回了ACK(4001),表示前4000序列号的数据已经被接收,即使ACK(1001,2001,3001)都丢包也没啥影响。ACK全部丢失的情况几乎不会发生,因为正常情况下丢包本身就是小概率时间,更何况全部丢包
Question2:如果数据(2001~4000)已经被接收,而数据(1 ~ 1000)一直没有到达,此时窗口会向后滑动吗? 答案:不会。此时服务器会重复发送ACK(1001)来向客户端索要数据,当ACK(1001)的返回次数超过一定阈值时,客户端会认为数据(1~1000)不是卡在半路,而是丢包了,客户端会重新发送数据
这叫做快速重传机制
4.2.5 流量控制&拥塞控制(安全机制)
1.流量控制
接收方处理数据的速度是有限的。如果发送方发的太快,导致接收方的缓冲区被打满,这个时候如果发送方继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应
因此TCP支持根据接收方的处理能力,来决定发送方的发送速度。例如,接收方数据缓冲区的大小还剩3000字节,那么接收方会将ACK中的窗口部分设置为3000,告诉发送方你应该将滑动窗口的大小设置为3000

这就叫做流量控制机制2.拥塞控制
网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据

流量控制&拥塞控制区别以及谁来决定窗口大小
流量控制是根据接收方的实际情况来反馈窗口大小;而拥塞控制是根据网络的拥塞情况来反馈窗口大小。取两种机制的最小值作为滑动窗口的大小
4.2.6 延时应答(效率机制)
如果接受方收到请求后就立刻返回ACK,此时接受发的数据缓冲区的剩余空间可能比较小,那么ACK的窗口大小就比较小。我们不妨让ACK稍作等待再返回,这时候接受方已经处理掉部分数据了,缓冲区的剩余空间就大一些,ACK返回的窗口大小就可以大一些。这样做是为了让发送方可以一次性发送更多的数据,提高传输效率。
至于具体ACK延时多久才返回,每个操作系统设置的阈值不同。
4.2.7 捎带应答
在延时应答的基础上,ACK可以携带接受方的响应数据一起返回

这样的好处是减少纯ACK的返回次数
当然,如果这段时间内实在没有可以携带的数据,ACK也不可能一直等到有数据才返回,而是独自返回
4.2.8 粘包问题
这不是TCP协议独有的问题,而是所有面向字节流的协议都会有的问题。
假设发送方的滑动窗口大小是10000字节,虽然发送方是1000字节为单位进行发送,但这些数据到达接受方的数据缓冲区后,仍然会揉在一起。这时候就需要区分出多少个字节是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界
- 1.对于定长的数据包,每次按照固定长度读取即可
- 2.对于变长的数据包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置
- 3.对于变长的数据包,还可以引入特殊符号作为分界线(可以参考我的TCP服务器代码Java EE(13)——网络编程——UDP/TCP回显服务器)
4.2.9 异常情况处理
1.进程崩溃
会进行正常的四次挥手,没啥问题2.主动关机
关机之前系统会强杀进程,此时也会触发四次挥手,但四次挥手未必能执行完毕
(1)四次挥手执行完毕,没啥问题
(2)四次挥手没执行完毕

服务器收到FIN后会返回ACK,然后再返回FIN,只不过这个FIN将不会得到客户端的回应。那么服务器就会重传FIN,重传一定次数后依然没有结果,服务器会单方面结束连接3.瞬间断电
(1)接收方断电 发送方发现没有ACK了。就会进行超时重传,依然没有ACK的话,就会进行"复位连接" 复位连接的意思是,发送方和接收方的所有数据全部重置(不会再次进行三次挥手) 复位连接依然没有结果,就会单方面结束连接
(2)发送方断电 接收方本来就在等待发送方发送数据,但迟迟没有数据发送过来,接收方就会发送一个"心跳包"(心跳包是周期性发送的没有实际数据的包)来询问发送方的状态,如果判定对方没有"心跳",就会进行复位连接,然后单方面断开连接
4.网络断开
断电是一方存在,一方不存在
网络断开是两方都存在,那么发送方会经历上述发送方断电的过程;接收方会经历接收方断电的过程



> - 3.服务器和客户端都启动

4669

被折叠的 条评论
为什么被折叠?



