还是老规矩,一步步的教大家如何建立前后端的 websocket 链接,并能完成互相传送数据的简单功能。由于网上找了半天发现很多帖子都是东一句西一句的,要不就是写的没什么注释和解释,导致我这个前端人员看后端代码非常折磨。
但是,总算慢慢摸索给整出来了,那现在我就把一个详细版的,用前端小伙伴也听得懂的大白话来说一下如何实现 websocket 功能。





防止有后端小伙伴想写个前端代码的,不知道写哪里。我给个结构图
代码直接复制,然后再views内创建一个vue文件放进去就可以了,我这是全部代码,不需要改动。

<template>
<!-- websocketceshi -->
<div class="layout">
<div class="msgBody">{{ msg }}</div>
<input v-model="sendMsg" style="width:200px;height:30px;margin-top:20px"/>
<button @click="sendMessage" style="width:100px;height:30px;">发送</button>
<button @click="close" style="width:100px;height:30px;">断开链接</button>
<button @click="init" style="width:100px;height:30px;">建立链接</button>
</div>
</template>
<script>
export default {
name: "LayOut",
data() {
return {
msg: "",
sendMsg: "",
//后台的地址,只需要动localhost:8001部分,改成你后端的地址。我自己电脑上本地开的就直接用本地的了。
//后面webSocket是后台设定的接口地址,admin是你这个前台的识别码id。用于区分,比如你多个地方链接后台,后台推送数据的时候需要根据这个id不同,给对应的人推送,不然就推送到所有建立链接的网页上了
path: "ws://localhost:8001/webSocket/admin",
//存websocket实例化的
socket: "",
};
},
methods: {
//用于前台发送数据到后台,调用websocket中的send方法把数据发过去。
sendMessage() {
this.socket.send(this.sendMsg);
},
//初始化建立前后台链接
init() {
if (typeof WebSocket === "undefined") {
alert("您的浏览器不支持socket");
} else {
// 实例化socket
this.socket = new WebSocket(this.path);
// 监听socket连接
this.socket.onopen = this.open;
// 监听socket错误信息
this.socket.onerror = this.error;
// 监听socket消息
this.socket.onmessage = this.getMessage;
this.socket.onclose = this.close;
}
},
//链接成功时的回调函数
open() {
console.log("socket连接成功");
},
//链接错误时的回调
error(err) {
console.log("连接错误" + err);
},
//后台消息推送过来,接收的函数,参数为后台推过来的数据。
getMessage(msg) {
this.msg = msg.data;
},
//链接关闭的回调
close(event) {
//socket是链接的实例,close就是关闭链接
this.socket.close()
console.log("断开链接成功");
},
},
created() {
//开局初始化建立链接
this.init();
},
};
</script>
<style scoped>
.layout {
position: relative;
width: 100%;
height: 100%;
}
.msgBody {
width: 500px;
height: 300px;
border: 1px solid rgb(95, 79, 79);
}
</style>
老规矩,防止前端小伙伴想写后端的代码,也放图解释一下结构,这里注明,我是用的idea,springboot写的后端。

位置在这:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
常见问题:把配置引入进去后会出现标红的报错,像这样。图片我网上随便找的标红报错图。

原因是你直接放进去并没有从新加载把这个配置引入成功。
解决办法: 点击右上角的这个maven从新加载一次这个配置就好了。

位置:config文件夹内的WebSocketConfig文件内

代码:
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 serverEndpointConfigurator(){
return new ServerEndpointExporter();
}
//如果打包成jar包运行,bean注入这个配置类,war包的不需要。
}
位置参考上面那个图,我标注出来了,写在controller文件夹内

