(1)传输层为允许在不同主机(Host)上的
进程提供了一种逻辑通信机制 (2)端系统(如手机、电脑)运行传输层协议
发送方:将来自应用层的消息进行封装并向下提交给 网络层接收方:将接收到的Segment进行组装并向上提交给应用层(3)传输层可以为应用提供多种协议,如UDP、TCP
逻辑通信机制:传输层提供的一种抽象服务,它使得不同主机上的应用程序能够直接进行数据传输,而无需关注底层网络的实现细节
网络层:提供主机之间的逻辑通信机制;传输层:提供应用程序之间的逻辑通信机制
用户数据报协议(UDP):可靠、按序的交付服务
传输控制协议(TCP):不可靠的交付服务
尽力而为的网络层,没有做(可靠性方面的)扩展
两种协议均不保证:
概念:套接字作为应用层和传输层之间的接口,充当了应用进程与网路协议栈(如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();
}
}
复用:在发送端,传输层将从不同应用进程接收到的数据合并成一个数据流,然后向下提交给网络层的过程。具体过程如下:
分用(解复用):在接收端,传输层将来自网络层的数据根据端口号分解,并将其正确地向上提交给对应的应用进程的过程。具体过程如下:


Datagram是
用户数据报协议(UDP)的数据传输单元,它将来自应用层的数据封装成数据报并添加UDP头部信息,然后向下提交给网络层 UDP报头由源目的端口号,UDP长度,UDP校验和组成。每个部分都是16位比特位,所以UDP报头一共占8字节
1.无连接:UDP的套接字在传输数据之前不需要建立连接,发送端直接键该数据发送给接收端,无需进行三次握手2.不可靠传输:不保证数据的可靠传输,也不提供错误检测和纠正机制,数据达到接收端时可能会丢失、重复或乱序达到,需要应用程序自行处理3.无序传输:不保证数据的顺序,接收端可能会接收乱序的数据,应用程序需要自行排序4.无流量控制:不提供流量控制机制,发送端可以以任意速率发送数据,这可能导致网络拥塞和数据丢失5.无拥塞控制:不会根据网络状况调整发送速率,在网络拥塞时可能会加剧拥塞问题6.面向数据报:数据传输以数据报为单位全双工:通信双方都能同时收发数据
Question:传输层中已经有了TCP协议,UDP存在的意义是什么? Answer:

Segment是
传输控制协议(TCP)中的数据传输单元,它将来自应用层的数据分割成多个段(segment),并在每个段中添加TCP头部信息,然后向下提交给网络层


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

遇到上述情况时,会触发TCP的超时重传机制
发送方等待一段时间后,没有收到服务器返回的ACK,那么就会默认该数据丢包了,会再次发送该数据,如果依然没有收到ACK,会再次重发,但每次重发的时间间隔会拉长,达到一定次数后就不再重重发。
Question:如果数据成功达到了,但是ACK丢包了,怎么办? Answer:虽然客户端会重发数据,但是服务器缓冲区会对接收的数据进行检查,相同序列号的数据不会接收
1.三次握手
Question:两次挥手能不能建立连接?四次挥手又那不能? Answer:两次不行,四次多余
我举例说明,此时张三和李四开黑,因为不在同一个地方,所以要开麦交流,那么在开黑之前需要确定双方的麦克风和听筒都没问题

至于四次握手没必要我上面以及已经说过了,服务器的ACK和SYN是可以合并发送的,能一次发送就不两次
2.三次握手的状态转换

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

3.四次挥手&状态转换
注意:当服务器收到ACK之后,就进入CLOSED状态彻底关闭连接; 而客户端会等待一段时间后才进入CLOSED状态,为了确保服务器收到ACKQuestion:服务器的ACK和FIN能不能合并? Answer:不能,因为ACK和FIN发送的时间大概率不同步,服务器需要处理完之前的数据才能发送FIN;如果正好处理完毕,ACK和FIN也有可能同步发送。但是一般来说ACK和FIN是不同步的,所以一般叫做四次挥手
确认应答/超时重传/连接管理都是安全机制,但也会降低传输效率。滑动窗口就是在保证可靠传输的基础上,尽可能地提高传输效率。 根据确认应答机制,客户端每发送一个请求都需要收到服务器的确认应答报文后才会传输发送下一条请求
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)不是卡在半路,而是丢包了,客户端会重新发送数据 这叫做快速重传机制
1.流量控制
接收方处理数据的速度是有限的。如果发送方发的太快,导致接收方的缓冲区被打满,这个时候如果发送方继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应
因此TCP支持根据接收方的处理能力,来决定发送方的发送速度。例如,接收方数据缓冲区的大小还剩3000字节,那么接收方会将ACK中的窗口部分设置为3000,告诉发送方你应该将滑动窗口的大小设置为3000
这就叫做流量控制机制
2.拥塞控制
网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据
流量控制是根据接收方的实际情况来反馈窗口大小;而拥塞控制是根据网络的拥塞情况来反馈窗口大小。取两种机制的最小值作为滑动窗口的大小
如果接受方收到请求后就立刻返回ACK,此时接受发的数据缓冲区的剩余空间可能比较小,那么ACK的窗口大小就比较小。我们不妨让ACK稍作等待再返回,这时候接受方已经处理掉部分数据了,缓冲区的剩余空间就大一些,ACK返回的窗口大小就可以大一些。这样做是为了让发送方可以一次性发送更多的数据,提高传输效率。
至于具体ACK延时多久才返回,每个操作系统设置的阈值不同。
在延时应答的基础上,ACK可以携带接受方的响应数据一起返回

这样的好处是减少纯ACK的返回次数
当然,如果这段时间内实在没有可以携带的数据,ACK也不可能一直等到有数据才返回,而是独自返回
这不是TCP协议独有的问题,而是所有面向字节流的协议都会有的问题。
假设发送方的滑动窗口大小是10000字节,虽然发送方是1000字节为单位进行发送,但这些数据到达接受方的数据缓冲区后,仍然会揉在一起。这时候就需要区分出多少个字节是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界
1.进程崩溃
会进行正常的四次挥手,没啥问题2.主动关机
关机之前系统会强杀进程,此时也会触发四次挥手,但四次挥手未必能执行完毕
(1)四次挥手执行完毕,没啥问题
(2)四次挥手没执行完毕
服务器收到FIN后会返回ACK,然后再返回FIN,只不过这个FIN将不会得到客户端的回应。那么服务器就会重传FIN,重传一定次数后依然没有结果,服务器会单方面结束连接
3.瞬间断电(1)接收方断电 发送方发现没有ACK了。就会进行超时重传,依然没有ACK的话,就会进行"复位连接" 复位连接的意思是,发送方和接收方的所有数据全部重置(不会再次进行三次挥手) 复位连接依然没有结果,就会单方面结束连接 (2)发送方断电 接收方本来就在等待发送方发送数据,但迟迟没有数据发送过来,接收方就会发送一个"心跳包"(心跳包是周期性发送的没有实际数据的包)来询问发送方的状态,如果判定对方没有"心跳",就会进行复位连接,然后单方面断开连接
4.网络断开
断电是一方存在,一方不存在
网络断开是两方都存在,那么发送方会经历上述发送方断电的过程;接收方会经历接收方断电的过程