整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

RabbitMQ实现即时通讯居然如此简单

时候我们的项目中会用到即时通讯功能,比如电商系统中的客服聊天功能,还有在支付过程中,当用户支付成功后,第三方支付服务会回调我们的回调接口,此时我们需要通知前端支付成功。最近发现RabbitMQ可以很方便的实现即时通讯功能,如果你没有特殊的业务需求,甚至可以不写后端代码,今天给大家讲讲如何使用RabbitMQ来实现即时通讯!

SpringBoot实战电商项目mall(40k+star)地址:https://github.com/macrozheng/mall

转自:https://www.jianshu.com/p/e132261456b5

参考:go语言中文文档:www.topgoer.com

MQTT协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,该协议构建于TCP/IP协议上。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。

MQTT相关概念

  • Publisher(发布者):消息的发出者,负责发送消息。
  • Subscriber(订阅者):消息的订阅者,负责接收并处理消息。
  • Broker(代理):消息代理,位于消息发布者和订阅者之间,各类支持MQTT协议的消息中间件都可以充当。
  • Topic(主题):可以理解为消息队列中的路由,订阅者订阅了主题之后,就可以收到发送到该主题的消息。
  • Payload(负载);可以理解为发送消息的内容。
  • QoS(消息质量):全称Quality of Service,即消息的发送质量,主要有QoS 0、QoS 1、QoS 2三个等级,下面分别介绍下: QoS 0(Almost Once):至多一次,只发送一次,会发生消息丢失或重复; QoS 1(Atleast Once):至少一次,确保消息到达,但消息重复可能会发生; QoS 2(Exactly Once):只有一次,确保消息只到达一次。

RabbitMQ启用MQTT功能

RabbitMQ启用MQTT功能,需要先安装然RabbitMQ然后再启用MQTT插件。

  • 首先我们需要安装并启动RabbitMQ,对RabbitMQ不了解的朋友可以参考《花了3天总结的RabbitMQ实用技巧,有点东西!》;
  • 接下来就是启用RabbitMQ的MQTT插件了,默认是不启用的,使用如下命令开启即可;
rabbitmq-plugins enable rabbitmq_mqtt
  • 开启成功后,查看管理控制台,我们可以发现MQTT服务运行在1883端口上了。

MQTT客户端

我们可以使用MQTT客户端来测试MQTT的即时通讯功能,这里使用的是MQTTBox这个客户端工具。

  • 首先下载并安装好MQTTBox,下载地址:http://workswithweb.com/mqttbox.html

  • 点击Create MQTT Client按钮来创建一个MQTT客户端;

  • 接下来对MQTT客户端进行配置,主要是配置好协议端口、连接用户名密码和QoS即可;

  • 再配置一个订阅者,订阅者订阅testTopicA这个主题,我们会向这个主题发送消息;

  • 发布者向主题中发布消息,订阅者可以实时接收到。

前端直接实现即时通讯

既然MQTTBox客户端可以直接通过RabbitMQ实现即时通讯,那我们是不是直接使用前端技术也可以实现即时通讯?答案是肯定的!下面我们将通过html+javascript实现一个简单的聊天功能,真正不写一行后端代码实现即时通讯!

  • 由于RabbitMQ与Web端交互底层使用的是WebSocket,所以我们需要开启RabbitMQ的MQTT WEB支持,使用如下命令开启即可;
rabbitmq-plugins enable rabbitmq_web_mqtt
  • 开启成功后,查看管理控制台,我们可以发现MQTT的WEB服务运行在15675端口上了;

  • WEB端与MQTT服务进行通讯需要使用一个叫MQTT.js的库,项目地址:https://github.com/mqttjs/MQTT.js

  • 实现的功能非常简单,一个单聊功能,需要注意的是配置好MQTT服务的访问地址为:ws://localhost:15675/ws
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <label>目标Topic:<input id="targetTopicInput" type="text"></label><br>
    <label>发送消息:<input id="messageInput" type="text"></label><br>
    <button onclick="sendMessage()">发送</button>
    <button onclick="clearMessage()">清空</button>
    <div id="messageDiv"></div>
