赞
踩
写在前面
WebSocket 简称 ws
本文通过介绍ws,然后一步步的使用ws协议进行前后端开发测试,最后使用SpringBoot
和vue
利用ws协议达实现一个在线聊天室的小demo(源码在文章末尾)。
看完此篇后将能够完成这样的小demo(需要会使用springboot和vue2以及element-ui)
WebSocket是在HTML5
开始提供的一种能在单个TCP
连接上进行全双工
通讯的网络通信协议
与HTTP协议可以简单理解为:
WebSocket协议与HTTP协议对比:
无状态(请求完成后,连接彻底断开,节省资源)
、无连接(客户端端向服务端请求数据,获取数据后即断开连接)
、单向(连接建立只能由客户端发起)
的应用层通信协议,采用请求/响应模型。
建立连接的特权只在客户端
手中,只能把事憋到客户端与服务端再次建立请求时一并告诉它。当有要紧事的时候,也无法联系客户端,即服务端无法向客户端推送数据
实时
。频繁访问耗费巨大资源洒下大网只有极少收获。要是有一种客户端能向服务器端发送数据,服务器端也能向客户端发送数据的协议就好了!即全双工通讯
。这种协议客户端不能与服务端断开连接,使得服务端端能够找到客户端继续发送数据即有状态连接
虽然会耗费资源,但在特定需求下,比HTTP协议那种频繁连接要好上不少。这种协议就是WebSocket协议
。总结webSocket就是支持持久
连接使得服务端与客户端都能够实时
通信的协议
WebSocket的应用场景
实时数据展示网站
HTTP方式:我们编写前后端分离的代码时,后端编写@RestController
接口,用来接收请求,前端编写封装axios
用来发送请求获取后端数据。其实这种方式就是遵守HTTP协议进行数据交互的,前端使用axios
发送的是HTTP请求,后端接口接收的也是HTTP请求。
WS方式:
因为常使用的@RestController
接口和axios
请求工具都是为HTTP协议服务的
,因此WS
虽然数据交互是这一流程(后端指定接口,前端获取数据),但要有自己的一套实现工具。使用@ServerEndpoint
到达类似于@RestController
的效果,使用WebSocket
对象达到类似于axios
的效果,具体使用后面会介绍。 Endpoint与webScoket的关系就像 Servlet
与Http的关系一样
生命周期
生命周期使我理解的,原话为 websocket事件
前端
let ws=new webSocket(ws://ip:端口号/资源名称)
函数名 | 描述 |
---|---|
ws.onopen | 建立连接 时触发 |
ws.onmessage | 接收 到后端消息时触发 |
ws.onerror | 通信过程发生错误 时触发 |
ws.onclose | 连接关闭 时触发 |
发送数据时 ws.send()
后端
注解名 | 描述 |
---|---|
@onClose | 连接关闭时触发 |
@onOpen | 初始化时触发 |
@onError | 出现错误时触发 |
@onMessage | 初始化后自动触发,获取前端传递的数据 |
ws实现流程概括
服务端不再像过去一样只负责客户端的数据响应,还可以主动向客户端推送数据
ws前后端都有一个生命周期的概念,后端的各个生命周期相当于把一个@RestController
进行拆分同时多了一个连接者的容器。
@ServerEndpoint("xx")
@OnOpen
标记@OnMessage
,可以使用同@RequestMapping()
的注解方式获取数据,如@PathParam("xxxx")
;注意每次前端发送数据,此方法都会被调用。但初始化时不会调用return
,而是使用获取连接池中保存的连接session,通过session将返回的信息进行传递。@OnClose
标注下的方法,出现错误时调用@OnError
标注下的方法webSocket有自己获取参数的获取方式,与HTTP的Controller的方式很像
路径
参数中获取数据 @PathParam("xxx")
使用起来与@PathValue("xxx")
一样有的注解下的方法必须含有某些参数
@OnError
标识下的方法列表中必须含有Throwable
对象,否则启动报错@OnMessage
此注解标识下的方法,在前端向后端发送消息时自动调用,要求此方法必须含有参数,否则启动报错问题?
在后端向前端发送信息时,怎样去调用?
整个生命周期在连接时能够自动调用的有两个方法,一个是在初始化时调用@OnOpen
标注下的方法,一次是在接受前端请求时 调用@OnMessage
下的方法(此方法前端每次发送数据都会被调用)。可以选用@OnMessage
标注下的方法作为后端向前端返回数据的引子,在此方法中调用返回前端的数据方法。
怎样发到即时通讯效果?
解决后端怎样向前端发送数据问题,即可以解决此问题。当A通过ws将发给B的消息传递到服务器,在接收前端数据的方法中自动调用返回前端数据的方法,在此方法中获取消息内容和目的方,然后去连接者容器中找到目的方的连接对象,然后将A发送的数据作为返回给B连接对象返回的数据。**这样思路就要求 B要在连接者的容器中,即B要保持连接 **
怎样使得前台一直接收后端自己产生的数据
后端自己产生的数据就代表前台只能触发一次,其余的数据不断返回前端都要靠着这一次触发。可以在第一次触发时调用一个类似于监听阻塞阻塞队列的方法,将产生的数据不断的放入队列中,方法中监听到数据就取出返回给前端。这样就是由第一次请求调用监听器,数据产生监听的去触发数据返回
前端怎样接收后端返回的数据?
当后端有数据返回时前端的对象的onmessage()
会被触发,在这里可以就行数据的赋值,页面的渲染
路径传参
:在初始化时通过路径进行参数传递 后端使用要在路径的映射中使用 例如: login/{xxx}
,然后在方法参数中使用@PathParam("xxx")
去接收
消息传参
:使用对象发送消息ws.send(“xxx”)
,可以将一些内容转为JSON形式进行发送,然后后端在@OnMessage
下的方法参数中进行接收和转换
请求头传参
:使用请求头,因为以上都已经介绍过,这里不加赘述。请求头才是这里重点
Sec-WebSocket-Protocol
这个属性,可以在为属性中赋值,后端从这个属性中取出。这个属性赋值没有单独的Api,只能在构造ws对象能够进行赋值
。例如 :
let ws = new WebSocket("ws://localhost:8888/xxxx", "请求参数1");
后端获取请求头 Sec-WebSocket-Protocol 得到的就是 请求参数1
let ws = new WebSocket("ws://localhost:8888/xxxx", ["请求参数1","请求参数2"]);
后端获取请求头 Sec-WebSocket-Protocol 得到的就是 请求参数1 请求参数2
@RequestHeader
直接获取,而是要继承的指定方法中获取
*
@Configuration @Slf4j public class WebSocketConfig extends ServerEndpointConfig.Configurator { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { //获取请求头 请求头的名字是固定的 List<String> list = request.getHeaders().get("Sec-WebSocket-Protocol"); //当Sec-WebSocket-Protocol请求头不为空时,需要返回给前端相同的响应 response.getHeaders().put("Sec-WebSocket-Protocol",list); super.modifyHandshake(sec, request, response); } }
@ServerEndpoint
注释中指名配置类注意:此类的中方法只会在创建连接时执行,在发送消息、出现错误、断开连接均不执行
个人理解:既然ws是创建一次就可以多次发送消息,在创建第一次时进行身份校验,同一个连接是不是只校验一次就可以了?http使用拦截器也是校验一次,因为其频繁连接才会频繁的验证?即在这个方法中充当http时的拦截器使用?
后端使用springBoot
,前端使用postman
发送ws请求
后端准备: 注意要tomcat7
以后才支持webSocket协议
后端除去环境依赖外,要导入关键的WebSocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
环境依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> </parent> <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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
向容器中注入 ServerEndpointExporter对象
创建一个配置类,在类中将对象注入容器
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();
}
}
接下来就可以写类似于接口的应用了
前端准备
第一个测试:建立连接后,前端向后端发送数据,后端接收数据,并向前端发送数据;只有一个连接 (不使用连接的session们容器)
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; @ServerEndpoint(value = "/wsserver/{username}") @Component @Slf4j public class WebSocketServer { private Session session; @OnOpen public void onOpen(@PathParam("username") String username, Session session){ this.session=session; log.info(username); log.info("连接创建初始化"); } @OnMessage public void onMessage(Session session,String message) throws IOException, InterruptedException { log.info("接收到数据"+message); Thread.sleep(10000); for (int i=0;i<10;i++){ this.session.getBasicRemote().sendText("来自后端的数据"); } } @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } @OnClose public void onClose(){ log.info("关闭连接"); } }
注释
测试结果
第二个测试:创建两个独立连接分别为A、B。A向B发送数据,B向A发送数据,在数据中要指定发送方
实现思路:
发送方
、接收方
以及数据
。在路径参数中指明发送方,在数据格式中设定规则,提取出接收方和数据。标识
和session对象
,这样就方便根据数据的接收方去找到其连接的session,然后将数据通过接收方的session进行返回@ServerEndpoint(value = "/wsserver/{username}") @Component @Slf4j public class WebSocketServer { public static final Map<String,Session> sessionMap=new ConcurrentHashMap<>(); @OnOpen public void onOpen(@PathParam("username") String username, Session session){ sessionMap.put(username,session); log.info("数据来自"+username); log.info("连接创建初始化"); for (Map.Entry<String,Session> entry:sessionMap.entrySet()) { System.out.println(entry); } } @OnMessage public void onMessage(Session session,String message) throws IOException, InterruptedException { log.info("接收到数据"+message); String[] split = message.split(":"); sessionMap.get(split[0]).getBasicRemote().sendText(split[1]); } @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } @OnClose public void onClose(){ log.info("关闭连接"); } }
注释
测试结果
注意:每一个请求对应一个WebSocketServer
对象,每次变动都会调用对象中指定的方法,例如:每个请求连接时都会触发一次@onOpen下的方法、每个请求发送数据都会调用一次@onMessage标识下的方法…
创建一个vue项目,因为后面项目会用到。这里也可以不创建,只要有一个能够运行js的环境即可,最简单的方式就是创建一个.html
页面然后在浏览器中打开
第一个测试 与服务器建立连接
创建一个ws
对象,利用此对象可以向后端发送数据
,获取后端数据
等。创建方式很简单,在创建时就会进行连接(是指执行到这里时,而不是指new出来后)
页面运行后,服务端会立即创建连接
第二个测试 在postman中的A向浏览中的C发送数据,C接收数据并打印到页面;
要在上文已经完成后端测试二的基础上
注意:onmessage是一直进行监听服务端发送过来的数据的
第三个测试 在测试二的基础上向postMan的A发送数据
利用wc对象的send(“xxx”)方法
<template> <div> {{receptionData}} <el-input size="mini" v-model="data"></el-input> <el-button @click="toSendMsg()">发送</el-button> </div> </template> <script> export default { data(){ return{ data:'', ws:undefined, receptionData:undefined } }, created(){ this.ws= new WebSocket('ws://localhost:8080/wsserver/C'); this.ws.onmessage=(msg)=>{ this.receptionData=msg.data } }, methods:{ toSendMsg(){ this.ws.send("A:cccccc") } } } </script>
测试结果:
注意:当前端网页完毕时,会自动触发后端的关闭处理
此外还有 sc.close()
手动进行关闭的一些前端处理、 sc.onerror()
出现异常的一些处理,用法与onmessage
相同,都是为其赋值一个函数
在测试过程中发现:数据只能被消费一次,即有多个客户端进行连接时,只能有一个客户端接收到服务端的数据。因为不同的客户端连接就有不同的连接session。由于服务端的代码使用map存储,key为唯一标识,value为session。当一个用户在多个客户端登录时,map中相同的key的旧值会被最新的值覆盖,就出现只有最后打开的网页能接收到服务端的数据。
本系统为自己原创,基于HTTP协议(例如:登录、注册)与ws协议(例如:实时聊天)的混合开发,目的是熟练的使用ws协议,所以在其他方面较为省略,可能会有bug或不对的地方,还望指正
前端使用vue和Element组价库(非必要
),使用axios发送HTTP的请求,使用webScocket对象发送ws请求。
项目的vue项目初始化部分这里不再一一介绍。
登录样式布局
代码部分:
<template> <div class="wrapper"> <div class="login_div"> <span class="title"><h2> 在线聊天室</h2></span> <div class="form_div"> <el-form ref="form" :model="form" label-width="80px"> <el-form-item> <el-input v-model="form.name" size="mini" placeholder="输入姓名"></el-input> </el-form-item> <el-form-item> <el-input v-model="form.password" size="mini" placeholder="输入密码" type="password"></el-input> </el-form-item> <el-form-item> <a href="#" style="color:blue">还没有账号?点我去注册</a> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm()" size="small" style="width:100%">提交</el-button> </el-form-item> </el-form> </div> </div> </div> </template> <script> import {login} from '../api/userApi' export default { data() { return { form:{ name:'', password:'' }, mock:{ } } }, methods: { submitForm(){ login(this.form).then(res=>{ }) } } } </script> <style scoped> .wrapper { height: 100vh; background-image: linear-gradient(to bottom right, #FC466B, #3F5EFB); width: 100%; overflow: hidden; position: relative; } .login_div{ width:500px; height: 300px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -70%); background-color:aliceblue; opacity:0.9; } .title{ text-align: center ; } .form_div{ margin-right: 100px; margin-top: 50px; } </style>
聊天页面布局
用css画一个渐变色的背景,然后在画出页面的整体布局
<template> <div style="min-height: 100vh" class="window_div"> <div class="chat_div"> <div class="chat_header"> <span class="hrader_title">当前用户:xxx</span> <span class="hrader_friend">正在和xxx 聊天</span> </div> <div class="chat_left"> <div class="message_div"></div> <div class="input_div"></div> </div> <div class="chat_right"> <div class="friend_div"> <div class="friend_list_title"></div> <div class="friend_list"></div> </div> <div class="sys_info_div"> <div class="sys_info_title"></div> <div class="sys_info"></div> </div> </div> </div> <div> </div> </div> </template> <script> export default { } </script> <style scoped> .window_div{ background-image: linear-gradient(to bottom right, #b1b1c5, #3F5EFB); overflow: hidden; position: relative; } .chat_div{ width: 60%; height: 65vh; border: 1px solid red; margin: 10vh auto; } .chat_header{ display: flex; width: 100%; height: 10vh; border: 1px solid red; } .chat_left{ width: 70%; height: 55vh; border: 1px solid red; float: left; } .chat_right{ width: 30%; height: 55vh; border: 1px solid red; float: right; } .hrader_title{ display: flexbox; line-height: 10vh; margin-left: 6%; color: white; width: 30%; border: 1px solid red; } .hrader_friend{ display: inline-block; margin-top: 6vh; height: 2vh; color: white; width: 25%; font-size: 12px; border: 1px solid red; } .friend_list_title{ width: 100%; height: 5vh; border: 1px solid red; } .friend_list{ width: 100%; height: 20vh; border: 1px solid red; } .friend_div{ width: 100%; height: 25vh; border: 1px solid black; } .sys_info_div{ width: 100%; height: 30vh; border: 1px solid black; } .sys_info_title{ width: 100%; height: 5vh; border: 1px solid red; } .sys_info{ width: 100%; height: 25vh; border: 1px solid red; } .message_div{ width: 100%; height: 40vh; border: 1px solid red; } .input_div{ width: 100%; height: 15vh; border: 1px solid red; } </style>
去掉边线进行填充后
此时前端样式除了消息部分已经完成,剩下去调试后端
设计功能:
设计用户姓名要唯一
消息类型
用户之间发送的消息、用户上线通知消息、用户下线通知消息、好友列表消息、系统通知(一些错误等)
消息体
发送发、接收方、消息类型、消息内容
思路总结:
后端核心代码:(由于篇幅太长,这里只做注释,详细代码已经上传到gitee中,可以自行访问查看)
到这里,后端所有功能都已经基本完成,只差前端接收不同类型的消息,并将其赋值后进行渲染
此处控制台测试发送消息、接收消息均已成功,接下来就是本次项目最让人头疼的消息显示
采用循环数组的方式,这种方式会和平常聊天习惯存在点差异,这个版本就不去改了
此外在线人员列表也采用同种方式
在线聊天1.0 版本到此完成
完整源码已经上传到gitee中 请点击访问 https://gitee.com/wang-yongyan188/websocketchat.git
提示:因为ws创建请求的token存储在了localStorage中,所以同一个浏览器即使的登录不同的账号也是同一个用户,所以要使用不同的浏览器去测试。如果只测试后端,更推荐使用postman去测试
所有代码皆为原创,博客和代码共用了两天时间,任何错误欢迎提示,如果觉得还可以,就请点个赞吧。
此项目的是理解ws协议,并能够使用springboot实现两种协议的混合开发。
代码页面比较简陋、功能也不完善(注册需要手动去数据库添加,只能和在线用户聊天等),如果喜欢,后续会推出比如:添加好友、消息未读提示、群聊、消息保存等,进一步完善成为一个小项目。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。