Netty(四)高级应用 之 手写Tomcat-Java专区论坛-技术-SpringForAll社区

Netty(四)高级应用 之 手写Tomcat

手写 Tomcat

确切地说,这里要手写的是一个 Web 容器,一个类似于 Tomcat 的容器,用于处理 HTTP请求。该 Web 容器没有实现 JavaEE 的 Servlet 规范,不是一个 Servlet 容器。但其是类比着Tomcat 来写的,这里定义了自己的请求、响应及 Servlet,分别命名为了 NettyRequest,NettyResponse 与 Servnet。

Tomcat底层是用的NIO,不是Netty

1. Tomcat 具体需求

我们这里要定义一个 Tomcat,这个 Web 容器提供给用户后,用户只需要按照使用步骤就可以将其自定义的 Servnet 发布到该 Tomcat 中。我们现在给出用户对于该 Tomcat 的使用步骤:

  • 用户只需将自定义的 Servnet 放入到指定的包中。例如,com.abc.webapp 包中。
  • 用户在访问时,需要将自定义的 Servnet 的简单类名全小写后的字符串作为该 Servnet的 Name 进行访问。
  • 若没有指定的 Servnet,则访问默认的 Servnet。

2. 创建工程 09-tomcat

(1) 创建工程

创建一个普通的 Maven 的 Java 工程:09-tomcat

(2) 导入依赖

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- netty-all 依赖 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
</dependencies>

3. 定义 Servnet 规范

(1) 定义请求接口 NettyRequest

/**
 * Servnet规范之请求规范
 */
public interface NettyRequest {
    // 获取URI,包含请求参数,即?后的内容
    String getUri();
    // 获取请求路径,其不包含请求参数
    String getPath();
    // 获取请求方法(GET、POST等)
    String getMethod();
    // 获取所有请求参数
    Map<String, List<String>> getParameters();
    // 获取指定名称的请求参数
    List<String> getParameters(String name);
    // 获取指定名称的请求参数的第一个值
    String getParameter(String name);
}

(2) 定义响应接口 NettyResponse

/**
 * ServNet规范之响应规范
 */
public interface NettyResponse {
    // 将响应写入到Channel
    void write(String content) throws Exception;
}

(3) 定义 Servnet 规范

/**
 * 定义Servnet规范
 */
public abstract class Servnet {
    public abstract void doGet(NettyRequest request, NettyResponse response) throws Exception;
    public abstract void doPost(NettyRequest request, NettyResponse response) throws Exception;
}

4. 定义 Tomcat 服务器

(1) 定义 DefaultNettyRequest 类

注意:

  • 我们用netty的io.netty.handler.codec.http.HttpRequest实现
/**
 * Tomcat中对Servnet规范的默认实现
 */
public class DefaultNettyRequest implements NettyRequest {
//我们用io.netty.handler.codec.http.HttpRequest实现Servnet规范
    private HttpRequest request;

//通过构造传进来
    public DefaultNettyRequest(HttpRequest request) {
        this.request = request;
    }

    @Override
    public String getUri() {
        return request.uri();
    }

    @Override
    public String getPath() {
    //io.netty.handler.codec.http.QueryStringDecoder
        QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
        return decoder.path();
    }

    @Override
    public String getMethod() {
        return request.method().name();
    }

    @Override
    public Map<String, List<String>> getParameters() {
        QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
        return decoder.parameters();
    }

    @Override
    public List<String> getParameters(String name) {
        return getParameters().get(name);
    }

    @Override
    public String getParameter(String name) {
        List<String> parameters = getParameters(name);
        if (parameters == null || parameters.size() == 0) {
            return null;
        }
        return parameters.get(0);
    }
}

(2) 定义 DefaultNettyResponse 类

/**
 * Tomcat中对Servnet规范的默认实现
 */
public class DefaultNettyResponse implements NettyResponse {
    private HttpRequest request;
    private ChannelHandlerContext context;
//需要获取到request判断是否是长连接
//需要ChannelHandlerContext将响应写回去
    public DefaultNettyResponse(HttpRequest request, ChannelHandlerContext context) {
        this.request = request;
        this.context = context;
    }

    @Override
    public void write(String content) throws Exception {

        // 处理content为空的情况
        if (StringUtil.isNullOrEmpty(content)) {
            return;
        }

        // 创建响应对象
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                // 根据响应体内容大小为response对象分配存储空间
                Unpooled.wrappedBuffer(content.getBytes("UTF-8")));

        // 获取响应头
        HttpHeaders headers = response.headers();
        // 设置响应体类型
        headers.set(HttpHeaderNames.CONTENT_TYPE, "text/json");
        // 设置响应体长度
        headers.set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        // 设置缓存过期时间
        headers.set(HttpHeaderNames.EXPIRES, 0);
        // 若HTTP请求是长连接,则响应也使用长连接
        if (HttpUtil.isKeepAlive(request)) {
            headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        // 将响应写入到Channel
        context.writeAndFlush(response);
    }
}

