netty中websocket

websocket

直接使用SpringBoot+Netty來支持WebSocket,并且需要支持wss,其需要注意事項(xiàng)有以下:

  1. wss支持
  2. websocket請求路徑中帶參數(shù)



針對第一個問題:wss支持比較簡單;

  1. 生成證書
  2. ChannelPipeline中添加ssl Handler,并且放在First即可。

生成證書

keytool -genkey -alias demo -keypass 123456 -keyalg RSA -keysize 1024 -validity 365 -keystore demo.liukun.com.keystore -storepass 123456 -dname "C=CN,ST=SH,L=SH,O=hfjy,CN=demo.liukun.com" -deststoretype pkcs12

導(dǎo)出證書:

keytool -export -alias demo -file demo.cer -keystore demo.liukun.com.keystore -storepass 123456

ssl/tls支持

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;

public class NettyServerHandlerInitializer extends ChannelInitializer<Channel> {
    // 證書中使用的密碼,因?yàn)閐emo,不做配置,直接寫死
    private String password = "123456";

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(new HttpServerCodec());
        ch.pipeline().addLast(new HttpObjectAggregator(65535));
        ch.pipeline().addLast(new ChunkedWriteHandler());
        // 對websocket url中的參數(shù)做解析處理的Handler
        ch.pipeline().addLast(new CustomUrlHandler());
        // 對websocket做支持,其路徑為/ws
        ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
        // 自定義業(yè)務(wù)邏輯處理的Handler
        ch.pipeline().addLast(new MyWebSocketHandler());

        // 以下為要支持wss所需處理
        KeyStore ks = KeyStore.getInstance("JKS");
        InputStream ksInputStream = new FileInputStream("/Users/liukun/ca/demo.liukun.com.keystore");
        ks.load(ksInputStream, password.toCharArray());
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, password.toCharArray());
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(), null, null);
        SSLEngine sslEngine = sslContext.createSSLEngine();
        sslEngine.setUseClientMode(false);
        sslEngine.setNeedClientAuth(false);
        // 需把SslHandler添加在第一位
        ch.pipeline().addFirst("ssl", new SslHandler(sslEngine));
    }
}



針對第二個問題:websocket的uri中帶參數(shù)問題。因?yàn)樵趯?shí)際項(xiàng)目中,很多情況下都需要使用到參數(shù),如對請求的URL進(jìn)行認(rèn)證等,如果需要傳遞參數(shù)通常有兩種做法:

  • 在Header中傳遞:這種需要對XMLHttpRequest進(jìn)行自定義,比較復(fù)雜,不建議
  • 直接在uri中帶參數(shù),如/ws?token=123,這種比較簡單,建議使用這種,以下處理也是基于這種方式

通過在uri中帶參數(shù)時,如果使用WebSocketServerProtocolHandler時會發(fā)現(xiàn)連接不成功,因?yàn)槠洳蛔R別這種處理方式。其解決方式:

在其處理之前對uri中的參數(shù)解析,并對uri進(jìn)行重寫:即把路徑中?及其后面參數(shù)去除。如業(yè)務(wù)中涉及認(rèn)證及關(guān)聯(lián)等都在此處理

見上面代碼中添加的ch.pipeline().addLast(new CustomUrlHandler())。其中CustomUrlHandler內(nèi)容如下:

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpRequest;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;

@ChannelHandler.Sharable
public class CustomUrlHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 只針對FullHttpRequest類型的做處理,其它類型的自動放過
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String uri = request.uri();
            int idx = uri.indexOf("?");
            if (idx > 0) {
                String query = uri.substring(idx + 1);
                // uri中參數(shù)的解析使用的是jetty-util包,其性能比自定義及正則性能高。
                MultiMap<String> values = new MultiMap<String>();
                UrlEncoded.decodeTo(query, values, "UTF-8");
                request.setUri(uri.substring(0, idx));
            }
        }
        ctx.fireChannelRead(msg);
    }
}

