Java Socket 如何演化成 Tomcat

相关文章:
Java Socket
Java Socket 聊天室原理
Java Socket 如何演化成 Tomcat

  • 模拟请求表单;
  • 简易服务器代码获取请求;
  • HTTP协议;
  • 抽象封装Response和Request;
  • 将与客户端的交互封装成Servlet;
  • 使用分发器多线程处理各种业务;
  • 分发器使用的上下文及全局变量;
  • 利用反射优化代码;
  • 在配置文件中设置路径与Servlet的对应关系;
  • 完整源码下载链接

一、请求表单

首先在前端模拟一个注册请求表单,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<title>注册</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
</head>
<body>
<form method="post" action="http://localhost:8888/index.html">
昵称:<input type="text" name="username" id="username"/>
密码:<input type="password" name="password" id="password"/>
兴趣:<input type="checkbox" name="fav" value="0"/>篮球
<input type="checkbox" name="fav" value="1"/>足球
<input type="checkbox" name="fav" value="2"/>冰球
<input type="submit" value="注册"/>
</form>
</body>
</html>

method是请求方式,默认为get,其数据量小,安全性不高,而post量大,安全性相对高;
action为请求的服务器路径;id用于前端区分唯一性;
name用于服务器区分唯一性,如要提交数据给后台,必须声明name;
此处fav的值对应多个,用于测试服务器解析参数。

二、简易服务器

用一个简易的服务器代码输出来自浏览器的请求信息。

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
public class Server {
private ServerSocket server;
private boolean isRunning = true;
// 开启服务
public void start() {
try {
server = new ServerSocket(8888);
while (isRunning) {
receive();
}
} catch (IOException e) {
stop();
}
}
// 处理请求并响应
private void receive() {
try {
Socket client = server.accept();
// 以下代码临时用于接受客户端的请求信息
byte[] data = new byte[1024 * 10];
int len = client.getInputStream().read(data);
String requestInfo = new String(data, 0, len).trim();
requestInfo = URLDecoder.decode(requestInfo, "utf-8");
System.out.println(requestInfo);
// 以下代码为返回信息,遵从HTTP协议
// TODO
} catch (IOException e) {
}
}
// 关闭服务
public void stop() {
isRunning = true;
CloseUtil.closeServerSocket(server);
}
}

开启上述服务器,以默认或GET方式发送表单,服务端接收到的信息是:

1
2
3
4
5
6
7
GET /index.html?username=先小涛&password=123456&fav=0&fav=2 HTTP/1.1
Host: localhost:8888
Accept: text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn
Connection: keep-alive
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/600.8.9 (KHTML, like Gecko) Version/8.0.8 Safari/600.8.9

以POST方式发送表单,服务端接收到的信息是:

1
2
3
4
5
6
7
8
9
10
11
12
POST /index.html HTTP/1.1
Host: localhost:8888
Content-Type: application/x-www-form-urlencoded
Origin: file://
Connection: keep-alive
Accept: text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/600.8.9 (KHTML, like Gecko) Version/8.0.8 Safari/600.8.9
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Content-Length: 102
username=先小涛&password=123456&fav=0&fav=2

三、HTTP协议

上述两段请求信息由于请求方式不同导致略有差别,而这些固定格式的信息是由遵从HTTP协议的浏览器负责生成的。服务端响应的信息也要遵从HTTP协议,否则客户端无法解析返回的数据。HTTP响应格式与请求一样,由三个部分构成,分别是:
1、HTTP协议版本、状态代码、描述
2、响应头(Response Head)
3、响应正文(Response Content)
可以用代码方式拼接响应字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 响应正文
StringBuilder responseContext = new StringBuilder();
responseContext.append("<html><title>HTTP响应示例</title><body>I defined a tomcat</body></html>");
// 响应全文
StringBuilder response = new StringBuilder();
response.append("HTTP/1.1").append(" ").append("200").append(" ").append("OK");
response.append("Server:xianxiaotao Server/0.0.1").append("\r\n");
response.append("Date:").append(new Date()).append("\r\n");
response.append("Content-type:text/html;charset=utf-8").append("\r\n");
response.append("Content-Length:").append(responseContext.toString().getBytes().length).append("\r\n");
response.append("\r\n");
response.append(responseContext);
// 返回数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
bw.write(response.toString());
bw.flush();
bw.close();

HTTP协议中常用的一些信息:
1、状态代码:
200 OK
400 Bad Request
404 Not Found
405 Method Not Allowed
500 Server Error

