本文节选自《Netty 4核心原理》
我们知道,Tomcat是基于J2EE规范的Web容器,主要入口是web.xml文件。web.xml文件中主要配置Servlet、Filter、Listener等,而Servlet、Filter、Listener在J2EE中只是抽象的实现,具体业务逻辑由开发者来实现。本章内容,就以最常用的Servlet为例来详细展开。
1 环境准备
1.1 定义GPServlet抽象类
首先,我们创建GPServlet类。我们都知道GPServlet生命周期中最常用的方法是doGet()方法和doPost()方法,而doGet()方法和doPost()方法是service()方法的分支实现,看下面的简易版Servlet源码实现。
代码语言:javascript复制
package com.tom.tomcat.http;
public abstract class GPServlet {
public void service(GPRequest request,GPResponse response) throws Exception{
//由service()方法决定是调用doGet()还是调用doPost()
if("GET".equalsIgnoreCase(request.getMethod())){
doGet(request, response);
}else{
doPost(request, response);
}
}
public abstract void doGet(GPRequest request,GPResponse response) throws Exception;
public abstract void doPost(GPRequest request,GPResponse response) throws Exception;
}
从上面的代码中,我们看到,doGet()方法和doPost()方法中有两个参数GPRequest和GPResponse对象,这两个对象是由Web容器创建的,主要是对底层Socket的输入输出的封装。其中GPRequest是对Input的封装,GPResponse是对Output的封装。
1.2 创建用户业务代码
下面基于GPServlet来实现两个业务逻辑FirstServlet和SecondServlet。FirstServlet类的实现代码如下。
代码语言:javascript复制
package com.tom.tomcat.servlet;
import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;
public class FirstServlet extends GPServlet {
public void doGet(GPRequest request, GPResponse response) throws Exception {
this.doPost(request, response);
}
public void doPost(GPRequest request, GPResponse response) throws Exception {
response.write("This is First Servlet");
}
}
SecondServlet类的实现代码如下。
package com.tom.tomcat.servlet;
import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;
public class SecondServlet extends GPServlet {
public void doGet(GPRequest request, GPResponse response) throws Exception {
this.doPost(request, response);
}
public void doPost(GPRequest request, GPResponse response) throws Exception {
response.write("This is Second Servlet");
}
}
1.3 完成web.properties配置
为了简化操作,我们用web.properties文件代替web.xml文件,具体内容如下。
代码语言:javascript复制
servlet.one.url=/firstServlet.do
servlet.one.className=com.tom.tomcat.servlet.FirstServlet
servlet.two.url=/secondServlet.do
servlet.two.className=com.tom.tomcat.servlet.SecondServlet
上述代码分别给两个Servlet配置了/firstServlet.do和/secondServlet.do的URL映射。
2 基于传统I/O手写Tomcat
下面我们来看GPRequest和GPResponse的基本实现。
2.1 创建GPRequest对象
GPRequest主要就是对HTTP的请求头信息进行解析。我们从浏览器发送一个HTTP请求,如在浏览器地址栏中输入 http://localhost:8080 ,后台服务器获取的请求其实就是一串字符串,具体格式如下。
示例:
代码语言:javascript复制
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
在GPRequest获得输入内容之后,对这一串满足HTTP的字符信息进行解析。我们来看GPRequest简单直接的代码实现。
代码语言:javascript复制
package com.tom.tomcat.http;
import java.io.InputStream;
/**
* Created by Tom.
*/
public class GPRequest {
private String method;
private String url;
public GPRequest(InputStream in){
try {
//获取HTTP内容
String content = "";
byte[] buff = new byte[1024];
int len = 0;
if ((len = in.read(buff)) > 0) {
content = new String(buff,0,len);
}
String line = content.split("\n")[0];
String [] arr = line.split("\s");
this.method = arr[0];
this.url = arr[1].split("\?")[0];
}catch (Exception e){
e.printStackTrace();
}
}
public String getUrl() {
return url;
}
public String getMethod() {
return method;
}
}
在上面的代码中,GPRequest主要提供了getUrl()方法和getMethod()方法。输入流InputStream作为GPRequest的构造参数传入,在构造函数中,用字符串切割的方法提取请求方式和URL。
2.2 创建GPResponse对象
接下来看GPResponse的实现,与GPRequest的实现思路类似,就是按照HTTP规范从Output输出格式化的字符串,具体格式如下:
示例:
代码语言:javascript复制
HTTP/1.1 200 OK
Server: Tomcat
Location: http://localhost:8080
Connection: Keep-Alive
Content-Type: text/html;
This My Servlet
下面来看代码。
代码语言:javascript复制
package com.tom.tomcat.http;
import java.io.OutputStream;
/**
* Created by Tom.
*/
public class GPResponse {
private OutputStream out;
public GPResponse(OutputStream out){
this.out = out;
}
public void write(String s) throws Exception {
//输出也要遵循HTTP
//状态码为200
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OKn")
.append("Content-Type: text/html;n")
.append("rn")
.append(s);
out.write(sb.toString().getBytes());
}
}
上面的代码中,输出流OutputStream作为GPResponse的构造参数传入,主要提供了一个write()方法。通过write()方法按照HTTP规范输出字符串。
2.3 创建GPTomcat启动类
前面2.1和2.2两节只是对J2EE规范的再现,接下来就是真正Web容器的实现逻辑,分为三个阶段:初始化阶段、服务就绪阶段、接受请求阶段。第一阶段:初始化阶段,主要是完成对web.xml文件的解析。
代码语言:javascript复制
package com.tom.tomcat;
import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* Created by Tom.
*/
public class GPTomcat {
private int port = 8080;
private ServerSocket server;
private Map<String,GPServlet> servletMapping = new HashMap<String,GPServlet>();
private Properties webxml = new Properties();
private void init(){
//加载web.xml文件,同时初始化ServletMapping对象
try{
String WEB_INF = this.getClass().getResource("/").getPath();
FileInputStream fis = new FileInputStream(WEB_INF "web.properties");
webxml.load(fis);
for (Object k : webxml.keySet()) {
String key = k.toString();
if(key.endsWith(".url")){
String servletName = key.replaceAll("\.url$", "");
String url = webxml.getProperty(key);
String className = webxml.getProperty(servletName ".className");
//单实例,多线程
GPServlet obj = (GPServlet)Class.forName(className).newInstance();
servletMapping.put(url, obj);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
上面代码中,首先从WEB-INF读取web.properties文件并对其进行解析,然后将URL规则和GPServlet的对应关系保存到servletMapping中。第二阶段:服务就绪阶段,完成ServerSocket的准备工作。在GPTomcat类中增加start()方法。
代码语言:javascript复制
public void start(){
//1.加载配置文件,初始化ServletMapping
init();
try {
server = new ServerSocket(this.port);
System.out.println("GPTomcat已启动,监听的端口是:" this.port);
//2.等待用户请求,用一个死循环来等待用户请求
while (true) {
Socket client = server.accept();
//3.HTTP请求,发送的数据就是字符串——有规律的字符串(HTTP)
process(client);
}
} catch (Exception e) {
e.printStackTrace();
}
}
第三阶段:接受请求阶段,完成每一次请求的处理。在GPTomcat中增加process()方法的实现。
代码语言:javascript复制
private void process(Socket client) throws Exception {
InputStream is = client.getInputStream();
OutputStream os = client.getOutputStream();
//4.Request(InputStrean)/Response(OutputStrean)
GPRequest request = new GPRequest(is);
GPResponse response = new GPResponse(os);
//5.从协议内容中获得URL,把相应的Servlet用反射进行实例化
String url = request.getUrl();
if(servletMapping.containsKey(url)){
//6.调用实例化对象的service()方法,执行具体的逻辑doGet()/doPost()方法
servletMapping.get(url).service(request,response);
}else{
response.write("404 - Not Found");
}
os.flush();
os.close();
is.close();
client.close();
}
每次客户端请求过来以后,从servletMapping中获取其对应的Servlet对象,同时实例化GPRequest和GPResponse对象,将GPRequest和GPResponse对象作为参数传入service()方法,最终执行业务逻辑。最后,增加main()方法。
代码语言:javascript复制
public static void main(String[] args) {
new GPTomcat().start();
}
服务启动后,运行效果如下图所示。