IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Netty权威指南——WebSocket协议开发 -> 正文阅读

[网络协议]Netty权威指南——WebSocket协议开发

一、简介

由于HTTP协议的开销,导致他们不适于用于低延迟应用,为了解决这些问题,WebSocket将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接字建立持久的连接,双方随时可以互发数据给对方,而不是之前由客户端控制的一请求一应答模式。

1.1 HTTP协议的弊端

  1. HTTP协议为半双工协议,半双工协议指数据可以在客户端和服务端两个方向上传输,但是不能同时传输,它意味着在同一时刻只有一个方向上的数据传送。
  2. HTTP消息冗长而繁琐,HTTP消息包含消息头、消息体、换行符等,通常情况采用文本方式传输,相比于其他的二进制通信协议,冗长而繁琐。
  3. 针对服务器推送的黑客攻击,例如长时间轮询。

现在很多的网站为了实现消息推送,所用的技术都是轮询,轮询是指在特定的时间间隔(如 每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端浏览器。这种传统的模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,然而HTTP request 的Header是非常冗长的,里面包含的可用数据比例可能非常低。这回占用很多的带宽和服务器资源。

为了解决HTTP协议效率低下的问题,HTML5定义了WebSocket协议,可以更好的节省服务器资源和带宽并达到实时通信。

二、WebSocket入门

2.1 WebSocket的特点

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,浏览器和服务器之间就形成了一条快速通道,两者就可以直接不想传送数据了。WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送,也可以接收消息。相比于HTTP的半双工协议,性能得到很大的提升。

  • 单一的TCP连接,采用双全工模式通信;
  • 对代理、防火墙、和路由器透明;
  • 无头部信息、Cookie、和身份验证;
  • 无安全开销;
  • 通过ping/pong帧保持链路激活;
  • 服务器可以主动传递消息给客户端,不在需要客户端轮询;

浏览器通过JavaScript向服务器发出建立WebSocket连接的请求,连接建立后,客户端和服务端可以通过TCP连接直接交换数据,因为WebSocket连接本质上就是一个TCP连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及Comet技术相比,具有很大的优势,所以可以说WebSocket是未来实时Web应用的首选方案。

2.2 WebSocket 连接建立

握手请求
握手响应
客户端
服务端

为建立一个WebSocket链接,客户端浏览器首先要向服务器发送发起一个HTTP请求;这个请求和通常的请求不同,包含了一些附加头信息,其中附加头信息”Upgrade:WebSocket“ 表名这是一个申请协议升级的HTTP请求。服务端解析这些附加的头信息,然后生成应答消息返回给客户端,客户端和服务端的WebSocket连接就建立起来了。双方可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务端的某一方主动关闭连接。

请求消息中的”Sec-WebSocket-Key“ 是随机的,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要,把”Sec-WebSocket-Key“ 加上一个字符串,使用SHA-1 加密,然后进行BASE-64编码,将结果作为”Sec-WebSocket-Accept“头的值,返回给客户端。

2.3 WebSocket 生命周期

握手成功后,服务器端和客户端就可以通过”message“的方式进行通信了,一个消息由一个或者多个组成,WebSocket 的消息并不一定对应一个特定的网络层的帧,它可以被分割成多个帧或者被合并。

都有自己对应的类型,属于同一个消息的多个帧具有相同类型的数据,从广义上讲,数据类型可以是文本数据、二进制数据和控制帧(协议级信令,如信号)

2.4 WebSocket 连接关闭

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

三、Netty WebSocket 协议开发

3.1 WebSocketServer

package com.lsh.netty.websocket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * @author :LiuShihao
 * @date :Created in 2021/6/28 12:23 下午
 * @desc :
 * WebSocketServer服务端功能介绍:
 * 支持WebSocket的浏览器通过WebSocket协议发送请求消息给服务端,服务端对请求消息进行判断,如果是合法的WebSocket请求
 * 则获取请求消息文本,并在后面追加字符串"欢迎使用Netty WebSocket 服务,现在时刻:系统时间"
 */
public class WebSocketServer {

    public void run(int port) throws Exception{
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap b = new ServerBootstrap();
            b.group(boosGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //添加HttpServerCodec:将请求和应答消息解码或者解码为HTTP消息
                            pipeline.addLast("http-codec",new HttpServerCodec());
                            //增加HttpObjectAggregator:目的是将HTTP消息的多个部分组合成一条完整的HTTP消息
                            pipeline.addLast("aggregator",new HttpObjectAggregator(65536));
                            // ChunkedWriteHandler:来向客户端发送HTML5文件,它主要用于支持浏览器和服务端进行WebSocket通信
                            ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
                            //增加WebSocket服务端Handler
                            pipeline.addLast("handler",new WebSocketServerHandler());

                        }
                    });
            Channel ch = b.bind(port).sync().channel();
            System.out.println("Web socket server started at port "+port);
            System.out.println("open your browser and navigate to http://localhost:"+port+"/");
            ch.closeFuture().sync();
        }finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        new WebSocketServer().run(port);
    }
}

3.2 WebSocketServerHandler

package com.lsh.netty.websocket;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * @author :LiuShihao
 * @date :Created in 2021/6/28 12:28 下午
 * @desc :
 */