</div>
</body>
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<script>
    //RabbitMQ的web-mqtt连接地址
    const url = 'ws://localhost:15675/ws';
    //获取订阅的topic
    const topic = getQueryString("topic");
    //连接到消息队列
    let client = mqtt.connect(url);
    client.on('connect', function () {
        //连接成功后订阅topic
        client.subscribe(topic, function (err) {
            if (!err) {
                showMessage("订阅topic:" + topic + "成功!");
            }
        });
    });
    //获取订阅topic中的消息
    client.on('message', function (topic, message) {
        showMessage("收到消息:" + message.toString());
    });

    //发送消息
    function sendMessage() {
        let targetTopic = document.getElementById("targetTopicInput").value;
        let message = document.getElementById("messageInput").value;
        //向目标topic中发送消息
        client.publish(targetTopic, message);
        showMessage("发送消息给" + targetTopic + "的消息:" + message);
    }

    //从URL中获取参数
    function getQueryString(name) {
        let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
        let r = window.location.search.substr(1).match(reg);
        if (r != null) {
            return decodeURIComponent(r[2]);
        }
        return null;
    }

    //在消息列表中展示消息
    function showMessage(message) {
        let messageDiv = document.getElementById("messageDiv");
        let messageEle = document.createElement("div");
        messageEle.innerText = message;
        messageDiv.appendChild(messageEle);
    }

    //清空消息列表
    function clearMessage() {
        let messageDiv = document.getElementById("messageDiv");
        messageDiv.innerHTML = "";
    }
</script>
</html>
  • 接下来我们订阅不同的主题开启两个页面测试下功能(页面放在了SpringBoot应用的resource目录下了,需要先启动应用再访问): 第一个订阅主题testTopicA,访问地址:http://localhost:8088/page/index?topic=testTopicA 第二个订阅主题testTopicB,访问地址:http://localhost:8088/page/index?topic=testTopicB
  • 之后互相发送消息,让我们来看看效果吧!

在SpringBoot中使用

没有特殊业务需求的时候,前端可以直接和RabbitMQ对接实现即时通讯。但是有时候我们需要通过服务端去通知前端,此时就需要在应用中集成MQTT了,接下来我们来讲讲如何在SpringBoot应用中使用MQTT。

  • 首先我们需要在pom.xml中添加MQTT相关依赖;
<!--Spring集成MQTT-->
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mqtt</artifactId>
</dependency>
  • 在application.yml中添加MQTT相关配置,主要是访问地址、用户名密码、默认主题信息;
rabbitmq:
  mqtt:
    url: tcp://localhost:1883
    username: guest
    password: guest
    defaultTopic: testTopic
  • 编写一个Java配置类从配置文件中读取配置便于使用;
/**
 * MQTT相关配置
 * Created by macro on 2020/9/15.
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix = "rabbitmq.mqtt")
public class MqttConfig {
    /**
     * RabbitMQ连接用户名
     */
    private String username;
    /**
     * RabbitMQ连接密码
     */
    private String password;
    /**
     * RabbitMQ的MQTT默认topic
     */
    private String defaultTopic;
    /**
     * RabbitMQ的MQTT连接地址
     */
    private String url;
}
  • 添加MQTT消息订阅者相关配置,使用@ServiceActivator注解声明一个服务激活器,通过MessageHandler来处理订阅消息;
/**
 * MQTT消息订阅者相关配置
 * Created by macro on 2020/9/15.
 */
@Slf4j
@Configuration
public class MqttInboundConfig {
    @Autowired
    private MqttConfig mqttConfig;

    @Bean
    public MessageChannel mqttInputChannel() {
        return new DirectChannel();
    }

    @Bean
    public MessageProducer inbound() {
        MqttPahoMessageDrivenChannelAdapter adapter =
                new MqttPahoMessageDrivenChannelAdapter(mqttConfig.getUrl(), "subscriberClient",
                        mqttConfig.getDefaultTopic());
        adapter.setCompletionTimeout(5000);
        adapter.setConverter(new DefaultPahoMessageConverter());
        //设置消息质量:0->至多一次;1->至少一次;2->只有一次
        adapter.setQos(1);
        adapter.setOutputChannel(mqttInputChannel());
        return adapter;
    }

