【十】Netty WebSocket协议栈开发

Netty WebSocket协议栈开发

  • 背景介绍
  • HTTP 协议的弊端
  • WebSocket 介绍
    • WebSocket 特点
    • WebSocket 连接建立
    • Socket生命周期
    • WebSocket关闭
  • 开发
    • WebSocket 服务端功能介绍
      • 流程图
    • 代码实现
      • jar 依赖
      • WebSocket 服务端启动类 WebSocketServer
      • 服务端业务处理类 WebSocketServerHandler
      • WebSocketServer.html
        • 说明
  • 测试
    • 打开html页面
      • 用Goole浏览器打开 WebSocketServer.html .
      • 启动服务端。
        • 服务端打印:
        • 刷新下html. 打印信息如下;
        • 点击 发送 WebSocket 请求消息,
      • 关闭服务器
        • 浏览器截图
  • 总结

背景介绍

一直以来,网络在很大程度都是围绕着HTTP的请求/响应模式而构建的。客户端加载一个网页,然后直到用户点击,否则什么都不会发送。后来 AJAX 开始让网络变得更加动态,但 HTTP通信仍然是由客户端控制,需要用户进行互动或定期轮询。各种技术让服务器得知有新数据时,立即将数据发送到客户端。比如 "推送"或 Comet长轮询。利用长轮询,客户端可以打开指向服务器的HTTP连接,而服务器会一直保持连接打开,直到发送响应。
但是,由于HTTP协议的开销,导致它们不适合低延迟应用。比如 IM 系统
为了解决这些问题,WebSocket 将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接字建立持久的连接,双方随时都可以互发数据给对方,而不是之前由客户端控制的一请求一应答模式。

HTTP 协议的弊端

(1)HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务端两个方向传输,但是不能同时传输。意味着在同一个时刻,只有一个方向的数据传输。
(2)HTTP消息冗长而繁琐。HTTP 消息包含消息头,消息体,换行符。通常情况下采用文本方式传输,相比于其他的二进制通信协议,冗长繁琐。
(3)容易受到攻击。长时间轮询。
为了解决HTTP协议效率低下的问题。HTML5 定义了WebSocket协议,能更好的节省服务器资源和宽带并达到实时通信。

WebSocket 介绍

WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通信的网络技术。WebSocket通信协议于2011年被IETF定为标准 RFC6455,WebSocket API 被W3C定位标准。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后 浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。WebSocket基于TCP双向全双工进行消息传递,在同一时空,即可以发送消息,也可以接收消息。相比于HTTP的半双工协议,性能得到大大提升。

WebSocket 特点

(1)单一的TCP连接,采用全双工模式通信
(2)对代理,防火墙和路由器透明
(3)五头部信息,Cookie和身份验证
(4)五安全开销
(5)通过 “ping/pong” 帧保持链路激活
(6)服务器可以主动传递消息给客户端,不再需要客户端轮询
WebSocket 设计出来就是要取代轮询和Comet技术。使客户端浏览器具备像C/S架构下桌面系统一样的实时通信能力。

WebSocket 连接建立

客户端和服务端建立连接。
请求消息中的Sec-WebSocket-Key 是随机的,服务端会用这些数据构造出一个SHA-1 的信息摘要。服务端会用这些数据来构造一个SHA-1的信息摘要。使用SHA-1 加密,然后进行BASE-64 编码,将结果作为"Sec-WebSocket-Accept"头的值,返回给客户端。
如图:
【十】Netty WebSocket协议栈开发_第1张图片

Socket生命周期

握手成功之后,服务端和客户端就可以通过"message"的方式进行通信了,一个消息由一个或多个帧组成,WebSocket 的消息不一定对应一个特定网络层的帧,它可以被分割成多个帧或者被合并。
帧都有自己对应的类型,属于同一个消息的多个帧具有相同类型的数据。从广义上将,数据类型可以是文本数据,二进制数据和控制帧。
Socket 生命周期图:
【十】Netty WebSocket协议栈开发_第2张图片

WebSocket关闭

为了关闭WebSocket 连接,客户端和服务端需要通过一个安全的方法关闭底层TCP连接以及TL会话。如果合适,丢弃任何可能已经接收的字节;必要时(比如受到攻击),可以通过任何可用的手段关闭连接。底层的TCP连接,在正常情况下,应该首先由服务器关闭。在异常情况下(例如在一个合理的时间周期后没有收到服务器的TCP Close),客户端可用发起 TCP Close。因此,当服务器被指示关闭WebSocket 连接时,他应该立即发起一个TCP Close 操作。客户端应该等待服务器的TCP Close
WebSocket 的握手关闭消息带有一个状态码和一个可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。

