天天看點

Netty學習(六)—WebSocket通信Netty學習(六)—WebSocket通信

Netty學習(六)—WebSocket通信

WebSocket是一種将Socket套接字引入到B/S架構中,使浏覽器和伺服器之間可以通過套接字建立持久連接配接,雙方都能發送即時消息給對方,而不是傳統模式下請求應答通信方式;

個人首頁:tuzhenyu’s page

原文位址:Netty學習(六)—WebSocket通信

(0) 原理

  • HTTP協定的弊端
    • HTTP協定是一種被動的半雙工的通信協定,也就是說同一時刻隻有一個方向的資料傳輸;半雙工的原因是通信模式采用的傳統的用戶端主導的請求—應答模式,隻有用戶端發送請求服務端才能發動應答,而服務端不能主動發送應答消息;
    • HTTP協定下每進行一次請求—應答通信都需要建立一次TCP連接配接,HTTP1,1對HTTP1.0進行改進,增添了keep-alive功能,也就是在一次TCP連接配接下進行多次請求—應答通信,但是通信仍然采用冗長的HTTP消息格式;
    • HTTP協定消息冗長,因為一個完整的HTTP消息包括消息頭,消息體和換行符等,一條消息中的可用資料比例較低,相對于其他通信協定較為冗長;
  • HTTP協定被動通信的解決方案
    • HTTP是一種被動的單雙工通信協定,隻有用戶端發送請求服務端才能發動應答,而服務端不能主動發送應答消息;在需要服務端主動推送資料的情況下,HTTP協定有極大的局限性能;
    • 通過Ajax輪詢的方式,通過前端Ajax技術定時發送輪詢請求,比如1秒發送一次請求,服務端收到請求後立即傳回,如果有資料則發送響應資料;
    • 通過長輪詢long poll的方式,發送請求後保持連接配接直到服務端有響應資料傳回,用戶端收到資料後再次發起long poll長輪詢;
    • 通過WebSocket方式,建立WebSocket連接配接後進行即時的全雙工的通信;
  • WebSocket協定
    • WebSocket協定是HTML5中提出的一種全雙工的持久連接配接的通信協定,通過HTTP協定握手建立連接配接後雙方可以進行即時的全雙工通信;
    • WebSocket是基于HTTP協定的,借用了HTTP的協定來完成一部分握手,完成握手後直接進行Socket通信;
  • WebSocket協定的優勢
    • 持久的TCP連接配接,無論是ajax輪詢還是long poll長輪詢都是不斷的建立新TCP連接配接,頻繁的連接配接建立和斷開極大消耗資源;WebSocket通過HTTP協定完成握手建立連接配接後,通過該TCP連接配接進行雙向的通信無需重複建立連接配接;
    • WebSocket建立連接配接後的雙向通信不再使用HTTP消息格式,沒有附加的消息頭,身份驗證等
    • WebSocket是一種全雙工的通信協定,服務端可以主動發送消息給用戶端,不再需要用戶端輪詢;

(1) WebSocket執行個體

用戶端

  • 建立WebSocket連接配接需要發送特定的HTTP消息,普通浏覽器通路無法實作需要在JS腳本裡建立連接配接
    • 通過new WebSocket()方法建立WebSocket連接配接
    • 回調判斷連接配接狀态觸發相應事件,包括連接配接建立事件,關閉事件,消息接收事件等
var socket;
    if (!window.WebSocket){
        window.WebSocket = window.MozWebSocket;
    }
    if(window.WebSocket){
       socket = new WebSocket("ws://localhost:8000");
       socket.onmessage = function (event) {
           var ta = document.getElementById('responseText');
           ta.value = "";
           ta.value = event.data;
       };
       socket.onopen = function (event) {
           var ta = document.getElementById('responseText');
           ta.value = "打開WebSocket正常";
       };
       socket.onclose = function (event) {
           var ta = document.getElementById('responseText');
           ta.value = "";
           ta.value = "WebSocket關閉";
       }
    }else {
        alert("抱歉,您的浏覽器不支援WebSocket協定");
    }
           
  • 發送WebSocket的消息
    • 連接配接建立完成後,直接發送webSocekt消息
