Netty-6 Http协议支持-websocket

一、websockt简介

一直以来,网络都是围绕着http协议进行开发的。然而,http会有一些固有的问题,所以websocket被引入。

(一)http缺点

  1. HTTP是半双工协议,也就是说,数据可在客户端和服务端两个方向上传输,但是,不能同时传输。
  2. HTTP消息冗长繁琐,HTTP消息包括请求头、消息体、换行符等。
  3. 为了实现消息推送,需要采用轮训方式由浏览器向客户端发送请求,来获取服务器信息,效率很低。

(二)websocket

由于http的一些缺点,引入了websocket,来解决浏览器和服务器交互的一些问题,websocket具有以下特点:

  1. 单一的tcp连接,采用全双工通讯模式;
  2. 对代理、防火墙和路由器透明;
  3. 无头部信息、Cookie和身份验证;
  4. 无安全开销;
  5. 通过"ping / pong " 帧保持链路激活;
  6. 服务器可以主动传送消息给客户端,不用再通过轮训来获取服务端信息;

二、Netty开发websocket

这里使用netty开发一个web群聊功能,来展示Netty对websocket协议的支持。

(一)服务端

package com.firewolf.java.io.http.websocket;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;

import com.firewolf.java.io.http.utils.NettyHttpUtils;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.stream.ChunkedWriteHandler;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.HashMap;
import java.util.Map;

/**
 * 作者:刘兴 时间:2019-05-30
 **/
public final class NettyWebChatServer {

  private String chatHtml = System.getProperty("user.dir")
      + "/netty-04-http/src/main/java/com/firewolf/java/io/http/websocket/chat.html";

  //创建websocket连接的工厂
  private WebSocketServerHandshakerFactory factory = null;

  //保存所有的连接
  private Map<Channel, WebSocketServerHandshaker> webConnections = new HashMap<>();

  private int port;

  public NettyWebChatServer(int port) {
    this.port = port;
  }

  public void startUp() {
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap b = new ServerBootstrap();
      b.option(ChannelOption.SO_BACKLOG, 1024);
      b.group(bossGroup, workerGroup)
          .channel(NioServerSocketChannel.class)
          .childHandler(
              new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                  socketChannel.pipeline()
                      .addLast(new HttpServerCodec()) //请求解码器和响应编码器,等价于下面两行
                      .addLast(new HttpObjectAggregator(65535))
                      .addLast(new ChunkedWriteHandler())
                      .addLast(new NettyWebChatServerHandler());
                }
              }

          );
      Channel ch = b.bind(port).sync().channel();
      System.out.println(String.format("聊天服务已经启动,端口号为:%s", port));
      ch.closeFuture().sync();
    } catch (Exception e) {
      e.printStackTrace();

    } finally {
      //关闭服务
      bossGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }


  class NettyWebChatServerHandler extends SimpleChannelInboundHandler<Object> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) {
      //传统的Http介入,
      if (msg instanceof FullHttpRequest) {
        handleHttpRequest(ctx, (FullHttpRequest) msg);
      } else if (msg instanceof WebSocketFrame) {
        //WebSocket信息接入
        handleWebMessage(ctx, (WebSocketFrame) msg);
      }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
      cause.printStackTrace();
      ctx.close();
    }

    /**
     * 处理http请求
     */
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
      if ("/".equals(request.uri())) {//如果请求的是主页:localhost:8888/,那么就返回聊天页面,
        responseChatPage(ctx, request);
      } else {
        //其他情况认为是前端发起了websocket连接
        handWebConnect(ctx, request);
      }
    }

    private void responseChatPage(ChannelHandlerContext ctx, HttpRequest req) {
      try {
        boolean keepAlive = HttpUtil.isKeepAlive(req);
        BufferedReader br = new BufferedReader(new FileReader(new File(chatHtml)));
        StringBuffer result = new StringBuffer();
        String line = null;
        while ((line = br.readLine()) != null) {
          result.append(line);
        }
        //构造响应数据
        FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK,
            Unpooled.wrappedBuffer(result.toString().getBytes()));
        response.headers()
            .set(CONTENT_TYPE, "text/html;charset=UTF-8"); //设置响应类型
        //给客户端响应信息
        NettyHttpUtils.sendResponse(ctx, req, keepAlive, response);
      } catch (Exception e) {
        e.printStackTrace();
      }

    }

    /**
     * 处理web连接
     */
    private void handWebConnect(ChannelHandlerContext ctx, FullHttpRequest request) {
      if (!"websocket".equals(request.headers().get("Upgrade"))) { //不是websock握手请求,不建立连接
        return;
      }
      if (factory == null) {
        factory = new WebSocketServerHandshakerFactory("ws://localhost:8888/websocket",
            null, false);
      }
      //建立websocket连接
      WebSocketServerHandshaker handlerShaker = factory.newHandshaker(request);
      if (handlerShaker == null) {
        WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
      } else {
        handlerShaker.handshake(ctx.channel(), request);
        webConnections.put(ctx.channel(), handlerShaker);
      }
    }

    /**
     * 处理webSocket信息
     */
    private void handleWebMessage(ChannelHandlerContext ctx, WebSocketFrame frame) {
      if (frame instanceof CloseWebSocketFrame) { //关闭连接
        webConnections.get(ctx.channel()).close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
        webConnections.remove(ctx.channel());
      } else if (frame instanceof PingWebSocketFrame) { //Ping信息,返回Pong
        ctx.channel().writeAndFlush(new PongWebSocketFrame());
      } else if (frame instanceof TextWebSocketFrame) { //文本信息,进行转发
        TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
        webConnections.keySet().forEach(x -> {
          String prefix = "有人说: ";
          if (x.id().asShortText().equals(ctx.channel().id().asShortText())) {
            prefix = "我说: ";
          }
          x.writeAndFlush(new TextWebSocketFrame(prefix + textFrame.text()));
        });
      } else { //其他的类型,不进行支持
        System.out.println("信息暂时不支持");
        ctx.channel().writeAndFlush(new TextWebSocketFrame("您的消息内容暂时不支持!!!"));
      }

    }

  }
  public static void main(String[] args) throws Exception {
    new NettyWebChatServer(8888).startUp();
  }
}