2、内容类型:
超文本:Content-type:text/html;charset=GBK
纯文本:Content-type:text/plain;charset=GBK
下载流:Content-type:application/octet-stream

响应信息都是固定格式,不必每次都要拼字符串,可抽取共性封装成Response对象。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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class Response {
public static final String CRLF = "\r\n"; // 回车符
public static final String BLANK = " "; // 空格符
private StringBuilder headInfo; // 头信息
private StringBuilder content; // 正文
private int len; // 正文长度
private BufferedWriter bw; // 推送信息到客户端的流
private Response() {
headInfo = new StringBuilder();
content = new StringBuilder();
len = 0;
}
public Response(Socket socket) {
this();
try {
bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
} catch (IOException e) {
headInfo = null;
}
}
// 构建正文
public Response print(String info) {
content.append(info);
len += info.getBytes().length;
return this;
}
// 构建正文+回车
public Response println(String info) {
content.append(info).append(CRLF);
len += (info + CRLF).getBytes().length;
return this;
}
// 生产响应头信息
private void createHeadInfo(int code) {
headInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);
switch (code) {
case 200:
headInfo.append("OK");
break;
case 404:
headInfo.append("NOT FOUND");
break;
case 500:
headInfo.append("SERVER ERROR");
break;
}
headInfo.append("Server:xianxiaotao Server/0.0.1").append(CRLF);
headInfo.append("Date:").append(new Date()).append(CRLF);
headInfo.append("Content-type:text/html;charset=utf-8").append(CRLF);
headInfo.append("Content-Length:").append(len).append(CRLF);
headInfo.append(CRLF);
}
// 推送到客户端
public void pushToClient(int code) throws IOException {
if (null == headInfo)
code = 500;
else {
createHeadInfo(code);
bw.append(headInfo.toString());
bw.append(content.toString());
bw.flush();
}
}
public void close() {
CloseUtil.closeIO(bw);
}
}

五、Request

