HTTP协议
HTTP协议
从Socket中解析Http协议实现通信_socket报文解析-CSDN博客
HTTP协议核心要素报文格式方法头部与状态码-开发者社区-阿里云
版本

每个版本的核心变化:
0.9:只支持 GET,只能返回纯文本。
1.0:引入请求头/响应头和多种文件格式支持。
1.1:默认使用持久连接以复用 TCP 管道。
2:引入二进制分帧和多路复用以解决队头阻塞。
3:将底层传输协议从 TCP 改为基于 UDP 的 QUIC,彻底解决队头阻塞。
版本对比总结
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输协议 | TCP | TCP | QUIC(基于UDP) |
| 数据格式 | 文本 | 二进制 | 二进制 |
| 连接复用 | 持久连接 | 多路复用(一个TCP连接) | 多路复用(无队头阻塞) |
| 头部压缩 | 无 | HPACK | QPACK |
| 安全性 | 依赖HTTPS(TLS) | 依赖HTTPS(TLS) | 默认加密(TLS 1.3) |
| 服务器推送 | 不支持 | 支持 | 支持 |
| 握手延迟 | 较高(TCP+TLS握手) | 较高(TCP+TLS握手) | 低(0-RTT/1-RTT) |
数据格式的文本和二进制分帧的意思:
- HTTP/1.1 的“文本”是什么意思?
-
它是给人(开发者)看的格式。它的命令、头部、状态码都是用纯ASCII字符书写的,遵循严格的文本行格式。
-
例子:当你发送一个请求时,数据流看起来像这样(人类可读):
GET /index.html HTTP/1.1 Host: www.example.com User-Agent: MyBrowser (一个空行) -
发送时:这段文本会被编码(如UTF-8)成二进制字节流在TCP上传输。
-
接收方:收到二进制流后,需要按行(
\r\n分隔)去解析这段文本,来理解请求方法、路径、头部等信息。这个过程是文本解析。
- HTTP/2 和 HTTP/3 的“二进制”是什么意思?
-
它是给机器(协议栈)处理的格式。它不再使用可读的文本行,而是定义了一组二进制帧(Frame)。
-
一个请求/响应被拆分成多个独立的、有类型和标识的二进制帧(如HEADERS帧、DATA帧)。
-
例子:HTTP/2帧的基本结构:
+-----------------------------------------------+ | 长度 (24位) | 类型 (8位) | 标志 (8位) | ... | +-----------------------------------------------+ | 流标识符 (31位) | +-----------------------------------------------+ | 帧有效载荷 | +-----------------------------------------------+ -
发送时:数据本身就是这种预定义的二进制结构。
-
接收方:无需进行复杂的文本行分割和解析,只需读取固定格式的帧头,就能高效地提取出“类型”、“属于哪个流”、“长度”等元数据。这个过程是二进制解析。
结论:任何数据在网络底层都是以二进制形式(比特流)传输的。这里的“文本”和“二进制”指的是 应用层协议的“格式”或“语法”,而不是物理层的比特信号。这里的“文本”与“二进制”是指应用层协议的“表示层”语法。HTTP/1.1传递的是“文本形式的指令”,而HTTP/2/3传递的是“结构化的二进制数据”。正是这种根本性的格式改变,使得HTTP/2/3能够实现多路复用等高级特性,性能得到巨大提升。
个人总结-文本和二进制分帧的意思:
http1.1是先构建这样的请求字符串:
GET /index.html HTTP/1.1 Host: www.example.com User-Agent: MyBrowser (一个空行)然后序列化成二进制的流进行传输。
而http2.0和http3.0的 是直接构建的时候就是2进制的数据,格式按+-----------------------------------------------+ | 长度 (24位) | 类型 (8位) | 标志 (8位) | ... | +-----------------------------------------------+ | 流标识符 (31位) | +-----------------------------------------------+ | 帧有效载荷 | +-----------------------------------------------+它无需序列化,可以直接发送。
请求与响应报文格式



实例
crul访问

