一、背景

某日,在 Java 技术群中看到网友讨论 tomcat 容器相关内容,然后想到自己能不能实现一个简单的 web 容器。于是翻阅资料和思考,最终通过 JavaSE 原生 API 编写出一个简单 web 容器(模拟 tomcat)。在此只想分享编写简单 web 容器时的思路和技巧。

二、涉及知识

Socket 编程:服务端通过监听端口,提供客户端连接进行通信。

Http 协议:分析和响应客户端请求。

多线程:处理多个客户端请求。

用到的都是 JavaSE 的基础知识。

三、初步模型

3.1 通过 Socket API 编写服务端

服务端的功能:接收客户端发送的的数据和响应数据回客户端。

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
package com.light.server;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class Server {

private static final String BLANK = " ";
private static final String RN = "\r\n";

private ServerSocket server;

public static void main(String[] args) {
Server server = new Server();
server.start();
}

/**
* 启动服务器
*/
public void start() {
try {
server = new ServerSocket(8080);
// 接收数据
this.receiveData();
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 接收数据
*/
private void receiveData() {
try {
Socket client = this.server.accept();
// 读取客户端发送的数据
byte[] data = new byte[10240];
int len = client.getInputStream().read(data);

String requestInfo = new String(data,0,len);
// 打印客户端数据
System.out.println(requestInfo);

// 响应正文
String responseContent = "<!DOCTYPE html>" +
"<html lang=\"zh\">" +
" <head> " +
" <meta charset=\"UTF-8\">"+
" <title>测试</title>"+
" </head> "+
" <body> "+
" <h3>Hello World</h3>"+
" </body> "+
"</html>";


StringBuilder response = new StringBuilder();
// 响应头信息
response.append("HTTP/1.1").append(BLANK).append("200").append(BLANK).append("OK").append(RN);
response.append("Content-Length:").append(responseContent.length()).append(RN);
response.append("Content-Type:text/html").append(RN);
response.append("Date:").append(new Date()).append(RN);
response.append("Server:nginx/1.12.1").append(RN);
response.append(RN);
// 添加正文
response.append(responseContent);

// 输出到浏览器
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
bw.write(response.toString());
bw.flush();
bw.close();

} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 关闭服务器
*/
public void stop() {

}

}

启动程序,通过浏览器访问 http://localhost:8080/login?username=aaa&password=bbb ,结果如下图:

响应信息与代码中设置的一致。

3.2 分析客户端数据

3.2.1 获取 get 方式的请求数据

打开浏览器,通过 get 方式请求 http://localhost:8080/login?username=aaa&password=bbb 服务端打印内容如下:

1
2
3
4
5
6
7
8
9
10
GET /login?username=aaa&password=bbb HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8
Cookie: SESSION=2b5369d6-9d94-4b54-9ef3-05e47fe63025; JSESSIONID=3B48C7BF26937058A433A29EB2F978BC

3.2.2 获取 post 方式的请求数据

编写一个简单的 html 页面,发送 post 请求,

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
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<form action="http://localhost:8080/login" method="post">
<table border="1">
<tr>
<td>用户名</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>爱好</td>
<td>
<input type="checkbox" name="likes" value="1">篮球&nbsp;
<input type="checkbox" name="likes" value="2">足球&nbsp;
<input type="checkbox" name="likes" value="3">棒球
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="提交">&nbsp;&nbsp;
<input type="reset" value="重置">
</td>
</tr>
</table>
</form>
</body>
</html>

服务端打印内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /login HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 41
Cache-Control: max-age=0
Origin: null
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8
Cookie: SESSION=2b5369d6-9d94-4b54-9ef3-05e47fe63025; JSESSIONID=3B48C7BF26937058A433A29EB2F978BC

username=aaa&password=bbb&likes=1&likes=2

通过分析和对比两种请求方式的数据,我们可以得到以下结论:

共同点:请求方式、请求 URL 和请求协议都是放在第一行。

不同点:get 请求的请求参数与 URL 拼接在一起,而 post 请求参数放在数据的最后一行。

四、封装请求和响应

Java 作为面向对象的程序开发语言,封装是其三大特性之一。

通过上文的结论,我们可以将请求数据和响应数据进行封装,让代码更具扩展性和阅读性。

4.1 封装请求对象

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
140
141
142
143
144
145
146
147
148
149
public class Request {

// 常量(回车+换行)
private static final String RN = "\r\n";
private static final String GET = "get";
private static final String POST = "post";
private static final String CHARSET = "GBK";
// 请求方式
private String method = "";
// 请求 url
private String url = "";
// 请求参数
private Map<String, List<String>> parameterMap;

private InputStream in;

private String requestInfo = "";

public Request() {
parameterMap = new HashMap<>();
}

public Request(InputStream in) {
this();
this.in = in;
try {
byte[] data = new byte[10240];
int len = in.read(data);
requestInfo = new String(data, 0, len);
} catch (IOException e) {
return;
}
// 分析头信息
this.analyzeHeaderInfo();
}

/**
* 分析头信息
*/
private void analyzeHeaderInfo() {
if (this.requestInfo == null || "".equals(this.requestInfo.trim())) {
return;
}

// 第一行请求数据: GET /login?username=aaa&password=bbb HTTP/1.1

// 1.获取请求方式
String firstLine = this.requestInfo.substring(0, this.requestInfo.indexOf(RN));
int index = firstLine.indexOf("/");
this.method = firstLine.substring(0,index).trim();

String urlStr = firstLine.substring(index,firstLine.indexOf("HTTP/1.1")).trim();
String parameters = "";
if (GET.equalsIgnoreCase(this.method)) {
if (urlStr.contains("?")) {
String[] arr = urlStr.split("\\?");
this.url = arr[0];
parameters = arr[1];
} else {
this.url = urlStr;
}

} else if (POST.equalsIgnoreCase(this.method)) {
this.url = urlStr;
parameters = this.requestInfo.substring(this.requestInfo.lastIndexOf(RN)).trim();
}

// 2. 将参数封装到 map 中
if ("".equals(parameters)) {
return;
}
this.parseToMap(parameters);
}

/**
* 封装参数到 Map 中
* @param parameters
*/
private void parseToMap(String parameters) {
// 请求参数格式:username=aaa&password=bbb&likes=1&likes=2

StringTokenizer token = new StringTokenizer(parameters, "&");
while(token.hasMoreTokens()) {
// keyValue 格式:username=aaa 或 username=
String keyValue = token.nextToken();
String[] kv = keyValue.split("=");
if (kv.length == 1) {
kv = Arrays.copyOf(kv, 2);
kv[1] = null;
}

String key = kv[0].trim();
String value = kv[1] == null ? null : this.decode(kv[1].trim(), CHARSET);

if (!this.parameterMap.containsKey(key)) {
this.parameterMap.put(key, new ArrayList<>());
}

this.parameterMap.get(key).add(value);
}
}

/**
* 根据参数名获取多个参数值
* @param name
* @return
*/
public String[] getParameterValues(String name) {
List<String> values = null;
if ((values = this.parameterMap.get(name)) == null) {
return null;
}

return values.toArray(new String[0]);
}

/**
* 根据参数名获取唯一参数值
* @param name
* @return
*/
public String getParameter(String name) {
String[] values = this.getParameterValues(name);
if (values == null) {
return null;
}
return values[0];
}


/**
* 解码中文
* @param value
* @param code
* @return
*/
private String decode(String value, String charset) {
try {
return URLDecoder.decode(value, charset);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}

public String getUrl() {
return url;
}
}

4.2 封装响应对象

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
public class Response {
// 常量
private static final String BLANK = " ";
private static final String RN = "\r\n";
// 响应内容长度
private int len;
// 存储头信息
private StringBuilder headerInfo;
// 存储正文信息
private StringBuilder contentInfo;
// 输出流
private BufferedWriter bw;

public Response() {
headerInfo = new StringBuilder();
contentInfo = new StringBuilder();
len = 0;
}

public Response(OutputStream os) {
this();
bw = new BufferedWriter(new OutputStreamWriter(os));
}

/**
* 设置头信息
* @param code
*/
private void setHeaderInfo(int code) {
// 响应头信息
headerInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);

if ("200".equals(code)) {
headerInfo.append("OK");

} else if ("404".equals(code)) {
headerInfo.append("NOT FOUND");

} else if ("500".equals(code)) {
headerInfo.append("SERVER ERROR");
}

headerInfo.append(RN);
headerInfo.append("Content-Length:").append(len).append(RN);
headerInfo.append("Content-Type:text/html").append(RN);
headerInfo.append("Date:").append(new Date()).append(RN);
headerInfo.append("Server:nginx/1.12.1").append(RN);
headerInfo.append(RN);
}

/**
* 设置正文
* @param content
* @return
*/
public Response print(String content) {
contentInfo.append(content);
len += content.getBytes().length;
return this;
}

/**
* 设置正文
* @param content
* @return
*/
public Response println(String content) {
contentInfo.append(content).append(RN);
len += (content + RN).getBytes().length;
return this;
}

/**
* 返回客户端
* @param code
* @throws IOException
*/
public void pushToClient(int code) throws IOException {
// 设置头信息
this.setHeaderInfo(code);
bw.append(headerInfo.toString());
// 设置正文
bw.append(contentInfo.toString());

bw.flush();
}


public void close() {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

改造 Server 类中的 receiveData 方法,将 IO 流操作替换成 Request 和 Response 对象:

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
public class Server {

private ServerSocket server;

public static void main(String[] args) {
Server server = new Server();
server.start();
}

/**
* 启动服务器
*/
public void start() {
try {
server = new ServerSocket(8080);
// 接收数据
this.receiveData();
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 接收数据
*/
private void receiveData() {
try {
Socket client = this.server.accept();
// 读取客户端发送的数据
Request request = new Request(client.getInputStream());

// 响应数据
Response response = new Response(client.getOutputStream());
response.println("<!DOCTYPE html>")
.println("<html lang=\"zh\">")
.println(" <head> ")
.println(" <meta charset=\"UTF-8\">")
.println(" <title>测试</title>")
.println(" </head> ")
.println(" <body> ")
.println(" <h3>Hello " + request.getParameter("username") + "</h3>")// 获取登陆名
.println(" </body> ")
.println("</html>");

response.pushToClient(200);

} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 关闭服务器
*/
public void stop() {

}

}

使用 post 请求方式提交表单,返回结果结果如下:

五、多线程

目前,程序启动后每接收一次请求,程序就会运行中断,这样就没法处理下个客户端请求。

因此,我们需要使用多线程处理多个客户端的请求。

创建一个 Runnable 作为请求分发器,处理客户端的请求:

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
public class Dispatcher implements Runnable {
// socket 客户端
private Socket socket;
// 请求对象
private Request request;
// 响应对象
private Response response;
// 响应码
private int code = 200;

public Dispatcher(Socket socket) {
this.socket = socket;
try {
this.request = new Request(socket.getInputStream());
this.response = new Response(socket.getOutputStream());
} catch (IOException e) {
code = 500;
return;
}

}

@Override
public void run() {
this.response.println("<!DOCTYPE html>")
.println("<html lang=\"zh\">")
.println(" <head> ")
.println(" <meta charset=\"UTF-8\">")
.println(" <title>测试</title>")
.println(" </head> ")
.println(" <body> ")
.println(" <h3>Hello " + request.getParameter("username") + "</h3>")// 获取登陆名
.println(" </body> ")
.println("</html>");

try {
this.response.pushToClient(code);
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}

}

}

改造 Server 类中的 receiveData 方法,将 Request 和 Response 对象传入分发器中处理请求:

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
public class Server {

private ServerSocket server;

private boolean isShutdown = false;

public static void main(String[] args) {
Server server = new Server();
server.start();
}

/**
* 启动服务器
*/
public void start() {
try {
server = new ServerSocket(8080);
// 接收数据
this.receiveData();
} catch (IOException e) {
this.stop();
}
}

/**
* 接收数据
*/
private void receiveData() {
try {
while(!isShutdown) {
new Thread(new Dispatcher(this.server.accept())).start();
}
} catch (IOException e) {
this.stop();
}
}

/**
* 关闭服务器
*/
public void stop() {
isShutdown = true;
try {
this.server.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

现在,不管浏览器发送几次请求,服务端程序都不会中断了。

但是,目前的 web 服务器只能处理一个简单的请求,不管浏览器端发送什么请求,都是只返回 Hello xxx 的页面,无法根据不同请求处理不同业务。

此问题将在下一篇文章中解决。

六、参考资料

未完待续。。。。。。