Netty-使用websocket进行通讯

WebSocket

WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

当WebSocket的客户端与服务端通信以后,就不需要之前握手请求HTTP协议的参与了

WebSocket的优点

  • 节省通信开销(HttpRequest中的head很长,占用带宽和资源)
  • 服务器可以主动传送数据给客户端
  • 实时通信
    因为HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

WebSocket的生命周期

WebSocket端点生命周期的第一个事件是打开通知,它用来指示到WebSocket会话另一端的连接已经建立。一旦打开通知被WebSocket对话的两端都接收到,参与的任意WebSocket后续就可以发送消息了。在WebSocket对话期间,可能会出现一些消息传递的错误。接受消息的WebSocket端点本身就可能产生错误,或者WebSocket实现本身在某些情况下也会产生错误。要注意对错误的处理。不管在WebSocket对话的哪一端准备结束对话,他都可以初始化关闭事件。下面从Java组件的视角来看看其生命周期如何呈现。

  • 打开事件:@OnOpen 此事件发生在端点上建立新连接时并且在任何其他事件发生之前

  • 消息事件:@OnMessage 此事件接收WebSocket对话中另一端发送的消息。

  • 错误事件:@OnError 此事件在WebSocket连接或者端点发生错误时产生

  • 关闭事件:@OnClose 此事件表示WebSocket端点的连接目前部分地关闭,它可以由参与连接的任意一个端点发出

Netty实现:

1、前端页面

<!DOCTYPE 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) {
        socket = new WebSocket("ws://localhost:8090/srws");
        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!";
        };
        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 == WebSocket.OPEN) {
            socket.send(message);
        }
        else {
            alert("WebSocket连接没有建立成功!");
        }
    }
</script>
<form onsubmit="return false;">
    <input type="text" name="message" value="Netty实践"/>
    <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>

2、服务端

server

package com.suirui.websocket_http.server;

import com.suirui.websocket_http.server.handler.HttpServerRequestHandler;
import com.suirui.websocket_http.server.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


/**
 * Created by zongx on 2019/12/26.
 */
public class Server {

    public static void main(String[] args) {
        int port ;
        if (args != null && args.length > 0) {
            port = Integer.valueOf(args[0]);
        } else {
            port = 8090;
        }
        System.out.println(port);
        new Server().bind(port);
    }

    public void bind(int port) {
        //配置服务端的NIO线程组
        //两个线程组的原因是:
        // 一个用于服务端接收客户端连接
        //一个用于进行socketChannel网络读写
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        // 业务用。http工作线程池,http消息处理都由这个线程池进行
        ExecutorService httpExecutor = Executors.newFixedThreadPool(8);

        //ServerBootstrap 是netty的辅助启动类
        ServerBootstrap b = new ServerBootstrap();
        // .group传入两个线程组
        // .channel设置创建的Channel类型为NioServerSocketChannel
        // .option 配置NioServerSocketChannel的TCP参数
        // .childHandler绑定IO事件的处理类 类似reactor模式中的handler:进行连接与读写socket。
        // ChildChannelHandler会重写initChannel,保证当创建NioServerSocketChannel成功之后,再进行初始化。
        /*************.handler 与   .childHandler的区别*******/
        // handler()和childHandler()的主要区别是,handler()是发生在初始化的时候,childHandler()是发生在客户端连接之后。
        b.group(bossGroup,workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChildChannelHandler(httpExecutor));
        //绑定端口,等待成功
        try {
            //.sync()是同步阻塞接口,等待绑定操作完成
            //ChannelFuture主要用于异步操作的通知回调
            ChannelFuture f = b.bind(port).sync();
            //等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }


    }

    //注意此处泛型不要使用ServerSocketChannel,否则客户端无法启动端口
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {

        private ExecutorService httpExecutor;


        public ChildChannelHandler( ExecutorService httpExecutor) {
            this.httpExecutor = httpExecutor;
        }

        @Override
        //websocket与http使用同一netty服务器是为了端口统一
        protected void initChannel(SocketChannel ch) throws Exception {
            //继承了ChannelHandlerAppender,并且创建了一个HttpRequestDecode和一个HttpResponseEncoder
            ch.pipeline().addLast(new HttpServerCodec());

            // 目的是将多个消息转换为单一的request或者response对象,参数为聚合后http请求的大小线程
            ch.pipeline().addLast(new HttpObjectAggregator(64 * 1024));

            //目的是支持异步大文件传输,websocket通讯需要
            ch.pipeline().addLast(new ChunkedWriteHandler());

            //http业务逻辑
            ch.pipeline().addLast(new HttpServerRequestHandler(httpExecutor, "/http"));

            //支持websocket通讯
            //插入位置,顺序非常重要,必须插在http编解码器的后面,必须插在当前处理器的前面
            //处理ping-pong 二帧。
            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/srws"));

            //websocket业务逻辑
            ch.pipeline().addLast(new WebSocketHandler());

        }
    }
}

HttpServerRequestHandler

package com.suirui.websocket_http.server.handler;

import com.alibaba.fastjson.JSONObject;

import com.suirui.websocket_http.server.controller.HttpWorkerThread;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders;
import org.apache.log4j.Logger;

import java.util.concurrent.ExecutorService;

/**
 * Created by zongx on 2019/12/31.
 */
public class HttpServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private static Logger logger = Logger.getLogger(HttpServerRequestHandler.class);