开发

Netty 基于HTTP协议栈开发了WebSocket 协议栈,利用Netty的WebSocket 协议栈可以非常方便的开发出WebSocket客户端和服务端。

WebSocket 服务端功能介绍

支持WebSocket 的浏览器通过 WebSocket 协议发送请求消息给服务端,服务端对请求消息进行判断,如果时合法的WebSocket请求,则获取请求消息体,并在后面追加字符串 "欢迎使用Netty WebSocket 服务,现在是 "+系统时间。

客户端端 HTML通过内嵌的JS脚本创建WebSocket连接,如果握手成功,在文本框中打印 “打开WebSocket 服务正常,浏览器支持WebSocket!” 。
如果浏览器不支持WebSocket ,回提示 sorry,您的浏览器不支持WebSocket 协议。

流程图

【十】Netty WebSocket协议栈开发_第3张图片

代码实现

jar 依赖

       <dependency>
            <groupId>io.nettygroupId>
            <artifactId>netty-allartifactId> 
            <version>5.0.0.Alpha1version>
            <scope>compilescope>
        dependency>

WebSocket 服务端启动类 WebSocketServer

public class WebSocketServer {
    public void run(int port){
        EventLoopGroup bossGroup=new NioEventLoopGroup();
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap=new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //将请求和应答消息编码或者解码为HTTP消息
                            socketChannel.pipeline()
                                    .addLast("http-codec",new HttpServerCodec());
                            //将多个部分组合成一条完整的HTTP消息
                            socketChannel.pipeline()
                                    .addLast("aggregator",new HttpObjectAggregator(65536));
                            //向客户端发送HTML5文件,主要用于支持浏览器和服务器进行WebSocket
                            socketChannel.pipeline()
                                    .addLast("http-chunked",new ChunkedWriteHandler());
                            socketChannel.pipeline()
                                    .addLast(new WebSocketServerHandler());
                        }
                    });
            //服务端绑定端口
            ChannelFuture future=bootstrap.bind(port).sync();
            System.out.println("server is started.");
            System.out.println("open your browser and navigate to http://127.0.0.1:"+port+"/");
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //优雅关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new WebSocketServer().run(8080);
    }
}

服务端业务处理类 WebSocketServerHandler

注释里写了详细的说明。

public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName());

    private WebSocketServerHandshaker handshaker;

    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
        //如果是传统的HTTP接入
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(channelHandlerContext, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(channelHandlerContext, (WebSocketFrame) msg);
        }
    }

    public void channelReadComplete(ChannelHandlerContext context) {
        context.flush();
    }

    /**
     * 第一次握手请求消息 由HTTP协议承载,
     * @param context
     * @param request
     */
    private void handleHttpRequest(ChannelHandlerContext context, FullHttpRequest request) {
        //如果Http解码失败,返回HTTP异常
        if (!request.getDecoderResult().isSuccess()
                || (!"websocket".equals(request.headers().get("Upgrade")))) {
            sendHttpResponse(context, request,
                    new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }

        //校验通过,开始构造握手工厂,然后构造握手处理类WebSocketServerHandshaker。
        //构造握手响应返回,本机测试
        WebSocketServerHandshakerFactory factory =
                new WebSocketServerHandshakerFactory("ws://127.0.0.1:8080/websocket",
                        null, false);
        handshaker=factory.newHandshaker(request);
        if (handshaker==null){
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(context.channel());
        }else {
            //将WebSocket 相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码
            // 有了WebSocket Encoder 和 WebSocket Decoder之后。服务端就可以自动对WebSocket消息进行编解码了。
            // 源码可以查看 WebSocketServerHandshaker.handshake
            handshaker.handshake(context.channel(),request);
        }

    }

    private void handleWebSocketFrame(ChannelHandlerContext context, WebSocketFrame socketFrame) {
        //判断是否关闭链路的指令
        if (socketFrame instanceof CloseWebSocketFrame){
            handshaker.close(context.channel(),(CloseWebSocketFrame) socketFrame.retain());
            return;
        }
        //判断是否是ping消息
        if (socketFrame instanceof PingWebSocketFrame){
            context.channel().write(new PongWebSocketFrame(socketFrame.content().retain()));
            return;
        }

        //本demo 只支持文本消息,不支持二进制消息
        //其他消息直接抛出异常
        if (!(socketFrame instanceof TextWebSocketFrame)){
            throw new UnsupportedOperationException(
                    String.format("%s frame types not supported",socketFrame.getClass().getName()));
        }
        //返回应答消息
        //从 TextWebSocketFrame 中获取请求消息字符串
        String requestStr=((TextWebSocketFrame) socketFrame).text();
        if (logger.isInfoEnabled()){
            logger.info(String.format("%s received %s",context.channel(),requestStr));
        }
        //通过处理后,通过构造新的TextWebSocketFrame 消息返回给客户端。
        // 之前添加了TextWebSocketFrame的编码类。
        // 所以可以直接发送TextWebSocketFrame对象
        String timeStr=new SimpleDateFormat("YYYY-MM-DD dd:mm:ss").format(new Date());
        context.channel().write(new TextWebSocketFrame(
                requestStr+" ,欢迎使用Netty WebSocket 服务,现在是 : "+timeStr));





    }


    private static void sendHttpResponse(ChannelHandlerContext context, FullHttpRequest request,
                                         FullHttpResponse response) {
        //返回应答给客户端
        if (response.getStatus().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(response.toString().toString(), CharsetUtil.UTF_8);
            response.content().writeBytes(buf);
            buf.release();
            setContentLength(response, response.content().readableBytes());
        }
        //如果是非Keep-Alive ,关闭连接
        ChannelFuture future = context.channel().writeAndFlush(response);
        if (!isKeepAlive(request) || response.getStatus().code() != 200) {
            future.addListener(ChannelFutureListener.CLOSE);
        }

    }

    public void exceptionCaught(ChannelHandlerContext context, Throwable cause) {
        cause.printStackTrace();
        context.close();
    }
}

