一、背景

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

二、涉及知识

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

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

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

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

三、初步模型

# 3.1 通过 Socket API 编写服务端

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

  1. package com.light.server;
  2. import java.io.BufferedWriter;
  3. import java.io.IOException;
  4. import java.io.OutputStreamWriter;
  5. import java.net.ServerSocket;
  6. import java.net.Socket;
  7. import java.util.Date;
  8. public class Server {
  9. private static final String BLANK = " ";
  10. private static final String RN = "\r\n";
  11. private ServerSocket server;
  12. public static void main(String[] args) {
  13. Server server = new Server();
  14. server.start();
  15. }
  16. /**
  17. * 启动服务器
  18. */
  19. public void start() {
  20. try {
  21. server = new ServerSocket(8080);
  22. // 接收数据
  23. this.receiveData();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. /**
  29. * 接收数据
  30. */
  31. private void receiveData() {
  32. try {
  33. Socket client = this.server.accept();
  34. // 读取客户端发送的数据
  35. byte[] data = new byte[10240];
  36. int len = client.getInputStream().read(data);
  37. String requestInfo = new String(data,0,len);
  38. // 打印客户端数据
  39. System.out.println(requestInfo);
  40. // 响应正文
  41. String responseContent = "<!DOCTYPE html>" +
  42. "<html lang=\"zh\">" +
  43. " <head> " +
  44. " <meta charset=\"UTF-8\">"+
  45. " <title>测试</title>"+
  46. " </head> "+
  47. " <body> "+
  48. " <h3>Hello World</h3>"+
  49. " </body> "+
  50. "</html>";
  51. StringBuilder response = new StringBuilder();
  52. // 响应头信息
  53. response.append("HTTP/1.1").append(BLANK).append("200").append(BLANK).append("OK").append(RN);
  54. response.append("Content-Length:").append(responseContent.length()).append(RN);
  55. response.append("Content-Type:text/html").append(RN);
  56. response.append("Date:").append(new Date()).append(RN);
  57. response.append("Server:nginx/1.12.1").append(RN);
  58. response.append(RN);
  59. // 添加正文
  60. response.append(responseContent);
  61. // 输出到浏览器
  62. BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
  63. bw.write(response.toString());
  64. bw.flush();
  65. bw.close();
  66. } catch (IOException e) {
  67. e.printStackTrace();
  68. }
  69. }
  70. /**
  71. * 关闭服务器
  72. */
  73. public void stop() {
  74. }
  75. }

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

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

# 3.2 分析客户端数据

# 3.2.1 获取 get 方式的请求数据

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

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

# 3.2.2 获取 post 方式的请求数据

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

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>测试</title>
  6. </head>
  7. <body>
  8. <form action="http://localhost:8080/login" method="post">
  9. <table border="1">
  10. <tr>
  11. <td>用户名</td>
  12. <td><input type="text" name="username"></td>
  13. </tr>
  14. <tr>
  15. <td>密码</td>
  16. <td><input type="password" name="password"></td>
  17. </tr>
  18. <tr>
  19. <td>爱好</td>
  20. <td>
  21. <input type="checkbox" name="likes" value="1">篮球&nbsp;
  22. <input type="checkbox" name="likes" value="2">足球&nbsp;
  23. <input type="checkbox" name="likes" value="3">棒球
  24. </td>
  25. </tr>
  26. <tr>
  27. <td colspan="2" align="center">
  28. <input type="submit" value="提交">&nbsp;&nbsp;
  29. <input type="reset" value="重置">
  30. </td>
  31. </tr>
  32. </table>
  33. </form>
  34. </body>
  35. </html>

服务端打印内容如下:

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

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

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

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

四、封装请求和响应

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

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

# 4.1 封装请求对象

  1. public class Request {
  2. // 常量(回车+换行)
  3. private static final String RN = "\r\n";
  4. private static final String GET = "get";
  5. private static final String POST = "post";
  6. private static final String CHARSET = "GBK";
  7. // 请求方式
  8. private String method = "";
  9. // 请求 url
  10. private String url = "";
  11. // 请求参数
  12. private Map<String, List<String>> parameterMap;
  13. private InputStream in;
  14. private String requestInfo = "";
  15. public Request() {
  16. parameterMap = new HashMap<>();
  17. }
  18. public Request(InputStream in) {
  19. this();
  20. this.in = in;
  21. try {
  22. byte[] data = new byte[10240];
  23. int len = in.read(data);
  24. requestInfo = new String(data, 0, len);
  25. } catch (IOException e) {
  26. return;
  27. }
  28. // 分析头信息
  29. this.analyzeHeaderInfo();
  30. }
  31. /**
  32. * 分析头信息
  33. */
  34. private void analyzeHeaderInfo() {
  35. if (this.requestInfo == null || "".equals(this.requestInfo.trim())) {
  36. return;
  37. }
  38. // 第一行请求数据: GET /login?username=aaa&password=bbb HTTP/1.1
  39. // 1.获取请求方式
  40. String firstLine = this.requestInfo.substring(0, this.requestInfo.indexOf(RN));
  41. int index = firstLine.indexOf("/");
  42. this.method = firstLine.substring(0,index).trim();
  43. String urlStr = firstLine.substring(index,firstLine.indexOf("HTTP/1.1")).trim();
  44. String parameters = "";
  45. if (GET.equalsIgnoreCase(this.method)) {
  46. if (urlStr.contains("?")) {
  47. String[] arr = urlStr.split("\\?");
  48. this.url = arr[0];
  49. parameters = arr[1];
  50. } else {
  51. this.url = urlStr;
  52. }
  53. } else if (POST.equalsIgnoreCase(this.method)) {
  54. this.url = urlStr;
  55. parameters = this.requestInfo.substring(this.requestInfo.lastIndexOf(RN)).trim();
  56. }
  57. // 2. 将参数封装到 map 中
  58. if ("".equals(parameters)) {
  59. return;
  60. }
  61. this.parseToMap(parameters);
  62. }
  63. /**
  64. * 封装参数到 Map 中
  65. * @param parameters
  66. */
  67. private void parseToMap(String parameters) {
  68. // 请求参数格式:username=aaa&password=bbb&likes=1&likes=2
  69. StringTokenizer token = new StringTokenizer(parameters, "&");
  70. while(token.hasMoreTokens()) {
  71. // keyValue 格式:username=aaa 或 username=
  72. String keyValue = token.nextToken();
  73. String[] kv = keyValue.split("=");
  74. if (kv.length == 1) {
  75. kv = Arrays.copyOf(kv, 2);
  76. kv[1] = null;
  77. }
  78. String key = kv[0].trim();
  79. String value = kv[1] == null ? null : this.decode(kv[1].trim(), CHARSET);
  80. if (!this.parameterMap.containsKey(key)) {
  81. this.parameterMap.put(key, new ArrayList<>());
  82. }
  83. this.parameterMap.get(key).add(value);
  84. }
  85. }
  86. /**
  87. * 根据参数名获取多个参数值
  88. * @param name
  89. * @return
  90. */
  91. public String[] getParameterValues(String name) {
  92. List<String> values = null;
  93. if ((values = this.parameterMap.get(name)) == null) {
  94. return null;
  95. }
  96. return values.toArray(new String[0]);
  97. }
  98. /**
  99. * 根据参数名获取唯一参数值
  100. * @param name
  101. * @return
  102. */
  103. public String getParameter(String name) {
  104. String[] values = this.getParameterValues(name);
  105. if (values == null) {
  106. return null;
  107. }
  108. return values[0];
  109. }
  110. /**
  111. * 解码中文
  112. * @param value
  113. * @param code
  114. * @return
  115. */
  116. private String decode(String value, String charset) {
  117. try {
  118. return URLDecoder.decode(value, charset);
  119. } catch (UnsupportedEncodingException e) {
  120. e.printStackTrace();
  121. }
  122. return null;
  123. }
  124. public String getUrl() {
  125. return url;
  126. }
  127. }

# 4.2 封装响应对象

  1. public class Response {
  2. // 常量
  3. private static final String BLANK = " ";
  4. private static final String RN = "\r\n";
  5. // 响应内容长度
  6. private int len;
  7. // 存储头信息
  8. private StringBuilder headerInfo;
  9. // 存储正文信息
  10. private StringBuilder contentInfo;
  11. // 输出流
  12. private BufferedWriter bw;
  13. public Response() {
  14. headerInfo = new StringBuilder();
  15. contentInfo = new StringBuilder();
  16. len = 0;
  17. }
  18. public Response(OutputStream os) {
  19. this();
  20. bw = new BufferedWriter(new OutputStreamWriter(os));
  21. }
  22. /**
  23. * 设置头信息
  24. * @param code
  25. */
  26. private void setHeaderInfo(int code) {
  27. // 响应头信息
  28. headerInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);
  29. if ("200".equals(code)) {
  30. headerInfo.append("OK");
  31. } else if ("404".equals(code)) {
  32. headerInfo.append("NOT FOUND");
  33. } else if ("500".equals(code)) {
  34. headerInfo.append("SERVER ERROR");
  35. }
  36. headerInfo.append(RN);
  37. headerInfo.append("Content-Length:").append(len).append(RN);
  38. headerInfo.append("Content-Type:text/html").append(RN);
  39. headerInfo.append("Date:").append(new Date()).append(RN);
  40. headerInfo.append("Server:nginx/1.12.1").append(RN);
  41. headerInfo.append(RN);
  42. }
  43. /**
  44. * 设置正文
  45. * @param content
  46. * @return
  47. */
  48. public Response print(String content) {
  49. contentInfo.append(content);
  50. len += content.getBytes().length;
  51. return this;
  52. }
  53. /**
  54. * 设置正文
  55. * @param content
  56. * @return
  57. */
  58. public Response println(String content) {
  59. contentInfo.append(content).append(RN);
  60. len += (content + RN).getBytes().length;
  61. return this;
  62. }
  63. /**
  64. * 返回客户端
  65. * @param code
  66. * @throws IOException
  67. */
  68. public void pushToClient(int code) throws IOException {
  69. // 设置头信息
  70. this.setHeaderInfo(code);
  71. bw.append(headerInfo.toString());
  72. // 设置正文
  73. bw.append(contentInfo.toString());
  74. bw.flush();
  75. }
  76. public void close() {
  77. try {
  78. bw.close();
  79. } catch (IOException e) {
  80. e.printStackTrace();
  81. }
  82. }
  83. }

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

  1. public class Server {
  2. private ServerSocket server;
  3. public static void main(String[] args) {
  4. Server server = new Server();
  5. server.start();
  6. }
  7. /**
  8. * 启动服务器
  9. */
  10. public void start() {
  11. try {
  12. server = new ServerSocket(8080);
  13. // 接收数据
  14. this.receiveData();
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. /**
  20. * 接收数据
  21. */
  22. private void receiveData() {
  23. try {
  24. Socket client = this.server.accept();
  25. // 读取客户端发送的数据
  26. Request request = new Request(client.getInputStream());
  27. // 响应数据
  28. Response response = new Response(client.getOutputStream());
  29. response.println("<!DOCTYPE html>")
  30. .println("<html lang=\"zh\">")
  31. .println(" <head> ")
  32. .println(" <meta charset=\"UTF-8\">")
  33. .println(" <title>测试</title>")
  34. .println(" </head> ")
  35. .println(" <body> ")
  36. .println(" <h3>Hello " + request.getParameter("username") + "</h3>")// 获取登陆名
  37. .println(" </body> ")
  38. .println("</html>");
  39. response.pushToClient(200);
  40. } catch (IOException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. /**
  45. * 关闭服务器
  46. */
  47. public void stop() {
  48. }
  49. }

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

五、多线程

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

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

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

  1. public class Dispatcher implements Runnable {
  2. // socket 客户端
  3. private Socket socket;
  4. // 请求对象
  5. private Request request;
  6. // 响应对象
  7. private Response response;
  8. // 响应码
  9. private int code = 200;
  10. public Dispatcher(Socket socket) {
  11. this.socket = socket;
  12. try {
  13. this.request = new Request(socket.getInputStream());
  14. this.response = new Response(socket.getOutputStream());
  15. } catch (IOException e) {
  16. code = 500;
  17. return;
  18. }
  19. }
  20. @Override
  21. public void run() {
  22. this.response.println("<!DOCTYPE html>")
  23. .println("<html lang=\"zh\">")
  24. .println(" <head> ")
  25. .println(" <meta charset=\"UTF-8\">")
  26. .println(" <title>测试</title>")
  27. .println(" </head> ")
  28. .println(" <body> ")
  29. .println(" <h3>Hello " + request.getParameter("username") + "</h3>")// 获取登陆名
  30. .println(" </body> ")
  31. .println("</html>");
  32. try {
  33. this.response.pushToClient(code);
  34. this.socket.close();
  35. } catch (IOException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }

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

  1. public class Server {
  2. private ServerSocket server;
  3. private boolean isShutdown = false;
  4. public static void main(String[] args) {
  5. Server server = new Server();
  6. server.start();
  7. }
  8. /**
  9. * 启动服务器
  10. */
  11. public void start() {
  12. try {
  13. server = new ServerSocket(8080);
  14. // 接收数据
  15. this.receiveData();
  16. } catch (IOException e) {
  17. this.stop();
  18. }
  19. }
  20. /**
  21. * 接收数据
  22. */
  23. private void receiveData() {
  24. try {
  25. while(!isShutdown) {
  26. new Thread(new Dispatcher(this.server.accept())).start();
  27. }
  28. } catch (IOException e) {
  29. this.stop();
  30. }
  31. }
  32. /**
  33. * 关闭服务器
  34. */
  35. public void stop() {
  36. isShutdown = true;
  37. try {
  38. this.server.close();
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. }

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

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

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

六、参考资料

未完待续。。。。。。