(3) 定义 DefaultServnet 类

/**
 * Tomcat中对Servnet规范的默认实现
 */
public class DefaultServnet extends Servnet {
    @Override
    public void doGet(NettyRequest request, NettyResponse response) throws Exception {
        // http://localhost:8888/someservnet/xxx/ooo?name=zs
        // uri:/someservnet/xxx/ooo?name=zs
        // path:/someservnet/xxx/ooo
        String sernetName = request.getUri().split("/")[1];
        response.write("404 - no this servnet : " + sernetName);
    }

    @Override
    public void doPost(NettyRequest request, NettyResponse response) throws Exception {
        doGet(request, response);
    }
}

(4) 定义服务器类 TomcatServer

注意:

  • Server端需要维护两个map,key都是servlet的名称,value第一个map放servlet的实例,第二个map放servlet的全路径类名
    其中放servlet的全路径类名的map,是没有线程安全问题,因为它在Server启动时会进行初始化,初始化完成后就不会变了
    放servlet实例的map,是有线程安全问题的,多个线程同时访问同一个servlet,如果servlet还没有初始化,这些线程会都进行初始化。
public class TomcatServer {
    // key为servnet的简单类名,value为对应servnet实例
    private Map<String, Servnet> nameToServnetMap = new ConcurrentHashMap<>();
    // key为servnet的简单类名,value为对应servnet类的全限定性类名
    private Map<String, String> nameToClassNameMap = new HashMap<>();
}

根据我们定义的需求,TomcatServer启动时需要一个发布的包路径,在启动的时候把包路径下servet注册到Server中的map

/**
 * Tomcat功能的实现
 */
public class TomcatServer {
    // key为servnet的简单类名,value为对应servnet实例
    private Map<String, Servnet> nameToServnetMap = new ConcurrentHashMap<>();
    // key为servnet的简单类名,value为对应servnet类的全限定性类名
    private Map<String, String> nameToClassNameMap = new HashMap<>();

    private String basePackage;

    public TomcatServer(String basePackage) {
        this.basePackage = basePackage;
    }

    // 启动tomcat
    public void start() throws Exception {
        // 加载指定包中的所有Servnet的类名(尚未实现)
        cacheClassName(basePackage);
        // 启动server服务(尚未实现)
        runServer();
    }
}

实现cacheClassName方法,加载指定包中的所有Servnet的类名:

private void cacheClassName(String basePackage) {
    // 获取指定包中的资源
    URL resource = this.getClass().getClassLoader()
            // com.abc.webapp  =>  com/abc/webapp
            .getResource(basePackage.replaceAll("\\.", "/"));
    // 若目录中没有任何资源,则直接结束
    if (resource == null) {
        return;
    }

    // 将URL资源转换为File资源
    File dir = new File(resource.getFile());
    // 遍历指定包及其子孙包中的所有文件,查找所有.class文件
    // 遍历有两种情况,要么是目录,要么是文件
// 如果是目录,就递归
// 如果是文件,并且是class文件则将类全路径名注册到map
    for (File file : dir.listFiles()) {
        if (file.isDirectory()) {
            // 若当前遍历的file为目录,则递归调用当前方法
            cacheClassName(basePackage + "." + file.getName());
        } else if (file.getName().endsWith(".class")) {
            String simpleClassName = file.getName().replace(".class", "").trim();
            // key为简单类名,value为全限定性类名
            nameToClassNameMap.put(simpleClassName.toLowerCase(), basePackage + "." + simpleClassName);
        }
    }
    // System.out.println(nameToClassNameMap);
}

实现runServer方法,启动Server服务:

private void runServer() throws Exception {
    EventLoopGroup parent = new NioEventLoopGroup();
    EventLoopGroup child = new NioEventLoopGroup();
    try {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(parent, child)
                // 指定存放请求的队列的长度
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 指定是否启用心跳机制来检测长连接的存活性,即客户端的存活性
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                //指定channel类型
                .channel(NioServerSocketChannel.class)
                //初始化channel管道
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new HttpServerCodec());
                        pipeline.addLast(new TomcatHandler(nameToServnetMap, nameToClassNameMap));
                    }
                });
//监听端口8888启动Server
        ChannelFuture future = bootstrap.bind(8888).sync();
        System.out.println("Tomcat启动成功:监听端口号为8888");
        future.channel().closeFuture().sync();
    } finally {
        parent.shutdownGracefully();
        child.shutdownGracefully();
    }
}

