1. 程式人生 > >自己動手寫servlet容器 2.2: 監聽埠接收請求

自己動手寫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就完成了。

分支step2