最近想自己实现一个 HTTP 代理服务器,查阅资料和编写过程中,发现 Java 对比 Python,不太容易找到成熟的 HTTP 请求处理包。不要说 Netty、Jetty 这些,只是想单纯的能够处理原始的 HTTP 请求,解析出里面的 Host、Request URI、Header、Cookie 等信息。正好看到有位外国朋友也做了类似的探索,将他的文章翻译转载到此处,原文可以看 Http Message Parsing in Java Web Servers 。
解析来自客户端的请求消息是构建 Web 服务器的开始。 典型请求可能如下所示:GET
1 2 3 4 5 6 GET /steventen HTTP/1.1\r\n Host : github.comAccept-Language : en-usAccept : text/jsonUser-Agent : curl/7.16.3Cookie : a=123; b=456;\r\n\r\n
HTTP 请求的语法和内容遵循 RFC 7230 和 RFC 7231 两个规范。当 Web 服务器收到消息时,它需要解析或解码其中的信息(路径、标头等),然后再启动接下来的过程,如路由、代理、身份验证等。
我花了一些时间寻找可以解析上述原始 HTTP 的标准 Java 库,希望支持将请求消息从字节或字符串到 Java 对象中,方便我编写程序访问 Header、Cookie 等信息。 我以为这是一个很常用的功能,以至于必须有一个标准库。 相反,在阅读了大多数用 Java 编写的流行的 Web 服务器库(如 Tomcat、Jetty、Netty 、Undertow 等)的源代码后,我发现每个人都实现了一个 HTTP 解析器。此外,当我查看每个实现时,我发现它们都不能作为独立使用。 每个库都需要除原始 HTTP 消息本身之外的其他参数。
以下部分展示了 HTTP 消息配对/解码在那些用 Java 编写的 Web 服务器库中的样子。 并描述了如何“使用”每个库中的 Http 解析器。
Jetty 在 Jetty 中,HttpParser 负责处理 HTTP 消息解析。然而,它需要一个 HttpHandler,具体示例如下:
1 2 3 4 5 6 ByteBuffer buffer = BufferUtil.toBuffer( "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Name: value\r\n\r\n" ); HttpParser.RequestHandler handler = new Handler(); HttpParser parser = new HttpParser(handler);
Undertow 主要的处理逻辑在 HttpRequestParser 中,代码如下:
1 2 3 4 5 6 7 8 byte [] in = "GET /somepath%2fotherPath HTTP/1.1\r\n\r\n" .getBytes();final ParseState context = new ParseState(10 );HttpServerExchange result = new HttpServerExchange(null ); HttpRequestParser.instance(OptionMap.create(UndertowOptions.ALLOW_ENCODED_SLASH, true )).handle(ByteBuffer.wrap(in), context, result); result.getRequestMethod() result.getRequestPath()
可以看到,解析时必须依赖 ParseState 类。
Netty Netty 的主要实现代码在 HttpRequestDecoder 中,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String crlf = "\r\n" ; String request = "GET https://github.com/steventen HTTP/1.1" + crlf + "Host: github.com" + crlf + "Accept-Language: en-us" + crlf + "Accept: text/json" + crlf + "Accept-Encoding: gzip, deflate, br" + crlf + "Cookie: a=123; b=456;c=wfhhnchauhd" + crlf + crlf; EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); channel.writeInbound(Unpooled.copiedBuffer(request, CharsetUtil.US_ASCII)); HttpRequest req = channel.readInbound(); req.headers(); req.method(); req.uri(); Set<Cookie> cookies = ServerCookieDecoder.LAX.decode(req.headers().get("Cookie" ));
比起其他两个来,可以在工程中引用 Netty 的 EmbeddedChannel 实现解析,但是需要注意性能问题。
Tomcat 这一唯一一个无法通过阅读源码来了解其工作原理的库。
我的实现 总结下来,发现 Java 世界中确实没有一个比较轻量的 HTTP 解析器。作者的这篇文章发表于 2021年,而且是他对国外技术栈考察的结果。
在这段时间,结合使用搜索工具与豆包等AI工具,我整理了一段代码,可以实现监听8080端口,并将HTTP GET请求转发到后端服务器,获取服务器响应后发送给客户端。
实现了基本的HTTP头解析逻辑,支持提取Host、URI、Port信息,欢迎大家阅提意见。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 import java.io.*;import java.net.*;import java.util.concurrent.*;public class HttpProxyServer { private static final int THREAD_POOL_SIZE = 10 ; private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); public static void main (String[] args) { int port = 8080 ; try ( ServerSocket serverSocket = new ServerSocket(port) ) { serverSocket.setSoTimeout(1000 * 60 ); System.out.println("Http Proxy Server listen at: " + port); final byte [] Request = new byte [1024 ]; byte [] Reply = new byte [4096 ]; while (true ) { Socket proxySocket = null ; Socket socket_client = null ; try { socket_client = serverSocket.accept(); final InputStream InputStreamClient = socket_client.getInputStream(); final OutputStream OutputStreamClient = socket_client.getOutputStream(); String requestHost = null ; int requestPort = 80 ; try { BufferedReader client_reader = new BufferedReader(new InputStreamReader(InputStreamClient)); String remoteRequest = "" ; String requestLine = client_reader.readLine(); if ( requestLine != null ){ String[] parts = requestLine.split(" " ); if (parts.length >= 3 ) { String requestMethod = parts[0 ]; String requestPath = parts[1 ]; String requestProtocol = parts[2 ]; if (requestPath.startsWith("http://" )){ requestPath = requestPath.substring(7 ); }else if ( requestPath.startsWith("https://" )){ requestPath = requestPath.substring(8 ); } int pathIndex = requestPath.indexOf('/' ); if (pathIndex != -1 ){ requestPath = requestPath.substring(pathIndex); } System.out.println("Request path : " + requestPath); remoteRequest = requestMethod + " " + requestPath + " " + requestProtocol + "\r\n" ; String line; while ((line = client_reader.readLine()) != null && !line.isEmpty()){ if (line.startsWith("Host: " )){ String[] hostParts = line.substring(6 ).split(":" ); requestHost = hostParts[0 ]; if (hostParts.length > 1 ){ requestPort = Integer.parseInt(hostParts[1 ]); } } remoteRequest += line + "\r\n" ; } System.out.println("Method: " + requestMethod); System.out.println("Path: " + requestPath); System.out.println("Protocol: " + requestProtocol); System.out.println("Host: " + requestHost); System.out.println("Port: " + requestPort); } } remoteRequest += "\r\n" ; System.out.println("Send Request to Remote Server:" ); System.out.println(remoteRequest); if ( requestHost == null ){ requestHost = "www.edulinks.cn" ; requestPort = 80 ; } proxySocket = new Socket(requestHost, requestPort); final InputStream prxoyInputStream = proxySocket.getInputStream(); final OutputStream proxyOutputStream = proxySocket.getOutputStream(); proxyOutputStream.write(remoteRequest.getBytes()); proxyOutputStream.flush(); int Bytes_Read; try { while ( (Bytes_Read = prxoyInputStream.read(Reply))!= -1 ){ System.out.println(Reply); OutputStreamClient.write(Reply, 0 , Bytes_Read); OutputStreamClient.flush(); } }catch (IOException e){ System.out.println("Get response Error !" ); System.out.println(e); e.printStackTrace(); } }catch (IOException e){ System.out.println("Send request Error !" ); System.out.println(e); } OutputStreamClient.close(); } catch (IOException e) { System.out.println("Proxy can not get response. " ); e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } } }
参考资料
Http Message Parsing in Java Web Servers