WebSocket通讯

小龙 708 2019-11-14

WebSocket本身指的就是在网页上进行Scoket连接,这样可以直接进行客户端与服务器端的通讯操作,WebSocket的出现是为了提升Ajax的处理性能,同时也可以得到良好的安全保证,最为常见的应用就是各个电商网站的在线咨询。

WebSocket简介

WebSocket是在HTML5一起推出的一个通讯技术,在HTNL里面本身是有一个Ajax2.0开发技术的,但是它又提供了一个更加高效的技术“WebkSocket”,如果想要充分的理解WebSocket设计的目的,那么首先就必须要充分的理解Ajax的设计问题
Ajax的产生是在HTTP协议基础之上完成的,其最大的特征是直接基于HTTP协议进行异步的请求处理,而所有HTTP请求里面最大的问题就在于,不会保留用户的状态,每一次都需要创建新的连接,这样对于服务器的性能是有严重损耗的,那么既然现在用户有可能性需要长期的使用通讯通道,最好的做法就是不关闭连接。
Ajaxpng.png

WebSocket协议和Http协议一样,都是在TCP协议的基础上进行系统构建,所有TCP的处理依然是现在操作的主流。但与HTTP相比,两者在通讯处理的时候有着不一样的操作机制。

  • HTTP通讯:在每一次进行请求发送之后都会有响应的回应进行该请求处理
  • WebSocket通讯:在进行通讯之前首先进行握手处理,确定当前已经可以获取一个有效的连接,随后才可以实现通讯。
    Http.png
    对于WebSocket程序的开发,可以直接在当前的WEB服务器上使用,而且最为重要的是WebSocket客户端不一定非要在WEB容器中使用,只要有相应的JavaScript支持就可以

WebSocket基础实现

如果要想快速的进行WebSocket程序的开发,那么只要在拥有支持WebSocket容器上直接进行代码的编写即可,例如:Tomcat容器。
在项目中引入如下依赖
Gradle

providedCompile group: 'javax.websocket', name: 'javax.websocket-api', version: '1.1'
providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
providedCompile group: 'javax.servlet.jsp', name: 'javax.servlet.jsp-api', version: '2.3.3'

Maven

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version>
    <scope>provided</scope>
</dependency>

要想实现WebSocket的程序开发,只需要设置一个WebSocket程序处理类即可

package cn.srthe.websocket;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/** @ServerEndpoint 该注解表示:运行Tomcat之后会自动使用此路径进行定义 */
@ServerEndpoint("/message")
public class EchoMessageServer {
    /**
     * 方法名称可随意
     *
     * @OnOpen 表示当前的方法作为连接创建时的操作
     */
    @OnOpen
    public void openChannel() {
        System.err.println("********************* 【EchoMessageServer】建立WebSocket连接 *********************");
    }
    /**
     * 进行消息通讯操作
     *
     * @OnMessage 进行消息处理
     */
    @OnMessage
    public void messageHandle(String message, Session session) {
        System.err.println("********************* 【EchoMessageServer】接收到消息内容" + message);
        try {
            session.getBasicRemote().sendText("【ECHO】" + message + "{" + session.getId() + "}");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /** @OnClose 关闭WebSocket连接 */
    @OnClose
    public void closeHandle() {
        System.err.println("********************* 【EchoMessageServer】关闭WebSocket连接");
    }
}

在使用WebSocket开发的时候,所有的操作方法都采用了注解的形式进行了动态的配置,这样的设计拥有极大的灵活性
将代码部署到Tomcat上,启动Tomcat即可。进行WebSocket通讯操作的时候实际上不要求WebSocket客户端代码必须与服务器端代码在一台服务器上,它可以随意的放在任何的位置,只要有相应的IP地址即可。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket通讯</title>
    <script type="text/javaScript">
        url = "ws://localhost/message";  //WebSocket服务器的连接地址
        window.onload = function () {
            webSocket = new WebSocket(url);  //创建WebSocket的连接
            webSocket.onopen = function (ev) {
                document.getElementById("contentDiv").innerHTML += "<p>服务器连接成功,开始进行消息交互处理!</p>";
            }
            webSocket.onclose = function()
            {
                document.getElementById("contentDiv").innerHTML += "<p>消息交互完毕,关闭连接通道!</p>";
            }
            document.getElementById("send").addEventListener("click", function () {
                inputMessage = document.getElementById("msg").value;//获取输入的消息内容
                webSocket.send(inputMessage);  //将输入的消息发送到WebSocket服务器端
                webSocket.onmessage = function (obj) {
                    document.getElementById("contentDiv").innerHTML += "<p>" + obj.data + "</p>";
                    document.getElementById("msg").value = "";
                }
            }, false);
        }
    </script>
</head>
<body>
<div id="contentDiv" style="height:300px;overflow:scroll;background:lightblue"></div>
<div id="inputDiv">
    请输入消息内容:<input type="text" name="msg" id="msg">
    <button type="button" id="send">发送</button>
</div>
</body>
</html>

此时的程序只要不关闭客户端的连接通道,那么就可以进行持续的信息交流,当前的这种操作就适合于频繁的WEB输入与输出操作。

使用Netty开发WebSocket

服务器端

如果要进行WebSocket通讯实现,服务器端只需进行消息的回应处理即可,创建一个WebSocketServerHandler程序类

public class WebSocketServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("【WebSocketServerHandler】消息类型:"+msg.getClass());
        //当前是否为文本数据
        if (msg instanceof TextWebSocketFrame){
            TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) msg;
            String echoMessage = "【ECHO】"+textWebSocketFrame.content().toString(CharsetUtil.UTF_8);
            ctx.writeAndFlush(new TextWebSocketFrame(echoMessage));
        }
    }
}