    @Bean
    @ServiceActivator(inputChannel = "mqttInputChannel")
    public MessageHandler handler() {
        return new MessageHandler() {

            @Override
            public void handleMessage(Message<?> message) throws MessagingException {
                //处理订阅消息
                log.info("handleMessage : {}",message.getPayload());
            }

        };
    }
}
  • 添加MQTT消息发布者相关配置;
/**
 * MQTT消息发布者相关配置
 * Created by macro on 2020/9/15.
 */
@Configuration
public class MqttOutboundConfig {

    @Autowired
    private MqttConfig mqttConfig;

    @Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        MqttConnectOptions options = new MqttConnectOptions();
        options.setServerURIs(new String[] { mqttConfig.getUrl()});
        options.setUserName(mqttConfig.getUsername());
        options.setPassword(mqttConfig.getPassword().toCharArray());
        factory.setConnectionOptions(options);
        return factory;
    }

    @Bean
    @ServiceActivator(inputChannel = "mqttOutboundChannel")
    public MessageHandler mqttOutbound() {
        MqttPahoMessageHandler messageHandler =
                new MqttPahoMessageHandler("publisherClient", mqttClientFactory());
        messageHandler.setAsync(true);
        messageHandler.setDefaultTopic(mqttConfig.getDefaultTopic());
        return messageHandler;
    }

    @Bean
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }
}
  • 添加MQTT网关,用于向主题中发送消息;
/**
 * MQTT网关,通过接口将数据传递到集成流
 * Created by macro on 2020/9/15.
 */
@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
    /**
     * 发送消息到默认topic
     */
    void sendToMqtt(String payload);

    /**
     * 发送消息到指定topic
     */
    void sendToMqtt(String payload, @Header(MqttHeaders.TOPIC) String topic);

    /**
     * 发送消息到指定topic并设置QOS
     */
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}
  • 添加MQTT测试接口,使用MQTT网关向特定主题中发送消息;
/**
 * MQTT测试接口
 * Created by macro on 2020/9/15.
 */
@Api(tags = "MqttController", description = "MQTT测试接口")
@RestController
@RequestMapping("/mqtt")
public class MqttController {

    @Autowired
    private MqttGateway mqttGateway;

    @PostMapping("/sendToDefaultTopic")
    @ApiOperation("向默认主题发送消息")
    public CommonResult sendToDefaultTopic(String payload) {
        mqttGateway.sendToMqtt(payload);
        return CommonResult.success(null);
    }

    @PostMapping("/sendToTopic")
    @ApiOperation("向指定主题发送消息")
    public CommonResult sendToTopic(String payload, String topic) {
        mqttGateway.sendToMqtt(payload, topic);
        return CommonResult.success(null);
    }
}
  • 调用接口向主题中发送消息进行测试;

  • 后台成功接收到消息并进行打印。
2020-09-17 14:29:01.689  INFO 11192 --- [ubscriberClient] c.m.mall.tiny.config.MqttInboundConfig   : handleMessage : 来自网页上的消息
2020-09-17 14:29:06.101  INFO 11192 --- [ubscriberClient] c.m.mall.tiny.config.MqttInboundConfig   : handleMessage : 来自网页上的消息
2020-09-17 14:29:07.384  INFO 11192 --- [ubscriberClient] c.m.mall.tiny.config.MqttInboundConfig   : handleMessage : 来自网页上的消息

总结

消息中间件应用越来越广泛,不仅可以实现可靠的异步通信,还可以实现即时通讯,掌握一个消息中间件还是很有必要的。如果没有特殊业务需求,客户端或者前端直接使用MQTT对接消息中间件即可实现即时通讯,有特殊需求的时候也可以使用SpringBoot集成MQTT的方式来实现,总之消息中间件是实现即时通讯的一个好选择!

项目源码地址

https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-mqtt

用php+swoole+redis 简单实现网页即时聊天,需要浏览器支持html5的websocket,

websocket是不同于http的另外一种网络通信协议,能够进行双向通信,基于此,可开发出各种实时通信产品,简单做了个聊天demo,顺便分享一下

效果图如下:

环境:

  • 系统 centos7.5
  • php7.2.9
  • redis5.0.0
  • swoole4.2.2
  • nginx 1.8

