还是老规矩,一步步的教大家如何建立前后端的 websocket 链接,并能完成互相传送数据的简单功能。由于网上找了半天发现很多帖子都是东一句西一句的,要不就是写的没什么注释和解释,导致我这个前端人员看后端代码非常折磨。
但是,总算慢慢摸索给整出来了,那现在我就把一个详细版的,用前端小伙伴也听得懂的大白话来说一下如何实现 websocket 功能。
效果图
防止有后端小伙伴想写个前端代码的,不知道写哪里。我给个结构图
代码直接复制,然后再views内创建一个vue文件放进去就可以了,我这是全部代码,不需要改动。
后端代码部分{{ msg }}
老规矩,防止前端小伙伴想写后端的代码,也放图解释一下结构,这里注明,我是用的idea,springboot写的后端。
位置在这:
org.springframework.boot spring-boot-starter-websocket
常见问题:把配置引入进去后会出现标红的报错,像这样。图片我网上随便找的标红报错图。
原因是你直接放进去并没有从新加载把这个配置引入成功。
解决办法: 点击右上角的这个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包的不需要。
}
(3)WebSocketService(也就是websocket的代码)
位置参考上面那个图,我标注出来了,写在controller文件夹内
注意事项:这里我把引入的import都贴出来了,正常复制应该没有问题。那么如果出现这样标红的一个情况可以这样解决代码:
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容器中,相当于配置文件中的断开链接,建立链接,后台推送信息,功能思路等简单聊聊。 分别简单说一下:) //泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类。 @Component //这个注解可以理解为@RequestMapping,后面是接口地址和参数 @ServerEndpoint("/webSocket/{username}")//encoders = { ServerEncoder.class },可选值,指定编码转换器,传输对象时用到,这里直接转json就ok public class WebSocketService { //定义的存储类,用于保存对应的用户名,向对应的用户推送消息 private static final Map TOKEN_SESSION = new HashMap<>(); private static final Map 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 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前后端交互的简单功能。
这是一个小思路的简单聊聊,顺着这个思路大家就可以去操作更多的了。
前端vue心跳重连版本写法 websocket介绍:先解释一下websocket心跳机制是什么,为什么要用他。因为我们前台和后台端口一直链接的时候,中间可能会因为很多情况导致断开链接,比如断网,或者不小心关了,或者防火墙看端口很久没有数据传输给你关闭了。都是有可能的,那么这时候,你断了链接,但是并不会触发到websocket的close事件,所以程序也就不知道断了链接,前台还在往后台发送数据,但是这些数据全都接收不到,都丢失了。这时候就需要我们建立一个心跳机制
心跳机制的工作流程:其实很简单,就是在你初始化websocket的时候开启心跳,也就是一个定时器,隔多久时间往后天发送一次信息,看看后台会不会返回给你数据,如果返回了代表链接正常不用管,把心跳的计时器时间重置,从新计时,如果发过去后没有回应了,那么就触发初始化websocket方法重新生成链接。
图例展示介绍:当后台有数据返回的时候,触发websocketonmessage方法,清空心跳的时间,如果没有返回数据,心跳的时间就会一直计时,到时间了往后台发一个信息看看有没有返回信息,有的话就清空心跳时间从新计时。
注意点:
如果你使用了我这个加入了心跳机制的前端版本,又用了我的后端代码。那需要注意把后端的这个方法删除。不然会出现心跳重连后把后台的定时器开很多个导致重复发送。
当然也可以让后台做一些处理,让他只会发送一次,但是我不会,所以就不写了。你们要是会的话就可以跳过我说的这句话。
删除的位置:
如果你不删除,就会这样,一直重复下去,后面可能叠加几十几百个定时任务。
{{ msg }}



