前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WebSocket实现在线聊天

WebSocket实现在线聊天

作者头像
每天学Java
发布2020-06-02 10:08:54
3.9K5
发布2020-06-02 10:08:54
举报
文章被收录于专栏:每天学Java每天学Java

这一篇文章前面部分我们会先介绍WebSocket协议的基本知识,在最后我们会用Spring Boot来集成WebSocket实现一个简单的在线聊天功能,我们也可以跨过前面的介绍直接看集成部分,后续在慢慢研究WebSocket。

前言

通常情况下,浏览器和服务器之间的消息通讯一般会使用Http协议,但是如果我们想服务器返回数据,必须先由浏览器发送请求给服务器,服务器才能响应这个请求。一般情况下Http协议基本能够满足我们需求,但是如果我们想打造一个网站在线聊天平台,这个时候我们发送一条消息,其他用户的浏览器该如何接受这条消息呢?

必须使用Http协议的情况下,我们可以使用轮询的方式让浏览器发送请求到服务器,查询是否有消息返回,这种方式能基本满足需求。但是在网站的后期会有一些问题衍生出来,比如实时性不够高,而且频繁的请求会给服务器带来极大的压力。

实时性的问题我们可以利用Http的Comet方式保持长链接,但是Comet本质上也是轮询,在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。这个机制暂时地解决了实时性问题,但是它带来了新的问题:以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。另外,一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发一些ping数据表示连接“正常工作”。

基于上面的请求,HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。

WebSocket介绍

WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。但是WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。

我们先看下面WebSocket请求的格式

General:
Request URL: ws://127.0.0.1:8080/websocket/%E6%AF%8F%E5%A4%A9%E5%AD%A6Java
Request Method: GET
Status Code: 101 

Response Header:
Connection: upgrade
Date: Sat, 30 Mar 2019 02:53:11 GMT
Sec-WebSocket-Accept: 4yMemfpbrCzWxZ1vpNqCI2IR+YI=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Upgrade: websocket

Resquest Header:
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: lD0B2FWxinrTPcNDMsO3hQ==
Sec-WebSocket-Version: 13

对比下面普通的Http请求

General:
Request URL: http://127.0.0.1:8080/index
Request Method: GET
Status Code: 200 
Remote Address: 127.0.0.1:8080
Referrer Policy: no-referrer-when-downgrade

Request Header:

我们会发现这些不同:

首先WebSocket的GET请求是以ws://开头的地址;

其次WebSocket的Status Code是101;

且WebSocket的请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;

在Resquest Header中:Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据,Sec-WebSocket-Version指定了WebSocket的协议版本。

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

Http为什么不能实现全双工通信呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。

而WebSocket连接建立以后,没有Http协议的限制,进而可以进行互相进行通讯

WebSocket特点

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP P协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。

浏览器对WebSocket支持情况

支持WebSocket的主流浏览器如下:

  • Chrome
  • Firefox
  • IE >= 10
  • Sarafi >= 6
  • Android >= 4.4
  • iOS >= 8

对于低版本不支持WebSocket浏览器,可以参考以下的解决方案

  • 使用轮询或长连接的方式实现伪websocket的通信
  • 使用flash或其他方法实现一个websocket客户端
  • ActiveX HTMLFile (IE)

实现聊天平台

我们先看以下实现的效果

搭建后台

这里我们使用Spring Boot来集成WebSocket

目录结构

当应用加载完之后...

第一步是要创立websocket endpoint

在Spring Boot中我们通过注入ServerEndpointExporter,来创建websocket endpoint

package com.example.websocket;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


@Configuration
public class WebSocketConfig {
   //这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

第二步是写websocket的具体实现类

实现类中注解@ServerEndpoint(value = "/websocket/{user}") 对应前端的请求方式为: new WebSocket("ws://127.0.0.1:8080/websocket/2");

而前端想通过 new WebSocket("ws://127.0.0.1:18080/testWebsocket?id=23&name=Lebron")来传递参数,我们就需要使用WebSocketHandler来实现websocket

CopyOnWriteArrayList小伙伴私下也可以研究下,应用与一些并发场景中来保证线程安全。

package com.example.websocket;


import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;


@ServerEndpoint(value = "/websocket/{user}")
@Component
public class MyWebSocket {
    // 通过类似GET请求方式传递参数的方法(服务端采用第二种方法"WebSocketHandler"实现)
//    websocket = new WebSocket("ws://127.0.0.1:18080/testWebsocket?id=23&name=Lebron");
    /**
     * 在线人数
     */
    public static AtomicInteger onlineNumber = new AtomicInteger(0);

