1. 程式人生 > >乞丐版servlet容器第2篇

乞丐版servlet容器第2篇

spa after ola status sleep 執行 權限不足 ror mar

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篇