手撕 HttpClient:自己实现简单的 HTTP 请求工具

HttpClient 是开发中常用的 HTTP 请求工具,例如 Apache HttpClient 或 Java 的 HttpURLConnection。但这些工具库底层的逻辑其实并不复杂,如果我们想深入了解 HTTP 协议,完全可以通过手撕代码实现一个简易版的 HttpClient

本文将带你从零实现一个简易的 HttpClient 工具类,支持 GET 和 POST 请求,并讨论其原理和扩展方向。


1. HTTP 请求的基本流程

一个完整的 HTTP 请求通常包含以下步骤:

  1. 建立连接:通过 Socket 连接到服务器。
  2. 发送请求:构造 HTTP 请求头并将其写入输出流。
  3. 接收响应:从输入流读取服务器返回的响应内容。
  4. 解析响应:解析响应状态、头部和内容。

2. 自定义 HttpClient 实现

我们将通过 Java 原生的 Socket 和 I/O 流实现一个简易的 HttpClient。


2.1 核心代码:HTTP GET 请求

实现代码
java 复制代码
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class SimpleHttpClient {

    public static String sendGet(String host, int port, String path) throws IOException {
        // 创建 Socket 连接
        try (Socket socket = new Socket(host, port)) {
            // 构造 HTTP GET 请求
            String request = "GET " + path + " HTTP/1.1\r\n" +
                             "Host: " + host + "\r\n" +
                             "Connection: close\r\n\r\n";

            // 发送请求
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(request.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();

            // 接收响应
            InputStream inputStream = socket.getInputStream();
            return readResponse(inputStream);
        }
    }

    private static String readResponse(InputStream inputStream) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        StringBuilder response = new StringBuilder();
        String line;

        // 逐行读取响应内容
        while ((line = reader.readLine()) != null) {
            response.append(line).append("\n");
        }
        return response.toString();
    }

    public static void main(String[] args) throws IOException {
        // 测试 GET 请求
        String response = sendGet("www.example.com", 80, "/");
        System.out.println(response);
    }
}

运行结果

运行上述代码后,你会得到类似如下的响应:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1256
Connection: close
...

<!doctype html>
<html>
<head>
    <title>Example Domain</title>
    ...
</head>
<body>
    ...
</body>
</html>

2.2 核心代码:HTTP POST 请求

POST 请求需要在请求体中传递数据,以下是实现。

实现代码
java 复制代码
public static String sendPost(String host, int port, String path, String body) throws IOException {
    // 创建 Socket 连接
    try (Socket socket = new Socket(host, port)) {
        // 构造 HTTP POST 请求
        String request = "POST " + path + " HTTP/1.1\r\n" +
                         "Host: " + host + "\r\n" +
                         "Content-Type: application/x-www-form-urlencoded\r\n" +
                         "Content-Length: " + body.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
                         "Connection: close\r\n\r\n" +
                         body;

        // 发送请求
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(request.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();

        // 接收响应
        InputStream inputStream = socket.getInputStream();
        return readResponse(inputStream);
    }
}

public static void main(String[] args) throws IOException {
    // 测试 POST 请求
    String response = sendPost("httpbin.org", 80, "/post", "key1=value1&key2=value2");
    System.out.println(response);
}

2.3 HTTP 请求工具类封装

为了更方便使用,我们将 GET 和 POST 请求封装到一个工具类中。

完整工具类
java 复制代码
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class SimpleHttpClient {

    public static String sendGet(String host, int port, String path) throws IOException {
        try (Socket socket = new Socket(host, port)) {
            String request = "GET " + path + " HTTP/1.1\r\n" +
                             "Host: " + host + "\r\n" +
                             "Connection: close\r\n\r\n";
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(request.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
            InputStream inputStream = socket.getInputStream();
            return readResponse(inputStream);
        }
    }

    public static String sendPost(String host, int port, String path, String body) throws IOException {
        try (Socket socket = new Socket(host, port)) {
            String request = "POST " + path + " HTTP/1.1\r\n" +
                             "Host: " + host + "\r\n" +
                             "Content-Type: application/x-www-form-urlencoded\r\n" +
                             "Content-Length: " + body.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
                             "Connection: close\r\n\r\n" +
                             body;
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(request.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
            InputStream inputStream = socket.getInputStream();
            return readResponse(inputStream);
        }
    }

    private static String readResponse(InputStream inputStream) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            response.append(line).append("\n");
        }
        return response.toString();
    }
}

3. 扩展方向

  1. 支持 HTTPS 请求

    • 使用 SSLSocket 替代普通的 Socket 来实现 HTTPS 请求。
  2. 自动解析响应

    • 将响应头和响应体分离,封装成对象,便于进一步处理。
  3. 支持更多 HTTP 方法

    • 实现 PUT、DELETE、PATCH 等方法的支持。
  4. 增加超时机制

    • 使用 Socket#setSoTimeout 设置连接和读取超时时间。
  5. 处理 Chunked Transfer-Encoding

    • 支持分块传输的响应解析。

4. 优缺点分析

优点

  • 轻量级:依赖少,直接使用 Java 原生 API。
  • 原理清晰:深入了解 HTTP 请求的底层逻辑。
  • 可扩展:可以根据实际需求添加自定义功能。

缺点

  • 功能有限:无法满足复杂场景(如 HTTPS、自动重试等)。
  • 重复造轮子:相比成熟的工具类(如 Apache HttpClient、OkHttp),需要大量额外开发工作。

5. 总结

通过手撕一个简易版的 HttpClient,我们能够更清晰地理解 HTTP 请求的核心流程。虽然实际项目中推荐使用成熟的工具类(如 OkHttp、Apache HttpClient 等),但通过手动实现,开发者可以更好地掌握底层原理,为后续的优化和扩展提供坚实基础。

如果需要处理复杂的 HTTP 场景,可以在此基础上进行扩展,打造一个更完善的 HTTP 请求工具!

相关推荐
可涵不会debug3 分钟前
【C++】在线五子棋对战项目网页版
linux·服务器·网络·c++·git
栗子~~4 分钟前
基于quartz,刷新定时器的cron表达式
java
杨过姑父10 分钟前
Servlet3 简单测试
java·servlet
chengxuyuan6666624 分钟前
python基础语句整理
java·windows·python
一只会飞的猪_26 分钟前
国密加密golang加密,java解密
java·开发语言·golang
清风-云烟27 分钟前
使用redis-cli命令实现redis crud操作
java·linux·数据库·redis·spring·缓存·1024程序员节
安和昂36 分钟前
effective Objective—C 第三章笔记
java·c语言·笔记
好像是个likun44 分钟前
spring Ioc 容器的简介和Bean之间的关系
java·后端·spring
向着开发进攻1 小时前
深入理解 Java 并发编程中的锁机制
java·开发语言
CURRY30_HJH1 小时前
JAVA 使用反射比较对象属性的变化,记录修改日志。使用注解【策略模式】,来进行不同属性枚举值到中英文描述的切换,支持前端国际化。
java·开发语言