代码:
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
//@component (把普通pojo实例化到spring容器中,相当于配置文件中的 <bean id="" class=""/>)
//泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。
@Component
//这个注解可以理解为@RequestMapping,后面是接口地址和参数
@ServerEndpoint("/webSocket/{username}")//encoders = { ServerEncoder.class },可选值,指定编码转换器,传输对象时用到,这里直接转json就ok
public class WebSocketService {
//定义的存储类,用于保存对应的用户名,向对应的用户推送消息
private static final Map<String, Session> TOKEN_SESSION = new HashMap<>();
private static final Map<String,String> SESSION_ID_TOKEN = new HashMap<>();
//定个时间格式
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss");
@PostConstruct
public void refreshDate(){
//开启定时任务,1秒一次向前台发送当前时间
Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(()->{
//调用sendMessage方法,发送时间。
//FORMAT.format代表格式化,按照上面定义的时间格式发送
sendMessage(FORMAT.format(new Date()));
},1000,1000, TimeUnit.MILLISECONDS);
}
//链接成功时调用的方法
@OnOpen
//onopen是websocket的依赖注解,包含了可选参数session,里面是请求的信息,username是用户发来的id
public void onOpen(Session session,@PathParam("username")String username){
System.out.println("新的连接进来了"+"识别码:"+session.getId());
//username是用户发来的id
if (username == null){
try {
//检测路径参数为空时断开链接。session.close是断开链接的意思。
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT,"username参数为空"));
} catch (IOException e) {
throw new IllegalStateException(e.getMessage());
}
}else {
//往存储类中添加用户的信息。
SESSION_ID_TOKEN.put(session.getId(),username);
TOKEN_SESSION.put(username,session);
System.out.println(username);
//这里是用来从新建立链接的时候继续执行定时器,发送时间到前台,如果不写断链接后从新链接就不会执行定时器了,因为没有触发到。
this.refreshDate();
}
}
//链接关闭时的方法
@OnClose
public void onColse(Session session){
System.out.println("断开连接"+session.getId());
}
//客户端发过来的数据
@OnMessage
public void onMessage(Session session,String message){
System.out.println("收到客户端发来的消息:"+message+"识别码:"+session.getId());
//接收到客户发来的消息后调用私发信息方法告诉用户已收到信息,参数为:对应id的人(用于区分用户)和发送的信息
sendMessageToTarget(SESSION_ID_TOKEN.get(session.getId()),message+"已收到");
}
//群发信息的方法
public void sendMessage(String message){
System.out.println("发送全体消息");
//循环全部人员信息
TOKEN_SESSION.values().forEach((session)->{
//向每个用户发送文本信息。这里getAsyncRemote()解释一下,向用户发送文本信息有两种方式,一种是getBasicRemote,一种是getAsyncRemote
//区别:getAsyncRemote是异步的,不会阻塞,而getBasicRemote是同步的,会阻塞,由于同步特性,第二行的消息必须等待第一行的发送完成才能进行。
// 而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
// 则避免尽量一次发送全部消息,使用部分消息来发送,可以看到下面sendMessageToTarget方法内就用的getBasicRemote,因为这个方法是根据用户id来私发的,所以不是全部一起发送。
session.getAsyncRemote().sendText(message);
});
}
//私发信息的方法(根据用户id把信息只发送给对应的人)
//参数为:token是用户的id。后面t是发送的信息
public <T> void sendMessageToTarget(String token,Object t){
System.out.println("发送指定token消息");
try {
//发送信息,从TOKEN_SESSION存储类中找到对应id的用户,用getBasicRemote().sendText发送信息
TOKEN_SESSION.get(token).getBasicRemote().sendText((String)t);
} catch (Exception e) {
//如果报错就抛出异常
e.printStackTrace();
}
}
}
前端部分:
前端可以通过 实例.close来把链接断开,也可以通过 init方法从新建立链接。
前端使用init方法的时候会触发到后端的onOpen方法。所以如果你们有什么需要建立链接后有什么操作,可以再这个后端的方法内写。就像我写的那个从新调用计时器一样。写那里就可以了。
同理,前端使用close的时候也会触发后端的onColse方法。
后端部分:
session.close:断开链接
sendMessage:群发到所有用户页面上
sendMessageToTarget:私发到对应id的用户页面上
简单的思路扩展:
那如果想要实现一个功能,比如我有一个需求,我有一个后台管理系统,我需要在我管理页面点击一个文件我审批了,然后这个文件审批的信息会发送到对应的员工的页面上。那么我们就可以前端写一个请求,后端从新定一个接口,接口内容为点击后审批了,比如改变了数据库的审核状态啊是否合格之类的,然后在下面再加一句,调用sendMessageToTarget方法把要发送的信息通过后台直接推送发给对应的员工页面上。这样就实现了一个简单的websocket前后端交互的简单功能。
这是一个小思路的简单聊聊,顺着这个思路大家就可以去操作更多的了。
先解释一下websocket心跳机制是什么,为什么要用他。因为我们前台和后台端口一直链接的时候,中间可能会因为很多情况导致断开链接,比如断网,或者不小心关了,或者防火墙看端口很久没有数据传输给你关闭了。都是有可能的,那么这时候,你断了链接,但是并不会触发到websocket的close事件,所以程序也就不知道断了链接,前台还在往后台发送数据,但是这些数据全都接收不到,都丢失了。这时候就需要我们建立一个心跳机制
其实很简单,就是在你初始化websocket的时候开启心跳,也就是一个定时器,隔多久时间往后天发送一次信息,看看后台会不会返回给你数据,如果返回了代表链接正常不用管,把心跳的计时器时间重置,从新计时,如果发过去后没有回应了,那么就触发初始化websocket方法重新生成链接。
当后台有数据返回的时候,触发websocketonmessage方法,清空心跳的时间,如果没有返回数据,心跳的时间就会一直计时,到时间了往后台发一个信息看看有没有返回信息,有的话就清空心跳时间从新计时。

