zoukankan      html  css  js  c++  java
  • 乞丐版servlet容器第2篇

    2. 监听端口接收请求

    上一步中我们已经定义好了Server接口,并进行了多次重构,但是实际上那个Server是没啥毛用的东西。
    现在要为其添加真正有用的功能。
    大师说了,饭要一口一口吃,衣服要一件一件脱,那么首先来定个小目标——启动ServerSocket监听请求,不要什么多线程不要什么NIO,先完成最简单的功能。
    下面还是一步一步来写代码并进行重构优化代码结构。

    关于Socket和ServerSocket怎么用,网上很多文章写得比我好,大家自己找找就好。

    代码写起来很简单:(下面的代码片段有很多问题哦,大神们请不要急着喷,看完再抽)

    public class SimpleServer implements Server {
        ... ...
        @Override
        public void start() {
            Socket socket = null;
            try {
                this.serverSocket = new ServerSocket(this.port);
                this.serverStatus = ServerStatus.STARTED;
                System.out.println("Server start");
                while (true) {
                    socket = serverSocket.accept();// 从连接队列中取出一个连接,如果没有则等待
                    System.out.println(
                            "新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
                }
            }
            catch (IOException e) {
                e.printStackTrace();
            }
            finally {
                if (socket != null) {
                    try {
                        socket.close();
                    }
                    catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    
        @Override
        public void stop() {
            try {
                if (this.serverSocket != null) {
                    this.serverSocket.close();
                }
            }
            catch (IOException e) {
                e.printStackTrace();
            }
            this.serverStatus = ServerStatus.STOPED;
            System.out.println("Server stop");
        }
    
        ... ...
    }
    

    添加单元测试:

    public class TestServerAcceptRequest {
        private static Server server;
        // 设置超时时间为500毫秒
        private static final int TIMEOUT = 500;
    
        @BeforeClass
        public static void init() {
            ServerConfig serverConfig = new ServerConfig();
            server = ServerFactory.getServer(serverConfig);
        }
    
        @Test
        public void testServerAcceptRequest() {
            // 如果server没有启动,首先启动server
            if (server.getStatus().equals(ServerStatus.STOPED)) {
                //在另外一个线程中启动server
                new Thread(() -> {
                    server.start();
                }).run();
                //如果server未启动,就sleep一下
                while (server.getStatus().equals(ServerStatus.STOPED)) {
                    System.out.println("等待server启动");
                    try {
                        Thread.sleep(500);
                    }
                    catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Socket socket = new Socket();
                SocketAddress endpoint = new InetSocketAddress("localhost",
                        ServerConfig.DEFAULT_PORT);
                try {
                    // 试图发送请求到服务器,超时时间为TIMEOUT
                    socket.connect(endpoint, TIMEOUT);
                    assertTrue("服务器启动后,能接受请求", socket.isConnected());
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                finally {
                    try {
                        socket.close();
                    }
                    catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        @AfterClass
        public static void destroy() {
            server.stop();
        }
    }
    

    运行单元测试,我檫,怎么偶尔一直输出“等待server启动",用大师的话说就算”只看见轮子转,不见车跑“。原因其实很简单,因为多线程咯,测试线程一直无法获取到另外一个线程中更新的值。大师又说了,早看不惯满天的System.out.println和到处重复的

    try {
        socket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    了。

    大师还说了,代码太垃圾了,问题很多:如果Server.start()时端口被占用、权限不足,start方法根本没有抛出异常嘛,调用者难道像SB一样一直等下去,还有,Socket如果异常了,while(true)就退出了,难道一个Socket异常,整个服务器就都挂了,这代码就是一坨屎嘛,滚去重构。

    首先为ServerStatus属性添加volatile,保证其可见性。

    public class SimpleServer implements Server {
        private volatile ServerStatus serverStatus = ServerStatus.STOPED;
    ... ...
    }
    

    然后引入sl4j+log4j2,替换掉漫天的System.out.println。

    然后编写closeQuietly方法,专门处理socket的关闭。

    public class IoUtils {
    
        private static Logger logger = LoggerFactory.getLogger(IoUtils.class);
    
        /**
        * 安静地关闭,不抛出异常
        * @param closeable
        */
        public static void closeQuietly(Closeable closeable) {
            if(closeable != null) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    logger.error(e.getMessage(),e);
                }
            }
        }
    }
    

    最后start方法异常时,需要让调用者得到通知,并且一个Socket异常,不影响整个服务器。

    重构后再跑单元测试:一切OK。
    到目前为止,一个单线程的可以接收请求的Server就完成了。

    3. Connector接口

    上一步后,我们完成了一个可以接收Socket请求的服务器。这时大师又说话了,昨天周末看片去了,有个单元测试TestServer
    没跑,你跑个看看,猜猜能跑过不。一跑果然不行啊,单元测试一直转圈,就不动。

    因为server.start();会让当前线程无限循环,不断等待Socket请求,所以下面的单元测试方法根本不会走到断言那一步,也不会退出,所以大家都卡住了。

    @Test
    public void testServerStart() throws IOException {
        server.start();
        assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
    }
    

    修改起来很简单,让server.start();在单独的线程里面执行就好,然后再循环判断ServerStatus是否为STARTED,等待服务器启动。
    如下:

    @Test
    public void testServerStart() throws IOException {  
        server.start();
        //如果server未启动,就sleep一下
        while (server.getStatus().equals(ServerStatus.STOPED)) {
            logger.info("等待server启动");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
        }
        assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
    }
    

    这时大师又说了,循环判断服务器是否启动的代码片段,和TestServerAcceptRequest里面有重复代码,启动Server的代码也是重复的,一看就是Ctrl+c Ctrl+v的,你就不会抽象出一个父类啊。再重构:

    public abstract class TestServerBase {
        private static Logger logger = LoggerFactory.getLogger(TestServerBase.class);
        /**
        * 在单独的线程中启动Server,如果启动不成功,抛出异常
        *
        * @param server
        */
        protected void startServer(Server server) {
            //在另外一个线程中启动server
            new Thread(() -> {
                try {
                    server.start();
                } catch (IOException e) {
                    //转为RuntimeException抛出,避免异常丢失
                    throw new RuntimeException(e);
                }
            }).start();
        }
        /**
        * 等待Server启动
        *
        * @param server
        */
        protected void waitServerStart(Server server) {
            //如果server未启动,就sleep一下
            while (server.getStatus().equals(ServerStatus.STOPED)) {
                logger.info("等待server启动");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }
    }
    

    和Server相关的单元测试都可以extends于TestServerBase。

    public class TestServer extends TestServerBase {
        ... ...
        @Test
        public void testServerStart() {
            startServer(server);
            waitServerStart(server);
            assertTrue("服务器启动后,状态是STARTED", server.getStatus().equals(ServerStatus.STARTED));
        }
        ... ...
    }
    
    public class TestServerAcceptRequest extends TestServerBase {
        ... ...
        @Test
        public void testServerAcceptRequest() {
            // 如果server没有启动,首先启动server
            if (server.getStatus().equals(ServerStatus.STOPED)) {
                startServer(server);
                waitServerStart(server);
                .... ...
        }  
        ... ...
    }
    

    再次执行单元测试,一切都OK。搞定单元测试后,大师又说了,看看你写的SimpleServer的start方法,
    SimpleServe当前就是用来监听并接收Socket请求的,start方法就应该如其名,只是启动监听,修改ServerStatus为STARTED,接受请求什么的和start方法有毛关系,弄出去。
    按照大师说的重构一下,单独弄个accept方法,专门用于接受请求。

        @Override
        public void start() throws IOException {
            //监听本地端口,如果监听不成功,抛出异常
            this.serverSocket = new ServerSocket(this.port);
            this.serverStatus = ServerStatus.STARTED;
            accept();
            return;
        }
        private void accept() {
            while (true) {
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                    logger.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
                } catch (IOException e) {
                    logger.error(e.getMessage(), e);
                } finally {
                    IoUtils.closeQuietly(socket);
                }
            }
        }
    

    这时大师又发话了 ,我要用SSL,你直接new ServerSocket有啥用,重构去。
    从start方法里面其实可以看到,Server启动接受\响应请求的组件后,组件的任何操作就和Server对象没一毛钱关系了,Server只是管理一下组件的生命周期而已。那么接受\响应请求的组件可以抽象出来,这样Server就不必和具体实现打交道了。
    按照Tomcat和Jetty的惯例,接受\响应请求的组件叫Connector,生命周期也可以抽象成一个接口LifeCycle。根据这个思路去重构。

    public interface LifeCycle {
        void start();
        void stop();
    }
    
    public abstract class Connector implements LifeCycle {
        @Override
        public void start() {
            init();
            acceptConnect();
        }
        protected abstract void init() throws ConnectorException;
        protected abstract void acceptConnect() throws ConnectorException;
    }
    

    将SimpleServer中和Socket相关的代码全部移动到SocketConnector里面

    public class SocketConnector extends Connector {
        ... ...
        @Override
        protected void init() throws ConnectorException {
            //监听本地端口,如果监听不成功,抛出异常
            try {
                this.serverSocket = new ServerSocket(this.port);
                this.started = true;
            } catch (IOException e) {
                throw new ConnectorException(e);
            }
        }
        @Override
        protected void acceptConnect() throws ConnectorException {
            new Thread(() -> {
                while (true && started) {
                    Socket socket = null;
                    try {
                        socket = serverSocket.accept();
                        LOGGER.info("新增连接:" + socket.getInetAddress() + ":" + socket.getPort());
                    } catch (IOException e) {
                        //单个Socket异常,不要影响整个Connector
                        LOGGER.error(e.getMessage(), e);
                    } finally {
                        IoUtils.closeQuietly(socket);
                    }
                }
            }).start();
        }
        @Override
        public void stop() {
            this.started = false;
            IoUtils.closeQuietly(this.serverSocket);
        }  
        ... ...
    }
    

    SimpleServer重构为

    public class SimpleServer implements Server {
        ... ...
        private SocketConnector socketConnector;
        ... ...
        @Override
        public void start() throws IOException {
            socketConnector.start();
            this.serverStatus = ServerStatus.STARTED;
        }
        @Override
        public void stop() {
            socketConnector.stop();
            this.serverStatus = ServerStatus.STOPED;
            logger.info("Server stop");
        }
        ... ...
    }
    

    跑单元测试,全部OK,证明代码没问题。
    大师瞄了一眼,说不 给你说了么,面向抽象编程啊,为毛还直接引用了SocketConnector,还有,我想要多个Connector,继续给我重构去。
    重构思路简单,将SocketConnector替换为抽象类型Connector即可,但是怎么实例化呢,总有地方要处理这个抽象到具体的过程啊,这时又轮到Factory类干这个脏活了。
    再次重构。
    增加ConnectorFactory接口,及其实现SocketConnectorFactory

    public class SocketConnectorFactory implements ConnectorFactory {
        private final SocketConnectorConfig socketConnectorConfig;
        public SocketConnectorFactory(SocketConnectorConfig socketConnectorConfig) {
            this.socketConnectorConfig = socketConnectorConfig;
        }
        @Override
        public Connector getConnector() {
            return new SocketConnector(this.socketConnectorConfig.getPort());
        }
    }
    

    SimpleServer也进行相应修改,不再实例化任何具体实现,只通过构造函数接收对应的抽象。

    public class SimpleServer implements Server {
        private static Logger logger = LoggerFactory.getLogger(SimpleServer.class);
        private volatile ServerStatus serverStatus = ServerStatus.STOPED;
        private final int port;
        private final List<Connector> connectorList;
        public SimpleServer(ServerConfig serverConfig, List<Connector> connectorList) {
            this.port = serverConfig.getPort();
            this.connectorList = connectorList;
        }
        @Override
        public void start() {
            connectorList.stream().forEach(connector -> connector.start());
            this.serverStatus = ServerStatus.STARTED;
        }
        @Override
        public void stop() {
            connectorList.stream().forEach(connector -> connector.stop());
            this.serverStatus = ServerStatus.STOPED;
            logger.info("Server stop");
        }
        ... ...
    }
    

    ServerFactory也进行修改,将Server需要的依赖传递到Server的构造函数中。

    public class ServerFactory {
        /**
        * 返回Server实例
        *
        * @return
        */
        public static Server getServer(ServerConfig serverConfig) {
            List<Connector> connectorList = new ArrayList<>();
            ConnectorFactory connectorFactory =
                    new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()));
            connectorList.add(connectorFactory.getConnector());
            return new SimpleServer(serverConfig,connectorList);
        }
    }
    

    这样我们就将对具体实现的依赖限制到了不多的几个Factory中,最核心的Server部分只操作了抽象。
    执行所有单元测试,再次全部成功。
    虽然目前为止,Server还是只能接收请求,但是代码结构还算OK,为下面编写请求处理做好了准备。
    完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step3

  • 相关阅读:
    关于gulp的压缩js和css
    关于vant的定制主题问题
    关于jquery-Validate
    关于bootstrap-table插件的问题
    Windows下sass无法编译
    Hibernate基础知识整理(三)
    Hibernate基础知识整理(二)
    Hibernate基础知识整理(一)
    学习Hibernate之Eclipse安装hibernate tools插件
    JDBC连接池的cvalidationQuery设置 (参考)
  • 原文地址:https://www.cnblogs.com/stillcoolme/p/8458302.html
Copyright © 2011-2022 走看看