    private final String httpPrefix;

    private ExecutorService httpExecutors;

    public HttpServerRequestHandler(ExecutorService httpExecutors, String httpPrefix) {
        this.httpPrefix = httpPrefix;
        this.httpExecutors = httpExecutors;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
        String uri = msg.getUri();
        if (uri.equalsIgnoreCase("/srws")) {
            //如果请求了WebSocket协议,则增加引用技术,并将它传递给下一个ChannelInboundHandler
            ctx.fireChannelRead(msg.retain());
        } else if (uri.startsWith(httpPrefix)) {
            //判断前端来的url路径是否正确
            JSONObject json = new JSONObject();
            json.put("code", 200);
            json.put("message", "操作成功");

            //获取要进行的操作与json参数
            int index = uri.indexOf("?");
            String op = uri.substring(httpPrefix.length(), index == -1 ? uri.length() : index);
            String param = (index == -1) ? "" : uri.substring(index + 1);
            String body = "";

            //获取请求体
            int len = msg.content().readableBytes();
            if (len > 0) {
                byte[] content = new byte[len];
                msg.content().readBytes(content);
                body = new String(content, "UTF-8");
            }

            //通过多线程线程池将处理结果返还
            HttpWorkerThread t = new HttpWorkerThread(
                    msg.getProtocolVersion(),
                    HttpHeaders.isKeepAlive(msg),
                    ctx.channel(),
                    param,
                    body,
                    op);
            httpExecutors.execute(t);

        }
    }
}
package com.suirui.websocket_http.server.controller;

import com.alibaba.fastjson.JSONObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.*;
import org.apache.log4j.Logger;

import java.io.UnsupportedEncodingException;

/**
 * Created by zongx on 2019/12/31.
 */

public class HttpWorkerThread implements Runnable {

    private Channel browserChannel;
    private String param;
    private String body;
    private String operation;
    private HttpVersion protocalVersion;
    private boolean keepAlive;

    private static Logger logger = Logger.getLogger(HttpWorkerThread.class);

    /**
     * @Description: 构造方法,保存
     * @Author: zongx
     * @Date: 2019/12/31
     * @Param: protocolVersion:http协议版本
     * @Param: keepAlive:是否是长连接
     * @Param: channel:http请求来的channel
     * @Param: param:参数
     * @Param: body:请求体
     * @Param: op:操作字符
     * @return
    */
    public HttpWorkerThread(HttpVersion protocolVersion, boolean keepAlive, Channel channel, String param, String body, String op) {
        this.browserChannel = channel;
        this.param = param;
        this.body = body;
        this.operation = op;
        this.keepAlive = keepAlive;
        this.protocalVersion = protocolVersion;
    }

    @Override
    public void run() {
        try {
            //获取请求内容
            JSONObject json = (JSONObject) JSONObject.parse(this.body);
            System.out.println(json);

            //构造响应体
            JSONObject result = new JSONObject();
            result.put("code", 200);
            switch (this.operation) {
                //通知当前服务邀请
                case "/add":
                    result.put("message", "增加成功");
                    break;
                //通知当前服务邀请
                case "/del":
                    result.put("message", "删除成功");
                    break;
                //通知当前服务邀请
                case "/update":
                    result.put("message", "更新成功");
                    break;
                //通知当前服务邀请
                case "/select":
                    result.put("message", "查找成功");
                    break;
            }

            //构造响应
            FullHttpResponse response = new DefaultFullHttpResponse(
                    this.protocalVersion,
                    HttpResponseStatus.OK,
                    Unpooled.wrappedBuffer(result.toJSONString().getBytes("UTF-8"))
            );
            response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
            response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
            if (this.keepAlive) {
                response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            }
            //返回response
            ChannelFuture future = browserChannel.writeAndFlush(response);

            // 如果不是长连接,要监听当前channel关闭
            if (!keepAlive) {
                future.addListener(ChannelFutureListener.CLOSE);
            }

            //关闭当前channel
            browserChannel.close();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }


    }
}

WebSocketHandler

package com.suirui.websocket_http.server.handler;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.apache.commons.lang3.StringUtils;

import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ExecutorService;

/**
 * Created by zongx on 2020/1/6.
 */
public class WebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception {
        if (msg instanceof TextWebSocketFrame) {
            TextWebSocketFrame request = (TextWebSocketFrame) msg;
            String receiveMsg = request.text();

            System.out.println("websocketServer 收到信息: " + receiveMsg);

            if(StringUtils.isNotBlank(receiveMsg)) {
                JSONObject jsonObject = JSON.parseObject(receiveMsg);
                String op =  jsonObject.getString("op");
                switch (op) {
                    //通知当前服务邀请
                    case "add":
                        jsonObject.put("message", "增加成功");
                        break;
                    //通知当前服务邀请
                    case "del":
                        jsonObject.put("message", "删除成功");
                        break;
                    //通知当前服务邀请
                    case "update":
                        jsonObject.put("message", "更新成功");
                        break;
                    //通知当前服务邀请
                    case "select":
                        jsonObject.put("message", "查找成功");
                        break;
                }
                String s = jsonObject.toJSONString();
                TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(s);
                ctx.channel().writeAndFlush(textWebSocketFrame);
            }

        }
    }
}

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值