整合营销服务商

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

免费咨询热线:

使用WebRTC搭建前端视频聊天室-入门篇

么是WebRTC?

众所周知,浏览器本身不支持相互之间直接建立信道进行通信,都是通过服务器进行中转。比如现在有两个客户端,甲和乙,他们俩想要通信,首先需要甲和服务器、乙和服务器之间建立信道。甲给乙发送消息时,甲先将消息发送到服务器上,服务器对甲的消息进行中转,发送到乙处,反过来也是一样。这样甲与乙之间的一次消息要通过两段信道,通信的效率同时受制于这两段信道的带宽。同时这样的信道并不适合数据流的传输,如何建立浏览器之间的点对点传输,一直困扰着开发者。WebRTC应运而生

WebRTC是一个开源项目,旨在使得浏览器能为实时通信(RTC)提供简单的JavaScript接口。说的简单明了一点就是让浏览器提供JS的即时通信接口。这个接口所创立的信道并不是像WebSocket一样,打通一个浏览器与WebSocket服务器之间的通信,而是通过一系列的信令,建立一个浏览器与浏览器之间(peer-to-peer)的信道,这个信道可以发送任何数据,而不需要经过服务器。并且WebRTC通过实现MediaStream,通过浏览器调用设备的摄像头、话筒,使得浏览器之间可以传递音频和视频

WebRTC已经在我们的浏览器中

这么好的功能,各大浏览器厂商自然不会置之不理。现在WebRTC已经可以在较新版的Chrome、Opera和Firefox中使用了,著名的浏览器兼容性查询网站caniuse上给出了一份详尽的浏览器兼容情况

另外根据36Kr前段时间的新闻Google推出支持WebRTC及Web Audio的Android 版Chrome 29@36kr和Android版Opera开始支持WebRTC,允许用户在没有任何插件的情况下实现语音和视频聊天,Android也开始支持WebRTC

三个接口

WebRTC实现了三个API,分别是:

* MediaStream:通过MediaStream的API能够通过设备的摄像头及话筒获得视频、音频的同步流

* RTCPeerConnection:RTCPeerConnection是WebRTC用于构建点对点之间稳定、高效的流传输的组件

* RTCDataChannel:RTCDataChannel使得浏览器之间(点对点)建立一个高吞吐量、低延时的信道,用于传输任意数据

这里大致上介绍一下这三个API

MediaStream(getUserMedia)

MediaStream API为WebRTC提供了从设备的摄像头、话筒获取视频、音频流数据的功能

W3C标准

W3C标准传送门

如何调用

同门可以通过调用navigator.getUserMedia(),这个方法接受三个参数:

1. 一个约束对象(constraints object),这个后面会单独讲

2. 一个调用成功的回调函数,如果调用成功,传递给它一个流对象

3. 一个调用失败的回调函数,如果调用失败,传递给它一个错误对象

浏览器兼容性

由于浏览器实现不同,他们经常会在实现标准版本之前,在方法前面加上前缀,所以一个兼容版本就像这样

var getUserMedia = (navigator.getUserMedia || 
                    navigator.webkitGetUserMedia || 
                    navigator.mozGetUserMedia || 
                    navigator.msGetUserMedia);

一个超级简单的例子

这里写一个超级简单的例子,用来展现getUserMedia的效果:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>GetUserMedia实例</title>
</head>
<body>
    <video id="video" autoplay></video>
</body>
 
 
<script type="text/javascript">
    var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
 
    getUserMedia.call(navigator, {
        video: true,
        audio: true
    }, function(localMediaStream) {
        var video = document.getElementById('video');
        video.src = window.URL.createObjectURL(localMediaStream);
        video.onloadedmetadata = function(e) {
            console.log("Label: " + localMediaStream.label);
            console.log("AudioTracks" , localMediaStream.getAudioTracks());
            console.log("VideoTracks" , localMediaStream.getVideoTracks());
        };
    }, function(e) {
        console.log('Reeeejected!', e);
    });
</script>
 
 
</html>