参考文档:

  • redis官网 https://redis.io
  • 教程 http://www.runoob.com/redis/
  • swoole 官网 https://swoole.com
  • swoole 的webSocket手册:https://wiki.swoole.com/wiki/page/397.html
  • php扩展库地址 http://pecl.php.net/

IP与端口:

  • 虚拟机的IP: 192.168.1.100
  • webSocket服务端口是 9520
  • redis服务端口是 6379

服务器端代码 websocket.php

<?php
class Server
{
private $serv;
private $conn = null;
private static $fd = null;
public function __construct()
{
$this->redis_connect();
$this->serv = new swoole_websocket_server("0.0.0.0", 9502);
$this->serv->set(array(
'worker_num' => 8,
'daemonize' => false,
'max_request' => 10000,
'dispatch_mode' => 2,
'debug_mode' => 1
));
echo "start \n";
$this->serv->on('Open', array($this, 'onOpen'));
$this->serv->on('Message', array($this, 'onMessage'));
$this->serv->on('Close', array($this, 'onClose'));
$this->serv->start();
}
function onOpen($server, $req)
{
echo "connection open: {$req->fd} \n";
// $server->push($req->fd, json_encode(33));
}
public function onMessage($server, $frame)
{
//echo "received data $frame->data \n";
//$server->push($frame->fd, json_encode(["hello", "world"]));
$pData = json_decode($frame->data,true);
$fd=$frame->fd;
if(empty($pData)){
echo "received data null \n";
return;
}
echo "received fd=>{$fd} message: {$frame->data}\n";
$data = [];
if (isset($pData['content'])) {
$f_fd = $this->getFd($pData['fid']); //获取绑定的fd
$data = $this->add($pData['uid'], $pData['fid'], $pData['content']); //保存消息
$server->push($f_fd, json_encode($data)); //推送到接收者
$json_data=json_encode($data);
echo "推送到接收者 fd=>{$f_fd} message: {$json_data}\n";
} else {
$this->unBind($pData['uid']); //首次接入,清除绑定数据
if ($this->bind($pData['uid'], $fd)) { //绑定fd
$data = $this->loadHistory($pData['uid'], $pData['fid']); //加载历史记录
} else {
$data = array("content" => "无法绑定fd");
}
}
$json_data=json_encode($data);
echo "推送到发送者 fd=>{$fd} message: {$json_data}\n";
$server->push($fd, json_encode($data)); //推送到发送者
}
public function onClose($server, $fd)
{
//$this->unBind($fd);
echo "connection close: {$fd}\n";
}
/*******************/
/**
* redis
* @param string $host
* @param string $port
* @return bool
*/
function redis_connect($host='127.0.0.1',$port='6379')
{
$this->conn = new Redis();
try{
$this->conn->connect($host, $port);
}catch (\Exception $e){
user_error(print_r($e));
}
return true;
}
/**
* 保存消息
* @param $uid 发送者uid
* @param $fid 接收者uid
* @param $content 内容
* @return array
*/
public function add($uid, $fid, $content)
{
$msg_data=[];
$msg_data['uid']=$uid;
$msg_data['fid']=$fid;
$msg_data['content']=$content;
$msg_data['time']=time();
$key=K::KEY_MSG;
$data=$this->conn->get($key);
if(!empty($data)){
$data=json_decode($data,true);
}else{
$data=[];
}
$data[]=$msg_data;
$this->conn->set($key,json_encode($data));
$return_msg[]=$msg_data;
return $return_msg;
}
/**
* 绑定FD
* @param $uid
* @param $fd
* @return bool
*/
public function bind($uid, $fd)
{
$key=K::KEY_UID."{$uid}";
$ret=$this->conn->set($key,$fd);
if(!$ret){
echo "bind fail \n";
return false;
}
return true;
}
/**
* 获取FD
* @param $uid
* @return mixed
*/
public function getFd($uid)
{
$key=K::KEY_UID."{$uid}";
$fd=$this->conn->get($key);
return $fd;
}
/**
* 清除绑定
* @param $uid
* @return bool
*/
public function unBind($uid)
{
$key=K::KEY_UID."{$uid}";
$ret=$this->conn->delete($key);
if(!$ret){
return false;
}
return true;
}
/**
* 历史记录
* @param $uid
* @param $fid
* @param null $id
* @return array
*/
public function loadHistory($uid, $fid)
{
$msg_data=[];
$key=K::KEY_MSG;
$this->conn->delete($key);
$data=$this->conn->get($key);
if($data){
echo $data;
$json_data=json_decode($data,true);
foreach ($json_data as $k=>$info){
if(($info['uid']==$uid&&$info['fid']==$fid)||($info['uid']==$fid&&$info['fid']==$uid)){
$msg_data[] = $info;
}
}
}
return $msg_data;
}
}
//Key 定义
class K{
const KEY_MSG = 'msg_data';
const KEY_FD = 'fd_data';
const KEY_UID = 'uid';
}
//启动服务器
$server = new Server();