注意点:
如果你使用了我这个加入了心跳机制的前端版本,又用了我的后端代码。那需要注意把后端的这个方法删除。不然会出现心跳重连后把后台的定时器开很多个导致重复发送。
当然也可以让后台做一些处理,让他只会发送一次,但是我不会,所以就不写了。你们要是会的话就可以跳过我说的这句话。
删除的位置:

如果你不删除,就会这样,一直重复下去,后面可能叠加几十几百个定时任务。

<template>
<!-- websocketceshi -->
<div class="layout">
<div class="msgBody">{{ msg }}</div>
<input
v-model="sendMsg"
style="width: 200px; height: 30px; margin-top: 20px"
/>
<button @click="websocketsend(sendMsg)" style="width: 100px; height: 30px">
发送
</button>
<button @click="websocketclose" style="width: 100px; height: 30px">
断开链接
</button>
<button @click="initWebSocket" style="width: 100px; height: 30px">
建立链接
</button>
</div>
</template>
<script>
export default {
name: "LayOut",
data() {
return {
websock: null, //建立的连接
lockReconnect: false, //是否真正建立连接
timeout: 20 * 1000, //20秒一次心跳
timeoutObj: null, //心跳心跳倒计时
serverTimeoutObj: null, //心跳倒计时
timeoutnum: null, //断开 重连倒计时
msg: "", //显示的值
sendMsg: "", //输入框的值
};
},
created() {
// //页面刚进入时开启长连接
this.initWebSocket();
},
destroyed() {
//页面销毁时关闭长连接
this.websocketclose();
},
methods: {
//建立连接,初始化weosocket
initWebSocket() {
//后台地址,前面的ws不动,后面是后台地址,我是本地运行的所以填的本地,自行更改。再后面webSocket是后端的接口地址,admin是参数
const wsuri = "ws://localhost:8001/webSocket/admin";
//建立连接
this.websock = new WebSocket(wsuri);
//连接成功
this.websock.onopen = this.websocketonopen;
//连接错误
this.websock.onerror = this.websocketonerror;
//接收信息
this.websock.onmessage = this.websocketonmessage;
//连接关闭
this.websock.onclose = this.websocketclose;
},
reconnect() {
//重新连接
var that = this;
//判断链接状态,true就是链接,false是断开,这里如果是链接状态就不继续执行了,跳出来。
if (that.lockReconnect) {
return;
}
//把链接状态改为true
that.lockReconnect = true;
//没连接上会一直重连,设置延迟避免请求过多
that.timeoutnum && clearTimeout(that.timeoutnum);
that.timeoutnum = setTimeout(function () {
//初始化新连接
that.initWebSocket();
//把链接状态改为false
that.lockReconnect = false;
}, 5000);
},
reset() {
//重置心跳
var that = this;
//清除时间
clearTimeout(that.timeoutObj);
clearTimeout(that.serverTimeoutObj);
//重启心跳
that.start();
},
start() {
//开启心跳
var self = this;
//有延迟时间的就清除掉
self.timeoutObj && clearTimeout(self.timeoutObj);
self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);
//从新创建计时器
self.timeoutObj = setTimeout(function () {
//这里发送一个心跳,后端收到后,返回一个心跳消息
if (self.websock.readyState == 1) {
//如果连接正常发送信息到后台
self.websock.send("ping");
} else {
//否则重连
self.reconnect();
}
self.serverTimeoutObj = setTimeout(function () {
//超时关闭
self.websock.close();
}, self.timeout);
}, self.timeout);
},
//链接成功时执行的方法
websocketonopen() {
//连接成功事件
this.websocketsend("发送数据");
//提示成功
console.log("连接成功", 3);
//开启心跳
this.start();
},
//连接失败事件
websocketonerror(e) {
//错误
console.log("WebSocket连接发生错误");
//重连
this.reconnect();
},
//连接关闭事件
websocketclose(e) {
this.websock.close();
//提示关闭
console.log("连接已关闭");
//重连
this.reconnect();
},
//接收服务器推送的信息
websocketonmessage(event) {
//打印收到服务器的内容
console.log("收到服务器信息", event.data);
this.msg = event.data;
//收到服务器信息,心跳重置
this.reset();
},
websocketsend(msg) {
//向服务器发送信息
this.websock.send(msg);
},
},
};
</script>
<style scoped>
.layout {
position: relative;
width: 100%;
height: 100%;
}
.msgBody {
width: 500px;
height: 300px;
border: 1px solid rgb(95, 79, 79);
}
</style>
我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘
我正在尝试修改当前依赖于定义为activeresource的gem:s.add_dependency"activeresource","~>3.0"为了让gem与Rails4一起工作,我需要扩展依赖关系以与activeresource的版本3或4一起工作。我不想简单地添加以下内容,因为它可能会在以后引起问题:s.add_dependency"activeresource",">=3.0"有没有办法指定可接受版本的列表?~>3.0还是~>4.0? 最佳答案 根据thedocumentation,如果你想要3到4之间的所有版本,你可以这
这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式rubyshell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f
我正在阅读SandiMetz的POODR,并且遇到了一个我不太了解的编码原则。这是代码:classBicycleattr_reader:size,:chain,:tire_sizedefinitialize(args={})@size=args[:size]||1@chain=args[:chain]||2@tire_size=args[:tire_size]||3post_initialize(args)endendclassMountainBike此代码将为其各自的属性输出1,2,3,4,5。我不明白的是查找方法。当一辆山地自行车被实例化时,因为它没有自己的initialize方法
如果我使用ruby版本2.5.1和Rails版本2.3.18会怎样?我有基于rails2.3.18和ruby1.9.2p320构建的rails应用程序,我只想升级ruby的版本,而不是rails,这可能吗?我必须面对哪些挑战? 最佳答案 GitHub维护apublicfork它有针对旧Rails版本的分支,有各种变化,它们一直在运行。有一段时间,他们在较新的Ruby版本上运行较旧的Rails版本,而不是最初支持的版本,因此您可能会发现一些关于需要向后移植的有用提示。不过,他们现在已经有几年没有使用2.3了,所以充其量只能让更
我安装了ruby版本管理器,并将RVM安装的ruby实现设置为默认值,这样'哪个ruby'显示'~/.rvm/ruby-1.8.6-p383/bin/ruby'但是当我在emacs中打开inf-ruby缓冲区时,它使用安装在/usr/bin中的ruby。有没有办法让emacs像shell一样尊重ruby的路径?谢谢! 最佳答案 我创建了一个emacs扩展来将rvm集成到emacs中。如果您有兴趣,可以在这里获取:http://github.com/senny/rvm.el
有人知道在发布新版本的Ruby和Rails时收到电子邮件的方法吗?他们有邮件列表,RubyonRails有一个推特,但我不想听到那些随之而来的喧嚣,我只想知道什么时候发布新版本,尤其是那些有安全修复的版本。 最佳答案 从therailsblog获取提要.http://weblog.rubyonrails.org/feed/atom.xml 关于ruby-on-rails-如何在发布新的Ruby或Rails版本时收到通知?,我们在StackOverflow上找到一个类似的问题:
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主
如thisquestion,当在其自己的赋值中使用未定义的局部变量时,它的计算结果为nil。x=x#=>nil但是当局部变量的名称与现有的方法名称冲突时,就比较棘手了。为什么下面的最后一个示例返回nil?{}.instance_eval{a=keys}#=>[]{}.instance_eval{keys=self.keys}#=>[]{}.instance_eval{keys=keys}#=>nil 最佳答案 在Ruby中,因为可以在没有显式接收器和括号的情况下调用方法,所以在局部变量引用和无接收器无参数方法调用之间存在语法歧义:f