前言 我们经常会使用 HTTP 服务。也许你没听过 HTTP,但是你一定浏览过网页,而浏览器与服务器之间就是使用 HTTP 协议来通信的。这篇笔记记录了实现一个简单的静态 HTTP 服务器的过程。
HTTP HTTP(Hyper Text Transfer Protocol) 是一种用于 Web 服务器和客户端之前交换数据的传送协议,它使用可靠的 TCP/IP 通信协议来传递数据。
一般情况下,总是由客户端发送 HTTP 请求,Web 服务器处理请求之后返回一个 HTTP 响应。
HTTP 请求 一个 HTTP 请求包含以下三个部分:
请求方法 统一资源标识符 协议/版本
请求头
请求实体
例如:
1 2 3 4 5 6 7 8 GET / HTTP/1.1 Host : 127.0.0.1:8000Cache-Control : no-cacheUser-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36Accept : text/htmlAccept-Encoding : gzip, deflate, brusername =xuewen&password=pwd123
请求行 请求的第一行包含了三个参数,分别表示请求方法、统一资源标识符、协议/版本:
其中请求方法为 GET
,统一资源标识符为 /
,协议/版本为 HTTP/1.1
。
每个 HTTP 请求都可以使用 HTTP 标准指定的请求方法之一,HTTP 1.1 支持的7中请求协议包括:
GET
POST
HEAD
PUT
DELETE
OPTION
TRACE
请求资源标识符 (URI) 是 Internet 资源的完整路径。URI 通常是相对于服务器根目录的路径,通常以 /
开始。
协议/版本说明了当前请求使用的 HTTP 协议的版本。
请求头 请求头指定了 HTTP 请求的一些参数,如:
1 2 3 4 5 Host : 127.0.0.1:8000Cache-Control : no-cacheUser-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36Accept : text/htmlAccept-Encoding : gzip, deflate, br
Host
指出了本次请求的 Web 服务器的主机名
Cache-Control
用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。
User-Agent
包含了一个特征字符串,用来让 Web 服务器识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。
Accept
用来告知(服务器)客户端可以处理的内容类型。
Accept-Encoding
用来说明客户端接能处理的内容编码方式,一般是压缩算法。
请求实体 在请求头的后面是一行空行,这个空行用来分隔请求头和请求体,即在空行的后面是本次请求的内容。
1 username=xuewen&password=pwd123
HTTP 响应 HTTP 响应与 HTTP 请求类似,也有三个组成部分:
例如:
1 2 3 4 5 6 7 8 HTTP/1.0 200 OKServer : SimpleHTTP/0.6 Python/3.6.8Date : Wed, 23 Oct 2019 12:42:06 GMTContent-type : text/htmlContent-Length : 6Last-Modified : Wed, 23 Oct 2019 12:42:00 GMThello
响应的第一行与请求类似,指明了 HTTP 协议的版本,响应的状态码 (200 表示请求成功)。
接下来的是响应头,与请求头类似,包含了许多参数。 可以点击查看HTTP 头部信息
发送 HTTP 请求 一般情况下我们都是使用浏览器来浏览网页,浏览器会帮助我们完成发送 HTTP 请求和解析服务器返回的数据。
我们也可以尝试自己发送 HTTP 请求,这样可以更好的理解 HTTP 协议。
在发送请求时,可以使用 Java 中的 Socket 来连接服务器。
下面给出完整的代码:
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 import java.io.*;import java.net.Socket;public class SendRequest { public static void main (String[] args) { try { Socket socket = new Socket ("127.0.0.1" , 8080 ); PrintWriter printWriter = new PrintWriter ( socket.getOutputStream(), true ); printWriter.println("GET / HTTP/1.1" ); printWriter.println("Host: 127.0.0.1" ); printWriter.println("Connection: close" ); printWriter.println(); printWriter.flush(); BufferedReader bufferedReader = new BufferedReader ( new InputStreamReader ( socket.getInputStream() ) ); StringBuilder stringBuilder = new StringBuilder (8096 ); boolean loop = true ; while (loop) { if (bufferedReader.ready()) { int i = 0 ; while (i != -1 ) { i = bufferedReader.read(); stringBuilder.append((char ) i); } loop = false ; } Thread.sleep(10 ); } System.out.println(stringBuilder.toString()); socket.close(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }
为了运行上面的程序,可以使用 Python 来开启一个 HTTP 服务器:
1 2 3 4 # 创建一个 HTML 文件 echo 'The Server received your request!' > index.html # 开启服务器 python3 -m http.server 8000
此时运行程序,可以在控制台得到以下输出,可以看到服务器返回的数据符合 HTTP 协议:
1 2 3 4 5 6 7 8 HTTP/1.0 200 OK Server: SimpleHTTP/0.6 Python/3.6.8 Date: Tue, 29 Oct 2019 11:39:59 GMT Content-type: text/html Content-Length: 34 Last-Modified: Tue, 29 Oct 2019 11:38:52 GMT The Server received your request!
创建 HTTP 服务器 这个简单的 HTTP 服务器将由三个类组成:
HttpServer
Request
Response
HttpServer 类 HttpServer 表示一个 HTTP 服务器,具体代码如下:
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 import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class HttpServer { private static int port; public static boolean shutdown = false ; private static String webRoot = "/servlet-learn/web-root" ; private static String response404 = "404.html" ; public static String shutdownCommand = "/shutdown" ; public static void main (String[] args) { HttpServer httpServer = new HttpServer (8080 ); httpServer.await(); } public HttpServer (int port) { this .port = port; } public void await () { ServerSocket serverSocket = null ; try { serverSocket = new ServerSocket (this .port); } catch (IOException e) { e.printStackTrace(); System.exit(1 ); } System.out.println("服务器已建立, http://localhost:" + port); while (!shutdown) { Socket socket; InputStream inputStream; OutputStream outputStream; try { socket = serverSocket.accept(); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); System.out.println("--------------------------------" ); Request request = new Request (inputStream); request.parse(); Response response = new Response (outputStream); response.setRequest(request); response.sendStaticResource(); inputStream.close(); outputStream.flush(); outputStream.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static String getWebRoot () { return webRoot; } public static String getResponse404 () { return response404; } }
这个 Web 服务器可以处理客户端对指定目录中静态资源的请求,默认资源目录为:
1 private static String webRoot = "/servlet-learn/web-root" ;
例如用户访问 http://localhost:8080/hello.html
时,程序会在 webRoot
目录下寻找 hello.html
文件并将文件返回给客户端。
若要关闭服务器,可以访问 http://localhost:8080/shutdown
,该命令在程序中定义:
1 public static String shutdownCommand = "/shutdown" ;
当服务器建立后会调用 await()
方法监听用户请求。
在 await()
方法中,调用了 socket.accept()
来等待用户连接,并根据 socket
对象来获取用户的输入输出流。
1 2 3 socket = serverSocket.accept(); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream();
然后在根据输入输出流来创建 request
、response
对象。
1 2 3 4 5 6 7 8 9 Request request = new Request (inputStream);request.parse(); Response response = new Response (outputStream);response.setRequest(request); response.sendStaticResource();
最后服务器关闭连接:
1 2 3 4 inputStream.close(); outputStream.flush(); outputStream.close(); socket.close();
Request 类 Request 类用于处理用户的请求,主要代码如下:
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 import java.io.*;public class Request { private InputStream inputStream; private String uri; private String method; private String version; public Request (InputStream inputStream) { this .inputStream = inputStream; } public void parse () throws IOException { parseHeader(); } public void parseHeader () throws IOException { } public String getUri () { return uri; } public String getMethod () { return method; } public String getVersion () { return version; } }
Request 类中保存了用户的输入流。在 parse()
方法中解析输入流来构建完整的 request
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void parse () throws IOException { parseHeader(); } public void parseHeader () throws IOException { BufferedReader bufferedReader = new BufferedReader ( new InputStreamReader ( inputStream ) ); String line = bufferedReader.readLine(); String[] ags = line.split(" " ); this .method = ags[0 ]; this .uri = ags[1 ]; this .version = ags[2 ]; System.out.println(String.format("请求头:%s, %s, %s" , method, uri, version)); }
Response 类 Response 类用于处理给用户的响应。主要的代码为根据 Request 对象来查找文件并写入到 Socket 的输出流中去,即发送给用户。
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 public void sendStaticResource () { int BUFFER_SIZE = 1024 * 4 ; String uri = this .request.getUri(); String webRoot = HttpServer.getWebRoot(); if (uri.equals("/" )) { uri = "/index.html" ; } else if (HttpServer.shutdownCommand.equals(uri)) { System.out.println("收到关闭指令,正在关闭服务器!" ); HttpServer.shutdown = true ; uri = HttpServer.shutdownCommand + ".html" ; } File file = new File (webRoot, uri); if (!file.exists()) { file = new File (HttpServer.getWebRoot(), HttpServer.getResponse404()); } FileInputStream fileInputStream; try { fileInputStream = new FileInputStream (file); byte [] buffer = new byte [BUFFER_SIZE]; int count = fileInputStream.read(buffer, 0 , BUFFER_SIZE); while (count != -1 ) { this .outputStream.write(buffer, 0 , count); count = fileInputStream.read(buffer, 0 , BUFFER_SIZE); } } catch (IOException e) { e.printStackTrace(); } }
在处理 Request 的时候,可以根据 URI 进行判断:
如果 URI 为 /
,则返回 index.html
文件
如果 URI 为 /shutdown
,则关闭服务器,并返回 shutdown.html
文件
测试客户端和服务器 在终端运行 HttpServer
,使用 SendRequest
进行请求,可以得到以下结果:
服务端:
1 2 3 服务器已建立, http://localhost:8080 -------------------------------- 请求头:GET, /, HTTP/1.1
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 HTTP/1.1 200 OK Connection: close <!doctype html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <title > Title</title > </head > <body > <p > Index File.</p > </body > </html >
总结 在这个实验中学到了一个简单的 Web 服务器是如何工作的。虽然功能很简单,但是有助于理解客户端和服务器之间使用 HTTP 通信的细节。