创建一个WebSocketServer程序类,进行服务器运行环境的配置

public class WebSocketServer {
    //服务器端主运行程序
    public void run() throws Exception{
        EventLoopGroup masterGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        System.out.println("************** 服务器已经运行,在"+ HttpServerInfo.PORT+"端口进行监听 **************");
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(masterGroup,workerGroup).channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new HttpResponseEncoder());
                    ch.pipeline().addLast(new HttpRequestDecoder());
                    ch.pipeline().addLast(new HttpObjectAggregator(10485760));
                    ch.pipeline().addLast(new ChunkedWriteHandler());
                    ch.pipeline().addLast(new WebSocketServerProtocolHandler("/message"));
                    ch.pipeline().addLast(new WebSocketServerHandler());
                }
            });
            serverBootstrap.option(ChannelOption.SO_BACKLOG,128);
            serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);
            ChannelFuture future = serverBootstrap.bind(HttpServerInfo.PORT).sync();
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            masterGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

编写服务器端的启动类StartWebSocketServerMain

public class StartWebSocketServerMain {
    public static void main(String[] args) throws Exception {
        new WebSocketServer().run();
    }
}

客户端

当用户通过浏览器进行WebSocket的连接时,浏览器会帮助用户省略掉许多的处理细节,所有的WebSocket进行连接控制的时候都需要考虑到对于握手的处理,如果说现在不使用浏览器,而直接使用Netty来实现客户端,那么在这个实现之中就必须手工进行握手的处理
定义一个接收键盘输入的类:KeyboardInputData