客户端代码 chat.html

<!DOCTYPE html>
<html lang="en">
<html>
<head>
 <title>CHAT A</title>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <script src="jquery.min.js"></script>
 <script src="jquery.json.min.js"></script>
 <style type="text/css">
 .talk_con{
 width:600px;
 height:500px;
 border:1px solid #666;
 margin:50px auto 0;
 background:#f9f9f9;
 }
 .talk_show{
 width:580px;
 height:420px;
 border:1px solid #666;
 background:#fff;
 margin:10px auto 0;
 overflow:auto;
 }
 .talk_input{
 width:580px;
 margin:10px auto 0;
 }
 .whotalk{
 width:80px;
 height:30px;
 float:left;
 outline:none;
 }
 .talk_word{
 width:420px;
 height:26px;
 padding:0px;
 float:left;
 margin-left:10px;
 outline:none;
 text-indent:10px;
 }
 .talk_sub{
 width:56px;
 height:30px;
 float:left;
 margin-left:10px;
 }
 .close{
 width:56px;
 height:30px;
 float:left;
 margin-left:10px;
 }
 .atalk{
 margin:10px;
 }
 .atalk span{
 display:inline-block;
 background:#0181cc;
 border-radius:10px;
 color:#fff;
 padding:5px 10px;
 }
 .btalk{
 margin:10px;
 text-align:right;
 }
 .btalk span{
 display:inline-block;
 background:#ef8201;
 border-radius:10px;
 color:#fff;
 padding:5px 10px;
 }
 </style>
 <script type="text/javascript">
 var uid = 'A'; //发送者uid
 var fid = 'B'; //接收者uid
 var wsUrl = 'ws://192.168.1.100:9502';
 var webSocket = new WebSocket(wsUrl);
 //创建Socket
 webSocket.onopen = function (event) {
 console.log('onOpen=' + event.data);
 //webSocket.send("hello webSocket");
 initData(); //初始化数据,加载历史记录
 };
 //接收数据事件
 webSocket.onmessage = function (event) {
 console.log('onMessage=' + event.data);
 loadData($.parseJSON(event.data)); //导入消息记录,加载新的消息
 }
 //关闭socket
 webSocket.onclose = function (event) {
 console.log('close');
 };
 //socket连接错误
 webSocket.onerror = function (event) {
 console.log('error-data:' + event.data);
 }
 //========================================================
 //向服务器发送数据
 function sendMsg() {
 var pData = {
 content: document.getElementById('content').value,
 uid: uid,
 fid: fid,
 }
 if (pData.content == '') {
 alert("消息不能为空");
 return;
 }
 webSocket.send($.toJSON(pData)); //发送消息
 }
 function initData() {
 //var Who = document.getElementById("who").value;
 console.log('initData uid:' + uid + ' fid:'+fid);
 var pData = {
 uid: uid,
 fid: fid,
 }
 webSocket.send($.toJSON(pData)); //获取消息记录,绑定fd
 var html = '<div class="atalk"><span id="asay">' + 'WebSocket连接成功' + '</div>';
 $("#words").append(html);
 }
 function loadData(data) {
 for (var i = 0; i < data.length; i++) {
 if(data[i].uid=='A'){
 var html = '<div class="atalk"><span id="asay">' + data[i].uid + '说: ' + data[i].content + '</div>';
 }else{
 var html = '<div class="btalk"><span id="asay">' + data[i].uid + '说: ' + data[i].content + '</div>';
 }
 $("#words").append(html);
 }
 }
 //关闭连接
 function closeWebSocket() {
 console.log('close');
 webSocket.close();
 var html = '<div class="atalk"><span id="asay">' + '已和服务器断开连接' + '</div>';
 $("#words").append(html);
 }
 </script>