    /**
     * 所有的对象,每次连接建立,都会将我们自己定义的MyWebSocket存放到List中,
     */
    public static List<MyWebSocket> webSockets = new CopyOnWriteArrayList<MyWebSocket>();

    /**
     * 会话,与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 每个会话的用户
     */
    private String user;

    /**
     * 建立连接
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("user") String user) {
        if (user == null || "".equals(user)) {
            try {
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }
        onlineNumber.incrementAndGet();
        for (MyWebSocket myWebSocket : webSockets) {
            if (user.equals(myWebSocket.user)) {
                try {
                    session.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return;
            }
        }
        this.session = session;
        this.user = user;
        webSockets.add(this);
        System.out.println("有新连接加入! 当前在线人数" + onlineNumber.get());
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose() {
        onlineNumber.decrementAndGet();
        webSockets.remove(this);
        System.out.println("有连接关闭! 当前在线人数" + onlineNumber.get());
    }

    /**
     * 收到客户端的消息
     *
     * @param message 消息
     * @param session 会话
     */
    @OnMessage
    public void onMessage(String message, Session session, @PathParam("user") String user) {
        System.out.println("来自" + user + "消息:" + message);
        pushMessage(user, message, null);
    }

    /**
     * 发送消息
     *
     * @param message 消息
     */
    public void sendMessage(String message) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 消息推送
     *
     * @param message
     * @param uuid    uuid为空则推送全部人员
     */
    public static void pushMessage(String user, String message, String uuid) {
        if (uuid == null || "".equals(uuid)) {
            for (MyWebSocket myWebSocket : webSockets) {
                myWebSocket.sendMessage(user + ":" + message);
            }
        } else {
            for (MyWebSocket myWebSocket : webSockets) {
                if (uuid.equals(myWebSocket.user)) {
                    myWebSocket.sendMessage(message);
                }
            }
        }

    }
}

第三步写服务器主动推送

package com.example.websocket;


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class WelcomeController {
    

    @RequestMapping(value = "/index")
    public void index() {
        MyWebSocket.pushMessage("群主", "你们好", null);
    }
}

前端

<html>
<head>
    <meta charset="UTF-8">
    <title>Web sockets test</title>
    <script type="text/javascript">
        var ws;

        function login() {
            if (!ws) {
                var user = document.getElementById("name").value;
                try {
                    ws = new WebSocket("ws://127.0.0.1:8080/websocket/" + user);//连接服务器
                    ws.onopen = function (event) {
                        console.log("已经与服务器建立了连接...");
                        alert("登陆成功,可以开始聊天了")
                    };
                    ws.onmessage = function (event) {
                        console.log("接收到服务器发送的数据..." + event.data);
                        document.getElementById("info").innerHTML += event.data + "<br>";
                    };
                    ws.onclose = function (event) {
                        console.log("已经与服务器断开连接...");
                    };
                    ws.onerror = function (event) {
                        console.log("WebSocket异常!");
                    };
                } catch (ex) {
                    alert(ex.message);
                }
                document.getElementById("login").innerHTML = "退出";
            } else {
                ws.close();
                ws = null;
            }
        }

        function SendData() {
            var data = document.getElementById("data").value;
            try {
                ws.send(data);
            } catch (ex) {
                alert(ex.message);
            }
        };

    </script>
</head>
<body>
<input id="name" value="" placeholder="用户名">
<button id="login" type="button" onclick="login()" value="">登陆</button>
<br/><br/>
<input id="data">
<button type="button" onclick='SendData();'>发送消息</button>
<br/><br/>
<div id="info">

</div>
</body>
</html>

到这里我们就实现了简单的聊天效果,对于WebSocket还不了解的小伙伴可以去试一试效果!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • WebSocket介绍
  • WebSocket特点
  • 浏览器对WebSocket支持情况
  • 实现聊天平台
    • 搭建后台
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档