其中MyWebSocketHandler作用為:收到客戶端發(fā)送的消息,然后重新傳給客戶端,注意寫數(shù)據(jù)時格式為TextWebSocketFrame:

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ChannelHandler.Sharable
public class MyWebSocketHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LoggerFactory.getLogger(MyWebSocketHandler.class);

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.info("與客戶端建立連接,通道開啟");
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("與客戶端斷開連接,通道關(guān)閉");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        TextWebSocketFrame message = (TextWebSocketFrame)msg;
        logger.info("服務(wù)端收到數(shù)據(jù): " + message.text());
        // 此處需注意返回的數(shù)據(jù)的格式為TextWebSocketFrame。否則客戶端收不到消息
        ctx.channel().writeAndFlush(new TextWebSocketFrame(message.text()));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error(cause.getMessage());
        cause.printStackTrace();
    }
}

其它代碼參考如下:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;

@Component
@Slf4j
public class NettyServer {
    private EventLoopGroup boss = new NioEventLoopGroup();
    private EventLoopGroup work = new NioEventLoopGroup();
    private Integer port = 8000;

    @PostConstruct
    public void start() throws InterruptedException {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss, work)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress("demo.liukun.com", port))
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childHandler(new NettyServerHandlerInitializer());
        ChannelFuture future = bootstrap.bind().sync();
        if (future.isSuccess()) {
            log.info("啟動Netty Server");
        }
    }

    @PreDestroy
    public void destory() throws InterruptedException {
        boss.shutdownGracefully().sync();
        work.shutdownGracefully().sync();
        log.info("關(guān)閉Netty");
    }
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class NettyApplication {
    public static void main(String[] args) {
        SpringApplication.run(NettyApplication.class, args);
    }
}

前端html頁面如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<textarea id="msgBoxs"></textarea><br>
待發(fā)送消息:<input type="text" id="msg"><input type="button" id="sendBtn" onclick="send()" value="發(fā)送">
<script type="application/javascript">
    var msgBoxs = document.getElementById("msgBoxs")
    var msgBox = document.getElementById("msg")
    document.cookie="token2=John Doe";
    var ws = new WebSocket("wss://demo.liukun.com:8000/ws?token=abc123&type=1")
    ws.onopen = function (evt) {
        console.log("Connection open ...");
        ws.send("Hello WebSocket!");
    }

    ws.onmessage = function (evt) {
        console.log("Received Message: ", evt.data)
        var msgs = msgBoxs.value
        msgBoxs.innerText = msgs + "\n" + evt.data
        msgBoxs.scrollTop = msgBoxs.scrollHeight;
    }

    ws.onclose = function (evt) {
        console.log("Connect closed.");
    }

    function send() {
        var msg = msgBox.value
        ws.send(msg)
        msgBox.value = ""
    }
</script>
</body>
</html>

測試

直接使用html頁面訪問wss服務(wù)器時,不同的瀏覽器會有不同的表現(xiàn)形式:

  • firefox不生效
  • chome必須先訪問其https網(wǎng)址,信任后才生效,否則不生效。
  • safari只要信任了證書,直接可以使用:直接在html頁面里面即可以訪問wss,有mac的同學(xué)建議直接使用此方式,最為簡單。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • WebSocket 機(jī)制 WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更...
    勇敢的_心_閱讀 2,375評論 0 4
  • Web 頁面的實(shí)現(xiàn) Web 基于 HTTP 協(xié)議通信 客戶端(Client)的 Web 瀏覽器從 Web 服務(wù)器端...
    毛圈閱讀 1,315評論 0 2
  • 今天閱讀了該書總結(jié)篇的內(nèi)容,回顧總結(jié)了其中的五大策略。 策略一:在日常工作中,認(rèn)清最重要的事 意識到抉擇點(diǎn)...
    Nicole93閱讀 488評論 0 1
  • 也許一切都是天意。 好在重歸平靜的生活呢。
    無隱閱讀 135評論 0 0
  • 文/金秋傲嬌碩果豐 (一) 寒風(fēng)凜冽好凄涼 滲透心肺和肝腸 眾生奔赴大商場 試買衣服換冬裝 (二) 人穿冬服舒而強(qiáng)...
    風(fēng)輕月朦朧閱讀 935評論 24 40

友情鏈接更多精彩內(nèi)容