</head>
<body>
<div class="talk_con">
 <div class="talk_show" id="words">
 <!--<div class="atalk"><span id="asay">A说:吃饭了吗?</span></div>-->
 <!--<div class="btalk"><span id="bsay">B说:还没呢,你呢?</span></div>-->
 </div>
 <div class="talk_input">
 <!--<select class="whotalk" id="who">-->
 <!--<option value="A" selected="selected">A说:</option>-->
 <!--<option value="B">B说:</option>-->
 <!--</select>-->
		<button class="close" onclick="closeWebSocket()">断开</button>
 <input type="text" class="talk_word" id="content">
 <input type="button" onclick="sendMsg()" value="发送" class="talk_sub" id="talksub"> 
 </div>
</div>
</body>
</html>

文件详情

  • 再复制一份客户端,修改一下发送者与接收者的uid,即可进行模拟实时聊天。
  • 此代码已经实现了加载历史记录的功能

使用方法:

安装完php、redis和swoole扩展之后,直接执行:

并可以观察下输出,看看websocket服务器是否正常

最近想做一个Web版的即时聊天为后面开发的各项功能做辅助,就需要浏览器与服务器能够实时通讯。而WebSocket这种双向通信协议,就很合适用来实现这种需求。

本篇文章主要解决C#如何实现WebSocket服务端和Javascript客户端基于wss协议的安全通信问题。

本文代码已开源至Github:https://github.com/hxsfx/WebSocketServerTest

环境

  • 编程语言:C#
  • Websocket开源库:fleck
  • SSL域名证书:腾讯云IIS版本域名证书

最终效果

代码实现

前端

1、HTML

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <link href="Content/index.css" rel="stylesheet" />
</head>
<body>
    <div id="ChatContainer">
        <div class="tip "></div>
        <div class="msgList"></div>
        <div class="msgInput">
            <textarea id="SendMsgContent"></textarea>
            <button id="SendMsgButton">发送</button>
        </div>
    </div>
    <script src="Scripts/index.js"></script>
</body>
</html>

2、JavaScript