主要解析请求信息中的请求方式和参数,并将结果封装成Request对象。

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
public class Request {
public static final String CRLF = "\r\n";
private String method; // 请求方式
private String url; // 请求资源
private Map<String, List<String>> parameterMapValues; // 请求参数
private BufferedInputStream bis;// 流
private String requestInfo; // 请求信息
private Request() {
method = "";
url = "";
parameterMapValues = new HashMap<>();
}
public Request(Socket client) {
this();
try {
this.bis = new BufferedInputStream(client.getInputStream());
byte[] buf = new byte[10240];
int len = bis.read(buf);
requestInfo = new String(buf, 0, len);
} catch (IOException e) {
return;
}
parseRequestInfo();
}
/**
* 解析请求信息
*/
private void parseRequestInfo() {
if (null == requestInfo || requestInfo.trim().length() == 0)
return;
String paramString = ""; // 请求参数
String firstLine = requestInfo.substring(0, requestInfo.indexOf(CRLF));
int idx = firstLine.indexOf("/");
this.method = firstLine.substring(0, idx).trim();
String urlStr = firstLine.substring(idx, firstLine.indexOf("HTTP/")).trim();
if (method.equalsIgnoreCase("post")) {
this.url = urlStr;
paramString = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
} else if (method.equalsIgnoreCase("get")) {
if (urlStr.contains("?")) { // 是否存在参数
String[] urlArray = urlStr.split("\\?");
this.url = urlArray[0];
paramString = urlArray[1];
} else {
this.url = urlStr;
}
}
if (paramString.equals(""))
return;
parseParams(paramString); // 解析请求参数
}
/**
* 解析请求信息中的参数
* @param paramString
*/
private void parseParams(String paramString) {
StringTokenizer token = new StringTokenizer(paramString, "&");
while (token.hasMoreTokens()) {
String str = token.nextToken();
String[] keyAndVal = str.split("=");
if (keyAndVal.length == 1) { // 只有参数没有值
keyAndVal = Arrays.copyOf(keyAndVal, 2);
keyAndVal[1] = null;
}
String key = keyAndVal[0].trim();
String val = (null == keyAndVal[1] ? null : decode(keyAndVal[1].trim(), "utf-8"));
if (!parameterMapValues.containsKey(key)) {
parameterMapValues.put(key, new ArrayList<>());
}
List<String> values = parameterMapValues.get(key);
values.add(val);
}
}
/**
* 解决中文问题
* @param value 需要正确显示中文的字符
* @param code 字符集(gbk、utf-8等)
* @return String 如异常则返回null
*/
private String decode(String value, String code) {
try {
return URLDecoder.decode(value, code);
} catch (UnsupportedEncodingException e) {}
return null;
}
/**
* 获取网页对应name的多个值
* @param name
* @return String[] 一个name有多个值
*/
public String[] getParameterValues(String name) {
List<String> values = parameterMapValues.get(name);
if (null == values)
return null;
else
return values.toArray(new String[0]);
}
/**
* 获取网页对应name的值
* @param name
* @return String name对应的值
*/
public String getParameter(String name) {
String[] values = getParameterValues(name);
return (null == values) ? null : values[0];
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}

需要注意参数值为中文、一参多值的情况。

六、Servlet

客户端与服务器端的交互形式不定,如登录、注册、查看信息等。可抽象出一个抽象类,如下:

1
2
3
4
5
6
7
8
public abstract class Servlet {
public void service(Request req, Response rep) throws Exception {
doGet(req, rep);
doPost(req, rep);
}
public abstract void doGet(Request req, Response rep) throws Exception;
public abstract void doPost(Request req, Response rep) throws Exception;
}

具体的交互业务类Servlet继承抽象类,利用JAVA的多态优化其他代码。示例如下:

1
2
3
4
5
6
7
8
9
10
public class RegisterServlet extends Servlet {
@Override
public void doGet(Request req, Response rep) throws Exception {
rep.println("<html><title>HTTP响应示例</title><body>欢迎用户" + req.getParameter("username") + "注册成功</body></html>");
}
@Override
public void doPost(Request req, Response rep) throws Exception {}
}

七、分发器Dispatcher

既然交互方式不定,同一种交互方式的交互次数不定,需要借助分发器(多线程)进行优化处理。

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
/**
* 分发器,多线程(一个请求响应,创建一个此对象)
* @author xianxiaotao
*/
public class Dispatcher implements Runnable {
private Socket client;
private Request req;
private Response rep;
private int code = 200; // 默认200
public Dispatcher(Socket client) {
this.client = client;
try {
req = new Request(client.getInputStream());
rep = new Response(client.getOutputStream());
} catch (IOException e) {
code = 500;
return;
}
}
@Override
public void run() {
try {
// 根据请求参数中的url找到对应的Servlet
Servlet servlet = WebApp.getServlet(req.getUrl());
if (null == servlet) // 找不到处理
this.code = 404;
else
servlet.service(req, rep);
rep.pushToClient(code);
} catch (Exception e) {
this.code = 500;
}
// 再推送一次
try {
rep.pushToClient(code);
} catch (IOException e) {}
CloseUtil.closeSocket(client);
}
}

八、上下文ServletContext和全局变量WebApp

分发器需要根据请求参数中的url,从上下文中找出对应的Servlet来做相应的处理。

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
/**
* 上下文
* @author xianxiaotao
*/
public class ServletContext {
private Map<String, String> mapping; // url(/register) ——> 别名(register)
private Map<String, Servlet> servlet; // 别名(register) ——> 对应的Servlet(RegisterServlet),后期使用反射其全类名获取对象
public ServletContext() {
mapping = new HashMap<>();
servlet = new HashMap<>();
}
public Map<String, String> getMapping() {
return mapping;
}
public void setMapping(Map<String, String> mapping) {
this.mapping = mapping;
}
public Map<String, Servlet> getServlet() {
return servlet;
}
public void setServlet(Map<String, Servlet> servlet) {
this.servlet = servlet;
}
}

创建WebApp,保存全局变量(上下文),以及对应的操作(解析、获取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class WebApp {
private static ServletContext context;
// 初始化请求资源路径与Servlet的对应关系,临时代码,后期解析配置文件获取对应关系
static {
context = new ServletContext();
Map<String, String> mapping = context.getMapping();
mapping.put("/reg", "register");
mapping.put("/register", "register");
Map<String, Servlet> servlet = context.getServlet();
servlet.put("register", new RegisterServlet());
}
/**
* 根据url获取对应的Servlet
* @param url 表单Action中的值
* @return Servlet
*/
public static Servlet getServlet(String url) {
if (null == url || url.trim().length() == 0)
return null;
return context.getServlet().get(context.getMapping().get(url));
}
}

如果将上下文中存放的Servlet对象,改成存放对象全类名字符串。需要对象时,利用JAVA中的反射技术创建对象,这将极大优化内存。

九、反射

1
2
3
4
5
6
7
8
9
Map<String, String > servlet = context.getServlet();
servlet.put("register", "com.xian.blog.xtomcat.RegisterServlet");
public static Servlet getServlet(String url) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (null == url || url.trim().length() == 0)
return null;
String servletName = context.getServlet().get(context.getMapping().get(url));
return (Servlet)Class.forName(servletName).newInstance();
}

十、Xml配置文件

将请求资源路径与Servlet的对应关系写死在程序中,不便于代码维护扩展。最好的方式将其配置的xml文件中,程序负责解析而不用经常修改代码。配置文件格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>register</servlet-name>
<servlet-class>com.xian.blog.xtomcat.RegisterServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>register</servlet-name>
<url-pattern>/reg</url-pattern>
<url-pattern>/register</url-pattern>
</servlet-mapping>
</web-app>

从文件中抽象出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
/**
* 存放配置信息:
* <servlet>
* <servlet-name>register</servlet-name>
* <servlet-class>com.xian.blog.xtomcat.RegisterServlet</servlet-class>
* </servlet>
* @author xianxiaotao
*/
public class Entity {
private String name;
private String clzz;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClzz() {
return clzz;
}
public void setClzz(String clzz) {
this.clzz = clzz;
}
}
/**
* 存放配置信息:
* <servlet-mapping>
* <servlet-name>register</servlet-name>
* <url-pattern>/reg</url-pattern>
* <url-pattern>/register</url-pattern>
* </servlet-mapping>
* @author xianxiaotao
*/
public class Mapping {
private String name;
private List<String> urlPattern;
public Mapping() {
urlPattern = new ArrayList<>();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getUrlPattern() {
return urlPattern;
}
public void setUrlPattern(List<String> urlPattern) {
this.urlPattern = urlPattern;
}
}

具体子类实现DefaultHandler中的方法:

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
public class WebHandler extends DefaultHandler {
private List<Entity> entityList;
private List<Mapping> mappingList;
private Entity entity;
private Mapping mapping;
private String beginTag;
private boolean isMapping;
@Override
public void startDocument() throws SAXException {
// 文档解析开始
entityList = new ArrayList<>();
mappingList = new ArrayList<>();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
// 开始元素
if (null != qName) {
beginTag = qName;
if (qName.equals("servlet")) {
isMapping = false;
entity = new Entity();
} else if (qName.equals("servlet-mapping")) {
isMapping = true;
mapping = new Mapping();
}
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// 处理元素
if (null != beginTag) {
String str = new String(ch, start, length);
if (isMapping) {
if (beginTag.equals("servlet-name")) {
mapping.setName(str);
} else if (beginTag.equals("url-pattern")) {
mapping.getUrlPattern().add(str);
}
} else {
if (beginTag.equals("servlet-name")) {
entity.setName(str);
} else if (beginTag.equals("servlet-class")) {
entity.setClzz(str);
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
// 结束元素
if (null != qName) {
if (qName.equals("servlet")) {
entityList.add(entity);
} else if (qName.equals("servlet-mapping")) {
mappingList.add(mapping);
}
}
beginTag = null;
}
@Override
public void endDocument() throws SAXException {
// 文档解析结束
}
public List<Entity> getEntityList() {
return entityList;
}
public void setEntityList(List<Entity> entityList) {
this.entityList = entityList;
}
public List<Mapping> getMappingList() {
return mappingList;
}
public void setMappingList(List<Mapping> mappingList) {
this.mappingList = mappingList;
}
}

解析配置文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 初始化请求资源路径与Servlet的对应关系,临时代码,后期解析配置文件获取对应关系
static {
try {
SAXParserFactory factory = SAXParserFactory.newInstance(); // 获取解析工厂
SAXParser parser = factory.newSAXParser();
WebHandler handler = new WebHandler();
parser.parse(Thread.currentThread().getContextClassLoader().getResourceAsStream("com/xian/blog/xtomcat/web.xml"), handler);
context = new ServletContext();
Map<String, String > servlet = context.getServlet();
for (Entity entity : handler.getEntityList())
servlet.put(entity.getName(), entity.getClzz());
Map<String, String> mapping = context.getMapping();
for (Mapping mapp : handler.getMappingList()) {
List<String> urlPatterns = mapp.getUrlPattern();
for (String url : urlPatterns) {
mapping.put(url, mapp.getName());
}
}
} catch (Exception e) {
}
}

十一、完整代码下载链接

https://pan.baidu.com/s/1bo3Dd0r