将这段内容保存在一个HTML文件中,放在服务器上。用较新版本的Opera、Firefox、Chrome打开,在浏览器弹出询问是否允许访问摄像头和话筒,选同意,浏览器上就会出现摄像头所拍摄到的画面了

注意,HTML文件要放在服务器上,否则会得到一个NavigatorUserMediaError的错误,显示PermissionDeniedError,最简单方法就是cd到HTML文件所在目录下,然后python -m SimpleHTTPServer(装了python的话),然后在浏览器中输入http://localhost:8000/{文件名称}.html

这里使用getUserMedia获得流之后,需要将其输出,一般是绑定到video标签上输出,需要使用window.URL.createObjectURL(localMediaStream)来创造能在video中使用src属性播放的Blob URL,注意在video上加入autoplay属性,否则只能捕获到一张图片

流创建完毕后可以通过label属性来获得其唯一的标识,还可以通过getAudioTracks()和getVideoTracks()方法来获得流的追踪对象数组(如果没有开启某种流,它的追踪对象数组将是一个空数组)

约束对象(Constraints)

约束对象可以被设置在getUserMedia()和RTCPeerConnection的addStream方法中,这个约束对象是WebRTC用来指定接受什么样的流的,其中可以定义如下属性:

* video: 是否接受视频流

* audio:是否接受音频流

* MinWidth: 视频流的最小宽度

* MaxWidth:视频流的最大宽度

* MinHeight:视频流的最小高度

* MaxHiehgt:视频流的最大高度

* MinAspectRatio:视频流的最小宽高比

* MaxAspectRatio:视频流的最大宽高比

* MinFramerate:视频流的最小帧速率

* MaxFramerate:视频流的最大帧速率

详情见Resolution Constraints in Web Real Time Communications draft-alvestrand-constraints-resolution-00

RTCPeerConnection

WebRTC使用RTCPeerConnection来在浏览器之间传递流数据,这个流数据通道是点对点的,不需要经过服务器进行中转。但是这并不意味着我们能抛弃服务器,我们仍然需要它来为我们传递信令(signaling)来建立这个信道。WebRTC没有定义用于建立信道的信令的协议:信令并不是RTCPeerConnection API的一部分

信令

既然没有定义具体的信令的协议,我们就可以选择任意方式(AJAX、WebSocket),采用任意的协议(SIP、XMPP)来传递信令,建立信道,比如我写的demo,就是用的node的ws模块,在WebSocket上传递信令

需要信令来交换的信息有三种:

* session的信息:用来初始化通信还有报错

* 网络配置:比如IP地址和端口啥的

* 媒体适配:发送方和接收方的浏览器能够接受什么样的编码器和分辨率

这些信息的交换应该在点对点的流传输之前就全部完成,一个大致的架构图如下:

通过服务器建立信道

这里再次重申,就算WebRTC提供浏览器之间的点对点信道进行数据传输,但是建立这个信道,必须有服务器的参与。WebRTC需要服务器对其进行四方面的功能支持:

1. 用户发现以及通信

2. 信令传输

3. NAT/防火墙穿越

4. 如果点对点通信建立失败,可以作为中转服务器

NAT/防火墙穿越技术

建立点对点信道的一个常见问题,就是NAT穿越技术。在处于使用了NAT设备的私有TCP/IP网络中的主机之间需要建立连接时需要使用NAT穿越技术。以往在VoIP领域经常会遇到这个问题。目前已经有很多NAT穿越技术,但没有一项是完美的,因为NAT的行为是非标准化的。这些技术中大多使用了一个公共服务器,这个服务使用了一个从全球任何地方都能访问得到的IP地址。在RTCPeeConnection中,使用ICE框架来保证RTCPeerConnection能实现NAT穿越

ICE,全名叫交互式连接建立(Interactive Connectivity Establishment),一种综合性的NAT穿越技术,它是一种框架,可以整合各种NAT穿越技术如STUN、TURN(Traversal Using Relay NAT 中继NAT实现的穿透)。ICE会先使用STUN,尝试建立一个基于UDP的连接,如果失败了,就会去TCP(先尝试HTTP,然后尝试HTTPS),如果依旧失败ICE就会使用一个中继的TURN服务器。