window.onload = function () {
    var TipElement = document.querySelector("#ChatContainer > div.tip");
    var MsgListElement = document.querySelector("#ChatContainer > div.msgList");
    var SendMsgContentElement = document.getElementById("SendMsgContent");
    var SendMsgButton = document.getElementById("SendMsgButton");
    window.wss = new WebSocket("wss://xxx.hxsfx.com:xxx");
    //监听消息状态
    wss.onmessage = function (e) {
        var dataJson = JSON.parse(e.data);
        loadData(dataJson.nickName, dataJson.msg, dataJson.date, dataJson.time, true);
    }
    //监听链接状态
    wss.onopen = function () {
        if (TipElement.className.indexOf("conn") < 0) {
            TipElement.className = TipElement.className + " conn";
        }
        if (TipElement.className.indexOf("disConn") >= 0) {
            TipElement.className = TipElement.className.replace("disConn", "");
        }
    }
    //监听关闭状态
    wss.onclose = function () {
        if (TipElement.className.indexOf("conn") >= 0) {
            TipElement.className = TipElement.className.replace("conn", "");
        }
        if (TipElement.className.indexOf("disConn") < 0) {
            TipElement.className = TipElement.className + " disConn";
        }
    }
    //监控输入框回车键(直接发送输入内容)
    SendMsgContentElement.onkeydown = function () {
        if (event.keyCode == 13 && SendMsgContentElement.value.trim() != "") {
            if (SendMsgContentElement.value.trim() != "") {
                SendMsgButton.click();
                event.returnValue = false;
            } else {
                SendMsgContentElement.value = "";
            }
        }
    }
    //发送按钮点击事件
    SendMsgButton.onclick = function () {
        var msgDataJson = {
            msg: SendMsgContentElement.value,
        };
        SendMsgContentElement.value = "";
        var today = new Date();
        var date = today.getFullYear() + "年" + (today.getMonth() + 1) + "月" + today.getDate() + "日";
        var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
        loadData("自己", msgDataJson.msg, date, time, false);
        let msgDataJsonStr = JSON.stringify(msgDataJson);
        wss.send(msgDataJsonStr);
    }
    //把数据加载到对话框中
    function loadData(nickName, msg, date, time, isOther) {
        let msgItemElement = document.createElement('div');
        if (isOther) {
            msgItemElement.className = "msgItem other";
        } else {
            msgItemElement.className = "msgItem self";
        }
        let chatHeadElement = document.createElement('div');
        chatHeadElement.className = "chatHead";
        chatHeadElement.innerHTML = "<svg viewBox=\"0 0 1024 1024\"><path d=\"M956.696128 512.75827c0 245.270123-199.054545 444.137403-444.615287 444.137403-245.538229 0-444.522166-198.868303-444.522166-444.137403 0-188.264804 117.181863-349.108073 282.675034-413.747255 50.002834-20.171412 104.631012-31.311123 161.858388-31.311123 57.297984 0 111.87909 11.128455 161.928996 31.311123C839.504032 163.650197 956.696128 324.494489 956.696128 512.75827L956.696128 512.75827M341.214289 419.091984c0 74.846662 38.349423 139.64855 94.097098 171.367973 23.119557 13.155624 49.151443 20.742417 76.769454 20.742417 26.64894 0 51.773154-7.096628 74.286913-19.355837 57.06467-31.113625 96.650247-96.707552 96.650247-172.742273 0-105.867166-76.664054-192.039781-170.936137-192.039781C417.867086 227.053226 341.214289 313.226864 341.214289 419.091984L341.214289 419.091984M513.886977 928.114163c129.883139 0 245.746984-59.732429 321.688583-153.211451-8.971325-73.739445-80.824817-136.51314-182.517917-167.825286-38.407752 34.55091-87.478354 55.340399-140.989081 55.340399-54.698786 0-104.770182-21.907962-143.55144-57.96211-98.921987 28.234041-171.379229 85.823668-188.368158 154.831344C255.507278 861.657588 376.965537 928.114163 513.886977 928.114163L513.886977 928.114163M513.886977 928.114163 513.886977 928.114163z\"></path></svg>";
        let msgMainElement = document.createElement('div');
        msgMainElement.className = "msgMain";
        let nickNameElement = document.createElement('div');
        nickNameElement.className = "nickName";
        nickNameElement.innerText = nickName;
        let msgElement = document.createElement('div');
        msgElement.className = "msg";
        msgElement.innerText = msg;
        let timeElement = document.createElement('div');
        timeElement.className = "time";
        let time_date_Element = document.createElement('span');
        time_date_Element.innerText = date;
        let time_time_Element = document.createElement('span');
        time_time_Element.innerText = time;
        timeElement.append(time_date_Element);
        timeElement.append(time_time_Element);
        msgMainElement.append(nickNameElement);
        msgMainElement.append(msgElement);
        msgMainElement.append(timeElement);
        msgItemElement.append(chatHeadElement);
        msgItemElement.append(msgMainElement);
        MsgListElement.append(msgItemElement);
        MsgListElement.scrollTop = MsgListElement.scrollHeight - MsgListElement.clientHeight;
    }
} 

3、CSS

