Netty学习(5):WebSocket协议

概述

Netty的应用场景中,socket通讯占了大多数,而WebSocket作为于H5推出内容中的重要功能点,Netty也做了很好的支持。在学习WebSocket的时候,首先要明确几个问题,WebSocket是什么,为什么要用推出WebSocket,其应用场景是什么,主要解决什么问题。

什么是WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

摘抄于百度百科

简单的来讲,WebSocket是可以让客户端和服务端进行双向通讯的应用层协议。用于在Web浏览器和服务器之间进行任意的双向数据传输的一种技术。WebSocket协议基于TCP协议实现,包含初始的握手过程,以及后续的多次数据帧双向传输过程。WebSocket目前支持两种统一资源标志符wswss,类似于HTTP和HTTPS。

  1. 发送一个GET请求,关键:Upgrade: websocket & Connection: Upgrade,这两个就告诉服务器,我要发起websocket协议,我不是HTTP
  2. 服务器收到了协议,返回一个 Switching Protocol, 这样就连接成功了,http升级为WebSocket
  3. 接下来的通信都是websocket, 这样就很好的连接了

解决什么问题&其应用场景

在早期的程序中,基于Http1.0的请求只能是客户端来发起的,服务端处于”被动“的状态,且协议无状态,请求与请求之间并没有什么联系,需要靠cookie或者session来维护用户信息。当需要有服务端向客户端发起请求(推送数据)的时候,就很麻烦。常用的做法是每一个客户端都对服务端进行轮询请求,而轮询的弊端显而易见,会有很多轮询的请求是无意义的,并且由于Http协议规则,导致每一次请求需要有请求头和请求体,请求的数据文本很大,设置很多情况下请求到的数据还没有请求头的长,这样就造成了服务端带宽的压力,即使之后推出的Http1.1有了keeplive的能力,由于其状态保持的时间很短,也只能视为是一种没有办法的办法。常见的”双向通讯“场景有消息推送、即时通讯类应用等。

WebSocket优势

  • 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。

  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。

  • 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

示例

服务端:

package com.leolee.netty.fifthExample;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

import java.net.InetSocketAddress;

/**
 * @ClassName MyServer
 * @Description: webSocket
 * @Author LeoLee
 * @Date 2020/8/30
 * @Version V1.0
 **/
public class MyServer {

    public static void main(String[] args) throws InterruptedException {
        //定义线程组 EventLoopGroup为死循环
        //boss线程组一直在接收客户端发起的请求,但是不对请求做处理,boss会将接收到的请i交给worker线程组来处理
        //实际可以用一个线程组来做客户端的请求接收和处理两件事,但是不推荐
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //启动类定义
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    //子处理器,自定义处理器,服务端可以使用childHandler或者handler,handlerr对应接收线程组(bossGroup),childHandler对应处理线程组(workerGroup)
                    .handler(new LoggingHandler(LogLevel.INFO))//日志处理器
                    .childHandler(new WebSocketChannelInitializer());

            //绑定监听端口
            ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
            //定义关闭监听
            channelFuture.channel().closeFuture().sync();
        } finally {
            //Netty提供的优雅关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}
package com.leolee.netty.fifthExample;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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.stream.ChunkedWriteHandler;

/**
 * @ClassName WebSocketChannelInitializer
 * @Description: TODO
 * @Author LeoLee
 * @Date 2020/8/30
 * @Version V1.0
 **/
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {


    @Override
    protected void initChannel(SocketChannel ch) throws Exception {

        ChannelPipeline pipeline = ch.pipeline();

        //Http处理
        pipeline.addLast("httpServerCodec", new HttpServerCodec());
        pipeline.addLast("chunkedWriteHandler", new ChunkedWriteHandler());//之后再学习
        //Netty对于http请求是分块或者分段的方式,比如一个请求发送的数据长度是1000,被切成了10段,该处理器就按照8192最大长度,去聚合这些请求数据
        pipeline.addLast("httpObjectAggregator", new HttpObjectAggregator(8192));

        //websocket处理
        //负责websocket的连接,以及控制frames(close Ping Pong)的处理,文字和二进制数据传递给下一个处理器处理,websocket的数据基于各种frames
        pipeline.addLast("webSocketServerProtocolHandler", new WebSocketServerProtocolHandler("/ws"));
        pipeline.addLast(new TextWebSocketFrameHandler());
    }
}
package com.leolee.netty.fifthExample;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.time.LocalDateTime;

/**
 * @ClassName TextWebSocketFrameHandler
 * @Description: 处理websocket文本数据
 * @Author LeoLee
 * @Date 2020/8/30
 * @Version V1.0
 **/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        System.out.println("收到客户端消息:" + msg.text());
        //写数据给客户端
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间:" + LocalDateTime.now()));
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

        System.out.println("handlerAdded:" + ctx.channel().id().asLongText());//channel的全局唯一id
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        System.out.println("handlerRemoved:" + ctx.channel().id().asLongText());//channel的全局唯一id
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        System.out.println("发生异常");
        ctx.close();
    }
}

