原文地址:
博客地址:
一、背景
某日,在 Java 技术群中看到网友讨论 tomcat 容器相关内容,然后想到自己能不能实现一个简单的 web 容器。于是翻阅资料和思考,最终通过 JavaSE 原生 API 编写出一个简单 web 容器(模拟 tomcat)。在此只想分享编写简单 web 容器时的思路和技巧。
二、涉及知识
Socket 编程:服务端通过监听端口,提供客户端连接进行通信。
Http 协议:分析和响应客户端请求。
多线程:处理多个客户端请求。
用到的都是 JavaSE 的基础知识。
三、初步模型
3.1 通过 Socket API 编写服务端
服务端的功能:接收客户端发送的的数据和响应数据回客户端。
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 = "" + "" + " " + " "+ "测试 "+ " "+ " "+ "Hello World
"+ " "+ ""; 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() { }}
启动程序,通过浏览器访问 ,结果如下图:
响应信息与代码中设置的一致。
3.2 分析客户端数据
3.2.1 获取 get 方式的请求数据
打开浏览器,通过 get 方式请求 服务端打印内容如下:
GET /login?username=aaa&password=bbb HTTP/1.1Host: localhost:8080Connection: keep-aliveCache-Control: max-age=0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.8Cookie: SESSION=2b5369d6-9d94-4b54-9ef3-05e47fe63025; JSESSIONID=3B48C7BF26937058A433A29EB2F978BC
3.2.2 获取 post 方式的请求数据
编写一个简单的 html 页面,发送 post 请求,
测试
服务端打印内容如下:
POST /login HTTP/1.1Host: localhost:8080Connection: keep-aliveContent-Length: 41Cache-Control: max-age=0Origin: nullUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36Content-Type: application/x-www-form-urlencodedAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.8Cookie: SESSION=2b5369d6-9d94-4b54-9ef3-05e47fe63025; JSESSIONID=3B48C7BF26937058A433A29EB2F978BCusername=aaa&password=bbb&likes=1&likes=2
通过分析和对比两种请求方式的数据,我们可以得到以下结论:
共同点:请求方式、请求 URL 和请求协议都是放在第一行。
不同点:get 请求的请求参数与 URL 拼接在一起,而 post 请求参数放在数据的最后一行。
四、封装请求和响应
Java 作为面向对象的程序开发语言,封装是其三大特性之一。
通过上文的结论,我们可以将请求数据和响应数据进行封装,让代码更具扩展性和阅读性。
4.1 封装请求对象
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> 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 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 封装响应对象
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 类:
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("") .println("") .println(" ") .println(" ") .println("测试 ") .println(" ") .println(" ") .println("Hello " + request.getParameter("username") + "
")// 获取登陆名 .println(" ") .println(""); response.pushToClient(200); } catch (IOException e) { e.printStackTrace(); } } /** * 关闭服务器 */ public void stop() { }}
使用 post 请求方式提交表单,返回结果结果如下:
五、多线程
目前,程序启动后每接收一次请求,程序就会运行中断,这样就没法处理下个客户端请求。
因此,我们需要使用多线程处理多个客户端的请求。
创建一个 Runnable 处理客户端请求:
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("") .println("") .println(" ") .println(" ") .println("测试 ") .println(" ") .println(" ") .println("Hello " + request.getParameter("username") + "
")// 获取登陆名 .println(" ") .println(""); try { this.response.pushToClient(code); this.socket.close(); } catch (IOException e) { e.printStackTrace(); } }}
改造 Server 类:
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(); } }}
现在,不管浏览器发送几次请求,服务端程序都不会中断了。
六、参考资料
未完待续。。。。。。