我们可以使用Google的STUN服务器:stun:stun.l.google.com:19302,于是乎,一个整合了ICE框架的架构应该长这个样子

浏览器兼容

还是前缀不同的问题,采用和上面类似的方法:

var PeerConnection = (window.PeerConnection ||
                    window.webkitPeerConnection00 || 
                    window.webkitRTCPeerConnection || 
                    window.mozRTCPeerConnection);

创建和使用

//使用Google的stun服务器
var iceServer = {
    "iceServers": [{
        "url": "stun:stun.l.google.com:19302"
    }]
};
//兼容浏览器的getUserMedia写法
var getUserMedia = (navigator.getUserMedia ||
                    navigator.webkitGetUserMedia || 
                    navigator.mozGetUserMedia || 
                    navigator.msGetUserMedia);
//兼容浏览器的PeerConnection写法
var PeerConnection = (window.PeerConnection ||
                    window.webkitPeerConnection00 || 
                    window.webkitRTCPeerConnection || 
                    window.mozRTCPeerConnection);
//与后台服务器的WebSocket连接
var socket = __createWebSocketChannel();
//创建PeerConnection实例
var pc = new PeerConnection(iceServer);
//发送ICE候选到其他客户端
pc.onicecandidate = function(event){
    socket.send(JSON.stringify({
        "event": "__ice_candidate",
        "data": {
            "candidate": event.candidate
        }
    }));
};
//如果检测到媒体流连接到本地,将其绑定到一个video标签上输出
pc.onaddstream = function(event){
    someVideoElement.src = URL.createObjectURL(event.stream);
};
//获取本地的媒体流,并绑定到一个video标签上输出,并且发送这个媒体流给其他客户端
getUserMedia.call(navigator, {
    "audio": true,
    "video": true
}, function(stream){
    //发送offer和answer的函数,发送本地session描述
    var sendOfferFn = function(desc){
            pc.setLocalDescription(desc);
            socket.send(JSON.stringify({ 
                "event": "__offer",
                "data": {
                    "sdp": desc
                }
            }));
        },
        sendAnswerFn = function(desc){
            pc.setLocalDescription(desc);
            socket.send(JSON.stringify({ 
                "event": "__answer",
                "data": {
                    "sdp": desc
                }
            }));
        };
    //绑定本地媒体流到video标签用于输出
    myselfVideoElement.src = URL.createObjectURL(stream);
    //向PeerConnection中加入需要发送的流
    pc.addStream(stream);
    //如果是发送方则发送一个offer信令,否则发送一个answer信令
    if(isCaller){
        pc.createOffer(sendOfferFn);
    } else {
        pc.createAnswer(sendAnswerFn);
    }
}, function(error){
    //处理媒体流创建失败错误
});
//处理到来的信令
socket.onmessage = function(event){
    var json = JSON.parse(event.data);
    //如果是一个ICE的候选,则将其加入到PeerConnection中,否则设定对方的session描述为传递过来的描述
    if( json.event === "__ice_candidate" ){
        pc.addIceCandidate(new RTCIceCandidate(json.data.candidate));
    } else {
         pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp));
    }
};


实例

由于涉及较为复杂灵活的信令传输,故这里不做简短的实例,可以直接移步到最后

RTCDataChannel

既然能建立点对点的信道来传递实时的视频、音频数据流,为什么不能用这个信道传一点其他数据呢?RTCDataChannel API就是用来干这个的,基于它我们可以在浏览器之间传输任意数据。DataChannel是建立在PeerConnection上的,不能单独使用

使用DataChannel

我们可以使用channel = pc.createDataCHannel("someLabel");来在PeerConnection的实例上创建Data Channel,并给与它一个标签

DataChannel使用方式几乎和WebSocket一样,有几个事件:

* onopen

* onclose

* onmessage

* onerror

同时它有几个状态,可以通过readyState获取:

* connecting: 浏览器之间正在试图建立channel

* open:建立成功,可以使用send方法发送数据了

* closing:浏览器正在关闭channel

* closed:channel已经被关闭了

两个暴露的方法:

* close(): 用于关闭channel

* send():用于通过channel向对方发送数据

通过Data Channel发送文件大致思路

JavaScript已经提供了File API从input[type='file']的元素中提取文件,并通过FileReader来将文件的转换成DataURL,这也意味着我们可以将DataURL分成多个碎片来通过Channel来进行文件传输

一个综合的Demo

SkyRTC-demo,这是我写的一个Demo。建立一个视频聊天室,并能够广播文件,当然也支持单对单文件传输,写得还很粗糙,后期会继续完善

使用方式

1.下载解压并cd到目录下

2.运行npm install安装依赖的库(express, ws, node-uuid)

3.运行node server.js,访问localhost:3000,允许摄像头访问

4.打开另一台电脑,在浏览器(Chrome和Opera,还未兼容Firefox)打开{server所在IP}:3000,允许摄像头和话筒访问

5.广播文件:在左下角选定一个文件,点击“发送文件”按钮

6.广播信息:左下角input框输入信息,点击发送

7.可能会出错,注意F12对话框,一般F5能解决

功能

视频音频聊天(连接了摄像头和话筒,至少要有摄像头),广播文件(可单独传播,提供API,广播就是基于单独传播实现的,可同时传播多个,小文件还好说,大文件坐等内存吃光),广播聊天信息

参考资料

WebRTC官方网站

W3C-GetUserMedia

W3C-WebRTC

Capturing Audio & Video in HTML5@html5rocks

Getting Started with WebRTC@html5rocks

caniuse

ICE交互式连接建立

文地址:https://dwz.cn/87Mq3Br1

作者:yizhiwazi

推荐WebSocket的三大理由:

  • 1、采用全双工通信,摆脱传统HTTP轮询的窘境。
  • 2、采用W3C国际标准,完美支持HTML5。
  • 3、简单高效,容易上手。

学习目标

快速学会通过WebSocket编写简单聊天功能。

温馨提示:

1、WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

2、浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

3、当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

使用教程

一、打造 WebSocket 聊天客户端

温馨提示:得益于W3C国际标准的实现,我们在浏览器JS就能直接创建WebSocket对象,再通过简单的回调函数就能完成WebSocket客户端的编写,非常简单!接下来让我们一探究竟。

使用说明:

使用步骤:1、获取WebSocket客户端对象。

例如: var webSocket = new WebSocket(url);

使用步骤:2、获取WebSocket回调函数。

