探討Netty獲取並檢查Websocket握手請求的兩種方式


在使用Netty開發Websocket服務時,通常需要解析來自客戶端請求的URL、Headers等等相關內容,並做相關檢查或處理。本文將討論兩種實現方法。

方法一:基於HandshakeComplete自定義事件

特點:使用簡單、校驗在握手成功之后、失敗信息可以通過Websocket發送回客戶端。

1.1 從netty源碼出發

一般地,我們將netty內置的WebSocketServerProtocolHandler作為Websocket協議的主要處理器。通過研究其代碼我們了解到在本處理器被添加到PiplinehandlerAdded方法將會被調用。此方法經過簡單的檢查后將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處理器之前檢測攔截請求信息。下面的例子主要做了四件事情:

  1. 從HTTP請求中提取關心的數據
  2. 安全檢查
  3. 將結果和其他數據綁定在Channel
  4. 觸發安全檢查完畢自定義事件
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傳遞事件數據,降低各模塊之間代碼耦合。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM