在使用Netty開發Websocket服務時,通常需要解析來自客戶端請求的URL、Headers等等相關內容,並做相關檢查或處理。本文將討論兩種實現方法。
方法一:基於HandshakeComplete自定義事件
特點:使用簡單、校驗在握手成功之后、失敗信息可以通過Websocket發送回客戶端。
1.1 從netty源碼出發
一般地,我們將netty內置的WebSocketServerProtocolHandler
作為Websocket協議的主要處理器。通過研究其代碼我們了解到在本處理器被添加到Pipline
后handlerAdded
方法將會被調用。此方法經過簡單的檢查后將WebSocketHandshakeHandler
添加到了本處理器之前,用於處理握手相關業務。
我們都知道Websocket協議在握手時是通過HTTP(S)協議進行的,那么這個WebSocketHandshakeHandler
應該就是處理HTTP相關的數據的吧?
下方代碼經過精簡,放心閱讀😄
package io.netty.handler.codec.http.websocketx; public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler { @Override public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline cp = ctx.pipeline(); if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) { // Add the WebSocketHandshakeHandler before this one. cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(), new WebSocketServerProtocolHandshakeHandler(serverConfig)); } //... } }
我們來看看WebSocketServerProtocolHandshakeHandler
都做了什么操作。
channelRead
方法會嘗試接收一個FullHttpRequest
對象,表示來自客戶端的HTTP請求,隨后服務器將會進行握手相關操作,此處省略了握手大部分代碼,感興趣的同學可以自行閱讀。
可以注意到,在確認握手成功后,channelRead
將會調用兩次fireUserEventTriggered
,此方法將會觸發自定義事件。其他(在此處理器之后)的處理器會觸發userEventTriggered
方法。其中一個方法傳入了WebSocketServerProtocolHandler
對象,此對象保存了HTTP請求相關信息。那么解決方案逐漸浮出水面,通過監聽自定義事件即可實現檢查握手的HTTP請求。
package io.netty.handler.codec.http.websocketx; /** * Handles the HTTP handshake (the HTTP Upgrade request) for {@link WebSocketServerProtocolHandler}. */ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { final FullHttpRequest req = (FullHttpRequest) msg; if (isNotWebSocketPath(req)) { ctx.fireChannelRead(msg); return; } try { //... if (!future.isSuccess()) { } else { localHandshakePromise.trySuccess(); // Kept for compatibility ctx.fireUserEventTriggered( WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE); ctx.fireUserEventTriggered( new WebSocketServerProtocolHandler.HandshakeComplete( req.uri(), req.headers(), handshaker.selectedSubprotocol())); } } finally { req.release(); } } }
1.2 解決方案
下面的代碼展示了如何監聽自定義事件。通過拋出異常可以終止鏈接,同時可以利用ctx
向客戶端以Websocket協議返回錯誤信息。因為此時握手已經完成,所以雖然這種方案簡單的過分,但是效率並不高,耗費服務端資源(都握手了又給人家踢了QAQ)。
private final class ServerHandler extends SimpleChannelInboundHandler<DeviceDataPacket> { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { // 在此處獲取URL、Headers等信息並做校驗,通過throw異常來中斷鏈接。 } super.userEventTriggered(ctx, evt); } }
1.3 ChannelInitializer實現
附上Channel初始化代碼作為參考。
private final class ServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast("http-codec", new HttpServerCodec()) .addLast("chunked-write", new ChunkedWriteHandler()) .addLast("http-aggregator", new HttpObjectAggregator(8192)) .addLast("log-handler", new LoggingHandler(LogLevel.WARN)) .addLast("ws-server-handler", new WebSocketServerProtocolHandler(endpointUri.getPath())) .addLast("server-handler", new ServerHandler()); } }
方法二:基於新增安全檢查處理器
特點:使用相對復雜、校驗在握手成功之前、失敗信息可以通過HTTP返回客戶端。
2.1 解決方案
編寫一個入站處理器,接收FullHttpMessage
消息,在Websocket處理器之前檢測攔截請求信息。下面的例子主要做了四件事情:
- 從HTTP請求中提取關心的數據
- 安全檢查
- 將結果和其他數據綁定在Channel
- 觸發安全檢查完畢自定義事件
public class SecurityServerHandler extends ChannelInboundHandlerAdapter { private static final ObjectMapper json = new ObjectMapper(); public static final AttributeKey<SecurityCheckComplete> SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY = AttributeKey.valueOf("SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY"); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if(msg instanceof FullHttpMessage){ //extracts device information headers HttpHeaders headers = ((FullHttpMessage) msg).headers(); String uuid = Objects.requireNonNull(headers.get("device-connection-uuid")); String devDescJson = Objects.requireNonNull(headers.get("device-description")); //deserialize device description DeviceDescription devDesc = json.readValue(devDescJson, DeviceDescriptionWithCertificate.class); //check ...... // SecurityCheckComplete complete = new SecurityCheckComplete(uuid, devDesc); ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete); ctx.fireUserEventTriggered(complete); } //other protocols super.channelRead(ctx, msg); } @Getter @AllArgsConstructor public static final class SecurityCheckComplete { private String connectionUUID; private DeviceDescription deviceDescription; } }
在業務邏輯處理器中,可以通過組合自定義的安全檢查事件和Websocket握手完成事件。例如,在安全檢查后進行下一步自定義業務檢查,在握手完成后發送自定義內容等等,就看各位同學自由發揮了。
private final class ServerHandler extends SimpleChannelInboundHandler<DeviceDataPacket> { public final AttributeKey<DeviceConnection> @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof SecurityCheckComplete){ log.info("Security check has passed"); SecurityCheckComplete complete = (SecurityCheckComplete) evt; listener.beforeConnect(complete.getConnectionUUID(), complete.getDeviceDescription()); } else if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { log.info("Handshake has completed"); SecurityCheckComplete complete = ctx.channel().attr(SecurityServerHandler.SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).get(); DeviceDataServer.this.listener.postConnect(complete.getConnectionUUID(), new DeviceConnection(ctx.channel(), complete.getDeviceDescription())); } super.userEventTriggered(ctx, evt); } }
2.2 ChannelInitializer實現
附上Channel初始化代碼作為參考。
private final class ServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast("http-codec", new HttpServerCodec()) .addLast("chunked-write", new ChunkedWriteHandler()) .addLast("http-aggregator", new HttpObjectAggregator(8192)) .addLast("log-handler", new LoggingHandler(LogLevel.WARN)) .addLast("security-handler", new SecurityServerHandler()) .addLast("ws-server-handler", new WebSocketServerProtocolHandler(endpointUri.getPath())) .addLast("packet-codec", new DataPacketCodec()) .addLast("server-handler", new ServerHandler()); } }
總結
上述兩種方式分別在握手完成后和握手之前攔截檢查;實現復雜度和性能略有不同,可以通過具體業務需求選擇合適的方法。
Netty增強了責任鏈模式,使用userEvent
傳遞自定義事件使得各個處理器之間減少耦合,更專注於業務。但是、相比於流動於各個處理器之間的"主線"數據來說,userEvent
傳遞的"支線"數據往往不受關注。通過閱讀Netty內置的各種處理器源碼,探索其產生的事件,同時在開發過程中加以善用,可以減少冗余代碼。另外在開發自定義的業務邏輯時,應該積極利用userEvent
傳遞事件數據,降低各模塊之間代碼耦合。