Netty章节六:Netty对websocket的支持

出现背景

WebSocket是一种规范,是Html5规范的一部分,websocket解决什么问题呢?解决http协议的一些不足。我们知道,http协议是一种无状态的,基于请求响应模式的协议

  • 无状态:客户端多次请求之间是没有关系的,这样的协议存在很多问题,例:无法追踪请求来自那个客户端以及这个客户端之前在服务器端存在一些信息。在web编程中的cookie与session就是为了解决这样的问题诞生的。
  • 基于请求与响应模式:这个请求的发起方一定是客户端比如浏览器,浏览器首先向服务器端发出一个请求,在发出一个请求之前客户端与服务端先建立好一个连接请求与响应都是在这个连接之上进行的,当这个连接建立好之后客户端就会向服务端发送他的请求数据,服务器端收到这个请求数据之后,就会进行相应的处理,处理完毕之后服务器端就会构建出对应的response对象,然后将response返回给客户端。
  • 如果是基于http1.0的话当服务器将数据返回给客户端之后这个连接立刻就断掉/失去了,当客户端再去向服务端发出请求的时候还需要重新建立新的连接。
  • 如果是基于http1.1,http1.1新增加了一个特性keepalive,keepalive表示客户端跟服务器端可以在短时间之内保持着一个连接(持续连接/persistent connection),持续连接:客户端与服务端先建立好一个连接,客户端向服务端发出一个请求服务端向客户端返回一个响应,在一个特定的时间(自己指定)
    • 如果客户端在这个特定的时间之内还会向服务端发出请求时,客户端是不会再与服务端重新建立一个连接,而是复用既有的连接,服务端也是在既有的连接之上将数据返回给客户端。
    • 如果过了这个特定的时间之后客户端不再向服务器端发送任何数据的话,那么这个持续保持了一段时间的连接就会自动的关闭掉,当下一次客户端再向服务端发起请求的时候会重新建立一个新的连接。

http协议存在一些问题,这些问题使得某些业务场景是无法实现
例如:网页聊天程序,客户端将要发送的数据发送给服务端,服务器将接收到的某个客户端数据广播给其他客户端。主要问题:服务端需要主动向客户端推送数据,而这个需求在http1.0或1.1来说是不可能实现的因为协议从根本上就禁止这么做。

早年解决办法采用轮循技术,轮循:客户端会在某个时间间隔之后会向服务端发起一个请求,请求的作用就是检查服务端还有没有接收到的一些数据,如果有服务端就以响应的方式发送给客户端,循环此操作。

这种方式存在很多问题:
1.客户端无法第一时间接收到消息/数据,而是在下一次轮循的时候接收到
2.客户端每经过一段时间就向服务端发起一个查询,但是在很多很多情况下这些请求都是没有结果的(如果服务器端没有数据要推给客户端,而客户端是不知道这件事情的)。会形成资源和网络带宽的浪费,并且由于http协议本身请求必须包含两部分内容header(头信息)/body(数据信息),而我们每次轮循就想的到数据本身,而由于http协议本身规定,我们每次请求都要把header带过去,每次服务器端返回结果时,不管有没有结果服务端都需要把header带上,而客户端并不关注这些信息,并且在很多情况下这个header信息的体积都远远超过了数据本身。(总结:大多数轮询请求的空轮询,造成大量的资源带宽的浪费,每次http请求携带了大量无用的头信息,而服务器端其实大多数都不关注这些头信息,而实际大多数情况下这些头信息都远远大于body信息,造成了资源的消耗。)

拓展
比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

WebSocket是什么?

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

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

websocket的出现就是解决了客户端与服务端的这种长连接问题,这种长连接是真正意义上的长连接。客户端与服务器一旦连接建立双方就是对等的实体,不再区分严格意义的客户端和服务端。

长连接只有在初次建立的时候,客户端才会向服务端发送一些请求,这些请求包括请求头和请求体,一旦建立好连接之后,客户端和服务器只会发送数据本身而不需要再去发送请求头信息,这样大量减少了网络带宽。

websocket协议本身是构建在http协议之上的升级协议,客户端首先向服务器端去建立连接,这个连接本身就是http协议只是在头信息中包含了一些websocket协议的相关信息,一旦http连接建立之后,服务器端读到这些websocket协议的相关信息就将此协议升级成websocket协议。

websocket协议也可以应用在非浏览器应用,只需要引入相关的websocket库就可以了。

HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket使用ws或wss的统一资源标志符,类似于HTTPS,其中wss表示在TLS之上的Websocket。如:

ws://example.com/wsapi
wss://secure.example.com/

1.WebSocket本身是一个真正意义上的长连接,可以实现客户端跟服务器端的双向数据传递,全双工的数据传递
2.WebSocket协议本身是基于http协议的
3.虽然WebSocket本身是html5的一部分,但是他也可以用在非浏览器的场合

优点

  • 较少的控制开销:相对与http请求的头部信息,websocket信息明显减少。
  • 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  • 保持连接状态。于HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

案例演示

案例功能:浏览器页面向服务器发送消息,服务器将当前消息发送时间反馈给浏览器页面。

服务端代码

服务端主启动类

