美文网首页java
分布式-- WebSocket 全双工通讯

分布式-- WebSocket 全双工通讯

作者: _凌浩雨 | 来源:发表于2018-05-14 20:45 被阅读30次

    1. WebSocket

    1). 背景

    B/S结构的软件项目中有时客户端需要实时的获得服务器消息,但默认HTTP协议只支持请求响应模式,这样做可以简化Web服务器,减少服务器的负担,加快响应速度,因为服务器不需要与客户端长时间建立一个通信链接,但不容易直接完成实时的消息推送功能,如聊天室、后台信息提示、实时更新数据等功能,但通过polling、Long polling、长连接、Flash Socket以及HTML5中定义的WebSocket能完成该功能需要。

    2). OSI 模型与 TCP/IP下载 pdf
    图1.png
    3). Socket简介

    Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。Socket的英文原义是“孔”或“插座”,作为UNIX的进程通信机制。Socket可以实现应用程序间网络通信。


    图2.png

    I. Socket可以使用TCP/IP协议或UDP协议。
    TCP/IP协议

    • TCP/IP协议是目前应用最为广泛的协议,是构成Internet国际互联网协议的最为基础的协议,由TCP和IP协议组成:
      => TCP协议:面向连接的、可靠的、基于字节流的传输层通信协议,负责数据的可靠性传输的问题。
      => IP协议:用于报文交换网络的一种面向数据的协议,主要负责给每台网络设备一个网络地址,保证数据传输到正确的目的地。
    • UDP协议
      UDP特点:无连接、不可靠、基于报文的传输层协议,优点是发送后不用管,速度比TCP快。

    II. B/S架构的系统多使用HTTP协议,HTTP协议的特点(底层通信使用Socket):

    • 无状态协议
    • 用于通过 Internet 发送请求消息和响应消息
    • 使用端口接收和发送消息,默认为80端口


      图3.png
    4).双向通信与消息推送
    • 轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
      �优点:后端程序编写比较容易。
      �缺点:请求中有大半是无用,浪费带宽和服务器资源。
      �实例:适于小型应用。
    • 长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
      �优点:在无消息的情况下不会频繁的请求,耗费资小。
      �缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。 Comet异步的ashx,
      �实例:WebQQ、Hi网页版、Facebook IM。
    • 长连接:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。
      �优点:消息即时到达,不发无用请求;管理起来也相对便。
      �缺点:服务器维护一个长连接会增加开销。
      �实例:Gmail聊天
    • Flash Socket:在页面中内嵌入一个使用了Socket类的 Flash 程序JavaScript通过调用此Flash程序提供的Socket接口与服务器端的Socket接口进行通信,JavaScript在收到服务器端传送的信息后控制页面的显示。
      �优点:实现真正的即时通信,而不是伪即时。
      �缺点:客户端必须安装Flash插件;非HTTP协议,无法自动穿越防火墙。
      �实例:网络互动游戏。
    • Websocket: WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。依靠这种技术可以实现客户端和服务器端的长连接,双向实时通信。
      �特点: 事件驱动、异步、使用ws或者wss协议的客户端socket、能够实现真正意义上的推送功能。
      �缺点:少部分浏览器不支持,浏览器支持的程度与方式有区别。
    图4.png

    2. 示例

    1). 客户端

    websocket允许通过JavaScript建立与远程服务器的连接,从而实现客户端与服务器间双向的通信。在websocket中有两个方法:  
        1、send() 向远程服务器发送数据
        2、close() 关闭该websocket链接
      websocket监听函数:
        1、onopen 当网络连接建立时触发该事件
        2、onerror 当网络发生错误时触发该事件
        3、onclose 当websocket被关闭时触发该事件
        4、onmessage 当websocket接收到服务器发来的消息的时触发的事件,也是通信中最重要的一个监听事件。msg.data
      websocket还定义了一个readyState属性,这个属性可以返回websocket所处的状态:
        1、CONNECTING(0) websocket正尝试与服务器建立连接
        2、OPEN(1) websocket与服务器已经建立连接
        3、CLOSING(2) websocket正在关闭与服务器的连接
        4、CLOSED(3) websocket已经关闭了与服务器的连接
      websocket的url开头是ws,如果需要ssl加密可以使用wss,当我们调用websocket的构造方法构建一个websocket对象(new WebSocket(url))的之后,就可以进行即时通信了。

    2). 创建Web工程

    I. 创建chat.html页面

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title>趣味聊天室</title>
            <style type="text/css">
                /* 处理浏览器适配问题 */
                * {margin: 0;padding: 0;}
                a {text-decoration: none;}
                body {font-size: 14px;font-family: "微软雅黑";}
                .wrap {width: 700px; position: relative; top: 50px;left: calc(50% - 350px);display: flex;flex-direction:column;background: #F8F8F9;border-radius: 8px;user-select:none;-webkit-user-select:none;}
                
                /* header start */
                .wrap .header {height: 80px;background: linear-gradient(200deg,#d6d1f1,#EE60E1);text-align: center;line-height: 80px;border-radius: 8px 8px 0 0;cursor: move;}
                .wrap .header span {font-size: 28px;letter-spacing: 2px;color: #f1f1f1;}
                .wrap .header #close_btn {position: absolute;right: 0px;top: 0px;width: 20px;height: 20px;background: url(img/close.png) no-repeat center;border-radius: 8px;}
                /* header end */
                
                /* main start */
                .wrap .main {display: flex;height: 300px;}
                
                /* left start */
                .wrap .left {width: 260px;border-right: 1px solid #e1e1e1;}
                .wrap .time {height: 30px;text-align: center;line-height: 30px;}
                .wrap .time .time_bg {width: 20px;height: 20px;display: inline-block;vertical-align: middle;cursor: pointer;}
                .wrap .time span {font-size: 14px;vertical-align: middle;color: #4EA9E9;cursor: pointer;}
                .wrap .left .left_people a{height: 25px;text-align: center;line-height: 25px;margin-top: 3px;display: block;}
                /* left end */
                
                /* right start */
                .wrap .right {width: 700px;background: #f9f9f9;}
                .wrap .right .right_record {height: 260px;padding-bottom: 10px;overflow: auto;}
                .wrap .right .right_record .record_item {width: 600px;margin: 0 auto;}
                .wrap .right .right_record .record_item p {text-align: center; font-size: 13px;color: #9A9797;}
                .wrap .right .right_record .record_item .record_item_txt p {text-align: left;margin: 5px 0 0 10px;}
                .wrap .right .right_record .record_item .record_item_txt .avatar {margin-left: 5px; color: #AC4BFD;font-size: 12px;}
                .wrap .right .right_record .record_item .record_item_txt .txt {color: #f1f1f1;}
                .wrap .right .right_record .record_item .record_item_txt p span {border-radius: 10px; text-align: left;background: skyblue;padding: 3px 5px;font-size: 14px;color: #E833F6;}
                /* right end */
                
                /* content bottom start */
                .wrap .content {height: 140px;border-top: 1px solid #e1e1e1;display: flex;flex-direction:column;}
                .wrap .content .content_tool {height: 30px;border-bottom: 1px solid #e1e1e1;display: flex;line-height: 41px;}
                .wrap .content .content_tool div {margin-left: 20px;}
                .wrap .content .content_tool .bg {width: 20px;height: 20px;display: inline-block;cursor: pointer;}
                
                .wrap .content .content_text {height: 90px;padding: 5px;outline: none;}
                .wrap .content .content_btn {display: flex;justify-content:space-between;line-height: 30px;border-top: 1px solid #e1e1e1;}
                .wrap .content .content_btn .content_btn_text {font-size: 12px;letter-spacing: 1px;line-height: 35px;color: #7E7D7D;margin-left: 10px;}
                .wrap .content .content_btn .btns {width: 140px;display: flex;margin-top: 2px;}
                .wrap .content .btn_item {width: 80px;height: 25px;text-align: center;line-height: 25px;border: 1px solid #c1c1c1;color: #313333;cursor: pointer;margin-right: 5px;letter-spacing: 1px;border-radius: 4px;}
                /* content bottom end */            
            </style>
        </head>
        <body>
            <!-- <canvas id="dot"></canvas> -->
            <div class="wrap">
                <div class="header">
                    <span>趣味聊天室</span>
                    <a href="javascript:void(0)" id="close_btn"></a>
                </div>
                <div class="main">
                    <!-- left -->
                    <!-- right -->
                    <div class="right">
                        <div class="time">
                            <i class="time_bg" style="background: url(img/clock.png) no-repeat;"></i>
                            <span>聊天记录</span>
                        </div>
                        <!-- 聊天记录 -->
                        <div class="right_record" id="outputMessage"></div>
                    </div>
                </div>
                
                <!-- content tool/txt -->
                <div class="content">
                    <div class="content_tool">
                        <div class="tool_font">
                            <i class="bg" style="background: url(img/font.png) no-repeat;"></i>
                        </div>
                        <div class="tool_emjo">
                            <i class="bg" style="background: url(img/emjo.png) no-repeat;"></i>
                        </div>
                    </div>
                    
                    <div class="content_text" contenteditable="true" id="inputMessage"></div>
                    
                    <div class="content_btn">
                        <div class="content_btn_text"><i>自己的DIY聊天室</i></div>
                        <div class="btns">
                            <div class="send btn_item" onclick="getMessage();">
                                <span>发送</span><span>(S)</span>
                            </div>
                            <div class="close btn_item">
                                <span>关闭</span><span>(C)</span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            <!-- 引入jQuery -->
            <!-- <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.js"></script> -->
            <script type="text/javascript">
                // 定义服务器地址和请求类型
                var wsUrl = "ws://localhost:8080/WebSocketTest/chatServer";
                // 客户端与服务建立连接,连接成功后,发出ws.onopen事件
                var ws = new WebSocket(wsUrl);
                /** 
                * 连接成功后,提示浏览器客户端输入昵称 
                */
                ws.onopen = function() {
                    var userName = prompt("请给自己取一个名字:");
                    // 发送用户名
                    ws.send(userName);
                }
                
                /**
                * 客户端收到服务器发送的消息
                */
                ws.onmessage = function(message) {
                    // 获取以后,在客户端显示
                    outputMessage.innerHTML = outputMessage.innerHTML + message.data;
                    // 获取outputMessage盒子
                    var msg = document.getElementById("outputMessage");
                    // 差值,溢出的高度 - 可视的高度
                    var distance = msg.scrollHeight - msg.offsetHeight;
                    // 将差值赋值给滚动条的高度
                    msg.scrollTop = distance;
                }
                
                /**
                * 获取某个用户输入的聊天内容,并发送给服务端,让服务端广播给所有人
                */
                function getMessage() {
                    // 获取输入框信息
                    var inputMessage = document.getElementById("inputMessage").innerText;
                    // 判断内容是否为空
                    if (typeof(inputMessage) == "undefined") {
                        alert("请输入您要发送的信息");
                    } else {
                        // 获取到输入的消息,发送给服务端,并广播给所有用户
                        ws.send(inputMessage);
                        document.getElementById("inputMessage").innerText = "";
                    }
                }
                
                /**
                * 当关闭页面的时候,或者用户退出的时候,会执行ws.close()方法
                */
                window.onbeforeunload = function() {
                    ws.close();
                }
                
                /**
                * 回车键发送消息
                */
                document.onkeyup = function(e) {
                    if (e.keyCode == 13) {
                        getMessage();
                    }
                }
                
            </script>
        </body>
    </html>
    

    II. 图片资源


    图5.png

    III. 目录结构


    图6.png
    3). 服务器ChatServer代码
    /**
     * 趣味聊天的服务端程序
     * 
     * @author mazaiting
     */
    // 声明WebSocket服务器的地址
    @ServerEndpoint("/chatServer")
    public class ChatServer {
        /** 是否是第一次进入 */
        private boolean isFirstFlag = true;
        private Session session;
        /** 用户名 */
        private String userName;
        /**
         * 记录此次聊天室的服务端有多少个连接 key代表此次客户端的Session ID,value代表此次连接对象
         */
        private static final HashMap<String, Object> connectMap = new HashMap<>();
        /**
         * 保存所有用户的昵称信息 key是Session ID,value才是用户名
         */
        private static final HashMap<String, String> userMap = new HashMap<>();
    
        /**
         * 服务端收到客户端连接请求,连接成功后会执行此方法
         * 
         * @param session
         */
        @OnOpen
        public void start(Session session) {
            this.session = session;
            connectMap.put(session.toString(), this);
        }
    
        /**
         * 发送消息
         * 
         * @param clientMessage
         *            消息/用户名
         * @param session
         *            会话
         */
        @OnMessage
        public void chat(String clientMessage, Session session) {
            // 消息
            String message;
            // 判断是否为第一次发送消息
            if (isFirstFlag) {
                // 用户名赋值
                this.userName = clientMessage;
                // 将新进来的用户保存到用户map
                userMap.put(session.getId(), userName);
                // 构造发送给客户端的提示信息
                message = htmlMessage("系统消息", userName + "进入聊天室");
                // 输入昵称后,代表isFirstFlag=false
                isFirstFlag = false;
            } else {
                // 构造发送给客户端的提示信息
                message = htmlMessage(userMap.get(session.getId()), clientMessage);
            }
            sendMessageForAll(message);
        }
    
        /**
         * ws.close()方法调用后,会触发后台的标注OnClose方法
         * 
         * @param session
         */
        @OnClose
        public void close(Session session) {
            // 当某个用户退出时,对其他用户进行广播
            String message = htmlMessage("系统消息", userMap.get(session.getId()) + "退出了聊天室");
            // 用户map移除
            userMap.remove(session.getId());
            // 链接map移除
            connectMap.remove(session.getId());
            sendMessageForAll(message);
        }
    
        /**
         * 格式化消息
         * @param userName 用戶名
         * @param message 消息
         * @return 消息
         */
        private String htmlMessage(String userName, String message) {
            StringBuffer messageBuffer = new StringBuffer();
            SimpleDateFormat sFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            messageBuffer.append("<div class='record_item'>");
            messageBuffer.append("<p class='record_item_time'>");
            messageBuffer.append("<span>" + sFormat.format(new Date()) + "</span>");
            messageBuffer.append("</p>");
            messageBuffer.append("<div class='record_item_txt'>");
            messageBuffer.append("<span class='avatar'>" + userName + "</span>");
            messageBuffer.append("<p>");
            messageBuffer.append("<span class='txt'>" + message + "</span>");
            messageBuffer.append("</p>");
            messageBuffer.append("</div>");
            messageBuffer.append("</div>");
            return messageBuffer.toString();
        }
    
        /**
         * 为所有用户发送消息
         * 
         * @param message
         *            消息
         */
        private void sendMessageForAll(String message) {
            // 当前对象
            ChatServer client = null;
            // 将消息广播给所有人
            for (String connectKey : connectMap.keySet()) {
                // 获取客户端
                client = (ChatServer) connectMap.get(connectKey);
                // 给对应的web端发送一个文本信息
                try {
                    client.session.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                ;
            }
        }
    }
    
    
    4). 服务端注解
    • @ServerEndpoint: 绑定服务器地址的注解方法
    • @OnOpen: WebSocket连接执行的注解方法
    • @OnMessage: 发送消息时执行的方法
    • @OnClose: 关闭时执行的方法
    5). 测试
    图7.png

    3. 开源框架

    1). 开源Java消息推送框架 Pushlet

    Pushlet 是一个开源的 Comet 框架,Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。
    Pushlet是一种comet实现:在Servlet机制下,数据从server端的Java对象直接推送(push)到(动态)HTML页面,而无需任何Javaapplet或者插件的帮助。它使server端可以周期性地更新client的web页面,这与传统的request/response方式相悖。浏览器client为兼容JavaScript1.4版本以上的浏览器(如InternetExplorer、FireFox),并使用JavaScript/DynamicHTML特性。而底层实现使用一个servlet通过Http连接到JavaScript所在的浏览器,并将数据推送到后者。

    代码下载

    相关文章

      网友评论

        本文标题:分布式-- WebSocket 全双工通讯

        本文链接:https://www.haomeiwen.com/subject/txvjdftx.html