乞丐版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
乞丐版servlet容器第2篇的更多相关文章
- 乞丐版servlet容器第1篇
		本系列参照pkpk1234大神的BeggarServletContainer,具体请访问:https://github.com/pkpk1234/BeggarServletContainer. 一步一 ... 
- 乞丐版servlet容器第4篇
		6. NIOConnector 现在为Server添加NIOConnector,添加之前可以发现我们的代码其实是有问题的.比如现在的代码是无法让服务器支持同时监听多个端口和IP的,如同时监听 127. ... 
- 乞丐版servlet容器第3篇
		4 EventListener接口 让我们继续看SocketConnector中的acceptConnect方法: @Override protected void acceptConnect() t ... 
- 对Servlet容器的补充和一个问题的请教
		[0]README 0.1)本文是对 一个servlet容器 的补充: 0.2)发这个博文的最终目的是为了请教各位前辈,帮我解决一个问题,问题描述在文末, 谢谢: [1]Servlet容器 1.1) ... 
- 【串线篇】spring boot使用外置的Servlet容器
		嵌入式Servlet容器:应用打成可执行的jar 优点:简单.便携: 缺点:默认不支持JSP.优化定制比较复杂 (使用定制器[ServerProperties/自定义EmbeddedServletCo ... 
- 【串线篇】spring boot嵌入式Servlet容器自动配置原理
		EmbeddedServletContainerAutoConfiguration:嵌入式的Servlet容器自动配置? @AutoConfigureOrder(Ordered.HIGHEST_PREC ... 
- 【串线篇】spring boot嵌入式Servlet容器启动原理;
		什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat: 获取嵌入式的Servlet容器工厂: 1).SpringBoot应用启动运行run方法 2).r ... 
- 【串线篇】spring boot配置嵌入式servlet容器
		SpringBoot默认使用Tomcat作为嵌入式的Servlet容器 问题? 一.如何定制和修改Servlet容器的相关配置 1.方法1修改和server有关的配置(ServerProperties ... 
- 深入剖析tomcat之一个简单的servlet容器
		上一篇,我们讲解了如果开发一个简单的Http服务器,这一篇,我们扩展一下,让我们的服务器具备servlet的解析功能. 简单介绍下Servlet接口 如果我们想要自定义一个Servlet,那么我们必须 ... 
随机推荐
- Centos  LVM 创建 删除 扩大 缩小
			新建LVM的过程1.使用fdisk 新建分区 修改ID为8e3.使用 pvcreate 创建 PV 4.使用 vgcreate 创建 VG 5.使用 lvcreate 创建 LV 6.格式化LV7.挂 ... 
- 机器学习入门-数据过采样(上采样)1. SMOTE
			from imblearn.over_sampling import SMOTE # 导入 overstamp = SMOTE(random_state=0) # 对训练集的数据进行上采样,测试集的 ... 
- attr 修改IMG src
			jQuery修改img的src的方法:$("#img_id").attr("src","new_src"); 定义和用法 attr() 方法 ... 
- Nginx 反向代理、后端检测模块
			简介: Nginx 反向代理模块:ngx_http_proxy_module.ngx_http_upstream_module 后端检测模块:nginx_http_upstream_check_mod ... 
- SPARK数据类型
			转自: http://www.cnblogs.com/tuitui1989/p/5331113.html 一.本地向量 有如下几个类: Vector(基类),DenseVector,SparseVec ... 
- form表单重置、清空方法记录
			myform 是form的id属性值 1.调用reset()方法 function fomrReset() { document.getElementById("myform"). ... 
- VS文件发布不了,这样设置可以解决
			在VS里面新增一些文件的时候,往往发布的时候会发布不了,比如:(*.rdlc,*.p12).在项目里面,这些项目已经包含在项目里了,但是发布后,会发现这些文件并没有被发布出来 解决办法:邮件选择文件, ... 
- 锁机制(Lock)       信号量机制(Semaphore)       事件机制(Event)
			IPC 进程间通信(inter-Process Communicate) 锁机制(Lock) l = Lock() 开启一个锁机制(实例化) 一把锁配一个钥匙 l.acquire() 获得钥匙 ... 
- Java finalize以及Garbage Collection
			Java的垃圾回收机制: Java的垃圾回收并不等于C++中的析构.Java中,只有在程序濒临存储空间用完的那一刻,对象占用的空间才会释放.所以,在JAVA程序中,我们通常只考虑创建对象,而从不关心对 ... 
- River Hopscotch
			River Hopscotch http://poj.org/problem?id=3258 Time Limit: 2000MS Memory Limit: 65536K Total Submi ... 