@Slf4j
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private WebSocketServerHandshaker handshaker;
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
        //传统http接入:第一次握手消息有HTTP协议承载,所以它是一个HTTP消息,,执行handleHTTPRequest方法来处理WebSocket握手请求
        if (msg instanceof FullHttpRequest){
            handleHttpRequest(ctx,(FullHttpRequest) msg);
        }else if (msg instanceof WebSocketFrame){
        //WebSocket接入:链路建立成功后的操作
            handleWebSocketFrame(ctx,(WebSocketFrame) msg);
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    /**
     *
     * 处理WebSocket握手请求
     * @param ctx
     * @param req
     * @throws Exception
     */
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
        // 如果解码失败返回异常
        // 对握手消息进行判断,消息头中没有包含Upgrade字段或者它的值不是websocket 则返回HTTP400响应
        if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))){
            sendHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }
        //握手请求简单校验通过后,开始构造握手工厂,
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false);
        //创建握手处理类WebSocketServerHandshaker,通过它构造握手响应消息返回给客户端
        //同时将WebSocket相关的编码和解码类动态的添加到 ChannelPipeline中,用于WebSocket消息的编解码
        //添加了WebSocket Encoder 和 WebSocket Decoder 之后,服务端就可以自动对WebSocket消息进行编解码了,后面的业务handler可以直接对WebSocket对象进行操作
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null){
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        }else {
            handshaker.handshake(ctx.channel(),req);
        }
    }

    /**
     * 客户端通过文本框提交请求消息给服务端,WebSocketServerHandler接收到的已经是解码后的WebSocketFrame消息了(WebSocket握手应答时动态增加了编解码handler)。
     * 首先需要对控制帧进行判断:如果是关闭链路的控制消息,就调用WebSocketServerHandlershaker的close方法关闭WebSocket连接
     * 如果是维持链路的Ping消息,则构造Pong消息返回
     * 本例程的WebSocket通信双方使用的都是文本消息,所以对请求消息的类型进行判断,不是文本的抛出异常。
     * 最后,从TextWebSocketFrame中获取请求消息字符串,对它处理后构造新的TextWebSocketFrame消息返回给客户端,
     * 由于握手应答时动态增加了TextWebSocketFrame的编码类,所以,可以直接发送TextWebSocketFrame对象。
     * @param ctx
     * @param fream
     * @throws Exception
     */
    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame fream) throws Exception {
        //判断是否关闭链路的指令
        if (fream instanceof CloseWebSocketFrame){
            handshaker.close(ctx.channel(),((CloseWebSocketFrame) fream).retain());
            return;
        }
        //判断是否是Ping消息
        if (fream instanceof PingWebSocketFrame){
            ctx.channel().write(new PongWebSocketFrame(fream.content().retain()));
            return;
        }
        //本实例只支持文本消息,不支持二进制消息
        if (! (fream instanceof TextWebSocketFrame)){
            throw new UnsupportedOperationException(String.format("%s frame types not supported",fream.getClass().getName()));
        }

        //返回应答消息
        String request = ((TextWebSocketFrame) fream).text();
        System.out.println("request:"+request);

        ctx.channel().write(new TextWebSocketFrame(request+", 欢迎使用Netty WebSocket 服务,现在时刻:"+new Date().toString()));

    }

    /**
     * 返回响应
     * @param ctx
     * @param req
     * @param response
     */
    private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse response) {
        //返回应答给客户端

        if(response.status().code() != 200){
            ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);
            response.content().writeBytes(buf);
            buf.release();
            //注意:此处设置请求头 response.content().readableBytes() 返回int类型
            response.headers().set("Content-Length",String.valueOf(response.content().readableBytes()));
        }
        //如果是非Keep-Alive ,关闭连接
        ChannelFuture f = ctx.channel().writeAndFlush(response);
        if (!req.headers().get("Connection").equals("keep-alive") || response.status().code() != 200){
            f.addListener(ChannelFutureListener.CLOSE);
        }

    }

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

3.3 WebSocketServer.html

<html>
<head>
    <meta charset="UTF-8">
    netty websocket 时间服务器
</head>
<br>
<body>
<br>
<script type="text/javascript">
    var socket;
    if(!window.WebSocket){
        window.WebSocket = window.MozWebSocket;
    }
    if(window.WebSocket){
        var  socket = new WebSocket("ws://localhost: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 = '';
            ta.value = "打开websocket服务正常,浏览器支持websocket!";
        };
        socket.onclose = function(event){
            var ta = document.getElementById('responseText');
            ta.value = '';
            ta.value = "websocket关闭!";
        };
    }else{
        alert("抱歉,您的浏览器不支持WebSocket 协议!");
    }
    function send(message){
        if(!window.WebSocket){
            return;
        }
        if(socket.readyState == window.WebSocket.OPEN){
            socket.send(message);
        }else{
            alert("WebSocket 还没有建立连接!")
        }
    }
</script>
<form onsubmit="return false;">
    <input type="text" name="message" vaule="Netty权威指南 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>

3.4 运行测试

在浏览器中打开html页面:

后台服务器还没有启动:
在这里插入图片描述
现在启动WebSocketServer
在这里插入图片描述

在页面发送请求:
在这里插入图片描述

控制台日志:
在这里插入图片描述

页面返回:
在这里插入图片描述
流程结束!

四、总结

源码

代码已上传仓库:https://gitee.com/L1692312138/netty-repository

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-09-11 19:10:26  更:2021-09-11 19:11:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年6日历 -2024/6/27 1:38:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码