Java NIO(三)手写自己的第一个tomcat

735 阅读3分钟

一.什么是servlet

为防有些读者忘却了当时大学老师在讲J2EE的场景,简单做个回忆杀. servlet 是JavaEE的web规范之一,简单来说就是一个定义了处理网络请求规范.而具体到代码来说是个接口.定义了这样几个方法:

  • init
  • destory
  • service
  • getServletConfig
  • getServletInfo

那就是说所有[JAVA]来处理网络请求的家伙都需要继承实现我,不然你什么网络请求都别干了.

那就是如下这张图. 在这里插入图片描述

1.1 为什么要定义标准接口

自己可以不可以写,可以,每个人都可以定义一个标准和方法.但是这样下去每个人都可能会定义一个标准和规范.反过来就不是标准,也不利于整个技术的演进和维护. 所以大家都遵从JavaEE定义的接口标准进行实现,那么就有了响应的厂商,诸如tomcat,jetty等web容器厂商实现的servlet容器了.

二.如何自己写一个tomcat servlet容器

tomcat基本的工作流程是这样.

1)接收到请求传递给Servlet容器 3)Servlet容器通过已经装配好的servlet获取指定的请求servlet实例 4)Servlet实例获取相应的请求信息进行业务处理 5)Servlet实例处理业务结果返回给响应对象

上面了解了基本的tomcat处理servlet流程.那么我们照葫芦画瓢,开干.

2.1 定义servlet

package com.craftsman.tomcat.tradition;

import com.craftsman.tomcat.nio.BXNIORequest;
import com.craftsman.tomcat.nio.BXNIOResponse;

/**
 * 抽象tomcat servlet
 * @author chenfanglin
 * @date 2021年05月16日
 */
public abstract class BXServlet {

    public void service(BXRequest request, BXResponse response)throws Exception{
        if("GET".equalsIgnoreCase(request.getMethod())){
            doGet(request,response);
        }else if("POST".equalsIgnoreCase(request.getMethod())){
            doPost(request,response);
        }
    }

  
    public abstract void doGet(BXRequest request,BXResponse response)throws Exception;

    public abstract void doPost(BXRequest request,BXResponse response)throws Exception;


}

如上定义了servlet 的service方法,此处省略了init destory等方法.

2.2 具体业务实现

package com.craftsman.tomcat.tradition;


public class FirstServlet extends BXServlet {

    public void doGet(BXRequest request, BXResponse response) throws Exception {
        doPost(request,response);
    }

    public void doPost(BXRequest request, BXResponse response) throws Exception {
        response.write("this is bx first servlet");
    }
}

如上实现了servlet协议,定义了业务处理逻辑.doPost方法.

2.3 tomcat 启动类

package com.craftsman.tomcat.tradition;

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;

public class BXTomcat {

    private int port=8080;

    private ServerSocket SERVER;
   
   //servlet 映射.类似于springmvc的路由分发器
    private Map<String,BXServlet> SERVLET_MAP =new HashMap<String,BXServlet>();

    private Properties WEB_XML =new Properties();