例如:webSocket.onmessage = function (event) {console.log('WebSocket收到消息:' + event.data);

使用步骤:3、发送消息给服务端

例如:webSokcet.send(jsonStr) 结合实际场景 本案例采用JSON字符串进行消息通信。

具体实现:

下面是本案例在线聊天的客户端实现的JS代码,附带详细注释。

<script>
 /**
 * WebSocket客户端
 *
 * 使用说明:
 * 1、WebSocket客户端通过回调函数来接收服务端消息。例如:webSocket.onmessage
 * 2、WebSocket客户端通过send方法来发送消息给服务端。例如:webSocket.send();
 */
 function getWebSocket() {
 /**
 * WebSocket客户端 PS:URL开头表示WebSocket协议 中间是域名端口 结尾是服务端映射地址
 */
 var webSocket = new WebSocket('ws://localhost:8080/chat');
 /**
 * 当服务端打开连接
 */
 webSocket.onopen = function (event) {
 console.log('WebSocket打开连接');
 };
 /**
 * 当服务端发来消息:1.广播消息 2.更新在线人数
 */
 webSocket.onmessage = function (event) {
 console.log('WebSocket收到消息:%c' + event.data, 'color:green');
 //获取服务端消息
 var message = JSON.parse(event.data) || {};
 var $messageContainer = $('.message-container');
 //喉咙发炎
 if (message.type === 'SPEAK') {
 $messageContainer.append(
 '<div class="mdui-card" style="margin: 10px 0;">' +
 '<div class="mdui-card-primary">' +
 '<div class="mdui-card-content message-content">' + message.username + ":" + message.msg + '</div>' +
 '</div></div>');
 }
 $('.chat-num').text(message.onlineCount);
 //防止刷屏
 var $cards = $messageContainer.children('.mdui-card:visible').toArray();
 if ($cards.length > 5) {
 $cards.forEach(function (item, index) {
 index < $cards.length - 5 && $(item).slideUp('fast');
 });
 }
 };
 /**
 * 关闭连接
 */
 webSocket.onclose = function (event) {
 console.log('WebSocket关闭连接');
 };
 /**
 * 通信失败
 */
 webSocket.onerror = function (event) {
 console.log('WebSocket发生异常');
 };
 return webSocket;
 }
 var webSocket = getWebSocket();
 /**
 * 通过WebSocket对象发送消息给服务端
 */
 function sendMsgToServer() {
 var $message = $('#msg');
 if ($message.val()) {
 webSocket.send(JSON.stringify({username: $('#username').text(), msg: $message.val()}));
 $message.val(null);
 }
 }
 /**
 * 清屏
 */
 function clearMsg(){
 $(".message-container").empty();
 }
 /**
 * 使用ENTER发送消息
 */
 document.onkeydown = function (event) {
 var e = event || window.event || arguments.callee.caller.arguments[0];
 e.keyCode === 13 && sendMsgToServer();
 };
</script>

========================================================================

二、打造 WebSocket 聊天服务端

温馨提示:得益于SpringBoot提供的自动配置,我们只需要通过简单注解@ServerEndpoint就就能创建WebSocket服务端,再通过简单的回调函数就能完成WebSocket服务端的编写,比起客户端的使用同样非常简单!

使用说明:

首先在POM文件引入spring-boot-starter-websocket 、thymeleaf 、FastJson等依赖。

使用步骤:1、开启WebSocket服务端的自动注册。

【这里需要特别提醒:ServerEndpointExporter 是由Spring官方提供的标准实现,用于扫描ServerEndpointConfig配置类和@ServerEndpoint注解实例。使用规则也很简单:1.如果使用默认的嵌入式容器 比如Tomcat 则必须手工在上下文提供ServerEndpointExporter。2. 如果使用外部容器部署war包,则不要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描服务端的行为交给外部容器处理。】

@Configuration
public class WebSocketConfig {
 @Bean
 public ServerEndpointExporter serverEndpointExporter() {
 return new ServerEndpointExporter();
 }
}

使用步骤:2、创建WebSocket服务端。

核心思路:

  • ① 通过注解@ServerEndpoint来声明实例化WebSocket服务端。
  • ② 通过注解@OnOpen、@OnMessage、@OnClose、@OnError 来声明回调函数。

  • ③ 通过ConcurrentHashMap保存全部在线会话对象。
@Component
@ServerEndpoint("/chat")//标记此类为服务端
public class WebSocketChatServer {
 /**
 * 全部在线会话 PS: 基于场景考虑 这里使用线程安全的Map存储会话对象。
 */
 private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>();
 /**
 * 当客户端打开连接:1.添加会话对象 2.更新在线人数
 */
 @OnOpen
 public void onOpen(Session session) {
 onlineSessions.put(session.getId(), session);
 sendMessageToAll(Message.jsonStr(Message.ENTER, "", "", onlineSessions.size()));
 }
 /**
 * 当客户端发送消息:1.获取它的用户名和消息 2.发送消息给所有人
 * <p>
 * PS: 这里约定传递的消息为JSON字符串 方便传递更多参数!
 */
 @OnMessage
 public void onMessage(Session session, String jsonStr) {
 Message message = JSON.parseObject(jsonStr, Message.class);
 sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getUsername(), message.getMsg(), onlineSessions.size()));
 }
 /**
 * 当关闭连接:1.移除会话对象 2.更新在线人数
 */
 @OnClose
 public void onClose(Session session) {
 onlineSessions.remove(session.getId());
 sendMessageToAll(Message.jsonStr(Message.QUIT, "", "下线了!", onlineSessions.size()));
 }
 /**
 * 当通信发生异常:打印错误日志
 */
 @OnError
 public void onError(Session session, Throwable error) {
 error.printStackTrace();
 }
 /**
 * 公共方法:发送信息给所有人
 */
 private static void sendMessageToAll(String msg) {
 onlineSessions.forEach((id, session) -> {
 try {
 session.getBasicRemote().sendText(msg);
 } catch (IOException e) {
 e.printStackTrace();
 }
 });
 }
}
  • ④ 通过会话对象 javax.websocket.Session 来发消息给客户端。