WebSocketServer.html

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Netty WebSockettitle>
head>
<body>
<br>
<script type="text/javascript">
    var socket;
    if(!window.WebSocket){
        window.WebSocket=window.MozWebSocket;

    }
    //判断浏览器是否支持WebSocket
    if(window.WebSocket){
         // 进行握手链接
        socket=new WebSocket("ws://127.0.0.1:8080/websocket");
        //如果服务端有数据返回,可以进行展示
        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 服务正常,浏览器支持WebSocket!";
        };
        //如果服务器关闭,显示 WebSocket 关闭
        socket.onclose=function(event){
            var ta=document.getElementById('responseText');
            ta.value="WebSocket 关闭!!";
        };
    }else{
        alert("sorry,您的浏览器 不支持 WebSocket 协议! ");
    }
    function send(message){
        if(!window.WebSocket){
           return ;
        }
        if(socket.readyState==WebSocket.OPEN){
            socket.send(message);
        }else{
            alert("WebSocket 连接没有建立成功!");
        }

    }


script>
<form onsubmit="return false;">
    <input type="text" name="message" value="Netty learning WebSocket"/>
    <br><br>
    <input type="button" value=" 发送 WebSocket 请求消息 " onclick="send(this.form.message.value)"/>
    <hr color="blue"/>
    <h3> 服务端返回应答消息h3>
    <textarea id="responseText" style="width:500px;height:300px;">textarea>
form>
body>
html>
说明

总体代码比较简单。特别是客户端的html代码,但是 很方便我们进行测试。各种打印信息,都能帮助我们更好的理解 WebSocket协议。

测试

打开html页面

用Goole浏览器打开 WebSocketServer.html .

此时浏览器的显示界面。能正确显示 服务器是关闭的。
【十】Netty WebSocket协议栈开发_第4张图片

启动服务端。

服务端打印:

【十】Netty WebSocket协议栈开发_第5张图片

刷新下html. 打印信息如下;

【十】Netty WebSocket协议栈开发_第6张图片

点击 发送 WebSocket 请求消息,

【十】Netty WebSocket协议栈开发_第7张图片
此时可以鼠标无限制点击,发现时间在变化,说明服务端在不停的返回数据。此时 http请求还是101。
和我们常见的HTTP请求显然不同
【十】Netty WebSocket协议栈开发_第8张图片

关闭服务器

此时服务端会给客户端发送 TCP Close。浏览器客户端接收到了消息后,会进行对应打印信息展示。

浏览器截图

【十】Netty WebSocket协议栈开发_第9张图片

总结

本章首先介绍了HTTP协议的弊端和产生WebSocket 的一些技术背景,随后介绍了下WebSocket的基本信息。然后写了一个小demo。但是 WebSocket 是很复杂的,这个demo 只能算一个入门的小例子。如果想深入学习 WebSocket。还需要不断的深入学习。

你可能感兴趣的:(netty,websocket,网络协议,网络,html5)