启动下面测试代码,用命令访问curl -X POST http://127.0.0.1:8080/xxx/roleVerify -d "name=张三&ad=25"
断点查看报文格式:
POST /xxx/roleVerify HTTP/1.1 // 请求行 方法,url,HTTP协议版本
Host: 127.0.0.1:8080 // 请求头
User-Agent: curl/8.4.0 // 客户端标识 curl/8.4.0: 使用curl命令行工具发起的请求,版本8.4.0
Accept: */* // 可接受的内容类型 */*: 接受任何类型的响应内容
Content-Length: 17 // 请求体的长度(字节数)
Content-Type: application/x-www-form-urlencoded //请求体的编码格式 application/x-www-form-urlencoded: HTML表单默认的提交格式。
// 空格 HTTP协议规定:请求头和请求体之间必须有一个空行。空行表示请求头结束。其是关键分隔符
name=张三&ad=25 // 请求体
浏览器访问

断点查看报文格式:GET无请求体
GET /wdy?ad=2 HTTP/1.1 ////// 请求行 方法,url,HTTP协议版本
Host: 127.0.0.1:8080 ////// 请求头
Connection: keep-alive
sec-ch-ua: "Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144" // 新的用户代理标识方式,替代传统User-Agent。格式:"浏览器品牌";v="版本", ...
sec-ch-ua-mobile: ?0 // 表示是否是移动设备。 ?0: 不是移动设备(0 = false) ?1: 是移动设备
sec-ch-ua-platform: "Windows" // 表示是的操作系统平台
Upgrade-Insecure-Requests: 1 // 告诉服务器:客户端更喜欢HTTPS 1: 启用,如果服务器支持,把HTTP升级为HTTPS
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0 // 传统用户代理字符串,包含了详细的信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 // 可接受的内容类型。客户端可以处理的MIME类型,按优先级排列:text/html(首选,默认q=1.0)......
Sec-Fetch-Site: none // 请求的来源与目标的关系 none: 直接输入URL或书签访问(不是从其他网站跳转)
Sec-Fetch-Mode: navigate // 请求的模式 navigate: 导航请求(浏览器地址栏输入或点击链接)
Sec-Fetch-User: ?1 // 是否是用户触发的请求 ?1: 是用户触发的(用户点击或输入)
Sec-Fetch-Dest: document // 请求的目标 document: HTML文档(网页)
Accept-Encoding: gzip, deflate, br, zstd // 支持的压缩算法,服务器可选择使用。 服务器返回压缩内容可以减少传输大小
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 // 语言偏好
////// 空格 HTTP协议规定:请求头和请求体之间必须有一个空行。空行表示请求头结束
为什么我用浏览器访问,发来一个请求之后,又发来一个请求,但是第二个请求卡会卡在in.readLine(),最后过一段时间是requestLine == null的。
浏览器访问有一个问题,发来请求后,里面又发一个请求,为什么?
解答:预连接(Preconnect)优化
浏览器同时建立了多个连接,即使只有一个请求,也可能提前建立备用连接。
Chrome/Edge 的优化策略:
- 预测用户可能需要更多资源
- 预先建立1-2个空闲连接
- 这些连接暂时没有数据可读
有了预连接之后,需要发送信息时直接向该预连接发信息就行,就不用再建立连接了,省去了TCP握手时间。
响应
代码
@WebServlet("/test")
public class HelloTest extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("Hello World");
}
}
服务器返回给浏览器的响应头。Tomcat会自动添加一些默认头部,但你也可以自定义。
# 即使你什么都不设置,Tomcat也会自动添加
# 当你不设置任何头部,只写内容时:
HTTP/1.1 200 OK # Tomcat自动生成
Server: Apache-Coyote/1.1 # Tomcat自动添加
Content-Type: text/plain # 自动检测(默认text/plain)
Content-Length: 11 # 自动计算
Date: Tue, 28 Jan 2025 02:30:00 GMT # 自动添加
Hello World # 你写的内容
测试代码
测试代码如下:
package com.dy.servlet;
import java.io.*;
import java.net.*;
import java.util.*;
public class SimpleHttpServer {
private static final int PORT = 8080;
// 创建一个www的文件夹,用于存放静态文件,里面存放index.html文件
private static final String WEB_ROOT = "www";
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(PORT);
System.out.println("Server started on port " + PORT);
System.out.println("Web root: " + new File(WEB_ROOT).getAbsolutePath());
// 创建web根目录
new File(WEB_ROOT).mkdirs();
while (true) {
try (Socket clientSocket = serverSocket.accept()) {
handleRequest(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void handleRequest(Socket clientSocket) throws IOException {
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
OutputStream out = clientSocket.getOutputStream();
// 断点的时候,使用new String(in.cb)查看http请求报文格式 TODO
// 读取HTTP请求
String requestLine = in.readLine();
if (requestLine == null) return;
System.out.println("Request: " + requestLine);
// 解析请求方法和路径
StringTokenizer tokens = new StringTokenizer(requestLine);
String method = tokens.nextToken();
String path = tokens.nextToken();
// 处理GET请求
if ("GET".equals(method)) {
serveFile(path, out);
} else {
sendError(out, 501, "Not Implemented");
}
out.flush();
out.close();
in.close();
}
private static void serveFile(String path, OutputStream out) throws IOException {
// 默认页面
if (path.equals("/")) {
path = "/index.html";
}
// 防止路径遍历攻击
if (path.contains("..")) {
sendError(out, 403, "Forbidden");
return;
}
File file = new File(WEB_ROOT + path);
if (!file.exists()) {
sendError(out, 404, "Not Found");
return;
}
// 读取文件内容
byte[] content = readFile(file);
// 根据文件扩展名设置Content-Type
String contentType = getContentType(path);
// 发送HTTP响应
PrintWriter pw = new PrintWriter(out);
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: " + contentType);
pw.println("Content-Length: " + content.length);
pw.println("Server: SimpleHttpServer");
pw.println();
pw.flush();
out.write(content);
}
private static byte[] readFile(File file) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
}
return baos.toByteArray();
}
private static String getContentType(String path) {
if (path.endsWith(".html") || path.endsWith(".htm")) {
return "text/html";
} else if (path.endsWith(".css")) {
return "text/css";
} else if (path.endsWith(".js")) {
return "application/javascript";
} else if (path.endsWith(".jpg") || path.endsWith(".jpeg")) {
return "image/jpeg";
} else if (path.endsWith(".png")) {
return "image/png";
} else {
return "application/octet-stream";
}
}
private static void sendError(OutputStream out, int statusCode, String message)
throws IOException {
PrintWriter pw = new PrintWriter(out);
String html = "<html><body><h1>Error " + statusCode +
": " + message + "</h1></body></html>";
pw.println("HTTP/1.1 " + statusCode + " " + message);
pw.println("Content-Type: text/html");
pw.println("Content-Length: " + html.length());
pw.println();
pw.println(html);
pw.flush();
}
}
各种服务器对比
原始ServerSocket提供http服务
ServerSocket的accept方法了解
Socket clientSocket = serverSocket.accept()
// 调用栈流程
ServerSocket.accept()
└── ServerSocket.implAccept(Socket)
└── PlainSocketImpl.accept(SocketImpl)
└── AbstractPlainSocketImpl.accept(SocketImpl)
└── DualStackPlainSocketImpl.socketAccept(SocketImpl)
└── DualStackPlainSocketImpl本地方法 native int accept0(int fd, InetSocketAddress[] isaa) // 最终调用 native 方法
└── 有连接来时,返回的连接就在 InetSocketAddress[] isaa 里面
阻塞发生在操作系统内核层面,而不是Java代码中:accept0是本地方法,这里阻塞。
accept()的阻塞发生在JVM调用的操作系统内核函数中,等待全连接队列出现新的连接。
底层机制
当调用 accept() 时:
-
检查队列状态:
客户端 SYN → 半连接队列(SYN队列) 完成三次握手 → 全连接队列(accept队列) ← serverSocket.accept() 从这里取- 查看TCP全连接队列(backlog)是否有已完成的连接
- backlog大小通过构造函数设置,默认50
-
阻塞条件:
- 如果队列为空:线程进入
BLOCKED状态,等待内核通知 - 如果队列有连接:立即返回,不阻塞
- 如果队列为空:线程进入
-
唤醒机制:
- 当客户端完成TCP三次握手后,内核将该连接放入全连接队列
- 内核唤醒阻塞在accept上的线程
关键特性
- 线程安全:多个线程可以同时调用accept(),但只有一个能获得连接
- 同步阻塞:默认是阻塞模式(可设为非阻塞)
- 不处理IO:只负责建立连接,不涉及数据读写
阻塞 I/O VS NIO(非阻塞)
阻塞 I/O:ServerSocket的accept()方法会阻塞,Socket的流的read()方法会阻塞。
NIO(非阻塞):ServerSocketChannel和SocketChannel可以设置为非阻塞模式!设置后ServerSocketChannel的accept()方法和SocketChannel的read()方法会立即返回,不会阻塞。注册到 Selector,关注事件时【register(selector, SelectionKey.OP_ACCEPT)】,只有非阻塞模式的Channel才能注册,负责会报错(具体可看register方法的源码)
- 为什么不让阻塞模式的注册:因为阻塞模式的通道会独占线程,导致 Selector 无法同时监听多个通道。多路复用的核心是“非阻塞轮询”——只有当通道处于非阻塞模式时,Selector 才能在一个线程中检查多个通道是否有就绪事件。若允许阻塞模式注册,一旦某个通道阻塞,整个选择操作将被卡住,失去“多路”的意义。因此,Java 强制要求注册到 Selector 的通道必须是非阻塞的。
| 特性 | 阻塞 I/O | NIO(非阻塞) |
|---|---|---|
| accept() | 阻塞直到有连接 | 立即返回(null或SocketChannel) |
| read() | 阻塞直到有数据 | 立即返回(可能读取0字节) |
| 线程模型 | 1连接1线程 | 1个线程处理多连接 |
| 资源消耗 | 高(线程栈内存) | 低 |
| 编程复杂度 | 简单 | 复杂(需处理半包、粘包) |
提供http服务
Socket clientSocket = serverSocket.accept()
连接:serverSocket.accept()获得连接成功后的Socket。
输入:Socket.getInputStream():从socket的InputStream里面读出客户端传送过来的信息,解析里面的数据就是上面说的http请求的报文格式。自己提取需要的信息。
输出:Socket.getOutputStream():写入数据到socket的OutputStream,把数据从服务器传输到客户端,传输的格式就是上面说的http响应的报文格式。自己构建正确的请求头信息等。
Tomcat提供http服务
Tomcat 使用 NIO 多路复用模型(主从Reactor)
它封装了上面的原生的那些连接过程,然后搭配Servlet使用。建立连接后的通信就直接发送给对应的Servlet接口的
public void service(ServletRequest req, ServletResponse res)方法。
所以一般我们直接继承HttpServlet就行,然后重写doGet和doPost方法就行。
// 一般直接继承HttpServlet就行。HttpServlet的父类是GenericServlet,GenericServlet实现了Servlet接口
@WebServlet("/test")
public class EncodingTest extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setHeader("Content-Type", "text/html;charset=UTF-8");
resp.getWriter().println("当前编码: " + System.getProperty("file.encoding"));
}
}
连接:Tomcat 使用 NIO 多路复用模型建立好
输入:HttpServletRequest:获取客户端传送过来的信息,都已经封装好的,有对应的get方法,不用自己去按照http请求的报文格式解析。
输出:HttpServletResponse:设置发送给客户端的信息,都已经封装好的,有对应的set方法,不用自己去按照http响应的报文格式组装。
HttpServletResponse和HttpServletRequest都是封装好的,无法从中获取到 Socket! 这是 Servlet 规范有意设计的限制。
- 它们都没有提供获取 Socket 的方法!只能获取经过封装的流对象。如果允许获取Socket,可能的安全问题
- ❌ Servlet规范故意不暴露Socket给应用层
- ✅ 这是安全性和架构设计的需要
- ✅ 可以通过标准API获取客户端/服务端地址信息
- ❌ 避免使用容器特定的API或反射
- ✅ 如果需要低级网络控制,考虑不使用Servlet容器
OkHttp3 和 Tomcat 是完全不同层级的东西!
维度 OkHttp3 Tomcat 定位 HTTP 客户端 库 HTTP 服务器 容器 使用场景 应用向其他服务发请求 接收并处理HTTP请求 类比 浏览器(发起请求) 网站服务器(接收请求)
Netty提供http服务
// Netty提供完全的底层控制
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
// 完全控制:可以自定义HTTP解析,或不用HTTP
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new CustomHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
// channelRead方法
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// msg就是上面创建的ByteBuf
ctx.fireChannelRead(msg); // 开始传递给业务Handler
}
连接:使用 NIO 多路复用模型建立好
输入:Handler里面的channelRead方法的参数Object msg里面就有发给服务器的数据,一开始从HeadContext起始处理,这个msg一般都是ByteBuf
-
// 客户端发送:GET /api/users HTTP/1.1\r\nHost: localhost\r\n\r\n // HeadContext收到的msg: ByteBuf内容(十六进制): 474554202f6170692f757365727320485454502f312e310d0a // GET /api/users HTTP/1.1\r\n 486f73743a206c6f63616c686f73740d0a0d0a // Host: localhost\r\n\r\n -
需要自己解析里面的数据,按照上面说的http请求的报文格式解析。不过netty内置了已经实现的handler可以直接用,里面有解析的代码。
输出:自己构建响应对象,按照http响应的报文格式构建。不过netty内置了已经实现的handler可以直接用。然后使用ChannelHandlerContext ctx发送出去
-
ctx.writeAndFlush(response);
Netty可以直接获取对应的Socket,需要自己实现或使用编解码器,所以其可以实现任何协议,不单单可以解析成http协议。
Content-Type详解
Content-Type 是HTTP协议中的一个头部字段,用于指示资源的媒体类型(MIME类型),告诉客户端或服务器如何处理传输的数据。
基本格式
Content-Type: type/subtype; charset=encoding; boundary=boundary
- type/subtype:主类型/子类型(必选)
- charset:字符编码(可选)
- boundary:边界分隔符(用于multipart类型)
例子:HTTP请求中的Content-Type
# 发送JSON数据
POST /api/users HTTP/1.1
Content-Type: application/json; charset=utf-8
{"name": "张三", "age": 25}
# 表单提交
POST /submit HTTP/1.1
Content-Type: application/x-www-form-urlencoded
name=张三&age=25
# 文件上传
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.jpg"
Content-Type: image/jpeg
...文件二进制数据...
例子:HTTP响应中的Content-Type
# 返回HTML页面
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
<!DOCTYPE html><html>...</html>
# 返回JSON数据
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"status": "success", "data": {...}}
# 文件下载
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="example.zip"
常见MIME类型分类
1. 文本类型
text/plain # 纯文本
text/html # HTML文档
text/css # CSS样式表
text/javascript # JavaScript代码(已废弃,推荐使用application/javascript)
text/csv # CSV数据
text/xml # XML文档
2. 应用类型
application/json # JSON数据
application/xml # XML数据
application/javascript # JavaScript代码
application/pdf # PDF文档
application/octet-stream # 二进制流数据(默认下载)
application/x-www-form-urlencoded # 表单编码数据
application/xhtml+xml # XHTML文档
3. 图像类型
image/jpeg
image/png
image/gif
image/svg+xml
image/webp
4. 音频/视频类型
audio/mpeg
audio/wav
video/mp4
video/webm
5. Multipart类型(用于表单上传)
multipart/form-data # 文件上传
multipart/byteranges # 部分内容
Content-Type与Accept的区别
| 头部字段 | 方向 | 用途 |
|---|---|---|
| Content-Type | 请求/响应 | 指示发送数据的类型 |
| Accept | 请求 | 告知服务器客户端希望接收的类型 |
tomcat会根据这个Content-Type构建HttpServletRequest吗?
是的,Tomcat会根据Content-Type来构建HttpServletRequest对象
源码位置:
tomcat-coyote模块:
org.apache.coyote.http11.Http11Processor
→ 处理HTTP请求的入口
tomcat-catalina模块:
org.apache.catalina.connector.Request
→ HttpServletRequest的实现类,核心解析逻辑
org.apache.catalina.connector.RequestFacade
→ Request的门面类
浙公网安备 33010602011771号