• WebSocket的介绍


    WebSocket

    关于websocket的一个小demo,是聊天室,源代码地址:

    聊天室的github源代码

    websocket的背景

    现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询或者long poll 。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

    websocket的特点

    • WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
    • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
    • 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
    • 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

    Ajax轮询

    ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
    场景再现:
    客户端:啦啦啦,有没有新信息(Request)
    服务端:没有(Response)
    客户端:啦啦啦,有没有新消息(Request)
    服务端:好啦好啦,有啦给你。(Response)
    客户端:啦啦啦,有没有新消息(Request)
    服务端:。。。。。没。。。。没。。。没有(Response) ---- loop

    long poll

    long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
    场景再现:
    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
    服务端:额。。 等待到有消息的时候。。来 给你(Response)
    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

    从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。

    从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。
    ajax轮询 需要服务器有很快的处理速度和资源。(速度)
    long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)
    所以ajax轮询 和long poll 缺点非常明显。

    websocket 与Http的关系

    WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)

    首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充,可以通过这样一张图理解

    首先,相对于HTTP这种非持久的协议来说,Websocket是一个持久化的协议。

    • HTTP还是一个无状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
    • HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。
    • 在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
    • 但是 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

    所以在这种情况下出现了,Websocket出现了。他解决了HTTP的难题。

    websocket协议建立

    WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。

    首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:

    GET ws://localhost:3000/ws/chat HTTP/1.1
    Host: localhost
    Upgrade: websocket
    Connection: Upgrade
    Origin: http://localhost:3000
    Sec-WebSocket-Key: client-random-string
    Sec-WebSocket-Version: 13
    

    该请求和普通的HTTP请求有几点不同:

    1. GET请求的地址不是类似/path/,而是以ws://开头的地址;
    2. 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接;
    3. Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
    4. Sec-WebSocket-Version指定了WebSocket的协议版本。

    服务器如果接受该请求,就会返回如下响应:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: server-random-string
    

    该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。

    这里开始就是HTTP最后负责的区域了,告诉客户端,已经成功切换协议啦~

    websocket的客户端简单实例

    var ws = new WebSocket("wss://localhost:8080/ws/asset");
    
    //连接建立成功调用的方法
    ws.onopen = function(evt) { 
      console.log("Connection open ..."); 
      //向服务端发送消息
      ws.send("Hello WebSockets!");
    };
    //收到服务端消息后调用的方法
    ws.onmessage = function(evt) {
      console.log( "Received Message: " + evt.data);
      ws.close();
    };
    //连接关闭调用的方法
    ws.onclose = function(evt) {
      console.log("Connection closed.");
    };
    

    webSocket的服务端简单实例

    @ServerEndpoint(value = "/ws/asset")
    @Component
    @Slf4j
    public class WebSocketServer {
        
    
        private static AtomicInteger OnlineCount = new AtomicInteger(0);
        // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
        private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
    
    
        /**
         * 连接建立成功调用的方法
         */
        @OnOpen
        public void onOpen(Session session) {
            SessionSet.add(session);
            int cnt = OnlineCount.incrementAndGet(); // 在线数加1
            log.info("有连接加入,当前连接数为:{}", cnt);
            SendMessage(session, "连接成功");
        }
    
        /**
         * 连接关闭调用的方法
         */
        @OnClose
        public void onClose(Session session) {
            SessionSet.remove(session);
            int cnt = OnlineCount.decrementAndGet();
            log.info("有连接关闭,当前连接数为:{}", cnt);
        }
    
        /**
         * 收到客户端消息后调用的方法
         *
         * @param message
         *            客户端发送过来的消息
         */
        @OnMessage
        public void onMessage(String message, Session session) {
            log.info("来自客户端的消息:{}",message);
            System.out.println(session.toString());
            SendMessage(session, "收到消息,消息内容:"+message+session.getId());
    
        }
    
        /**
         * 出现错误
         * @param session
         * @param error
         */
        @OnError
        public void onError(Session session, Throwable error) {
            log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
            error.printStackTrace();
        }
    
        /**
         * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
         * @param session
         * @param message
         */
        public static void SendMessage(Session session, String message) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("发送消息出错:{}", e.getMessage());
                e.printStackTrace();
            }
        }
    
        /**
         * 群发消息
         * @param message
         * @throws IOException
         */
        public static void BroadCastInfo(String message) throws IOException {
            for (Session session : SessionSet) {
                if(session.isOpen()){
                    SendMessage(session, message);
                }
            }
        }
    
        /**
         * 指定Session发送消息
         * @param sessionId
         * @param message
         * @throws IOException
         */
        public static void SendMessage(String message,String sessionId) throws IOException {
            Session session = null;
            for (Session s : SessionSet) {
                if(s.getId().equals(sessionId)){
                    session = s;
                    break;
                }
            }
            if(session!=null){
                SendMessage(session, message);
            }
            else{
                log.warn("没有找到你指定ID的会话:{}",sessionId);
            }
        }
    }
    
    

    websocket的心跳机制

    websockt心跳机制,不得不说很形象;那何为心跳机制,就是表明client与server的连接是否还在的检测机制;

    如果不存在检测,那么网络突然断开,造成的后果就是client、server可能还在傻乎乎的发送无用的消息,浪费了资源;怎样检测呢?原理就是定时向server发送消息,如果接收到server的响应就表明连接依旧存在;
    这个心跳机制在分布式中也很常见,

    demo

    聊天室demo

    (1)Client:客户端说明

    ​ 客户端的代码主要是使用H5的WebSocket进行实现,在前端网页中使用WebSocket进行连接服务端,然后建立Socket连接进行通讯。

    (2)Server:服务端说明

    ​ 服务端主要是建立多个客户端的关系,进行消息的中转等。客户端成功连接到服务端之后,就可以通过建立的通道进行发送消息到服务端,服务端接收到消息之后在群发给所有的客户端。

    (3)客户端和服务端连接

    var websocket = new WebSocket("ws://localhost:8080/myWs");  
    

    (4)客户端和服务端怎么发送消息?

    客户端可以使用webSocket提供的send()方法,如下代码:

    var message = document.getElementById('text').value;  
    websocket.send(message);  
    

    服务端怎么发送消息呢?主要是使用在成功建立连接的时候,创建的Session对象进行发送,如下代码:

    session.getAsyncRemote().sendText("恭喜您成功连接上WebSocket");  
    

    (5)客户端和服务端怎么接受消息?

    客户端接收消息消息使用的是websocket的onmessage回调方法,如下代码:

    websocket.onmessage = function(event) {  
               //文本信息直接显示,如果是json信息,需要转换下在显示.  
           var data = event.data;  
           document.getElementById('message').innerHTML += data;  
    }  
    

    服务端:

    @OnMessage  
    public void onMessage(String message, Session session) {  
            System.out.println("来自客户端的消息:" + message);  
    }
    
    

    (6)群聊原理(群发消息)

    服务端在和客户端建立连接的时候,会创建一个webSocket对象,我们会将每个连接创建的对象进行报错到一个列表中,比如:CopyOnWriteArraySet(这是线程安全的);在要进行群发的时候,编写我们的列表对象进行群发消息。

    (7)单聊原理(一对一消息)

    聊的时候,就无需遍历列表,而是需要知道发送者和接受者各自的Session对象,这个Session对象怎么获取呢?Session可以获取到sessionId,发送者在发送消息的时候,携带接收消息的sessionId,那么问题就演变成了:发送者怎么知道接受者的sessionId,那就是加入一个在线用户列表即可,在线用户列表中有用户的基本信息,包括sessionId。

    websocket的实时推送

    对比聊天室的demo,不同之处在于,客户端连入服务器时候,会开启一个线程,在线程中对客户端进行推送数据。

    关键代码:

    /**
         * 接收到消息
         *
         * @param text
         */
        @OnMessage
        public void onMsg(Session session,@PathParam("param") String param) throws IOException {
            //记录客户端
            webSocketMaps.put(session, param);
            //实例化工作任务
            Operater operater =new Operater(session,param);
            //开启线程
            Thread thread = new Thread(operater);
            thread.start();
            logger.info("发送线程启动成功");
        }
    
    

    目前业务还不是很复杂,后期功能添加时候,再进行扩展,关于这个实时推送,大概开了50个窗口就连接失败了。关于websocket的高并发,可以考虑。

  • 相关阅读:
    结对第二次作业
    结对项目第一次作业
    2017 软工第二次作业
    2017软工实践--第一次作业
    软工--最后的作业
    软件产品案例分析
    个人技术博客—作业向
    软工结队第二次作业
    软工第二次作业---数独
    软工实践第一次作业----阅读有感
  • 原文地址:https://www.cnblogs.com/jimlau/p/12375447.html
Copyright © 2020-2023  润新知