简单的静态 HTTP 服务器
我们经常会使用 HTTP 服务。也许你没听过 HTTP,但是你一定浏览过网页,而浏览器与服务器之间就是使用 HTTP 协议来通信的。这篇笔记记录了实现一个简单的静态 HTTP 服务器的过程。
HTTP
HTTP(Hyper Text Transfer Protocol) 是一种用于 Web 服务器和客户端之前交换数据的传送协议,它使用可靠的 TCP/IP 通信协议来传递数据。
一般情况下,总是由客户端发送 HTTP 请求,Web 服务器处理请求之后返回一个 HTTP 响应。
HTTP 请求
一个 HTTP 请求包含以下三个部分:
- 请求方法 统一资源标识符 协议/版本
- 请求头
- 请求实体
例如:
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
请求行
请求的第一行包含了三个参数,分别表示请求方法、统一资源标识符、协议/版本:
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 请求的一些参数,如:
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
用来说明客户端接能处理的内容编码方式,一般是压缩算法。
请求实体
在请求头的后面是一行空行,这个空行用来分隔请求头和请求体,即在空行的后面是本次请求的内容。
username=xuewen&password=pwd123
HTTP 响应
HTTP 响应与 HTTP 请求类似,也有三个组成部分:
- 协议/版本 状态码描述
- 响应头
- 响应体
例如:
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 来连接服务器。
下面给出完整的代码:
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 服务器:
# 创建一个 HTML 文件
echo 'The Server received your request!' > index.html
# 开启服务器
python3 -m http.server 8000
此时运行程序,可以在控制台得到以下输出,可以看到服务器返回的数据符合 HTTP 协议:
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 服务器,具体代码如下:
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 服务器可以处理客户端对指定目录中静态资源的请求,默认资源目录为:
private static String webRoot = "/servlet-learn/web-root";
例如用户访问 http://localhost:8080/hello.html
时,程序会在 webRoot
目录下寻找 hello.html
文件并将文件返回给客户端。
若要关闭服务器,可以访问 http://localhost:8080/shutdown
,该命令在程序中定义:
public static String shutdownCommand = "/shutdown";
当服务器建立后会调用 await()
方法监听用户请求。
httpServer.await();
在 await()
方法中,调用了 socket.accept()
来等待用户连接,并根据 socket
对象来获取用户的输入输出流。
socket = serverSocket.accept();
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
然后在根据输入输出流来创建 request
、response
对象。
// 创建并解析请求对象
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();
Request 类
Request 类用于处理用户的请求,主要代码如下:
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
对象。
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 的输出流中去,即发送给用户。
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
进行请求,可以得到以下结果:
服务端:
服务器已建立, http://localhost:8080
--------------------------------
请求头:GET, /, HTTP/1.1
客户端:
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 通信的细节。