public class KeyboardInputData {
    private static final BufferedReader KEYBOARD_INPUT  = new BufferedReader(new InputStreamReader(System.in));
    private KeyboardInputData(){}
    public static String getString(String prompt){
        System.out.println(prompt);
        String result = null;
        boolean flag = true;
        while (flag){
            try {
                result = KEYBOARD_INPUT.readLine();
                if (result == null || "".equals(result)){
                    System.out.println("错误,输入放入内容不允许为空,请重新输入:");
                }else {
                    flag = false;
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return result;
    }
}

创建一个WebSocketClientHandler类,负责进行所有的回调操作,通过键盘输入实现数据的交互处理。

public class WebSocketClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * WebSocket握手处理类
     */
    private WebSocketClientHandshaker handlershaker;
    private ChannelPromise handshakerFuture;
    /**
     * 描述当前的关闭状态
     */
    private boolean closeFlag = false;
    /**
     * 所有的握手处理操作都是在连接的位置上进行的,WebSocketClient连接
     * @param handlershaker 握手对象
     */
    public WebSocketClientHandler(WebSocketClientHandshaker handlershaker) {
        this.handlershaker = handlershaker;
    }
    public ChannelPromise getHandshakerFuture(){
        return this.handshakerFuture;
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //当进行通道创建的时候需要进行握手的处理
        //进行握手处理
        this.handlershaker.handshake(ctx.channel());
    }
    /**
     * 在进行通道注册的时候触发操作
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        this.handshakerFuture = ctx.newPromise();
    }
    /**
     * 消息读取完毕之后触发
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //当内容读取完毕之后应该考虑进行消息的发送处理
        //当前的通道还未关闭
        if (this.closeFlag == false){
            String inputMesssage = KeyboardInputData.getString("请输入要发送的数据").trim();
            if ("exit".equalsIgnoreCase(inputMesssage)){
                //当前的操作结束
                ctx.writeAndFlush(new CloseWebSocketFrame());
            }else if ("ping".equalsIgnoreCase(inputMesssage)){
                ctx.writeAndFlush(new PingWebSocketFrame());
            }else {
                TextWebSocketFrame text = new TextWebSocketFrame(inputMesssage);
                ctx.writeAndFlush(text);
            }
        }
    }
    /**
     * 进行通道信息读取
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //获取当前的操作通道
        Channel channel = ctx.channel();
        if (!this.handlershaker.isHandshakeComplete()){
            //判断是否已经成功握手了
            try {
                //握手处理
                this.handlershaker.finishHandshake(channel, (FullHttpResponse) msg);
                System.err.println("【WebSocketClientHandler - channelRead()】成功进行了服务器的连接");
                //握手完成
                this.handshakerFuture.setSuccess();
            }catch (Exception e){
                System.out.println("【WebSocketClientHandler - channelRead()】服务连接失败。");
                //握手失败
                this.handshakerFuture.setFailure(e);
            }
            //后续的处理暂时不执行
            return;
        }
        //如果执行后续的处理代码,则一定表示已经握手成功了,服务器开始进行响应处理了
        if (!(msg instanceof HttpResponse)){
            //回应的不是Response就表示有数据
            //获取响应的数据内容
            WebSocketFrame frame = (WebSocketFrame) msg;
            if (frame instanceof TextWebSocketFrame){
                //当前文本信息
                TextWebSocketFrame text = (TextWebSocketFrame) frame;
                System.out.println("【WebSocketClientHandler - channelRead()】接收到的文本数据:"+text.text());
            }else if (frame instanceof PongWebSocketFrame){
                PongWebSocketFrame pong = (PongWebSocketFrame) frame;
                System.out.println("【WebSocketClientHandler - channelRead()】连接测试数据Pong....");
            }else if (frame instanceof CloseWebSocketFrame){
                //当前通道已经关闭
                this.closeFlag = true;
                //关闭当前的操作通道
                channel.close();
            }
        }
    }
}

定义WebSocketServer类

public class WebSocketServer {
    //服务器端主运行程序
    public void run() throws Exception{
        EventLoopGroup masterGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        System.out.println("************** 服务器已经运行,在"+ HttpServerInfo.PORT+"端口进行监听 **************");
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(masterGroup,workerGroup).channel(NioServerSocketChannel.class);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new HttpResponseEncoder());
                    ch.pipeline().addLast(new HttpRequestDecoder());
                    ch.pipeline().addLast(new HttpObjectAggregator(10485760));
                    ch.pipeline().addLast(new ChunkedWriteHandler());
                    ch.pipeline().addLast(new WebSocketServerProtocolHandler("/message"));
                    ch.pipeline().addLast(new WebSocketServerHandler());
                }
            });
            serverBootstrap.option(ChannelOption.SO_BACKLOG,128);
            serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);
            ChannelFuture future = serverBootstrap.bind(HttpServerInfo.PORT).sync();
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            masterGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

定义客户端启动程序类:StartWebSocketServerMain

public class StartWebSocketServerMain {
    public static void main(String[] args) throws Exception {
        new WebSocketServer().run();
    }
}