一个真实的 HTTP POST 请求长这样
java
POST /api/user/login HTTP/1.1
Host: api.xxx.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: application/json, text/plain, */*
Content-Type: application/json; charset=UTF-8
Content-Length: 56
Connection: keep-alive
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Cookie: SESSIONID=abc123456; userId=1001
Origin: https://www.xxx.com
Referer: https://www.xxx.com/login
{"username":"zhangsan","password":"123456","rememberMe":true}
HTTP 请求固定由 3 部分组成:
1. 请求行(第一行)
GET /user HTTP/1.1
- 方法:GET / POST / PUT / DELETE
- 路径:/user
- 协议版本:HTTP/1.1
2. 请求头(Header)
Host: api.xxx.com
Content-Type: application/json
Authorization: Bearer xxxxx
键值对格式,告诉服务器:我是谁、我要什么、我发什么格式、长度多少...
3. 空行(必须有!)
(这里空一行)
HTTP 规定:头和体之间必须空一行,服务器读到空行才开始读 body。
4. 请求体(Body)
{"id":1,"name":"test"}
GET 一般没有 BodyPOST/PUT 才有
常见问题总结:
1 Accept与Content-Type区别
Accept: application/json, text/plain, */*
-
作用:客户端向服务器声明「我能理解的响应数据格式优先级」
-
优先要
application/json格式的响应 -
其次可以接受
text/plain纯文本 -
*/*兜底:任何格式我都能兼容
-
-
它只管「响应 」,完全不涉及「请求体」的格式。
Content-Type: application/json; charset=UTF-8
-
作用:客户端向服务器声明「我这次请求的请求体(body)是什么格式」
-
明确告诉服务器:我发的 body 是 JSON 格式,编码是 UTF-8
-
服务器收到后,会用对应的解析器(比如 JSON 解析器)来处理请求体,而不是用表单、XML 等其他解析器
-
-
它只管「请求体」,和服务器返回什么格式的响应完全无关。
2 http中请求头(Request Headers)组成主要有什么?
Host
Host: api.xxx.com
-
必须有
-
表示你要访问的服务器域名
-
一台服务器可以有多个网站,靠 Host 区分
User-Agent
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
-
客户端身份标识
-
服务器知道你是:浏览器 / Java / 小程序 / App / 爬虫
Accept
Accept: application/json
-
告诉服务器:我希望你返回 JSON 格式给我
-
不写也可以,但规范要写
Content-Type(最重要之一)
Content-Type: application/json; charset=UTF-8
-
表示请求体 body 是什么格式
-
POST 必须带,否则后端不知道怎么解析
常见 3 种:
-
application/json→ JSON 格式(现在最常用) -
application/x-www-form-urlencoded→ 普通表单 -
multipart/form-data→ 文件上传
Content-Length
Content-Length: 56
-
请求体 body 的字节长度
-
服务器读到这么多字节就知道 body 结束了
-
你用 Hutool/OkHttp 会自动计算,不用手写
Connection
Connection: keep-alive
-
keep-alive:复用 TCP 连接,不立即断开 -
close:一次请求完就关闭 TCP -
HTTP/1.1 默认 keep-alive
Authorization
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
-
身份凭证
-
登录令牌、Token
-
接口鉴权专用
Cookie
Cookie: SESSIONID=abc123; userId=1001
-
浏览器 / 客户端存储的会话信息
-
用来维持登录状态
Origin
Origin: https://www.xxx.com
-
表示当前请求来自哪个域名
-
跨域 CORS 核心判断依据
Referer
Referer: https://www.xxx.com/login
-
从哪个页面跳过来的
-
防盗链、日志统计用
3 开发中最常用的 MIME 类型分类详解
文本类(最常用,接口开发核心)
| MIME 类型 | 说明 | 典型场景 |
|---|---|---|
application/json |
JSON 格式数据,前后端接口的「标准格式」 | RESTful 接口的请求 / 响应体、AJAX 数据传输 |
text/plain |
纯文本,无格式 | 简单字符串响应、日志、纯文本文件 |
text/html |
HTML 网页 | 浏览器打开网页、服务端渲染页面返回 |
application/xml / text/xml |
XML 格式数据 | 传统 SOAP 接口、银行 / 金融系统的老接口 |
application/x-www-form-urlencoded |
表单默认编码格式 | 普通 HTML 表单提交(key=value 格式) |
multipart/form-data |
表单二进制编码 | 文件上传、带文件的表单提交 |
text/css |
CSS 样式表 | 网页样式文件 |
application/javascript |
JavaScript 脚本 | 网页 JS 文件 |
二进制 / 文件类(文件传输、下载常用)
| MIME 类型 | 说明 | 典型场景 |
|---|---|---|
application/octet-stream |
通用二进制流 | 任意未知格式的文件下载、强制浏览器下载 |
application/pdf |
PDF 文件 | PDF 预览 / 下载 |
image/jpeg / image/png / image/gif |
图片格式 | 图片展示、上传 |
audio/mpeg / video/mp4 |
音视频格式 | 音视频播放 |
application/zip / application/x-rar-compressed |
压缩包 | 压缩文件下载 |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
Excel (.xlsx) | Excel 文件下载 |
application/msword |
Word (.doc) | Word 文件下载 |
其他高频实用类型
| MIME 类型 | 说明 | 典型场景 |
|---|---|---|
*/* |
通配符,代表「所有格式都接受」 | Accept 头的兜底配置,兼容任意响应 |
application/problem+json |
JSON 格式的错误详情 | REST 接口的标准化错误响应(RFC 7807) |
application/json;charset=UTF-8 |
带编码的 JSON | 明确指定 JSON 的字符编码,避免乱码 |
4 在head与body中间必须要有一个空行
(这里是空行,不能少)
作用:
-
头和体的分隔符
-
服务器读到空行,就知道:接下来全是 body 内容
5 Java 原生 Socket 手动拼接 HTTP POST 报文 + 生产级代码
java
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* 原生 Socket 手动拼接 HTTP POST 请求
* 这就是所有 HTTP 工具的底层原理!
*/
public class ManualHttpPostBySocket {
// 生产环境必须配置的超时
private static final int CONNECT_TIMEOUT = 3000;
private static final int SO_TIMEOUT = 5000;
public static String sendPost(String host, int port, String path, String jsonBody) {
// 1. 拼接完整 HTTP POST 报文(最关键!)
String httpRequest = buildHttpRequest(host, path, jsonBody);
// 2. 通过 Socket(TCP) 发送
try (Socket socket = new Socket()) {
// 连接超时
socket.connect(new java.net.InetSocketAddress(host, port), CONNECT_TIMEOUT);
// 读取超时
socket.setSoTimeout(SO_TIMEOUT);
try (OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) {
// 发送 HTTP 协议报文
out.write(httpRequest.getBytes(StandardCharsets.UTF_8));
out.flush();
// 读取服务器返回的 HTTP 响应
byte[] buffer = new byte[8192];
int len = in.read(buffer);
if (len > 0) {
return new String(buffer, 0, len, StandardCharsets.UTF_8);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 手动拼接完整 HTTP POST 报文
* 这里就是 HTTP 协议的真面目!
*/
private static String buildHttpRequest(String host, String path, String jsonBody) {
// HTTP 请求规定:头和体之间必须空一行 \r\n\r\n
return "POST " + path + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"User-Agent: Java-Manual-Http\r\n" +
"Content-Type: application/json; charset=UTF-8\r\n" +
"Content-Length: " + jsonBody.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
"Connection: close\r\n" +
"\r\n" + // 必须空行!分割 Header 和 Body
jsonBody;
}
public static void main(String[] args) {
// 测试:发送 POST 请求
String host = "jsonplaceholder.typicode.com"; // 免费测试接口
int port = 80;
String path = "/posts";
String json = "{\"title\":\"手动拼HTTP\",\"body\":\"这是Socket发的\",\"userId\":1}";
String response = sendPost(host, port, path, json);
System.out.println("=== 服务器返回的完整 HTTP 响应 ===");
System.out.println(response);
}
}
你代码发送出去的 HTTP 长这样:
POST /posts HTTP/1.1
Host: jsonplaceholder.typicode.com
User-Agent: Java-Manual-Http
Content-Type: application/json; charset=UTF-8
Content-Length: 62
Connection: close
{"title":"手动拼HTTP","body":"这是Socket发的","userId":1}
服务器返回的 HTTP 响应长这样:
HTTP/1.1 201 Created
Date: ...
Content-Type: application/json
Content-Length: ...
{
"id": 101,
"title": "手动拼HTTP"
}
HTTP 就是一段严格格式的文本
你完全手写拼出来,发给服务器。
HTTP 底层就是 TCP (Socket)
没有任何魔法
-
Socket= TCP 连接 -
你写的 HTTP 报文 = 数据
Hutool/HttpClient 只是帮你拼报文而已
它们底层和这段代码一模一样
-
拼接请求头
-
处理 Content-Length
-
处理空行
-
解析响应
6 TCP 和 HTTP 是什么关系
HTTP 是跑在 TCP 上面的 "业务协议",TCP 只是底层传输通道,本身没有任何格式要求。可以只用 TCP 裸奔,也可以在 TCP 上跑 HTTP、MQTT、Redis、MySQL 等各种协议。TCP 本身没有「请求 / 响应」的概念 ,它只是一个可靠的字节流传输管道。
① TCP(底层通道)
-
只管把字节流可靠地从 A 传到 B
-
不关心你传的是文字、图片、JSON 还是乱码
-
没有格式要求:你发
123、abc、hello都行 -
这叫裸 TCP 通信 / 自定义协议
② HTTP(上层应用协议)
-
是大家约定好的一种消息格式
-
必须长这样:
plaintext
POST /xxx HTTP/1.1 Host: xxx Content-Type: xxx 内容 -
服务器按这个格式解析,才叫 HTTP
-
HTTP 底层一定是 TCP
-
总结就是: HTTP 是「应用层协议」,TCP 是「传输层协议」;HTTP 必须基于 TCP(或 TLS/SSL 封装的 TCP,即 HTTPS)来传输,永远不能脱离 TCP 单独存在。
-
TCP 通讯代码示例(银行 8583 风格)
java// 银行核心系统 TCP 服务端(简化版) @Slf4j public class TcpServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); log.info("银行TCP服务端启动,端口8080"); while (!Thread.currentThread().isInterrupted()) { Socket socket = serverSocket.accept(); // 长连接,每个连接一个线程处理 new Thread(() -> handleClient(socket)).start(); } } private static void handleClient(Socket socket) { try (InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream()) { // 银行标准:长度域解决粘包,前4字节存报文长度 byte[] lengthBuf = new byte[4]; while (in.read(lengthBuf) == 4) { int length = ByteBuffer.wrap(lengthBuf).getInt(); byte[] dataBuf = new byte[length]; in.readFully(dataBuf); // 读完整报文 // 处理8583报文:解密、验签、业务处理 byte[] response = processIso8583(dataBuf); // 回写响应:先写长度,再写数据 out.write(ByteBuffer.allocate(4).putInt(response.length).array()); out.write(response); out.flush(); } } catch (Exception e) { log.error("TCP连接异常", e); } finally { try { socket.close(); } catch (IOException e) { log.error("关闭TCP连接失败", e); } } } private static byte[] processIso8583(byte[] data) { // 银行8583报文处理逻辑:SM2解密、验签、业务处理 return new byte[0]; } }
案例二 与银行前置机采取NC模式TCP交互代码
生产级 TCP 客户端(银行前置 / 银联前置专用)
java
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.ReentrantLock;
/**
* 银行前置 / 银联前置 TCP 客户端(生产级)
* 支持:长连接、自动重连、4字节长度域、线程安全、超时控制
*/
@Slf4j
public class BankFrontTcpClient {
// 基础配置
private final String ip;
private final int port;
private final int connectTimeout = 5000;
private final int readTimeout = 15000;
// TCP 连接
private Socket socket;
private DataOutputStream out;
private DataInputStream in;
// 线程安全(高并发必须加锁)
private final ReentrantLock lock = new ReentrantLock();
// 连接状态
private volatile boolean connected = false;
public BankFrontTcpClient(String ip, int port) {
this.ip = ip;
this.port = port;
}
// -------------------------------------------------------------------------
// 建立连接(长连接)
// -------------------------------------------------------------------------
public void connect() throws Exception {
if (connected && socket != null && socket.isConnected() && !socket.isClosed()) {
return;
}
close();
// 创建 TCP Socket
socket = new Socket();
socket.setTcpNoDelay(true); // 银行前置必须开(低延迟)
socket.setKeepAlive(true); // 长连接保活
socket.setSoTimeout(readTimeout); // 读取超时
// 连接银行前置
socket.connect(new java.net.InetSocketAddress(ip, port), connectTimeout);
// 获取流
out = new DataOutputStream(socket.getOutputStream());
in = new DataInputStream(socket.getInputStream());
connected = true;
log.info("银行前置 TCP 连接成功 → {}:{}", ip, port);
}
// -------------------------------------------------------------------------
// 发送报文 + 接收响应(核心方法)
// 规则:4字节长度头 + 报文
// -------------------------------------------------------------------------
public byte[] sendAndReceive(byte[] requestData) throws Exception {
lock.lock(); // 高并发必须加锁
try {
// 1. 确保连接可用
connect();
// 2. 发送:先发送4字节长度,再发送内容
byte[] lengthBytes = ByteBuffer.allocate(4).putInt(requestData.length).array();
out.write(lengthBytes);
out.write(requestData);
out.flush();
log.info("发送成功,报文长度:{}", requestData.length);
// 3. 接收:先读4字节长度,再读内容
byte[] lenBuf = new byte[4];
int readLen = in.read(lenBuf);
if (readLen != 4) {
throw new IOException("读取长度头失败,连接已断开");
}
// 解析报文总长度
int msgLen = ByteBuffer.wrap(lenBuf).getInt();
if (msgLen <= 0 || msgLen > 1024 * 1024 * 10) { // 10M 限制
throw new IOException("报文长度非法:" + msgLen);
}
// 读取完整报文
byte[] response = new byte[msgLen];
in.readFully(response);
log.info("接收成功,响应长度:{}", msgLen);
return response;
} catch (SocketTimeoutException e) {
log.error("接收银行前置响应超时");
close();
throw e;
} catch (Exception e) {
log.error("通讯异常,准备重连", e);
close();
throw e;
} finally {
lock.unlock();
}
}
// -------------------------------------------------------------------------
// 关闭连接
// -------------------------------------------------------------------------
public void close() {
connected = false;
try {
if (in != null) in.close();
if (out != null) out.close();
if (socket != null) socket.close();
} catch (Exception e) {
log.error("关闭连接异常", e);
}
}
}
三、使用示例(对接银联 / 银行前置)
java
public class TestBankFront {
public static void main(String[] args) throws Exception {
// 1. 创建客户端(银行前置地址)
BankFrontTcpClient client = new BankFrontTcpClient("192.168.1.100", 8888);
try {
// 2. 你的业务报文(8583 / 加密后报文 / 自定义二进制)
byte[] sendData = "你的银行报文或加密串".getBytes("UTF-8");
// 3. 发送并接收
byte[] respData = client.sendAndReceive(sendData);
// 4. 处理响应
String resp = new String(respData, "UTF-8");
System.out.println("银行前置响应:" + resp);
} finally {
client.close();
}
}
}
7 TCP 概述 -- 三次握手 四次挥手
一、TCP 概述:
TCP 是面向连接、可靠、全双工、字节流的传输层协议。核心靠三样保证可靠:
-
序列号 + 确认号
-
超时重传
-
滑动窗口、流量控制、拥塞控制
连接过程分为三部分:
-
三次握手(建立连接)
-
数据传输
-
四次挥手(断开连接)
二、TCP 头部关键字段
-
SYN:同步序列号,用于建立连接
-
ACK:确认号有效
-
FIN:发送方无数据要发,请求关闭
-
RST:强制断开连接(异常关闭)
-
Seq:本方发送数据的起始字节编号
-
Ack:期望收到对方下一个字节的编号 = 对方 Seq + 数据长度
-
Window:滑动窗口大小,流量控制
-
源端口 / 目的端口:对应快递的「寄件人电话 / 收件人电话」,用来区分同一个主机上的不同应用(比如浏览器、微信、游戏)。
-
校验和:检查数据在传输中有没有损坏,就像快递的「防伪码」,坏了就丢弃。
-
PSH:推送位,告诉对方「立刻把数据交给应用层,别缓存」,比如 Telnet 交互。
三、三次握手(Connection Establish)
- 目的
-
同步双方初始序列号 ISN
-
确认双方发送能力、接收能力都正常
-
防止已失效的连接请求报文再次建立连接,浪费资源
- 详细过程
初始状态:
-
客户端:CLOSED
-
服务端:LISTEN
第一次握手(客户端 → 服务端)
-
客户端发送:SYN=1, Seq=client_isn
-
不携带数据
-
客户端状态:CLOSED → SYN_SENT
第二次握手(服务端 → 客户端)
-
服务端回复:SYN=1, ACK=1, Seq=server_isn, Ack=client_isn+1
-
服务端状态:LISTEN → SYN_RCVD
-
含义:
-
ACK:我收到你的 SYN
-
SYN:我也同步我的序列号
-
第三次握手(客户端 → 服务端)
-
客户端发送:ACK=1, Seq=client_isn+1, Ack=server_isn+1
-
客户端状态:SYN_SENT → ESTABLISHED
-
服务端收到后:SYN_RCVD → ESTABLISHED
连接建立完成,双方进入数据传输状态。
- 为什么必须是三次?不能两次?
-
两次只能确认:客户端能发、服务端能收能发
-
但服务端不知道客户端能不能正常接收
-
若只有两次握手,网络延迟导致旧 SYN 到达,服务端直接建立连接,会造成资源浪费
-
如果是「两次握手」:服务器收到 SYN 就直接建连
-
服务器收到这个迟到的
SYN_旧,会直接认为是新的连接请求 ,立刻分配内核资源(socket 缓冲区、进程 / 线程、定时器等),给客户端回一个 SYN+ACK 包,然后就把这个连接设为ESTABLISHED(已建立)状态,等待客户端发数据。 -
但问题来了:
-
客户端 A 早就完成了上一次连接,并且已经关闭了,根本不会再给这个
SYN_旧回 ACK 包。 -
服务器就会一直等,直到超时才会释放这个连接。
-
如果有大量这种延迟的旧 SYN 包(比如网络抖动、恶意攻击),服务器会被无数个「半开连接」占满资源,正常的新连接请求就会被拒绝,这就是资源浪费 ,严重时会导致拒绝服务(DoS)。
-
- 半连接队列 / 全连接队列
-
SYN 队列(半连接):收到 SYN 但未完成三次握手
-
Accept 队列(全连接):已完成三次握手,等待应用 accept
-
SYN 攻击:大量发送 SYN 不回 ACK,占满半连接队列,导致正常请求无法入队
四、数据传输过程(核心机制)
连接建立后双方都是 ESTABLISHED。
- 序列号与确认号规则
-
每发送一字节数据,Seq 自动 +1
-
Ack = 对方 Seq + 数据长度
-
收到 Ack 表示之前所有数据都已被正确接收
-
可靠传输机制
-
超时重传发送后未收到 ACK,超时则重传
-
快速重传收到 3 个重复 ACK,不等超时立即重传
-
滑动窗口允许连续发送多个包,提高吞吐量
-
**流量控制(滑动窗口)**接收方通过 Window 字段告诉发送方 "我还能收多少"
-
拥塞控制慢启动、拥塞避免、快重传、快恢复防止网络拥塞崩溃
-
正常传输流程
发送方发数据 → 接收方回 ACK → 窗口滑动 → 继续发送无丢包则平稳传输;丢包则触发重传。
五、四次挥手(Connection Termination)
- 为什么是四次?
TCP 是全双工的,也就是:
客户端 → 服务器 是一条路
服务器 → 客户端 是另一条路
要断开连接,两条路都要分别关,所以要挥手 4 次。
简单说就是四步:
-
客户端说:我发完了,我要关我的发送通道
-
服务器说:收到,我知道你关了
-
服务器说:我也发完了,我也要关我的发送通道
-
客户端说:收到,我知道你关了
-
详细过程
第一次挥手:客户端 → 服务器(FIN 包)
发送方:客户端
核心动作:发起关闭请求,告诉服务器「我发完了」
| 字段 | 取值 | 含义 |
|---|---|---|
| FIN | 1 | 核心标志位:表示「我(客户端)已经发完所有数据,要关闭我的发送通道,不再给你发数据了」 |
| ACK | 1(强制) | 表示这个包的确认号有效,是对服务器之前发的最后一个数据的确认 |
| SEQ(序号) | X(例:1000) | 客户端当前的发送序号,这个 FIN 包本身占 1 个序号,所以客户端下一个要发的序号是 X+1 |
| ACK_NUM(确认号) | Y(例:5000) | 表示「我已经收到你(服务器)到序号 Y-1 为止的所有数据,下一个期望收到 Y」,是对服务器之前数据的确认 |
状态变化:
-
客户端进入
FIN_WAIT_1状态:等待服务器确认我的 FIN -
服务器收到 FIN 后,进入
CLOSE_WAIT状态:等待自己发完数据,再关闭
第二次挥手:服务器 → 客户端(ACK 包)
发送方:服务器
核心动作:确认收到客户端的 FIN,告诉客户端「我知道你要关了」
| 字段 | 取值 | 含义 |
|---|---|---|
| FIN | 0 | 表示我(服务器)还没发完数据,暂时不关闭我的发送通道 |
| ACK | 1(强制) | 表示确认号有效,专门用来确认客户端的 FIN |
| SEQ(序号) | Y(例:5000) | 服务器当前的发送序号 |
| ACK_NUM(确认号) | X+1(例:1001) | 核心确认:表示「我已经收到你(客户端)的 FIN(序号 X),下一个期望收到 X+1」,正式确认客户端的关闭请求 |
状态变化:
-
服务器进入
CLOSE_WAIT状态:继续给客户端发剩余数据,发完后再发起自己的 FIN -
客户端收到 ACK 后,进入
FIN_WAIT_2状态:等待服务器发完数据,发起 FIN
第三次挥手:服务器 → 客户端(FIN 包)
发送方:服务器
核心动作:服务器发完数据,发起自己的关闭请求,告诉客户端「我也发完了」
| 字段 | 取值 | 含义 |
|---|---|---|
| FIN | 1 | 核心标志位:表示「我(服务器)也发完所有数据,要关闭我的发送通道,不再给你发数据了」 |
| ACK | 1(强制) | 表示确认号有效,是对客户端之前数据的确认 |
| SEQ(序号) | Y(例:5000) | 服务器当前的发送序号,这个 FIN 包本身占 1 个序号,服务器下一个要发的序号是 Y+1 |
| ACK_NUM(确认号) | X+1(例:1001) | 再次确认客户端的 FIN,保证客户端知道服务器收到了关闭请求 |
状态变化:
-
服务器进入
LAST_ACK状态:等待客户端确认我的 FIN -
客户端收到 FIN 后,进入
TIME_WAIT状态:等待 2MSL(最长报文寿命),确保服务器收到 ACK
第四次挥手:客户端 → 服务器(ACK 包)
发送方:客户端
核心动作:确认收到服务器的 FIN,告诉服务器「我知道你关了,连接彻底断开」
| 字段 | 取值 | 含义 |
|---|---|---|
| FIN | 0 | 表示我(客户端)已经关闭发送通道,不再发数据 |
| ACK | 1(强制) | 表示确认号有效,专门用来确认服务器的 FIN |
| SEQ(序号) | X+1(例:1001) | 客户端当前的发送序号(第一次挥手的 FIN 占了 1 个序号,所以 + 1) |
| ACK_NUM(确认号) | Y+1(例:5001) | 核心确认:表示「我已经收到你(服务器)的 FIN(序号 Y),下一个期望收到 Y+1」,正式确认服务器的关闭请求 |
状态变化:
-
客户端收到服务器的 FIN 后,发完这个 ACK,进入
TIME_WAIT状态,等待 2MSL 后彻底关闭 -
服务器收到这个 ACK 后,直接进入
CLOSED状态,彻底断开连接
补充 2 个零基础必懂的关键问题
- 为什么第二次和第三次挥手不能合并?
-
理论上可以合并(服务器发 ACK+FIN 一起发,看起来像 3 次挥手),但逻辑上还是 4 次
-
不能强制合并的原因:服务器收到客户端的 FIN 后,可能还有数据要发给客户端(比如文件还没传完),必须等数据发完,才能发自己的 FIN所以必须先回 ACK 确认,等数据发完,再发 FIN,分成两步
- 为什么客户端要等 2MSL?
-
MSL 是「最长报文寿命」,网络里的包最多存活 MSL 时间
-
等 2MSL 的原因:
-
确保服务器收到最后一个 ACK:如果 ACK 丢了,服务器会重发 FIN,客户端在 2MSL 内会收到,重新发 ACK
-
防止旧连接的包干扰新连接:等 2MSL,旧连接的所有包都消失了,新连接不会收到旧数据
-
六、完整状态流转
客户端状态
CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT1 → FIN_WAIT2 → TIME_WAIT → CLOSED
服务端状态
CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
TCP 连接的核心状态(三次握手涉及的)
| 状态 | 全称 | 核心含义 | 对应阶段 |
|---|---|---|---|
CLOSED |
Closed | 连接完全关闭,无任何连接 | 初始状态 |
LISTEN |
Listen | 服务器监听中,等待客户端连接请求 | 服务器初始状态 |
SYN_SENT |
SYN Sent | 客户端已发送 SYN,等待服务器的 SYN+ACK | 第一次握手后(客户端) |
SYN_RCVD |
SYN Received | 服务器已收到客户端 SYN,发送了 SYN+ACK,等待客户端 ACK | 第二次握手后(服务器) |
ESTABLISHED |
Established | 连接完全建立,可正常收发数据 | 三次握手完成(双方) |
8 什么是粘包、什么是拆包
TODO 下次再补充
9 全双工是什么意思
两边可以同时说话,互不干扰。
用最通俗的话讲
-
单工:只能单向说话例:广播、电视 → 只能它发,你听。
-
半双工 :同一时间只能一方说话例:对讲机你说完 "收到",松开按键,对方才能说话。不能同时说。
-
全双工 :两边可以同时 收发例:打电话、微信语音通话你说话的同时,也能听到对方说话,双向同时进行。
放到 TCP 里是什么意思?
TCP 是全双工协议:
-
客户端 → 服务器 可以发数据
-
服务器 → 客户端 也可以发数据
-
而且这两个方向完全独立、可以同时进行
这就是为什么:
-
客户端有一套自己的序号(发往服务器)
-
服务器有另一套自己的序号(发往客户端)
-
两套序号互不影响
因为是两条独立的 "单向通道" 绑在一起,变成一条全双工连接。