手动实现 Tomcat 底层机制+自己设计 Servlet
1.Tomcat 整体架构分析
●说明: Tomcat 有三种运行模式(BIO, NIO, APR), 这里主要说明Tomcat 如何接收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟.[绘图]
2.实现任务阶段 1- 编写自己 Tomcat, 能给浏览器返回 hi llp,你好tomcat
1.基于 socket 开发服务端-流程
2.需求分析/图解
需求分析如图, 浏览器请求 http://localhost:8080/??, 服务端返回 hi llp,你好tomcat
3.分析+代码实现
客户端(浏览器)请求tomcat,建立socket链接;服务端处理请求并响应给服务端。
tcp协议的数据没有封装成http协议的格式就返回给浏览器会出现解析问题,火狐支持直接解析
public class TomcatV1 {
public static void main(String[] args) throws IOException {
//1. 创建ServerSocket, 在 8080端口监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=======mytomcat在8080端口监听======");
while (!serverSocket.isClosed()) {
//等待浏览器/客户端的连接
//如果有连接来,就创建一个socket
//这个socket就是服务端和浏览器端的连接/通道
Socket socket = serverSocket.accept();
//先接收浏览器发送的数据
//inputStream 是字节流=> BufferedReader(字符流)
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String mes = null;
System.out.println("=======接收到浏览器发送的数据=======");
//循环的读取
while ((mes = bufferedReader.readLine()) != null) {
//判断mes的长度是否为0,不做判断当读取到的数据长度为0时无法退出循环
if (mes.length() == 0) {
break;//退出while
}
System.out.println(mes);
}
//我们的tomcat会送-http响应方式
OutputStream outputStream = socket.getOutputStream();
//构建一个http响应的头
//\r\n 表示换行
//http响应体,需要前面有两个换行 \r\n\r\n
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String resp = respHeader + "hi, llp";
System.out.println("========我们的tomcat 给浏览器会送的数据======");
System.out.println(resp);
outputStream.write(resp.getBytes());//将resp字符串以byte[] 方式返回
outputStream.flush();
outputStream.close();
//关闭外层流即可关闭底层流
bufferedReader.close();
socket.close();
}
}
}
3.实现任务阶段 2- 使用 BIO 线程模型,支持多线程
1.BIO 线程模型介绍
2.需求分析/图解
需求分析如图, 浏览器请求 http://localhost:8080, 服务端返回 hi , 孙悟空~, 后台tomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造
3.分析+代码实现
TomcatV2
public class TomcatV2 {
public static void main(String[] args) throws IOException {
//在8080端口监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=======tomcatV2 在8080监听=======");
//只要 serverSocket没有关闭,就一直等待浏览器/客户端的连接
while (!serverSocket.isClosed()) {
//1. 接收到浏览器的连接后,如果成功,就会得到socket
//2. 这个socket 就是 服务器和 浏览器的数据通道
Socket socket = serverSocket.accept();
//3. 创建一个线程对象,并且把socket给该线程
RequestHandler requestHandler =
new RequestHandler(socket);
new Thread(requestHandler).start();
}
}
}
RequestHandler线程对象
public class RequestHandler implements Runnable{
private Socket socket;
public RequestHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String readLine = null;
System.out.println("请求部分: "+Thread.currentThread().getName()+"-"+Thread.currentThread().hashCode());
while ((readLine = bufferedReader.readLine())!=null){
if(readLine.length() == 0){
break;
}
System.out.println(readLine);
}
OutputStream outputStream = socket.getOutputStream();
//这里两个换行,响应头和响应体之间必须有换一行否则响应体内容无法正常解析
String respHeader = "HTTP/1.1 200 OK\r\n"+"Content-Type: text/html;charset=utf-8\r\n\r\n";
String respBody = "hi ,孙悟空~";
String resp = respHeader+respBody;
System.out.println("响应部分=========");
System.out.println(resp);
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.实现任务阶段 3- 处理 Servlet
1.Servlet 生命周期-回顾
2.需求分析/图解
●需求分析如图, 浏览器请求 http://localhost:8080/hspCalServlet, 提交数据,完成计算任务,如果 servlet 不存在,返回 404
3.分析+代码实现
●分析示意图
开发流程-一图胜千言
1.Tomcat启动类
public class Tomcat3 {
/**
* 定义一个map用于存放web.xml中读取到的Servlet实例;
* key为ServletName
* value为HttpServlet实例
*/
public static final ConcurrentMap<String, LlpHttpServlet> servletMap = new ConcurrentHashMap<>();
/**
* 用于存放Servlet-Mapping部分内容
* key为:ServletPattern
* value为ServletName
* 浏览器访问时,可通过uri即map的key直接获取到对应的vlue-ServletName
* 从而从servletMap中获取对应的HttpServlet实例,如果Servlet实例不存在则返回404
*/
public static final ConcurrentMap<String,String> servletMappingMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws Exception {
init();
run();
}
public static void init() throws Exception {
SAXReader saxReader = new SAXReader();
//llp-tomcat\target\classes\web.xml 这里我将web.xml直接拷贝到了\target\classes\目录下
String path = Tomcat3.class.getResource("/").getPath();
Document document = saxReader.read(new File(path+"web.xml"));
//获取根本标签
Element rootElement = document.getRootElement();
List<Element> elements = rootElement.elements();
for (Element element : elements) {
/**
* <servlet>
* <servlet-name>LlpCalServlet</servlet-name>
* <servlet-class>com.llp.tomcat.servlet.LlpCalServlet</servlet-class>
* </servlet>
* <servlet-mapping>
* <servlet-name>LlpCalServlet</servlet-name>
* <url-pattern>/llpCalServlet</url-pattern>
* </servlet-mapping>
*/
String name = element.getName();
if("servlet".equals(name)){
String classFullPath = element.element("servlet-class").getText().trim();
Class<?> aClass = Class.forName(classFullPath);
LlpHttpServlet llpHttpServlet = (LlpHttpServlet) aClass.newInstance();
servletMap.put(element.element("servlet-name").getText().trim(),llpHttpServlet);
}
if("servlet-mapping".equals(name)){
String servletName = element.element("servlet-name").getText().trim();
String urlPattern = element.element("url-pattern").getText().trim();
servletMappingMap.put(urlPattern,servletName);
}
}
System.out.println(servletMap);
System.out.println(servletMappingMap);
}
public static void run() throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
new Thread(new RequestHandler(socket)).start();
}
serverSocket.close();
}
}
2.处理请求线程类RequestHandler
public class RequestHandler implements Runnable {
private Socket socket;
public RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
LlpRequest llpRequest = new LlpRequest(socket.getInputStream());
LlpResponse llpResponse = new LlpResponse(socket.getOutputStream());
// LlpCalServlet llpCalServlet = new LlpCalServlet();
// llpCalServlet.doGet(llpRequest,llpResponse);
//通过反射动态获取LlpHttpServlet对象执行Service方法,这里利用java多态的特性进行了值传递
//eg: uri = /llpCalServlet
String uri = llpRequest.getUri();
//判断请求的是不是静态资源,以.html为例
if(WebUtil.isHtml(uri)){
String url = uri.substring(1);
String content = WebUtil.readHtml(url);
System.out.println("content: "+content);
OutputStream outputStream = llpResponse.getOutputStream();
outputStream.write((LlpResponse.responseHeader+content).getBytes());
outputStream.flush();
return;
}
String servletName = Tomcat3.servletMappingMap.get(uri);
if (servletName == null) {
servletName = "";
}
LlpHttpServlet llpHttpServlet = Tomcat3.servletMap.get(servletName);
if (llpHttpServlet != null) {
llpHttpServlet.service(llpRequest, llpResponse);
} else {
OutputStream outputStream = llpResponse.getOutputStream();
outputStream.write((LlpResponse.responseHeader + "404 Not Found").getBytes());
outputStream.flush();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.自定义Servlet
1.自定义Servlet接口-LlpServlet
public interface LlpServlet {
void init();
void service(LlpRequest request, LlpResponse response);
void destroy();
}
2.自定义Servlet抽象类-LlpHttpServlet
public abstract class LlpHttpServlet implements LlpServlet{
@Override
public void service(LlpRequest request, LlpResponse response) {
if("GET".equalsIgnoreCase(request.getMethod())){
doGet(request,response);
}else if("POST".equalsIgnoreCase(request.getMethod())){
doPost(request,response);
}
}
protected abstract void doPost(LlpRequest request, LlpResponse response);
protected abstract void doGet(LlpRequest request, LlpResponse response);
@Override
public void init() {
}
@Override
public void destroy() {
}
}
3.自定义HttpServletRequest、HttpServletResponse
public class LlpRequest {
private String method; //GET
private String uri; // /cal
private Map<String, String> paramterMap = new HashMap<>(); //num1=12&num2=12
private InputStream inputStream = null;
//GET /cal?num1=12&num2=21 HTTP/1.1
public LlpRequest(InputStream inputStream) {
this.inputStream = inputStream;
encapHttpRequest();
}
public String getParameter(String key){
return paramterMap.get(key);
}
/**
* 将http请求相关的数据进行封装
*/
public void encapHttpRequest(){
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//请求行
//GET /cal?num1=12&num2=21 HTTP/1.1
String requestLine = bufferedReader.readLine();
String[] requestLineArr = requestLine.split(" ");
method = requestLineArr[0];
System.out.println("method: "+method);
/**
* /cal?num1=12&num2=21
* 1.判断是否含有参乎上
* 2.无参则取出uri
* 3.有参则分成两段: /cal num1=12&num2=21
*/
String url = requestLineArr[1];
if(url.indexOf("?")==-1){
uri = url;
}else {
uri = url.substring(0, url.indexOf("?"));
String paramters = url.substring(url.indexOf("?") + 1);
//防止用户输入 /cal?
if(paramters!=null && !"".equals(paramters)){
String[] split = paramters.split("&");
if(split!=null && split.length!=0){
for (String s : split) {
// num1=12 num2=21
String[] split1 = s.split("=");
paramterMap.put(split1[0],split1[1]);
}
}
}
System.out.println("uri: "+uri);
System.out.println("paramterMap: "+paramterMap);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
}
public class LlpResponse {
public static final String responseHeader = "HTTP/1.1 200 OK\r\n"+"Content-Type: text/html;charset=utf-8\r\n\r\n";
private OutputStream outputStream = null;
public LlpResponse(OutputStream outputStream){
this.outputStream = outputStream;
}
public OutputStream getOutputStream() {
return outputStream;
}
}
4.自定义Servlet业务类
public class LlpCalServlet extends LlpHttpServlet{
@Override
public void doPost(LlpRequest request, LlpResponse response) {
}
@Override
public void doGet(LlpRequest request, LlpResponse response) {
try {
Integer num1 = WebUtil.parseInteger(request.getParameter("num1"),0);
Integer num2 = WebUtil.parseInteger(request.getParameter("num2"),0);
OutputStream outputStream = response.getOutputStream();
//这里两个换行,响应头和响应体之间必须有换一行否则响应体内容无法正常解析
String respHeader = LlpResponse.responseHeader;
String resp = respHeader +"<h1>"+num1+"+"+num2+"="+(num1+num2)+" llp Tomcat</h1>";
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.工具类
public class WebUtil {
public static Integer parseInteger(String value,Integer deafultValue){
int num;
try {
num = Integer.parseInt(value);
} catch (NumberFormatException e) {
System.out.println("value不能转换为数字类型");
return deafultValue;
}
return num;
}
public static boolean isHtml(String uri){
return uri.endsWith(".html");
}
public static String readHtml(String url){
BufferedReader bufferedReader = null;
StringBuilder builder = new StringBuilder();
try {
String path = WebUtil.class.getResource("/").getPath()+url;
bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
String readLine = null;
while ((readLine= bufferedReader.readLine())!=null){
builder.append(readLine);
}
} catch (Exception e) {
return "404 Not Found";
}finally {
if(bufferedReader!=null){
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return builder.toString();
}
}