简单的静态 HTTP 服务器

前言

我们经常会使用 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:8000
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html
Accept-Encoding: gzip, deflate, br

username=xuewen&password=pwd123

请求行

请求的第一行包含了三个参数,分别表示请求方法、统一资源标识符、协议/版本:

1
GET / HTTP/1.1

其中请求方法为 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:8000
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html
Accept-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 OK
Server: SimpleHTTP/0.6 Python/3.6.8
Date: Wed, 23 Oct 2019 12:42:06 GMT
Content-type: text/html
Content-Length: 6
Last-Modified: Wed, 23 Oct 2019 12:42:00 GMT

hello

响应的第一行与请求类似,指明了 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 socket = new Socket("127.0.0.1", 8080);
PrintWriter printWriter = new PrintWriter(
socket.getOutputStream(), true
);

// 向服务器发送 HTTP 请求,请求的格式遵守 HTTP 协议
printWriter.println("GET / HTTP/1.1");
printWriter.println("Host: 127.0.0.1");
printWriter.println("Connection: close");
// 按照 HTTP 协议,此处空一行
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() 方法监听用户请求。

1
httpServer.await();

await() 方法中,调用了 socket.accept() 来等待用户连接,并根据 socket 对象来获取用户的输入输出流。

1
2
3
socket = serverSocket.accept();
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();

然后在根据输入输出流来创建 requestresponse 对象。

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
14
15
16
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 通信的细节。