/**
 * WebSocket 聊天消息类
 */
package com.hehe.chat;
import com.alibaba.fastjson.JSON;
/**
 * WebSocket 聊天消息类
 */
public class Message {
 public static final String ENTER = "ENTER";
 public static final String SPEAK = "SPEAK";
 public static final String QUIT = "QUIT";
 private String type;//消息类型
 private String username; //发送人
 private String msg; //发送消息
 private int onlineCount; //在线用户数
 public static String jsonStr(String type, String username, String msg, int onlineTotal) {
 return JSON.toJSONString(new Message(type, username, msg, onlineTotal));
 }
 public Message(String type, String username, String msg, int onlineCount) {
 this.type = type;
 this.username = username;
 this.msg = msg;
 this.onlineCount = onlineCount;
 }
 //这里省略get/set方法 请自行补充
}

三、WebSocket在线聊天案例的视频演示

1、源码下载

至此,我们完成了客户端和服务端的编码,由于篇幅有限,本教程的页面代码并未完整贴上,想要完整的体验效果请在Github下载源码。

2、视频演示

上面一顿操作猛如虎,实际到底是啥样子呢,接下来由哈士奇童鞋为我们演示最终版的在线聊天案例:

四、全文总结

1、使用WebSocket用于实时双向通讯的场景,常见的如聊天室、跨系统消息推送等。

2、创建WebSocket客户端使用JS内置对象+回调函数+send方法发送消息。

3、创建WebSocket服务端使用注解声明实例+使用注解声明回调方法+使用Session发送消息。

们使用springboot和webSocket结合来编写聊天室。

首先先了解下什么是WebSocket

WebSocket 是什么?

WebSocket 是一种网络通信协议。RFC6455 定义了它的通信标准。WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

为什么需要 WebSocket ?

了解计算机网络协议的人,应该都知道:HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。

WebSocket 如何工作?

Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的HTTP连接不同,对服务器有重要的影响。基于多线程或多进程的服务器无法适用于 WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的 WebSockets 服务器端实现都需要一个异步服务器。

一.使用idea创建springboot项目


创建项目


如果不使用上述方法导入maven的 请使用

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

二.spring注入bean

package com.example.websocket.demo;
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
 public ServerEndpointExporter serverEndpointExporter() {
 return new ServerEndpointExporter();
 }
}

三.编写websocket服务类

