手写tomcat+servlet,浅析原理
最近回头看了servlet,写了一个简易版的tomcat+servlet。代码都上传到了github,请大家下载点星,感谢!!
https://github.com/kangwenzhuang/mytomcat
个人理解,观点可能片面,欢迎在评论区喷我,这样我才能成长
适合新手,高手请绕开
概念
首先弄懂http,servlet,http
socket是使用TCP/IP或者UDP协议在服务器与客户端之间进行传输的技术,是网络编程的基础
servlet是使用http协议在服务器与客户端之间通信的技术。是Socket的一种应用
http协议:是在tcp/ip协议之上进一步封装的一层协议,关注的事数据传输的格式是否规范,底层的数据传输还是运用了socket和tcp/ip
也就是说我们可以借助socket,实现servlet
逻辑
- 开启服务,开socket服务端口(8888),接听…
- 初始化,读取自定义web.xml,获取自定义servlet(定义servletMap保存servletname和对应的servlet对象,需要用到反射实现)和servletMapping(定义mappingMap保存url和对应的servletname),这样就能通过url找到对应的servlet对象了,然后就能执行里面的东东了,是不是这样的!
- 客户端访问,可以是浏览器(最好不要用谷歌,竟然连着发送两次get请求),还可以是postman,客户端访问的同时是不是就意味着socket服务端接受了连接
- 输入url,回车,本是一个http请求,但因为请求中带有端口号(8888),8888端口就一个,那么socket正好得到此http请求报文(1.请求行 2.请求头 3.请求体)
- 接下来解析http请求报文,需要知道请求方法,请求参数,对了还有一个最重要的请求路径,等等。有了这个请求路径是不是就能在servletMapping找到是哪个对象了,调用里面的方法就能得到结果了
- 得到的结果在服务器上,怎么在让请求者得到数据,没错,就是让该socket捎回去
大致的过程就是这样
手写实现
便于管理我就在资源文件夹新建property.properties文件,可以设置端口
port=8888
当然还要有web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.kang.servlet.ext.Myservlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
新建servlet之前可定要先定义Request和Response
在这里就要把Request和resonse和socket绑定,通过socket告诉Request请求报文,告诉Response响应报文
package com.kang.http;
import com.kang.utils.GetParm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class Request {
private Socket client;
private String url; // 请求资源
private String method; // 请求方式
private String protocal; // 协议
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
private Map<String, String> map; // 参数列表
//工具类解析参数
private GetParm getParm;
public Request() {
}
public Request(Socket client) {
this.client = client;
map = new HashMap<String, String>();
getParm = new GetParm();
try {
InputStream is = client.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 先读取第一行
String firstLine = br.readLine();
System.out.println("通过socket查看http请求报文:");
System.out.println("**************************************************************");
System.out.println(firstLine);
String[] split = firstLine.split(" ");
String rl;
while (true) {
if ((rl = br.readLine()).equals("")) break;
System.out.println(rl);
}
System.out.println("**************************************************************");
// 把提交方式、请求资源、协议取出
method = split[0];
url = split[1];
protocal = split[2];
// 解析url,分析参数
if (method.equalsIgnoreCase("get")) {
if (url.contains("?")) {
String[] split2 = url.split("[?]");
url = split2[0];
// 参数行
String property = split2[1];
map = getParm.getParm(property);
}
} else {
BufferedReader br1 = new BufferedReader(new InputStreamReader(is));
int length = 0;
while (br1.ready()) {
String line = br1.readLine();
if (line.contains("Content-Length")) {
String[] split2 = line.split(" ");
length = Integer.parseInt(split2[1]);
}
if (line.equals("")) {
break;
}
}
String info = null;
char[] ch = new char[length];
br.read(ch, 0, length);
info = new String(ch, 0, length);
map = getParm.getParm(info);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getProtocal() {
return protocal;
}
public void setProtocal(String protocal) {
this.protocal = protocal;
}
public void setUrl(String url) {
this.url = url;
}
public void setMethod(String method) {
this.method = method;
}
public Socket getClient() {
return client;
}
// 获得get post 方法
public String getMethod() {
return method;
}
// 获得url
public String getUrl() {
return url;
}
}
package com.kang.http;
import com.kang.utils.GetParm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class Request {
private Socket client;
private String url; // 请求资源
private String method; // 请求方式
private String protocal; // 协议
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
private Map<String, String> map; // 参数列表
//工具类解析参数
private GetParm getParm;
public Request() {
}
public Request(Socket client) {
this.client = client;
map = new HashMap<String, String>();
getParm = new GetParm();
try {
InputStream is = client.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 先读取第一行
String firstLine = br.readLine();
System.out.println("通过socket查看http请求报文:");
System.out.println("**************************************************************");
System.out.println(firstLine);
String[] split = firstLine.split(" ");
String rl;
while (true) {
if ((rl = br.readLine()).equals("")) break;
System.out.println(rl);
}
System.out.println("**************************************************************");
// 把提交方式、请求资源、协议取出
method = split[0];
url = split[1];
protocal = split[2];
// 解析url,分析参数
if (method.equalsIgnoreCase("get")) {
if (url.contains("?")) {
String[] split2 = url.split("[?]");
url = split2[0];
// 参数行
String property = split2[1];
map = getParm.getParm(property);
}
} else {
BufferedReader br1 = new BufferedReader(new InputStreamReader(is));
int length = 0;
while (br1.ready()) {
String line = br1.readLine();
if (line.contains("Content-Length")) {
String[] split2 = line.split(" ");
length = Integer.parseInt(split2[1]);
}
if (line.equals("")) {
break;
}
}
String info = null;
char[] ch = new char[length];
br.read(ch, 0, length);
info = new String(ch, 0, length);
map = getParm.getParm(info);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getProtocal() {
return protocal;
}
public void setProtocal(String protocal) {
this.protocal = protocal;
}
public void setUrl(String url) {
this.url = url;
}
public void setMethod(String method) {
this.method = method;
}
public Socket getClient() {
return client;
}
// 获得get post 方法
public String getMethod() {
return method;
}
// 获得url
public String getUrl() {
return url;
}
}
新建Myservlet,在这之前写一个servlet吧,毕竟有些方法每次都要用
package com.kang.servlet;
import com.kang.http.Request;
import com.kang.http.Response;
public abstract class Servlet {
public void service(Request request, Response response) {
//判断是调用doget 还是 dopost
if ("get".equalsIgnoreCase(request.getMethod())) {
this.doGet(request, response);
} else {
this.doPost(request, response);
}
}
public abstract void doGet(Request request, Response response);
public abstract void doPost(Request request, Response response);
}
package com.kang.servlet.ext;
import com.kang.http.Request;
import com.kang.http.Response;
import com.kang.servlet.Servlet;
import java.util.Map;
public class Myservlet extends Servlet {
@Override
public void doGet(Request request, Response response) {
System.out.println("进入了我的第一个servlet");
Map<String, String> map=request.getMap();
for(Map.Entry<String,String> m:map.entrySet()){
System.out.println(m.getKey()+":"+m.getValue());
}
response.setContentType("text/html");
response.setWrite("我的名字是:"+map.get("name")+"\n"+"我的年龄是:"+map.get("age"));
}
@Override
public void doPost(Request request, Response response) {
}
}
需要用到的工具类
解析参数
package com.kang.utils;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
//模拟浏览器发送请求给服务器
public class GetParm {
private Map<String, String> map = new HashMap<String, String>() ;
public GetParm() {}
public Map<String,String> getParm(String property){
String[] split3 = property.split("&");
for (int i = 0; i < split3.length; i++) {
String[] split4 = split3[i].split("=");
map.put(split4[0], URLDecoder.decode(split4[1]));
}
System.out.println(map);
return map;
}
}
解析xml
package com.kang.utils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.util.List;
public class UtilsXml {
//定义解析器和文档对象
public SAXReader saxReader;
public Document document;
public UtilsXml(String path) {
//获取解析器
saxReader = new SAXReader();
try {
//获取文档对象
document = saxReader.read(path);
} catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 根据节点名称获取内容
*
* @param name 节点名称
* @return 节点内容
*/
public String getElementText(String name) {
//定位根节点
Element root = document.getRootElement();
List<Element> mapp = root.elements("servlet-mapping");
List<Element> servlet = root.elements("servlet");
String serveltName = "";
String classpath = "";
for (Element e : mapp) {
if (e.element("url-pattern").getText().equals(name)) {
serveltName = e.element("servlet-name").getText();
break;
}
}
for (Element e : servlet) {
if (e.element("servlet-name").getText().equals(serveltName)) {
classpath = e.element("servlet-class").getText();
break;
}
}
return classpath;
}
/**
* 获取节点下的所有节点
*
* @param root
* @param name
* @return
*/
public List<Element> getNodes(String name) {
Element root = document.getRootElement();
return root.elements(name);
}
public static void main(String[] args) {
UtilsXml xml = new UtilsXml(UtilsXml.class.getResource("/") + "web.xml");
//System.out.println(xml.getElementText("/myhtml.html"));
List<Element> list = xml.getNodes("servlet");
for (Element element : list) {
System.out.println(element.element("servlet-name").getText());
System.out.println(element.element("servlet-class").getText());
}
}
}
重头戏
还原tomcat
package com.kang;
import com.kang.socket.SocketProcess;
import com.kang.utils.UtilsXml;
import org.dom4j.Element;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
public class MyTomcat {
private static final int port;
private static Properties prop;
public static final HashMap<String, Object> servletMapping = new HashMap<String, Object>();
public static final HashMap<String, Object> servlet = new HashMap<String, Object>();
static {
prop = new Properties();
try {
File file = new File(MyTomcat.class.getClassLoader().getResource("property.properties").getFile());
prop.load(new FileInputStream(file));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
port = Integer.parseInt(prop.getProperty("port"));
}
private void init() {
InputStream io = null;
String basePath;
try {
System.out.println("加载配置文件开始");
//读取web.xml
UtilsXml xml = new UtilsXml(MyTomcat.class.getClassLoader().getResource("web.xml").getFile());
//讲所有的类都存储到容器中 并且创造对象
List<Element> list = xml.getNodes("servlet");
for (Element element : list) {
servlet.put(element.element("servlet-name").getText(), Class.forName(element.element("servlet-class").getText()).newInstance());
}
//映射关系创建
List<Element> list2 = xml.getNodes("servlet-mapping");
for (Element element : list2) {
servletMapping.put(element.element("url-pattern").getText(), element.element("servlet-name").getText());
}
System.out.println("加载配置文件结束");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (io != null) {
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private void start() {
try {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Tomcat 服务已启动,地址:localhost ,端口:" + port);
this.init();
//持续监听
do {
Socket socket = serverSocket.accept();
//处理任务
Thread thread = new SocketProcess(socket);
thread.start();
} while (true);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyTomcat tomcat = new MyTomcat();
tomcat.start();
}
}
在这里需要一个线程运行Myservlet里的函数,同时将结果让socket输出
package com.kang.socket;
import com.kang.MyTomcat;
import com.kang.http.Request;
import com.kang.http.Response;
import com.kang.servlet.Servlet;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class SocketProcess extends Thread {
protected Socket socket;
public SocketProcess(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
Request request = new Request(socket);
Response response = new Response(socket);
//从映射中找
System.out.println("url:" + request.getUrl());
String servelName = (String) MyTomcat.servletMapping.get(request.getUrl());
System.out.println("servelName:" + servelName);
if (servelName != null && !servelName.isEmpty()) {
HashMap<String, Object> servlet1 = MyTomcat.servlet;
for (Map.Entry<String, Object> m : servlet1.entrySet()) {
System.out.println("key:" + m.getKey());
}
//映射有的话找到对应的对象
Servlet servlet = (Servlet) MyTomcat.servlet.get(servelName);
if (servlet != null) {
servlet.doGet(request, response);
} else {
System.out.println("找不到对应的servlet");
}
} else {
System.out.println("找不到对应的servletMapping");
}
String responseHeader = "HTTP/1.1 200 \r\n"
+ "Content-Type: " + response.getContentType() + "\r\n"
+ "\r\n";
String res = responseHeader + response.getWrite();
System.out.println("通过socket查看http响应报文:");
System.out.println("**************************************************************");
System.out.println(res);
System.out.println("**************************************************************");
OutputStream outputStream=response.getPs();
// OutputStream outputStream = socket.getOutputStream();
outputStream.write(res.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
回顾
-
通过在Request打印出socket的接收,对http请求报文的结构有了一定的认识
-
在获得http的请求行,请求头,请求体时,我只是写了一个大概,我的写法是
while (true) {
if ((rl = br.readLine()).equals("")) break;
System.out.println(rl);
}
其实这样不是很好,第一我把空行给去掉了,第二似乎也没分析去请求体,不过后来我也知道怎么获得了
一开始的写法是,这样就可以把整个报文都能分析出来,太天真了
while (true) {
if ((rl = br.readLine())==null) break;
System.out.println(rl);
}
这样写的问题是,服务器没有回应,debug也不报错,后来我就想了是br.readLine()发生了阻塞,一直在等待,想到了jdk自带的工具jconsole.exe,查看之后果然是这种原因
- 刚开始用servlet的时候有没有人纳闷,doGet中只是形参,并没有实参传递,怎么就会有效果呢
真正的并不是直接运行servlet,而是SocketProcess获取servlet实例,SocketProcess中有Request和Response实参,然后传递参数给doGet(Request,Response),同时修改Request和Response,由于是引用传递,那就能解释了 - 不加"\r\n"可以吗?肯定不行,这样的话不符合http超文本传输协议,网络识别不出来这到底是什么东西
String responseHeader = "HTTP/1.1 200 \r\n"
+ "Content-Type: " + response.getContentType() + "\r\n"
+ "\r\n";