function send(message) {
    if(!window.WebSocket)
        return;
    if (socket.readyState == WebSocket.OPEN){
        console.log("send the massage");
        socket.send(message);
    }
    else
        alert("WebSocket 連接配接沒有建立成功");
}
           

服務端

  • 使用Netty建立服務端,往ChannelPipeline管道中添加相應的Handler添加相應的Handler處理器
public void run(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
       .channel(NioServerSocketChannel.class)
       .childHandler(new ChannelInitializer<SocketChannel>() {

      @Override
      protected void initChannel(SocketChannel ch)
         throws Exception {
          ChannelPipeline pipeline = ch.pipeline();
          pipeline.addLast("http-codec", new HttpServerCodec());
          pipeline.addLast("aggregator",new HttpObjectAggregator());
          pipeline.addLast("http-chunked",new ChunkedWriteHandler());
          pipeline.addLast("handler", new WebSocketServerHandler());
      }
       });

    Channel ch = b.bind(port).sync().channel();
    ch.closeFuture().sync();
} finally {
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
}
   }
           
  • 消息經過管道傳遞到自定義的處理器WebSocketServerHander中
    • 調用channelRead()方法判斷傳入的消息類型,分為傳統HTTP消息和WebSocket消息
public void channelRead0(ChannelHandlerContext ctx, Object msg)
    throws Exception {
if (msg instanceof FullHttpRequest) {
    handleHttpRequest(ctx, (FullHttpRequest) msg);    // 傳統的HTTP接入
}else if (msg instanceof WebSocketFrame) {
    handleWebSocketFrame(ctx, (WebSocketFrame) msg);    // WebSocket接入
}
   }
           
  • 對于傳統的HTTP進行WebSocket連接配接握手處理
private void handleHttpRequest(ChannelHandlerContext ctx,
    FullHttpRequest req) throws Exception {


// 如果HTTP解碼失敗,傳回HHTP異常
if (!req.getDecoderResult().isSuccess()
   || (!"websocket".equals(req.headers().get("Upgrade")))) {
    sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,
       BAD_REQUEST));
    return;
}

// 構造握手響應傳回,本機測試
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
   "ws://localhost:8080/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
    WebSocketServerHandshakerFactory
       .sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
    handshaker.handshake(ctx.channel(), req);
}
   }
           
  • 對于WebSocket消息,對其指令類型判斷後進行處理,WebSocket消息類型分為:關閉連接配接指令,Ping心跳消息和普通消息等;
private void handleWebSocketFrame(ChannelHandlerContext ctx,
    WebSocketFrame frame) {

// 判斷是否是關閉鍊路的指令
if (frame instanceof CloseWebSocketFrame) {
    handshaker.close(ctx.channel(),
       (CloseWebSocketFrame) frame.retain());
    return;
}
// 判斷是否是Ping消息
if (frame instanceof PingWebSocketFrame) {
    ctx.channel().write(
       new PongWebSocketFrame(frame.content().retain()));
    return;
}
// 本例程僅支援文本消息,不支援二進制消息
if (!(frame instanceof TextWebSocketFrame)) {
    throw new UnsupportedOperationException(String.format(
       "%s frame types not supported", frame.getClass().getName()));
}

// 傳回應答消息
String request = ((TextWebSocketFrame) frame).text();
if (!"start".equals(request)){
   stop = true;
   System.out.println("stop");
}

if (logger.isLoggable(Level.FINE)) {
    logger.fine(String.format("%s received %s", ctx.channel(), request));
}
while (!stop){
   try{
      ctx.channel().writeAndFlush(
            new TextWebSocketFrame("歡迎使用Netty WebSocket服務,現在時刻:"
                  + new java.util.Date().toString()));
      Thread.sleep();
   }catch (Exception e){
      e.printStackTrace();
   }
}
   }
           

總結

  • WebSocket通信是一種對HTTP長連接配接keep-alive的一種改進,通過HTTP協定完成握手連接配接後能夠進行雙向即時通信,無需重複建立TCP連接配接并且服務端能夠主動的發送消息;

繼續閱讀