package com.cloudkd.websocket;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
//@ServerEndpoint("/websocket/{user}")
@ServerEndpoint("/websocket")
@Component
public class WebSocketServer {
 private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
 //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
 private static int onlineCount = 0;
 //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
 //与某个客户端的连接会话,需要通过它来给客户端发送数据
 private Session session;
 /**
 * 连接建立成功调用的方法
 */
 @OnOpen
 public void onOpen(Session session) {
 this.session = session;
 //加入set中
 webSocketSet.add(this);
 //在线数加1
 addOnlineCount();
 log.info("有新连接加入!当前在线人数为" + getOnlineCount());
 try {
 sendMessage("连接成功");
 } catch (IOException e) {
 log.error("websocket IO异常");
 }
 }
 // //连接打开时执行
 // @OnOpen
 // public void onOpen(@PathParam("user") String user, Session session) {
 // currentUser = user;
 // System.out.println("Connected ... " + session.getId());
 // }
 /**
 * 连接关闭调用的方法
 */
 @OnClose
 public void onClose() {
 webSocketSet.remove(this); //从set中删除
 subOnlineCount(); //在线数减1
 log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
 }
 /**
 * 收到客户端消息后调用的方法
 *
 * @param message 客户端发送过来的消息
 */
 @OnMessage
 public void onMessage(String message, Session session) {
 log.info("来自客户端的消息:" + message);
 //群发消息
 for (WebSocketServer item : webSocketSet) {
 try {
 item.sendMessage(message);
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 }
 /**
 * @param session
 * @param error
 */
 @OnError
 public void onError(Session session, Throwable error) {
 log.error("发生错误");
 error.printStackTrace();
 }
 public void sendMessage(String message) throws IOException {
 this.session.getBasicRemote().sendText(message);
 }
 /**
 * 群发自定义消息
 */
 public static void sendInfo(String message) {
 log.info(message);
 for (WebSocketServer item : webSocketSet) {
 try {
 item.sendMessage(message);
 } catch (IOException ignored) {
 }
 }
 }
 private static synchronized int getOnlineCount() {
 return onlineCount;
 }
 private static synchronized void addOnlineCount() {
 WebSocketServer.onlineCount++;
 }
 private static synchronized void subOnlineCount() {
 WebSocketServer.onlineCount--;
 }
}

四.编写一个前端客户端

图中位置创建一个简单的index.html页面



<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
 <script>
 var websocket = null;
 //判断当前浏览器是否支持WebSocket
 if ('WebSocket' in window) {
//这里ws://192.168.1.111:8080/websocket 写自己的ip和端口号
 websocket = new WebSocket("ws://192.168.1.111:8080/websocket");
 }
 else {
 alert('Not support websocket')
 }
 //连接发生错误的回调方法
 websocket.onerror = function () {
 setMessageInnerHTML("error");
 };
 //连接成功建立的回调方法
 websocket.onopen = function (event) {
 setMessageInnerHTML("open");
 }
 //接收到消息的回调方法
 websocket.onmessage = function (event) {
 setMessageInnerHTML(event.data);
 }
 //连接关闭的回调方法
 websocket.onclose = function () {
 setMessageInnerHTML("close");
 }
 //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
 window.onbeforeunload = function () {
 websocket.close();
 }
 //将消息显示在网页上
 function setMessageInnerHTML(innerHTML) {
 document.getElementById('message').innerHTML += innerHTML + '<br/>';
 }
 //关闭连接
 function closeWebSocket() {
 websocket.close();
 }
 //发送消息
 function send() {
 var message = document.getElementById('text').value;
 websocket.send(message);
 }
 </script>
</head>
<body>
 <h3>Welcome</h3><br/>
 <input id="text" type="text"/>
 <button onclick="send()">Send</button>
 <button onclick="closeWebSocket()">Close</button>
 <div id="message"></div>
</body>
</html>

五.启动项目,做个测试.



点击启动项目


Ok启动完成

访问index.html测试一下


Ok成功启动

后台日志也有记录



说个话试试



也可以多开几个页面测试下.

Ok可以了. 下面完来完成服务端向客户端推消息

六.服务端向客户端推消息(后台主动)

产生一个消息:产生消息场景有多种,http(s),定时任务,mq等,这里我用一个http请求的controller代码完成

编写一个pushWebController类

package com.example.websocket.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class PushWebController {
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 @GetMapping(value = "/pushWeb")
 public Map<String, Object> pushVideoListToWeb(String message) {
 Map<String, Object> result = new HashMap<String, Object>();
 try {
 WebSocketServer.sendInfo("有新客户呼入,message:" + message);
 result.put("operationResult", true);
 } catch (Exception e) {
 result.put("operationResult", true);
 }
 return result;
 }
}

重新启动项目.测试下




成功了

到此位置. demo已经成功实现了. 自己感受下乐趣把.

如果你恰好也有可以用WebSocket实现的类似场景,希望对你有帮助。

Github地址:https://github.com/liangyisen/springboot_websocket