既然要手写tomcat,那么从哪里入手呢?我们都知道tomcat是web容器,所以如果理解了tomcat的作用,我们应该就知道如何设计它了:
- tomcat要负责接受http请求,所以需要一个 MYRequest
- tomcat要负责返回响应,所以需要一个 MYResponse
- tomcat要负责实例化Servlet,所以需要一个 MYServlet 规范
那这三者如何统一起来呢,或者说谁来处理映射关系呢?配置文件 + MYTomcat。
1.MYRequest
Request 本质就是 InputStream,主要工作下:
- 读取 InputStream 的内容,并保存
- 对读取的结果进行解码,提取关键信息(URL,请求方式,请求餐胡)
所以,它会对外提供 getUrl 方法,getMethod 方法,getParameter 方法
public class MYRequest {
// SocketChannel的封装
private ChannelHandlerContext ctx;
private HttpRequest req;
public MYRequest(ChannelHandlerContext ctx, HttpRequest req) {
this.ctx = ctx;
this.req = req;
}
public String getUrl() {
return req.uri();
}
public String getMethod() {
return req.method().name();
}
public String getParameter(String name) {
Map<String, List<String>> params = getParameters();
List<String> param = params.get(name);
if (null == param) {
return null;
} else {
return param.get(0);
}
}
public Map<String, List<String>> getParameters() {
QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
return decoder.parameters();
}
}
2.MYResponse
Response 本质就是 OutputStream,主要工作是:
- 将 Servlet 处理结果编码成 http 协议格式
- 通过 OutputStream 写出
所以,它只需要对外提供 write 方法即可。
public class MYResponse {
// SocketChannel的封装
private ChannelHandlerContext ctx;
private HttpRequest req;
public MYResponse(ChannelHandlerContext ctx, HttpRequest req) {
this.ctx = ctx;
this.req = req;
}
public void write(String out) throws Exception {
try {
if (out == null || out.length() == 0) {
return;
}
// 设置 http协议及请求头信息
FullHttpResponse response = new DefaultFullHttpResponse(
// 设置http版本为1.1
HttpVersion.HTTP_1_1,
// 设置响应状态码
HttpResponseStatus.OK,
// 将输出值写出 编码为UTF-8
Unpooled.wrappedBuffer(out.getBytes("UTF-8")));
response.headers().set("Content-Type", "text/html;");
// 当前是否支持长连接
// if (HttpUtil.isKeepAlive(r)) {
// // 设置连接内容为长连接
// response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
// }
ctx.write(response);
} finally {
ctx.flush();
ctx.close();
}
}
}
3.MYServlet
Servlet 就是一个应用于 web 的对象,具有规范作用。这里的 MYServlet 是一个抽象类,它的主要工作是:
- 提供Servlet规范,即每个处理业务逻辑的Servlet都要继承它,重写doPost和doGet这俩模板方法
- 完成请求方式与对应方法的映射,对外提供统一方法 service。这里其实采用了模板方法模式。
PS:Servlet 对象一般是单例的,会在 Tomcat 初始化时创建
public abstract class MYServlet {
// 注:这里的request与response都是Tomcat对象创建好然后传进来的
public void service(MYRequest request, MYResponse response) throws Exception {
if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
} else{
doPost(request, response);
}
}
// 这里是模板方法模式,交给子类去具体实现
protected abstract void doPost(MYRequest request, MYResponse response) throws Exception;
protected abstract void doGet(MYRequest request, MYResponse response) throws Exception;
}
4.MYTomcat(核心)
MYTomcat主要做了两件事:
- 初始化 tomcat:
- 加载web.properties文件,在这里其实相当于Tocmat中的web.xml
- 寻找url与servlet的映射关系,即对配置文件进行解析
- 将url与Servlet实例保存在Map中,到时可直接根据url获取到处理业务的servlet(单例模式)
- 启动 tomcat:
- 调用init,目的是得到servletMapping的映射关系
- 通过BIO创建socket的服务端,在指定端口开始监听
- 用一个死循环持续等待并处理用户请求,处理用户请求的具体逻辑是:
- 创建IO流,并包装成Request与Response
- 获取请求URL,寻找相应Servlet进行处理。如果能找到就调用 servlet 的 service 方法进行处理;找不到就写出404。
- 等处理完之后关闭本次连接的相关资源
public class MYTomcat {
private int port = 8080;
private ServerSocket server;
// 用来保存路径与Servlet的映射关系(servlet单例模式)
private Map<String, MYServlet> servletMapping = new HashMap<>();
// web.xml
private Properties webxml = new Properties();
// 加载web.xml文件,同时初始化 ServletMapping对象
private void init(){
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();
// 以url结尾的key就是要映射的路径
if(key.endsWith(".url")){
String servletName = key.replaceAll("\\.url$", "");
String url = webxml.getProperty(key);
// 拿到对应servlet全类名后,通过反进行实例化,之后放入map中(单例模式)
// 注:这里是将所有Servlet都强转为MyServlet,所以一定要继承MyServlet
String className = webxml.getProperty(servletName + ".className");
MYServlet obj = (MYServlet)Class.forName(className).newInstance();
servletMapping.put(url, obj);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 启动tomcat
* 1. 调用init方法,加载web.xml
* 2.等待用户请求,并对每个请求进行处理
*/
public void start() {
init();
// netty封装了nio,Reactor模型,Boss,worker
// Boss线程
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
// Worket线程
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// Netty服务
ServerBootstrap server = new ServerBootstrap();
// 链路式编程
server.group(bossGroup, workerGroup)
// 主线程处理类,看到这样的写法,底层就是用反射
.channel(NioServerSocketChannel.class)
// 子线程处理类 , Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
// 客户端初始化处理
protected void initChannel(SocketChannel client) throws Exception {
// 无锁化串行编程
// Netty对HTTP协议的封装,顺序有要求
// HttpResponseEncoder 编码器
client.pipeline().addLast(new HttpResponseEncoder());
// HttpRequestDecoder 解码器,解码的结果是 HttpRequest 对象
client.pipeline().addLast(new HttpRequestDecoder());
// 业务逻辑处理
client.pipeline().addLast(new MYTomcatHandler());
}
})
// 针对主线程的配置 分配线程最大数量 128
.option(ChannelOption.SO_BACKLOG, 128)
// 针对子线程的配置 保持长连接
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 启动服务器
ChannelFuture f = server.bind(port).sync();
System.out.println("MYTomcat 已启动,监听的端口是:" + port);
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
}
public class MYTomcatHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 消息经过 HttpRequestDecoder 解码后是 HTTPReqest 对象
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg;
// 转交给我们自己的request实现
MYRequest request = new MYRequest(ctx, req);
// 转交给我们自己的response实现
MYResponse response = new MYResponse(ctx, req);
// 实际业务处理
String url = request.getUrl();
if (servletMapping.containsKey(url)) {
servletMapping.get(url).service(request, response);
} else {
response.write("404 - Not Found");
}
}
}
}
public static void main(String[] args) {
new MYTomcat().start();
}
}
好了,到这里手写的所有代码都结束了,下面就测试一下吧。
成果演示
首先,我们写了一个FirstServlet,他继承了 MYServlet 并且重写了 doGet 与 doPost
public class FirstServlet extends MYServlet {
@Override
protected void doPost(MYRequest request, MYResponse response) throws IOException {
response.write("this is FirstServlet!");
}
@Override
protected void doGet(MYRequest request, MYResponse response) throws IOException {
doPost(request, response);
}
}
然后就是配置文件 web.properties 了,配置的具体内容如下,其实就是配置url与全类名的映射关系
servlet.one.url=/firstServlet.do
servlet.one.className=com.xupt.yzh.tomcat.servlet.FirstServlet
servlet.two.url=/secondServlet.do
servlet.two.className=com.xupt.yzh.tomcat.servlet.SecondServlet
好了,到了最激动人心的时刻了 --启动 Tomcat!

很好,没有报错,下面我们在浏览器上通过8080端口进行访问。
总结一下
Tomcat 是一个 web 容器,但说到底它底层就是做的网络 IO 的工作:
- MYRequest:读取 web 端发来的请求
- MYTomcat:调用 Servlet 对象中的方法去处理请求
- MYResponse:对处理结果编码编码并写回给 web 端
所以,为了高性能 IO ,我们这里采用了 Netty 框架!
PS:Servlet 可以理解成一种规范,它定义了我们如何写处理请求的方法,然后 Tomcat 在初始化时会实例化 Servlet 对象(单例)。
完整代码在我的 GitHub 上,点击这里跳转…









还没有评论,来说两句吧...