原文地址:JavaSE 手写 Web 服务器(一)

博客地址:http://www.extlight.com

一、背景

某日,在 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 = "<!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 服务端打印内容如下:

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 请求,

<!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>

服务端打印内容如下:

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 封装请求对象

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 封装响应对象

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("<!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 处理客户端请求:

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 类:

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();
}
} }

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

六、参考资料

未完待续。。。。。。

JavaSE 手写 Web 服务器(一)的更多相关文章

  1. JavaSE 手写 Web 服务器(二)

    原文地址:JavaSE 手写 Web 服务器(二) 博客地址:http://www.extlight.com 一.背景 在上一篇文章 <JavaSE 手写 Web 服务器(一)> 中介绍了 ...

  2. HTTP网络协议与手写Web服务容器

    Http协议 1.深入概念 Http:HyperText Transfer Protocol,即是超文本传输协议. 2.浅出概念(使用浏览器访问服务器端网页时需要遵循的一系列规则) Http:将各种不 ...

  3. 黑马vue---40、结合Node手写JSONP服务器剖析JSONP原理

    黑马vue---40.结合Node手写JSONP服务器剖析JSONP原理 一.总结 一句话总结: 服务端可以返回js代码给script标签,那么标签会执行它,并且可带json字符串作为参数,这样就成功 ...

  4. 【项目】手写FTP服务器-C++实现FTP服务器

    X_FTP_server 手写FTP服务器-C++实现FTP服务器 项目Gitee链接:https://gitee.com/hsby/ftp_Server 简介 一个基于libevent的高并发FTP ...

  5. 手写Tomcat服务器

    预备知识 编写服务器用到的知识点 1) Socket 编程2) HTML3) HTTP 协议4) 反射5) XML 解析6) 服务器编写 Socket编程 https://www.cnblogs.co ...

  6. 手写Javaweb服务器

    简单web服务器 回忆socket 创建客服端(在httpClient_1包下) public class Client {    public static void main(String[] a ...

  7. python写web服务器

    #coding = utf-8 from http.server import BaseHTTPRequestHandler, HTTPServer class RequestHandler(Base ...

  8. 利用html 5 websocket做个山寨版web聊天室(手写C#服务器)

    在之前的博客中提到过看到html5 的websocket后很感兴趣,终于可以摆脱长轮询(websocket之前的实现方式可以看看Developer Works上的一篇文章,有简单提到,同时也说了web ...

  9. 不使用tomcat,仅适用javaSE手写服务器--模拟登陆

    1.搭建框架 我们只是简单模拟,框架简单分三个模块 a,服务器端server包 b,servlet,根据不同的请求url,利用反射生产对应的servlet c,IO工具包,用来关闭IO流 d,编写we ...

随机推荐

  1. 剑指offer算法总结

    剑指offer算法学习总结 节选剑指offer比较经典和巧妙的一些题目,以便复习使用.一部分题目给出了完整代码,一部分题目比较简单直接给出思路.但是不保证我说的思路都是正确的,个人对算法也不是特别在行 ...

  2. 第八天 1-7 实战:创建一个root无法删除的文件

    实战:创建一个root无法删除的文件 简介:Linux文件的最底层(内核级别)属性的查看与修改 命令:lsattr.chattr Linux文件除了具有基本权限rwx,及特殊权限(SUID.SGID. ...

  3. Highcharts 动态图

    Highcharts 动态图 每秒更新曲线图 chart.events chart.event 属性中添加 load 方法(图表加载事件).在 1000 毫秒内随机产生数据点并生成图表. chart: ...

  4. 为什么是link-visited-hover-active

    前言 通常我们在设置链接的一些伪类(link,visited,hover,active)样式时,要让不同的状态显示正确的样式,我们需要按一定的顺序设置这些伪类的样式.这里我就按CSS2规范中推荐的顺序 ...

  5. MVC和EF中的 Model First 和 Code First

    准备:先引入MVC和EF的dll包 *命令方法:打开工具——库程序包管理器——程序包管理器控制台,选择自己的项目 a)     Install-Package EntityFramework -Ver ...

  6. 重构Java代码的既有设计-影片出租店

    案例:计算每位顾客的消费金额并打印详细信息.顾客租赁了哪些影片,租期多长,根据租赁时间和影片类型计算出费用.影片分为3类:儿童片,新片,普通片.此外需计算该顾客的积分. Movie: public c ...

  7. (转)MapReduce Design Patterns(chapter 4 (part 2))(八)

    Binning Pattern Description 分箱模式,跟前面的类似,分类记录且不考虑记录的顺序. Intent 归档数据集中的每条记录到一个或多个类别. Motivation 分箱和分区很 ...

  8. [AirFlow]AirFlow使用指南一 安装与启动

    1. 安装 通过pip安装: xiaosi@yoona:~$ pip install airflow 如果速度比较慢,可以使用下面提供的源进行安装: xiaosi@yoona:~$ pip insta ...

  9. 我也说说Emacs吧(4) - 光标的移动

    在说基本编辑命令之前,我们先加一个小tip,说说如何将函数和键绑定在一起. (define-key global-map [?\C-l] 'recenter-top-bottom) define-ke ...

  10. visual studio 一些小技巧 整理

    本博客将会陆续的整理一些作者在实际开发中的一些小技巧,一些挺有意思的东西,将会持续更新, 如果有问题,可以加群讨论,QQ群:592132877 #warning的使用 #warning 的意思是在程序 ...