* {
  padding: 0;
  margin: 0;
}
html,
body {
  font-size: 14px;
  height: 100%;
}
body {
  padding: 2%;
  box-sizing: border-box;
  background-color: #a3aebc;
}
#ChatContainer {
  padding: 1% 25px 0 25px;
  width: 80%;
  max-width: 850px;
  height: 100%;
  background-color: #fefefe;
  border-radius: 10px;
  box-sizing: border-box;
  margin: auto;
}
#ChatContainer .tip {
  height: 30px;
  line-height: 30px;
  text-align: center;
  align-items: center;
  justify-content: center;
  color: #999999;
}
#ChatContainer .tip:before {
  content: "连接中";
}
#ChatContainer .tip.disConn {
  color: red;
}
#ChatContainer .tip.disConn:before {
  content: "× 连接已断开";
}
#ChatContainer .tip.conn {
  color: green;
}
#ChatContainer .tip.conn:before {
  content: "√ 已连接";
}
#ChatContainer .msgList {
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
  overflow-y: auto;
  height: calc(100% - 100px);
}
#ChatContainer .msgList .msgItem {
  display: flex;
  margin: 5px;
}
#ChatContainer .msgList .msgItem .chatHead {
  height: 36px;
  width: 36px;
  background-color: #ffffff;
  border-radius: 100%;
}
#ChatContainer .msgList .msgItem .msgMain {
  margin: 0 5px;
  display: flex;
  flex-direction: column;
}
#ChatContainer .msgList .msgItem .msgMain .nickName {
  color: #666666;
}
#ChatContainer .msgList .msgItem .msgMain .msg {
  padding: 10px;
  line-height: 30px;
  color: #333333;
}
#ChatContainer .msgList .msgItem .msgMain .time {
  color: #999999;
  font-size: 9px;
}
#ChatContainer .msgList .msgItem .msgMain .time span:first-child {
  margin-right: 3px;
}
#ChatContainer .msgList .self {
  flex-direction: row-reverse;
}
#ChatContainer .msgList .self .nickName {
  text-align: right;
}
#ChatContainer .msgList .self .msg {
  border-radius: 10px 0 10px 10px;
  background-color: #d6e5f6;
}
#ChatContainer .msgList .self .time {
  text-align: right;
}
#ChatContainer .msgList .other .msg {
  border-radius: 0 10px 10px 10px;
  background-color: #e8eaed;
}
#ChatContainer .msgInput {
  margin: 15px 0;
  display: flex;
}
#ChatContainer .msgInput textarea {
  font-size: 16px;
  padding: 0 5px;
  width: 80%;
  box-sizing: border-box;
  height: 40px;
  line-height: 40px;
  overflow: hidden;
  color: #333333;
  border-radius: 10px 0 0 10px;
  border: none;
  outline: none;
  border: 1px solid #eee;
  resize: none;
}
#ChatContainer .msgInput button {
  width: 20%;
  text-align: center;
  height: 40px;
  line-height: 40px;
  color: #fefefe;
  background-color: #2a6bf2;
  border-radius: 0 10px 10px 0;
  border: 1px solid #2a6bf2;
}

后端

创建控制台程序(通过cmd命令调用,可修改源码为直接运行使用),然后进入Gnet安装fleck,其中的主要代码如下(完整源码移步github获取):

//组合监听地址
var loaction = webSocketProtocol + "://" + ListenIP + ":" + ListenPort;
var webSocketServer = new WebSocketServer(loaction);
if (loaction.StartsWith("wss://"))
{
    webSocketServer.Certificate = new X509Certificate2(pfxFilePath, pfxPassword
   , X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet
    );
    webSocketServer.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12;
}//当为安全链接时,将证书信息写入链接
//开始侦听
webSocketServer.Start(socket =>
{
    var socketConnectionInfo = socket.ConnectionInfo;
    var clientId = socketConnectionInfo.ClientIpAddress + ":" + socketConnectionInfo.ClientPort;
    socket.OnOpen = () =>
    {
        if (!ip_scoket_Dic.ContainsKey(clientId))
        {
            ip_scoket_Dic.Add(clientId, socket);
        }
        Console.WriteLine(CustomSend("服务端", $"[{clientId}]加入"));
    };
    socket.OnClose = () =>
    {
        if (ip_scoket_Dic.ContainsKey(clientId))
        {
            ip_scoket_Dic.Remove(clientId);
        }
        Console.WriteLine(CustomSend("服务端", $"[{clientId}]离开"));
    };
    socket.OnMessage = message =>
    {
        //将发送过来的json字符串进行解析
        var msgModel = JsonConvert.DeserializeObject<MsgModel>(message);
        Console.WriteLine(CustomSend(clientId, msgModel.msg, clientId));
    };
});
//出错后进行重启  
webSocketServer.RestartAfterListenError = true;
Console.WriteLine("【开始监听】" + loaction);
//服务端发送消息给客户端
do
{
    Console.WriteLine(CustomSend("服务端", Console.ReadLine()));
} while (true);

问题及解决方法

问题:WebSocket connection to 'wss://xxx.xxx.xxx.xxx:xxxx/' failed:

解决方法:要建立WSS安全通道,必须要先申请域名SSL证书,同时在防火墙中开放指定端口,以及前端WSS请求域名要跟SSL证书域名相同。