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

乞丐版servlet容器第4篇

6. NIOConnector

現在為Server新增NIOConnector,新增之前可以發現我們的程式碼其實是有問題的。比如現在的程式碼是無法讓伺服器支援同時監聽多個埠和IP的,如同時監聽 127.0.0.1:18080和0.0.0.0:18443現在是無法做到的。因為當期的埠號是Server的屬性,並且只有一個,但是埠其實應該是Connector的屬性,因為Connector專門負責了Server的IO。

重構一下,將埠號從Server中去掉,取而代之的是Connector列表;將當期的Connector抽象類重新命名為AbstractConnector,再新建介面Connector,新增getPort和getHost兩個方法,讓Connector支援將監聽繫結到不同IP的功能。

去掉getPort方法

public interface Server {
    /**
     * 啟動伺服器
     */
    void start() throws IOException;

    /**
     * 關閉伺服器
     */
    void stop();

    /**
     * 獲取伺服器啟停狀態
     * @return
     */
    ServerStatus getStatus();

    /**
     * 獲取伺服器管理的Connector列表
     * @return
     */
    List<Connector> getConnectorList();
}

去掉port屬性和方法

public class SimpleServer implements Server {
    private static Logger logger = LoggerFactory.getLogger(SimpleServer.class);
    private volatile ServerStatus serverStatus = ServerStatus.STOPED;
    private final List<AbstractConnector> connectorList;

    public SimpleServer(List<AbstractConnector> connectorList) {
        this.connectorList = connectorList;
    }
    ... ...
}

新增HOST屬性繫結IP,新增backLog屬性設定ServerSocket的TCP屬性SO_BACKLOG。修改init方法,支援ServerSocket繫結IP。

public class SocketConnector extends AbstractConnector<Socket> {
    private static final Logger LOGGER = LoggerFactory.getLogger(SocketConnector.class);
    private static final String LOCALHOST = "localhost";
    private static final int DEFAULT_BACKLOG = 50;
    private final int port;
    private final String host;
    private final int backLog;
    private ServerSocket serverSocket;
    private volatile boolean started = false;
    private final EventListener<Socket> eventListener;

    public SocketConnector(int port, EventListener<Socket> eventListener) {
        this(port, LOCALHOST, DEFAULT_BACKLOG, eventListener);
    }

    public SocketConnector(int port, String host, int backLog, EventListener<Socket> eventListener) {
        this.port = port;
        this.host = StringUtils.isBlank(host) ? LOCALHOST : host;
        this.backLog = backLog;
        this.eventListener = eventListener;
    }


    @Override
    protected void init() throws ConnectorException {

        //監聽本地埠,如果監聽不成功,丟擲異常
        try {
            InetAddress inetAddress = InetAddress.getByName(this.host);
            this.serverSocket = new ServerSocket(this.port, backLog, inetAddress);
            this.started = true;
        } catch (IOException e) {
            throw new ConnectorException(e);
        }
    }

執行單元測試,一切OK。現在可以開始新增NIO了。

根據前面一步一步搭建的架構,需要新增支援NIO的EventListener和EventHandler兩個實現即可。

NIOEventListener中莫名其妙出現了SelectionKey,表面這個類和SelectionKey是強耦合的,說明Event這塊的架構設計是很爛的,勢必又要重構,今天先不改了,完成功能先。

public class NIOEventListener extends AbstractEventListener<SelectionKey> {
    private final EventHandler<SelectionKey> eventHandler;

    public NIOEventListener(EventHandler<SelectionKey> eventHandler) {
        this.eventHandler = eventHandler;
    }

    @Override
    protected EventHandler<SelectionKey> getEventHandler(SelectionKey event) {
        return this.eventHandler;
    }
}

同意的道理,NIOEchoEventHandler也不應該和SelectionKey強耦合,echo功能簡單,如果是返回檔案內容的功能,那樣的話,大段大段的檔案讀寫程式碼是完全無法複用的。

public class NIOEchoEventHandler extends AbstractEventHandler<SelectionKey> {
    @Override
    protected void doHandle(SelectionKey key) {
        try {
            if (key.isReadable()) {
                SocketChannel client = (SocketChannel) key.channel();
                ByteBuffer output = (ByteBuffer) key.attachment();
                client.read(output);
            } else if (key.isWritable()) {
                SocketChannel client = (SocketChannel) key.channel();
                ByteBuffer output = (ByteBuffer) key.attachment();
                output.flip();
                client.write(output);
                output.compact();
            }
        } catch (IOException e) {
            throw new HandlerException(e);
        }
    }
}

修改ServerFactory,新增NIO功能,這裡的程式碼也是有很大設計缺陷的,ServerFactory只應該根據傳入的config資訊構造Server,而不是每次都去改工廠。

public class ServerFactory {
    /**
     * 返回Server例項
     *
     * @return
     */
    public static Server getServer(ServerConfig serverConfig) {
        List<Connector> connectorList = new ArrayList<>();
        SocketEventListener socketEventListener =
                new SocketEventListener(new FileEventHandler(System.getProperty("user.dir")));
        ConnectorFactory connectorFactory =
                new SocketConnectorFactory(new SocketConnectorConfig(serverConfig.getPort()), socketEventListener);
        //NIO
        NIOEventListener nioEventListener = new NIOEventListener(new NIOEchoEventHandler());
        //監聽18081埠
        SocketChannelConnector socketChannelConnector = new SocketChannelConnector(18081,nioEventListener);

        connectorList.add(connectorFactory.getConnector());
        connectorList.add(socketChannelConnector);
        return new SimpleServer(connectorList);
    }
}

執行BootStrap,啟動Server,telnet訪問18081埠,功能是勉強實現了,但是架構設計是有重大缺陷的,進一步新增功能之前,需要重構好架構才行。

7. Connection介面

繼續抽象的過程,無論Socket還是SocketChannle,其實都可以抽象為一個表示通訊連線的Connection介面。每當Connector監聽到埠有請求時,即建立了一個Connection。

NIO的介面和BIO的介面差別實在太大了,沒辦法只能加了一個不倫不類的ChannelConnection介面,肯定有更好的方案,但是以我現在的水平暫時只能這樣設計下了。等以後看了Netty或者Undertow的原始碼再重構吧。

重構後UML大致如下:
659358-20180226100029717-2061674215.png

Server包含了1個或者多個Connector,Connector包含一個EventListener,一個EventListener包含一個EventHandler。

每當Connector接受到請求時,就構造一個Connection,Connector將Connection傳遞給EventListener,EventListener再傳遞給EventHandler。EventHandler呼叫Connection獲取請求資料,並寫入響應資料。

之後如果需要加入Servlet的功能,則需要新增對於的EventHandler,再通過EventHandler將請求Dispatcher到相應的Servlet中,而伺服器的其餘部分基本不用修改。

面向物件的設計模式功力比較弱,先設計一個勉強能用的架構先。這樣單執行緒Server的IO部分基本就搞好了。