服务端的代码和之前学习中的基本一致,需要注意的是TextWebSocketFrameInitializer中继承SimpleChannelInboundHandler时候,泛型传递的是TextWebSocketFrame,TextWebSocketFrame是继承于WebSocketFrame,这里为什么不像之前socket通讯的时候写String类型呢,是因为WebSocket协议的相关规定,WebSocket可以传递的数据类型有6种,可以在WebSocketFrame子类中查看。

客户端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket-client</title>
</head>
<body>

    <form onsubmit="return false;">
        <textarea name="message" style="width: 300px;height: 150px"></textarea>
        <input type="button" value="发送" onclick="sendMessage(this.form.message.value)"/>
        <h3>服务端输出:</h3>
        <textarea id="responseText" style="width: 300px;height: 150px"></textarea>
        <input type="button" onclick="javascript: document.getElementById('responseText').value=''" value="clear"/>
    </form>

</body>
<script type="text/javascript">

    var socket;

    if (window.WebSocket) {
        //建立于服务端的连接
        socket = new WebSocket("ws://127.0.0.1:8899/ws")
        var serverTextArea = document.getElementById("responseText");
        //收到服务端消息的时候的回调
        socket.onmessage = function (event) {
            serverTextArea.value = serverTextArea.value + "\n" + event.data;
        }
        //连接建立成功回调
        socket.onopen = function (event) {
            serverTextArea.value = "连接建立成功";
        }
        //连接断开
        socket.onclose = function (event) {
            serverTextArea.value = serverTextArea.value + "\n" + "连接断开";
        }
    } else {
        alert("浏览器不支持WebSocket")
    }

    function sendMessage(message) {
        if (!window.WebSocket) {
            return;
        } else {
            if (socket.readyState == WebSocket.OPEN) {
                socket.send(message);
            } else {
                alert("连接尚未建立");
            }
        }
    }


</script>
</html>

启动服务端,如下输出:

八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x1e87326c] REGISTERED
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x1e87326c] BIND: 0.0.0.0/0.0.0.0:8899
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] ACTIVE

启动客户端,在idea中对html文件右键,run!,idea会帮我们启动一个服务在浏览器中可访问:

客户端在浏览器初始化完成后会自动建立与服务端的 连接,服务端的handlerAdded触发:

八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x1e87326c] REGISTERED
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x1e87326c] BIND: 0.0.0.0/0.0.0.0:8899
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] ACTIVE
八月 30, 2020 3:00:06 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0xf219f5ab, L:/127.0.0.1:8899 - R:/127.0.0.1:52817]
八月 30, 2020 3:00:06 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
handlerAdded:9cb6d0fffedebcb5-00006ec0-00000001-0822684926b0801c-f219f5ab

尝试发送消息给服务端:

收到客户端消息:hello server

服务端收到客户端的消息并写信息到客户端。

关闭客户端页面服务端触发handlerRemove:

handlerRemoved:9cb6d0fffedebcb5-00006ec0-00000001-0822684926b0801c-f219f5ab

关闭服务端,客户端触发socket.onclose回调:

由于不是网络原因断开的连接,所以会触发客户端的回调,同理服务端也是可以收到客户端的断开回调。如果是因为网络原因造成的断开,就需要心跳机制了。

分析

Status Code:101 Switching Protocols 表示协议的切换,证明WebSocket建立服务端连接时候,需要http握手,建立成功后,切换协议到WebSocket。

Upgrade:websocket,表示协议升级

chrome network控制台中会实时监控服务的收发情况

需要代码的来这里拿嗷:demo项目地址

猜你喜欢

转载自blog.csdn.net/qq_25805331/article/details/108305562