    private void init(){
         //读取web.properties资源的值
        String webInf=this.getClass().getResource("/").getPath();

        try {
            FileInputStream fis=new FileInputStream(webInf+"web.properties");
            WEB_XML.load(fis);
            for(Object k: WEB_XML.keySet()){
                String key=k.toString();
                if(key.endsWith(".url")){
                    String servletName=key.replaceAll("\\.url$","");
                    String url= WEB_XML.getProperty(key);
                    String className= WEB_XML.getProperty(servletName+".className");
                    BXServlet obj=(BXServlet) Class.forName(className).newInstance();
                    //加载固定的url与对应的处理器.url不能重复
                    SERVLET_MAP.put(url,obj);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void start(){
    
        //1.初始化servlet
        init();
        try {
            //2.启动监听端口
            SERVER =new ServerSocket(this.port);
            System.out.println("bx tomcat 已启动 端口:"+this.port);
            //3.死循环用以接收用户请求
            while (true){
                Socket client= SERVER.accept();
                process(client);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void process(Socket client)throws Exception{
    
        //1.获取输入输出流
        InputStream is=client.getInputStream();
        OutputStream os=client.getOutputStream();

        //2.获取对应request response 对象
        BXRequest request=new BXRequest(is);
        BXResponse response=new BXResponse(os);

        //3.处理请求request 并进行分发处理.
        String url=request.getUrl();
        if(SERVLET_MAP.containsKey(url)){
            SERVLET_MAP.get(url).service(request,response);
        }else{
            response.write("this url is not founded");
        }

        os.flush();
        os.close();
        is.close();
        client.close();

    }

    public static void main(String[] args) {
        new BXTomcat().start();
        System.out.println("this bx tomcat already started");
    }

}

如上初始化容器,准备url分发器,接收前端请求.

三.netty 改造现有的传统模式

netty有天然支持http协议解码和编码的优势,尝试使用netty的方法来改造现有的tomcat容器.

直接来看现有的tomcat容器部分

3.1 tomcat容器改造

package com.craftsman.tomcat.nio;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import java.io.FileInputStream;
import java.net.ServerSocket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;


/**
 * 用以改造既有传统socket方式tomcat servlet 容器
 * @author chenfanglin
 * @date 2021年05月16日
 */
public class BXNIOTomcat {

    private int port=8081;

    private Map<String,BXNIOServlet> SERVLET_MAP =new HashMap<String,BXNIOServlet>();

    private Properties WEB_XML =new Properties();

    private void init(){
        String webInf=this.getClass().getResource("/").getPath();

        try {
            FileInputStream fis=new FileInputStream(webInf+"web-nio.properties");
            WEB_XML.load(fis);
            for(Object k: WEB_XML.keySet()){
                String key=k.toString();
                if(key.endsWith(".url")){
                    String servletName=key.replaceAll("\\.url$","");
                    String url= WEB_XML.getProperty(key);
                    String className= WEB_XML.getProperty(servletName+".className");
                    BXNIOServlet obj=(BXNIOServlet) Class.forName(className).newInstance();
                    SERVLET_MAP.put(url,obj);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void start(){

        //1.初始化servlet
        init();
        //定义用以处理请求的主线程和子线程,都是子线程在做事
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try {
            //2.启动nio对象bootstrap
            ServerBootstrap serverBootstrap=new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) throws Exception {
                           // 处理http协议的解码器和编码器
                            ch.pipeline().addLast(new HttpResponseEncoder());
                            ch.pipeline().addLast(new HttpRequestDecoder());
                            //业务处理器
                            ch.pipeline().addLast(new BXTomcatHandler());
                        }
                    })
                    //设置主线程最大线程数
                    .option(ChannelOption.SO_BACKLOG,128)
                    //子线程保持长链接
                    .childOption(ChannelOption.SO_KEEPALIVE,true);
            ChannelFuture f=serverBootstrap.bind(this.port).sync();
            System.out.println("bx nio tomcat 已经启动 监听的端口:"+this.port);
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    //业务处理器
    public  class BXTomcatHandler extends ChannelInboundHandlerAdapter {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if(msg instanceof HttpRequest){
                System.out.println("接收到前端请求msg"+msg);
                HttpRequest request=(HttpRequest) msg;
                BXNIORequest realRequest=new BXNIORequest(ctx,request);
                BXNIOResponse realResponse=new BXNIOResponse(ctx,request);
                String url=realRequest.getRequest().uri();
                if(SERVLET_MAP.containsKey(url)){
                    //分发业务请求
                    SERVLET_MAP.get(url).service(realRequest,realResponse);
                }else{
                    realResponse.write("nio tomcat not found request url");
                }
            }
        }
    }
    public static void main(String[] args) {
        new BXNIOTomcat().start();
        System.out.println("this bx nio tomcat already started");
    }

}

踩坑: 小问题 在获取resources路径下配置文件时,发现resources文件中不会被打包进入target文件.

在这里插入图片描述 最后通过在pom文件添加对应resources路径以及对应的maven插件后解决.再次记录下.

<build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.6</version>
            </plugin>
        </plugins>
    </build>

具体代码参见:

搭建自己的tomcat,请摸我