HTTP协议

HTTP协议

从Socket中解析Http协议实现通信_socket报文解析-CSDN博客

HTTP协议核心要素报文格式方法头部与状态码-开发者社区-阿里云

版本

image-20260128174452936

每个版本的核心变化:

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)

数据格式的文本和二进制分帧的意思:

  1. HTTP/1.1 的“文本”是什么意思?
  • 它是给人(开发者)看的格式。它的命令、头部、状态码都是用纯ASCII字符书写的,遵循严格的文本行格式。

  • 例子:当你发送一个请求时,数据流看起来像这样(人类可读):

    GET /index.html HTTP/1.1
    Host: www.example.com
    User-Agent: MyBrowser
    (一个空行)
    
  • 发送时:这段文本会被编码(如UTF-8)成二进制字节流在TCP上传输。

  • 接收方:收到二进制流后,需要按行(\r\n分隔)去解析这段文本,来理解请求方法、路径、头部等信息。这个过程是文本解析

  1. 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位)                 |
+-----------------------------------------------+
|                   帧有效载荷                    |
+-----------------------------------------------+

它无需序列化,可以直接发送。

请求与响应报文格式

4e19e753c097f2073993ce89d837dc6a

dc6616c2c88985de4ca1368b462c4359

dd6e86cd2d434a08a07efdbedc2d8e6c

实例

crul访问

image-20260128154227580

启动下面测试代码,用命令访问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    						   // 请求体
浏览器访问

image-20260128154854572

断点查看报文格式: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. 预先建立1-2个空闲连接
  3. 这些连接暂时没有数据可读

有了预连接之后,需要发送信息时直接向该预连接发信息就行,就不用再建立连接了,省去了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方法了解

Java网络编程 - deyang - 博客园

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() 时:

  1. 检查队列状态

    客户端 SYN → 半连接队列(SYN队列)
          完成三次握手 → 全连接队列(accept队列) ← serverSocket.accept() 从这里取
    
    • 查看TCP全连接队列(backlog)是否有已完成的连接
    • backlog大小通过构造函数设置,默认50
  2. 阻塞条件

    • 如果队列为空:线程进入BLOCKED状态,等待内核通知
    • 如果队列有连接:立即返回,不阻塞
  3. 唤醒机制

    • 当客户端完成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的门面类
posted @ 2026-02-14 14:34  deyang  阅读(1)  评论(0)    收藏  举报