在使用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傳遞事件資料,降低各模塊之間代碼耦合,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/117765.html
標籤:Java
下一篇:MyBatis辟邪劍譜
