vlambda博客
学习文章列表

用Netty手撸Tomcat,手动实现Tomcat

最近啊,花了一个星期学习了Netty

对于不知道Netty的小伙伴们,我先介绍一下

你们应该都知道Java NIO吧,Java的五种IO之一。


Java NIO的开发难度是很大的,较于传统的BIO,也就是同步阻塞IO来说,代码量是它的两倍不止,Bug还满天飞

所以Netty出现了,它不仅简化了Java NIO,而且还统一了BIO的写法,可以平稳的从BIO过度到NIO

Netty的底层IO模型是多路复用模型,不知道的小伙伴,可以看上一篇文章,很详细

你们知道Tomcat本质就是Socket的容器,那么我就产生了个大胆的想法,干嘛不自己用Netty手撸一个Tomcat呢!


首先,了解下一个请求过来,处理的流程

用Netty手撸Tomcat,手动实现Tomcat

Tomcat也就是对应Netty实现的服务器,在启动时,会加载web.properties

web.properties存放的什么呢?

我们将这些类实例化,并且以URL作为Key存放入ServletMapping中

通过request获得URL,再从ServletMapping中查找对应的servlet

处理完业务后进行编码,发给客户端

这个流程是不是很熟悉、很简单

其实这个就是Tomcat最核心的功能,请求响应


话不多说,我们看具体实现

1.Maven依赖

 <dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>

2.自定义Request

public class BoRequest {
private ChannelHandlerContext ctx;
private HttpRequest request;
public BoRequest(ChannelHandlerContext ctx,HttpRequest request){
this.ctx=ctx;
this.request=request;
}
public String getUri(){
return request.uri();
}
public String getMethod(){
return request.method().name();
}
public Map<String, List<String>> getParameters(){
QueryStringDecoder decoder=new QueryStringDecoder(request.uri());
return decoder.parameters();
}
public String getParameter(String name){
Map<String,List<String>> params=getParameters();
List<String> param=params.get(name);
if(param==null)return null;
else return param.get(0);
}
}

3.自定义Response

public class BoResponse {
private ChannelHandlerContext ctx;
private HttpRequest request;
private String code = "UTF-8";

public BoResponse(ChannelHandlerContext ctx, HttpRequest request) {
this.ctx = ctx;
this.request = request;
}

public void write(String out) throws Exception {
try {
if (out == null || out.length() == 0) return;
//设置HTTP及请求头信息
FullHttpResponse response = null;
response = new DefaultFullHttpResponse(
//设置版本
HttpVersion.HTTP_1_1,
//设置响应状态码
HttpResponseStatus.OK,
//设置输出格式
Unpooled.wrappedBuffer(out.getBytes(code)));
response.headers().set("Content-Type", "text/html;");
ctx.write(response);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} finally {
ctx.flush();
ctx.close();
}


}
}

3.自定义servlet

public abstract class BoServlet {
public void service(BoRequest request,BoResponse response) throws Exception{
if("GET".equalsIgnoreCase(request.getMethod())){
doGet(request,response);
}else{
doPost(request,response);
}
}

protected abstract void doPost(BoRequest request, BoResponse response) throws Exception;

protected abstract void doGet(BoRequest request, BoResponse response) throws Exception;
}

4.写自己的业务servlet,我写了两个,一个是FirstSFervlet,一个是SecondServlet

public class FirstServlet extends BoServlet {
protected void doPost(BoRequest request, BoResponse response) throws Exception {
response.write("This is FirstServlet");
}

protected void doGet(BoRequest request, BoResponse response) throws Exception {
doPost(request,response);
}
}


public class SecondServlet extends BoServlet {
protected void doPost(BoRequest request, BoResponse response) throws Exception {
response.write("This is SecondServlet");
}

protected void doGet(BoRequest request, BoResponse response) throws Exception {
doPost(request,response);
}
}

5.配置web.properties ,主要是你的servlet类

servlet.one.url=/firstServlet.do
servlet.one.className=bobo.silence.netty.servlet.FirstServlet

servlet.two.url=/secondServlet.do
servlet.two.className=bobo.silence.netty.servlet.SecondServlet

6.最关键的Tomcat代码

public class BoTomcat {
private int port = 8080;
private Map<String, BoServlet> servletMapping = new HashMap<String, BoServlet>();
private Properties webxml = new Properties();
//初始化读取配置文件
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();
if (key.endsWith("url")) {
String servletName = key.replaceAll("\\.url$", "");
String url = webxml.getProperty(key);
String className = webxml.getProperty(servletName + ".className");
BoServlet servlet = (BoServlet) Class.forName(className).newInstance();
servletMapping.put(url, servlet);
}
}

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public void start() {
doStart();
}

public void start(int port) {
this.port = port;
doStart();
}

//启动Netty
private void doStart() {
init();
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
try {
server.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
//客户端连接时启动
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel client) throws Exception {
//响应编码器
client.pipeline().addLast(new HttpResponseEncoder());
//请求解码器
client.pipeline().addLast(new HttpRequestDecoder());
//自定义处理器
client.pipeline().addLast(new BoTomcatHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture f = server.bind(port).sync();
System.out.println("BoTomcat 已启动!");
//监听关闭状态启动
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//关闭线程池
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
//处理请求
public class BoTomcatHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof HttpRequest){
HttpRequest req= (HttpRequest) msg;
BoRequest request=new BoRequest(ctx,req);
BoResponse response=new BoResponse(ctx,req);
String url=request.getUri();
if(servletMapping.containsKey(url)){
servletMapping.get(url).service(request,response);
}else{
response.write("404");
}
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}

public static void main(String[] args) {
new BoTomcat().start();
}

}

7.启动我们写的Tomcat,右击启动


8.我们测试下我们写的servlet

用Netty手撸Tomcat,手动实现Tomcat

嘿嘿没问题,搞定

https://github.com/suzy8808/demo/tree/master/BoTomcat github.com/suzy8808/demo/tree/master/BoTomcat


你们可能不太懂Netty服务启动部分的代码

我来简单地解释下,以后会写文章详细说下Netty的使用,上面的代码就是标准的Netty服务端启动代码,会写一次,以后就都会了,都是这个模式

首先讲下多路复用IO模型的结构

用Netty手撸Tomcat,手动实现Tomcat

selector连接着多个客户端,接收他们的请求,selector通过指定的channel对buffer进行IO操作,当buffer IO操作完成后,selector被激活,响应对应的客户端

再讲一下代码中的bossGroup和workGroup你可以理解为两个线程池

不,就是线程池

我首先讲下Netty的线程模型,分别是单线程模型,多线程模型和主从多线程模型

上面的代码就是多线程主从模型

模型结构如下:



Acceptor连接着多个Client(客户端)

Reactor其实就是之前说的多路复用IO模型中的seletor,监听着多个Handler,当某个Handler链完成后,selector激活,返回结果

其中的连接线其实可以看做channel

Acceptor线程池就是bossGroup

NIO线程池就是workGroup

然后指定Channel的类:NioServerSocketChannel.class

设定Handler链,Handler链有两种方向,这里就不说了,你们只需要知道:

解码器是Inbound,是接受请求的方向

编码器是Outbound,是响应请求的方向

我们的处理Handler一般是Inbound

Inbound执行的过程是从上往下的

Outbound执行过程是从下往上的

之后我会单独写一篇文章讲讲Handler链的执行顺序

option指的是启动时运行的参数,我们设定的128指的是等待连接的socket最大值

childOption指的是和客户端连接时运行的参数,SO_KEEPALIVE指长连接

其他的很好理解了吧


是不是很简单,我们也可以手撸Tomcat啦