乞丐版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大致如下:
Server包含了1個或者多個Connector,Connector包含一個EventListener,一個EventListener包含一個EventHandler。
每當Connector接受到請求時,就構造一個Connection,Connector將Connection傳遞給EventListener,EventListener再傳遞給EventHandler。EventHandler呼叫Connection獲取請求資料,並寫入響應資料。
之後如果需要加入Servlet的功能,則需要新增對於的EventHandler,再通過EventHandler將請求Dispatcher到相應的Servlet中,而伺服器的其餘部分基本不用修改。
面向物件的設計模式功力比較弱,先設計一個勉強能用的架構先。這樣單執行緒Server的IO部分基本就搞好了。