//WebSocket示例服务端
public class MyServer {
    public static void main(String[] args) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new WebSocketChannelInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

初始化器 (Initializer)

public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        /*
            HttpServerCodec http编解码器
            ChunkedWriteHandler 以块的方式写
            HttpObjectAggregator 通道处理器,会对一个http消息进行聚合
                会将HttpMessage和HttpContent等等... 聚合到一个FullHttpRequest或者FullHttpResponse,这个取决于它是处理请求的还是响应的
                后面就不会有HttpContent了
            HttpObjectAggregator(maxContentLength) 构造器
                maxContentLength以字节的方式来聚合内容的最大长度,如果聚合的长度超过了maxContentLength最大长度
                就会调用handleOversizedMessage方法
            WebSocketServerProtocolHandler WebSocket服务器协议,他负责WebSocket的握手
                以及控制帧的处理(关闭,心跳),文本和二进制数据帧将传递到管道中的下一个处理程序(由您实现)以进行处理
                WebSocket的数据传递都是以frames(帧)的形式传递,具体类型以WebSocketFrame子类
                
         */
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new HttpObjectAggregator(8192));
        //ws://server:port/context_path
        //ws://localhost:9999/ws
        //参数指的是contex_path
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        /websocket定义了传递数据的6中frame类型/websocket定义了传递数据的6中frame类型
        pipeline.addLast(new TextWebSocketFrameHandler());
    }
}

WebSocketServerProtocolHandler:参数是访问路径,这边指定的是ws,服务客户端访问服务器的时候指定的url是:ws://localhost:8899/ws
它负责websocket握手以及处理控制框架(Close,Ping(心跳检检测request),Pong(心跳检测响应))。 文本和二进制数据帧被传递到管道中的下一个处理程序进行处理。


WebSocket规范中定义了6种类型的桢,netty为其提供了具体的对应的POJO实现。
WebSocketFrame:所有桢的父类,所谓桢就是WebSocket服务在建立的时候,在通道中处理的数据类型。本列子中客户端和服务器之间处理的是文本信息。所以范型参数是TextWebSocketFrame。

请输入图片描述

自定义处理器 (Handler)

//用于处理文本的数据传送	泛型是TextWebSocketFrame专门用于处理文本帧的类型的类
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    /**
     * @param ctx 上下文对象
     * @param msg 客户端发送来的消息的类型
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("收到消息:" + msg.text());
        //这里不能直接传输一个字符串,handler没法处理 因为泛型是TextWebSocketFrame
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间:" + LocalDateTime.now()));
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        /*每一个channel都会有一个全局唯一的id  通过channel.id()获取
            会返回一个channelId,里面有两个属性
            asLongText表示整个id,全局唯一的
            asShortText表示id的简写,全局不唯一的
         */
        System.out.println("handlerAdded:" + ctx.channel().id().asLongText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved:" + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发生");
        ctx.close();
    }
}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket客户端</title>
</head>
<script type="text/javascript">
    var socket;
    //是否支持WebSocket
    if (window.WebSocket){
        //参数就是与服务器连接的地址
        socket = new WebSocket("ws://localhost:8899/ws");
        //当客户端接收到服务端发送来的消息时 onmessage就会被调用
        //event 服务器返回的数据
        socket.onmessage = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "
" + event.data;
        }
        //连接建立成功时,onopen被调用
        socket.onopen = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = "连接开启!";
        }
        //连接断开时,onclose被调用
        socket.onclose = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "
" + "连接断开!";
        }

    }else {
        alert("浏览器不支持WebSocket!");
    }

    function send(message) {
        if(!window.WebSocket){
            //不支持WebSocket直接返回
            return;
        }
        //socket的状态等于连接的话
        //readyState返回socket的状态
        if(socket.readyState == WebSocket.OPEN){
            //send(数据) WebSocket向后台发送数据的方法
            //类似java的writeAndFlush
            socket.send(message);
        }else {
            alert("连接尚未开启!");
        }

    }

</script>
<body>
    <form onsubmit="return false;">
        <textarea name="message" style=" 400px;height: 200px"></textarea>
        <!--this.form.message.value  取出当前页面中 from标签下name为message的value值-->
        <input type="button" value="发送数据" onclick="send(this.form.message.value)">

        <h3>客户端输出:</h3>
        <textarea id="responseText" style=" 400px;height: 300px"></textarea>
        <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空内容">

    </form>

</body>
</html>

测试

启动服务器,然后运行客户端页面,当客户端和服务器端连接建立的时候,服务器端执行handlerAdded回调方法,客户端执行onopen回调方法

服务器端控制台输出:

handlerAdded:005056fffec00008-0000a2e8-00000001-563d2303e0af4546-52bcc0f1

页面:
请输入图片描述

客户端发送消息,服务器端进行响应

请输入图片描述

服务端控制台打印:

收到消息:来自客户端的问候

客户端也收到服务器端的响应:
请输入图片描述

打开开发者工具
请输入图片描述

在从标准的HTTP或者HTTPS协议切换到WebSocket时,将会使用一种升级握手的机制。因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确定时刻特定与应用程序;它可能会发生在启动时候,也可能会发生在请求了某个特定的IURL之后。

开发者工具中可以查看所有通信纪录

请输入图片描述

原文地址:https://www.cnblogs.com/mikisakura/p/12983538.html