注意:

  • option(ChannelOption.SO_BACKLOG,1024)
    指定存放连接请求的队列长度,这个是Socket的标准参数,TCP的参数
    option是处理parent EventLoopGroup的

  • childOption(ChannelOption.SO_KEEPALIVE,true)
    指定是否启用心跳机制来检测长连接的存活性,即客户端的存活性(这个心跳机制是操作系统层实现的!也是TCP的标准参数
    childOption是处理child EventLoopGroup的

  • 添加HttpServerCodec处理器等于同时添加了HttpRequestDecoder和HttpResponseEncoder,用来处理http请求

    /**
     * A combination of {@link HttpRequestDecoder} and {@link HttpResponseEncoder}
     * which enables easier server side HTTP implementation.
     * 
     * HttpServerCodec是HttpRequestDecoder解码器和HttpResponseEncoder编码器的结合
     */
    public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder> implements HttpServerUpgradeHandler.SourceCodec {
    ...
    }
    

(5) 定义服务器端处理器

注意:

  • 处理器最好继承ChannelInboundHandlerAdapter
  • 处理器的处理逻辑:
    • 1)从用户请求URI中解析出要访问的Servnet名称
    • 2)从nameToServnetMap中查找会否存在该名称的key,若存在则直接使用该实例,否则执行第三步
    • 3)从nameToClassnameMap中查找是否存在该名称的key,若存在,则获取到其对应的全限定性类名使用反射机制创建相应的Servnet实例,并写入到nameToServnetMap,若不存在则直接访问默认Servnet
/**
 * Tomcat服务端处理器
 *
 *   1)从用户请求URI中解析出要访问的Servnet名称
 *   2)从nameToServnetMap中查找是否存在该名称的key。若存在,则直接使用该实例,否则执行第3)步
 *   3)从nameToClassNameMap中查找是否存在该名称的key,若存在,则获取到其对应的全限定性类名,
 *      使用反射机制创建相应的sernet实例,并写入到nameToServnetMap中,若不存在,则直接访问默认Servnet
 *
 */
public class TomcatHandler extends ChannelInboundHandlerAdapter {
    private Map<String, Servnet> nameToServnetMap;
    private Map<String, String> nameToClassNameMap;

    public TomcatHandler(Map<String, Servnet> nameToServnetMap, Map<String, String> nameToClassNameMap) {
        this.nameToServnetMap = nameToServnetMap;
        this.nameToClassNameMap = nameToClassNameMap;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            // 从请求中解析出要访问的Servnet名称
            String servnetName = request.uri().split("/")[1];
// 直接给Servnet赋值一个默认的
            Servnet servnet = new DefaultServnet();
            if (nameToServnetMap.containsKey(servnetName)) {
                servnet = nameToServnetMap.get(servnetName);
            } else if (nameToClassNameMap.containsKey(servnetName)) {
                // double-check,双重检测锁
                // 当读到消息的时候根据请求获取对应的Servnet
                // 如果不存在创建的时候用双重检查锁,避免线程安全问题
                if (nameToServnetMap.get(servnetName) == null) {
                    synchronized (this) {
                        if (nameToServnetMap.get(servnetName) == null) {
                            // 获取当前Servnet的全限定性类名
                            String className = nameToClassNameMap.get(servnetName);
                            // 使用反射机制创建Servnet实例
                            servnet = (Servnet) Class.forName(className).newInstance();
                            // 将Servnet实例写入到nameToServnetMap
                            nameToServnetMap.put(servnetName, servnet);
                        }
                    }
                }
            } //  end-else if

            // 代码走到这里,servnet肯定不空
            NettyRequest req = new DefaultNettyRequest(request);
            NettyResponse res = new DefaultNettyResponse(request, ctx);
            // 根据不同的请求类型,调用servnet实例的不同方法
            if (request.method().name().equalsIgnoreCase("GET")) {
                servnet.doGet(req, res);
            } else if(request.method().name().equalsIgnoreCase("POST")) {
                servnet.doPost(req, res);
            }
            ctx.close();
        }
    }

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

(6) 定义启动类 TomcatStarter

public class TomcatStarter {
    public static void main(String[] args) throws Exception {
        TomcatServer server = new TomcatServer("com.abc.webapp");
        server.start();
    }
}

5. 定义业务 Servnet

/**
 * 业务Servnet
 */
public class OneServnet extends Servnet {
    @Override
    public void doGet(NettyRequest request, NettyResponse response) throws Exception {
        String uri = request.getUri();
        String path = request.getPath();
        String method = request.getMethod();
        String name = request.getParameter("name");

        String content = "uri = " + uri + "\n" +
                         "path = " + path + "\n" +
                         "method = " + method + "\n" +
                         "param = " + name;
        response.write(content);
    }

    @Override
    public void doPost(NettyRequest request, NettyResponse response) throws Exception {
        doGet(request, response);
    }
}

6. 启动测试

在这里插入图片描述
在这里插入图片描述

 

Netty(四)高级应用 之 手写Tomcat_netty 手写tomcat-CSDN博客

请登录后发表评论

    没有回复内容