美文网首页
从零手写实现 tomcat-06-servlet bio/thr

从零手写实现 tomcat-06-servlet bio/thr

作者: 老马啸西风2020 | 来源:发表于2024-05-08 08:52 被阅读0次

    创作缘由

    平时使用 tomcat 等 web 服务器不可谓不多,但是一直一知半解。

    于是想着自己实现一个简单版本,学习一下 tomcat 的精髓。

    系列教程

    从零手写实现 apache Tomcat-01-入门介绍

    从零手写实现 apache Tomcat-02-web.xml 入门详细介绍

    从零手写实现 tomcat-03-基本的 socket 实现

    从零手写实现 tomcat-04-请求和响应的抽象

    从零手写实现 tomcat-05-servlet 处理支持

    从零手写实现 tomcat-06-servlet bio/thread/nio/netty 池化处理

    从零手写实现 tomcat-07-war 如何解析处理三方的 war 包?

    从零手写实现 tomcat-08-tomcat 如何与 springboot 集成?

    从零手写实现 tomcat-09-servlet 处理类

    从零手写实现 tomcat-10-static resource 静态资源文件

    从零手写实现 tomcat-11-filter 过滤器

    从零手写实现 tomcat-12-listener 监听器

    拓展阅读

    Netty 权威指南-01-BIO 案例

    Netty 权威指南-02-NIO 案例

    Netty 权威指南-03-AIO 案例

    Netty 权威指南-04-为什么选择 Netty?Netty 入门教程

    问题

    现在的实现看起来一切都好,但是有一个问题,会导致阻塞。

    为了一步步演示,我们把代码简化一下。

    v1-bio

    最基本的版本

    package com.github.houbb.minicat.bs.servlet;
    
    import com.github.houbb.log.integration.core.Log;
    import com.github.houbb.log.integration.core.LogFactory;
    import com.github.houbb.minicat.exception.MiniCatException;
    import com.github.houbb.minicat.util.InnerHttpUtil;
    
    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 老马啸西风
     * @since 0.1.0
     */
    public class MiniCatBootstrapBioSocket {
    
        private static final Log logger = LogFactory.getLog(MiniCatBootstrapBioSocket.class);
    
        /**
         * 启动端口号
         */
        private final int port;
    
        /**
         * 服务端 socket
         */
        private ServerSocket serverSocket;
    
        public MiniCatBootstrapBioSocket() {
            this.port = 8080;
        }
    
        public void start() {
            logger.info("[MiniCat] start listen on port {}", port);
            logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
    
            try {
                this.serverSocket = new ServerSocket(port);
    
                while (true) {
                    Socket clientSocket = serverSocket.accept(); // 等待客户端连接
    
                    // 从Socket获取输入流
                    logger.info("readRequestString start");
                    String requestString = readRequestString(clientSocket);
                    logger.info("readRequestString end");
    
                    // 这里模拟一下耗时呢
                    TimeUnit.SECONDS.sleep(5);
    
                    // 写回到客户端
                    logger.info("writeToClient start");
                    writeToClient(clientSocket, requestString);
                    logger.info("writeToClient end");
    
                    // 关闭连接
                    clientSocket.close();
                }
    
    
            } catch (Exception e) {
                logger.error("[MiniCat] start meet ex", e);
                throw new MiniCatException(e);
            }
        }
    
        private void writeToClient(Socket clientSocket, String requestString) throws IOException {
            OutputStream outputStream = clientSocket.getOutputStream();
            String httpText = InnerHttpUtil.http200Resp("ECHO: \r\n" + requestString);
            outputStream.write(httpText.getBytes("UTF-8"));
        }
    
        private String readRequestString(Socket clientSocket) throws IOException {
            // 从Socket获取输入流
            BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            StringBuilder requestBuilder = new StringBuilder();
            String line;
            // 读取HTTP请求直到空行(表示HTTP请求结束)
            while ((line = reader.readLine()) != null && !line.isEmpty()) {
                requestBuilder.append(line).append("\n");
            }
            return requestBuilder.toString();
        }
    
    }
    

    这种实现方式每次只能处理一个请求。

    当然,我们可以引入 thread 线程池。

    v2-bio+thread

    package com.github.houbb.minicat.bs.servlet;
    
    import com.github.houbb.log.integration.core.Log;
    import com.github.houbb.log.integration.core.LogFactory;
    import com.github.houbb.minicat.exception.MiniCatException;
    import com.github.houbb.minicat.util.InnerHttpUtil;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 实际测试还是会阻塞
     *
     * @author 老马啸西风
     * @since 0.1.0
     */
    public class MiniCatBootstrapBioThreadSocket {
    
        private static final Log logger = LogFactory.getLog(MiniCatBootstrapBioThreadSocket.class);
    
        /**
         * 启动端口号
         */
        private final int port;
    
        /**
         * 服务端 socket
         */
        private ServerSocket serverSocket;
    
        private final ExecutorService threadPool;
    
        public MiniCatBootstrapBioThreadSocket() {
            this.port = 8080;
    
            threadPool = Executors.newFixedThreadPool(10);
        }
    
        public void start() {
            logger.info("[MiniCat] start listen on port {}", port);
            logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
    
            try {
                this.serverSocket = new ServerSocket(port);
    
                while (true) {
                    Socket clientSocket = serverSocket.accept(); // 等待客户端连接
    
                    // 从Socket获取输入流
                    threadPool.submit(new Runnable() {
                        @Override
                        public void run() {
                            handleSocket(clientSocket);
                        }
                    });
                }
    
    
            } catch (Exception e) {
                logger.error("[MiniCat] start meet ex", e);
                throw new MiniCatException(e);
            }
        }
    
        private void handleSocket(Socket clientSocket) {
            try {
                logger.info("readRequestString start");
                String requestString = readRequestString(clientSocket);
                logger.info("readRequestString end");
    
                // 这里模拟一下耗时呢
                TimeUnit.SECONDS.sleep(5);
    
                // 写回到客户端
                logger.info("writeToClient start");
                writeToClient(clientSocket, requestString);
                logger.info("writeToClient end");
    
                // 关闭连接
                clientSocket.close();
            } catch (IOException | InterruptedException e) {
                logger.error("");
            }
        }
    
        private void writeToClient(Socket clientSocket, String requestString) throws IOException {
            OutputStream outputStream = clientSocket.getOutputStream();
            String httpText = InnerHttpUtil.http200Resp("ECHO: \r\n" + requestString);
            outputStream.write(httpText.getBytes("UTF-8"));
        }
    
        private String readRequestString(Socket clientSocket) throws IOException {
            // 从Socket获取输入流
            BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            StringBuilder requestBuilder = new StringBuilder();
            String line;
            // 读取HTTP请求直到空行(表示HTTP请求结束)
            while ((line = reader.readLine()) != null && !line.isEmpty()) {
                requestBuilder.append(line).append("\n");
            }
            return requestBuilder.toString();
        }
    
    }
    

    其实这个还是不够的,测试发现这里的 socket 其实还是阻塞的。

    v3-nio

    nio 可以让 socket 不再阻塞

    package com.github.houbb.minicat.bs.servlet;
    
    import com.github.houbb.log.integration.core.Log;
    import com.github.houbb.log.integration.core.LogFactory;
    import com.github.houbb.minicat.exception.MiniCatException;
    import com.github.houbb.minicat.util.InnerHttpUtil;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    public class MiniCatBootstrapNioSocket {
    
        private static final Log logger = LogFactory.getLog(MiniCatBootstrapNioSocket.class);
    
        private final int port;
        private ServerSocketChannel serverSocketChannel;
        private Selector selector;
    
        public MiniCatBootstrapNioSocket() {
            this.port = 8080;
        }
    
        public void start() {
            logger.info("[MiniCat] start listen on port {}", port);
            logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
    
            try {
                serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.bind(new InetSocketAddress(port));
                serverSocketChannel.configureBlocking(false);
    
                selector = Selector.open();
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
                while (true) {
                    int readyChannels = selector.select();
                    if (readyChannels == 0) {
                        continue;
                    }
    
                    Set<SelectionKey> selectedKeys = selector.selectedKeys();
                    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    
                    while (keyIterator.hasNext()) {
                        SelectionKey key = keyIterator.next();
    
                        if (key.isAcceptable()) {
                            handleAccept(key);
                        } else if (key.isReadable()) {
                            handleRead(key);
                        }
    
                        keyIterator.remove();
                    }
                }
            } catch (IOException | InterruptedException e) {
                logger.error("[MiniCat] start meet ex", e);
                throw new MiniCatException(e);
            }
        }
    
        private void handleAccept(SelectionKey key) throws IOException {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        }
    
        private void handleRead(SelectionKey key) throws IOException, InterruptedException {
            logger.info("handle read start");
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            StringBuilder requestBuilder = new StringBuilder();
    
            int bytesRead = socketChannel.read(buffer);
            while (bytesRead > 0) {
                buffer.flip();
                while (buffer.hasRemaining()) {
                    requestBuilder.append((char) buffer.get());
                }
                buffer.clear();
                bytesRead = socketChannel.read(buffer);
            }
    
            String requestString = requestBuilder.toString();
            logger.info("handle read requestString={}", requestString);
    
            TimeUnit.SECONDS.sleep(5); // 模拟耗时操作
    
            logger.info("start write");
            writeToClient(socketChannel, requestString);
            logger.info("end writeToClient");
    
            socketChannel.close();
        }
    
        private void writeToClient(SocketChannel socketChannel, String requestString) throws IOException {
            String httpText = InnerHttpUtil.http200Resp("ECHO: \r\n" + requestString);
            ByteBuffer buffer = ByteBuffer.wrap(httpText.getBytes("UTF-8"));
            socketChannel.write(buffer);
        }
    
    }
    

    v4-nio+thread

    不过测试发现,依然会阻塞在 sleep 的地方。

    调整如下:

    package com.github.houbb.minicat.bs.servlet;
    
    import com.github.houbb.log.integration.core.Log;
    import com.github.houbb.log.integration.core.LogFactory;
    import com.github.houbb.minicat.exception.MiniCatException;
    import com.github.houbb.minicat.util.InnerHttpUtil;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    import java.util.Set;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    public class MiniCatBootstrapNioThreadSocket {
    
        private static final Log logger = LogFactory.getLog(MiniCatBootstrapNioThreadSocket.class);
    
        private final int port;
        private ServerSocketChannel serverSocketChannel;
        private Selector selector;
        private ExecutorService threadPool;
    
        public MiniCatBootstrapNioThreadSocket() {
            this.port = 8080;
            this.threadPool = Executors.newFixedThreadPool(10); // 10个线程的线程池
        }
    
        public void start() {
            logger.info("[MiniCat] start listen on port {}", port);
            logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
    
            try {
                serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.bind(new InetSocketAddress(port));
                serverSocketChannel.configureBlocking(false);
    
                selector = Selector.open();
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
                while (true) {
                    int readyChannels = selector.select();
                    if (readyChannels == 0) {
                        continue;
                    }
    
                    Set<SelectionKey> selectedKeys = selector.selectedKeys();
                    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    
                    while (keyIterator.hasNext()) {
                        SelectionKey key = keyIterator.next();
    
                        if (key.isAcceptable()) {
                            handleAccept(key);
                        } else if (key.isReadable()) {
                            handleRead(key);
                        }
    
                        keyIterator.remove();
                    }
                }
            } catch (IOException e) {
                logger.error("[MiniCat] start meet ex", e);
                throw new MiniCatException(e);
            }
        }
    
        private void handleAccept(SelectionKey key) throws IOException {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        }
    
        private void handleRead(SelectionKey key) throws IOException {
            threadPool.execute(() -> {
                try {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    StringBuilder requestBuilder = new StringBuilder();
    
                    int bytesRead = socketChannel.read(buffer);
                    while (bytesRead > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            requestBuilder.append((char) buffer.get());
                        }
                        buffer.clear();
                        bytesRead = socketChannel.read(buffer);
                    }
    
                    String requestString = requestBuilder.toString();
                    logger.info("read requestString={}", requestString);
    
                    TimeUnit.SECONDS.sleep(5); // 模拟耗时操作
                    writeToClient(socketChannel, requestString);
                    logger.info("writeToClient done");
                    socketChannel.close();
                } catch (InterruptedException | IOException e) {
                    logger.error("[MiniCat] error processing request", e);
                }
            });
        }
    
        private void writeToClient(SocketChannel socketChannel, String requestString) throws IOException {
            String httpText = InnerHttpUtil.http200Resp("ECHO: \r\n" + requestString);
            ByteBuffer buffer = ByteBuffer.wrap(httpText.getBytes("UTF-8"));
            socketChannel.write(buffer);
        }
    
        public void shutdown() {
            try {
                threadPool.shutdown();
                threadPool.awaitTermination(5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                logger.error("[MiniCat] error shutting down thread pool", e);
                Thread.currentThread().interrupt();
            } finally {
                try {
                    selector.close();
                    serverSocketChannel.close();
                } catch (IOException e) {
                    logger.error("[MiniCat] error closing server socket", e);
                }
            }
        }
    
    }
    

    v5-netty

    看的出来,我们废了很大的精力才实现了 nio。

    其实 netty 就是针对 nio api 设计的过于复杂的问题,做了大量的改进和优化。

    我们来一起欣赏一下 netty 的版本:

    package com.github.houbb.minicat.bs.servlet;
    
    import com.github.houbb.log.integration.core.Log;
    import com.github.houbb.log.integration.core.LogFactory;
    import com.github.houbb.minicat.exception.MiniCatException;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    
    
    public class MiniCatBootstrapNetty {
    
        private static final Log logger = LogFactory.getLog(MiniCatBootstrapNetty.class);
    
        /**
         * 启动端口号
         */
        private final int port;
    
        public MiniCatBootstrapNetty() {
            this.port = 8080;
        }
    
        public void start() {
            logger.info("[MiniCat] start listen on port {}", port);
            logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
    
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            //worker 线程池的数量默认为 CPU 核心数的两倍
            EventLoopGroup workerGroup = new NioEventLoopGroup();
    
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
                serverBootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new MiniCatNettyServerHandler());
                            }
                        })
                        .option(ChannelOption.SO_BACKLOG, 128)
                        .childOption(ChannelOption.SO_KEEPALIVE, true);
    
                // Bind and start to accept incoming connections.
                ChannelFuture future = serverBootstrap.bind(port).sync();
    
                // Wait until the server socket is closed.
                future.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                logger.error("[MiniCat] start meet ex", e);
                throw new MiniCatException(e);
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    
    }
    
    package com.github.houbb.minicat.bs.servlet;
    
    import com.github.houbb.log.integration.core.Log;
    import com.github.houbb.log.integration.core.LogFactory;
    import com.github.houbb.minicat.util.InnerHttpUtil;
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.ChannelFutureListener;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    
    import java.nio.charset.Charset;
    import java.util.concurrent.TimeUnit;
    
    public class MiniCatNettyServerHandler extends ChannelInboundHandlerAdapter {
    
        private static final Log logger = LogFactory.getLog(MiniCatNettyServerHandler.class);
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            String requestString = new String(bytes, Charset.defaultCharset());
            logger.info("channelRead requestString={}", requestString);
    
            // Simulating some processing time
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
            String respText = InnerHttpUtil.http200Resp("ECHO: \r\n" + requestString);;
            ByteBuf responseBuf = Unpooled.copiedBuffer(respText.getBytes());
            ctx.writeAndFlush(responseBuf)
                    .addListener(ChannelFutureListener.CLOSE); // Close the channel after sending the response
            logger.info("channelRead writeAndFlush DONE");
        }
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.flush();
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            logger.error("exceptionCaught", cause);
            ctx.close();
        }
    }
    

    开源地址

     /\_/\  
    ( o.o ) 
     > ^ <
    

    mini-cat 是简易版本的 tomcat 实现。别称【嗅虎】(心有猛虎,轻嗅蔷薇。)

    开源地址:https://github.com/houbb/minicat

    相关文章

      网友评论

          本文标题:从零手写实现 tomcat-06-servlet bio/thr

          本文链接:https://www.haomeiwen.com/subject/robdfjtx.html