(二)页面

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset = utf-8"/>
  <title>WebSocket客户端</title>
  <script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
      window.WebSocket = window.MozWebSocket;
    }

    if (window.WebSocket) {
      socket = new WebSocket("ws://127.0.0.1:8888/websocket");
      socket.onmessage = function (event) {
        var ta = document.getElementById('responseContent');
        ta.value += event.data + "\r\n";
      };

      socket.onopen = function (event) {
        var ta = document.getElementById('responseContent');
        ta.value = "你当前的浏览器支持WebSocket,请进行后续操作\r\n";
      };

      socket.onclose = function (event) {
        var ta = document.getElementById('responseContent');
        ta.value = "";
        ta.value = "WebSocket连接已经关闭\r\n";
      };
    } else {
      alert("您的浏览器不支持WebSocket");
    }

    function send(message) {
      if (!window.WebSocket) {
        return;
      }
      if (socket.readyState == WebSocket.OPEN) {
        socket.send(message);
        document.getElementById("inputMessage").value = "";
      } else {
        alert("WebSocket连接没有建立成功!!");
      }
    }
  </script>
</head>
<body style="padding-left: 300px;padding-top: 80px;">
<form onSubmit="return false;">
  <h2 style="color: red;">Web群聊聊天大厅!!!</h2><br>
  <textarea id="responseContent" style="width:500px; height:300px" readonly></textarea>
  <br/><br/>
  <input type="text" id="inputMessage" name="message" value="" style="width: 390px; height: 30px;"/>
  <button style="width: 100px; height: 30px;margin-left: 10px;" onClick="send(this.form.message.value)"> 发送消息</button>
</form>
</body>
</html>

(三)测试

启动项目后浏览器输入:http://localhost:8888,会展示出聊天页面:
在这里插入图片描述
我们打开两个聊天窗口,就可以进行聊天了:
在这里插入图片描述
这里面为了简单,页面直接写到了前台,实际上我们可以配合springboot,或者是nginx来专门单独的管理chat.html。

猜你喜欢

转载自blog.csdn.net/mail_liuxing/article/details/90713164