1. WebSocket介绍
WebSocket是基于TCP连接实现双向绑定的协议,提供了持久化连接,也就是说不需要每次都重新连接服务器去接收消息,服务器可以往客户端发送消息, 客户端也可以往服务器发送消息,实时通信器;
2. WebSocket的前身
2.1 轮询
在早期,WebSocket所用的技术是轮询,轮询分为两种一个是短轮询,另外一个是长轮询:
l 短轮询:其技术就是需要在一段时间才会向服务器发送请求,然后才能返回最新的数据;
l 长轮询:就是他会一定时间内会去问发送请求,如没有数据,他则会一种处于等待状态,等待服务器提供新的消息,有则会立刻对网页更新,但不会一直保持与服务器连接
2.2 轮询中短轮询与长轮询的区别:
短轮询是客户端不断地、周期性地向服务器发送请求;
长轮询是客户端发送请求后,服务器会保持连接直到有数据可发送。长轮询通常比短轮询更高效,因为它减少了不必要的请求次数;
2.3 轮询与WebSocket区别:
WebSocket:适用于需要实时数据传输的场景,如在线游戏、实时聊天应用等,因为它提供了低延迟的双向通信。
3. WebSocket生命周期
3.1 六大生命周期
WebSocket共有六个生命周期:分别是:握手.连接建立.数据传输.心跳检测.连接关闭.错误处理 这六种
-
握手 (Handshake) :客户端通过HTTP请求发起WebSocket连接,服务器响应建立连接;
-
连接建立(Connection Establishment):握手成功后,TCP连接升级为WebSocket连接,双方开启通信;
-
数据传输(Data Transfer):客户端和服务器交换文本或二进制数据;
-
心跳检测(Heartbeat):为了保持连接的活跃性,WebSocket协议允许发送心跳(ping/pong)帧。这些帧用于检测连接是否仍然可用,并防止因为网络空闲而被某些中间设备(如NAT)关闭;
-
连接关闭(Connection Closure):当通信结束时,通过发送特定的关闭帧来关闭WebSocket连接,双方确认后断开TCP连接;
-
错误处理 (Error Handling) :就是字面意思去处理掉WebSocket执行时发生的异常;
3.2 生命周期核心与生命周期示意图
最主要的生命周期:就是连接建立,数据交换以及连接关闭,这三个阶段涵盖了WebSocket从建立到结束的整个生命周期,是WebSocket通信过程中最核心的部分;
握手请求:客户端(Client)向服务器(Server)发送一个握手请求。这个请求通常是HTTP请求,包含了特定的头部信息,告诉服务器客户端想要建立WebSocket连接。
握手响应:服务器接收到握手请求后,如果同意建立WebSocket连接,会发送一个握手响应给客户端。这个响应也是HTTP响应,包含了确认连接的相关信息。
WebSocket : 连接开放:一旦握手成功,客户端和服务器之间的WebSocket连接就被认为是开放的,可以开始交换数据。
WebSocket : 连接关闭:当通信结束时,任一端可以发起关闭WebSocket连接的请求。
WebSocket : 连接关闭完成:另一端接收到关闭请求后,会确认关闭连接,至此WebSocket连接完全关闭。
下方是生命周期示意图:
4. WebSocket特点及对象
4.1 特点
-
全双工通信:WebSocket协议支持服务器和客户端之间的全双工通信,客户端和服务器可以同时发送消息。
-
持久连接:WebSocket连接一旦建立,将持续保持打开状态,直到客户端或服务器关闭连接。
-
跨域通信:WebSocket协议支持跨域通信,允许不同域的服务器与客户端建立连接。
-
限制:不提供加密功能,如果在安全上有需求,可以采用如:SSL协议,限制访问权限设置白名单只允许特定的IP地址或者域名的客户端连接
-
兼容:在IE10之前的版本是不知道,需要使用AJAX对其替代数据传输
4.2 对象
4.2.1 对象的创建
通过let ws=new WebSocket(URL); URL说明: 1.各式:协议://ip地址/访问路径 2.协议:协议名称为WS;
4.2.2 对象的相关的事件
WebSocket的对象事件有4种 :Open(连接),Message(接受发送消息),Close(断开连接),Error(处理异常):
事件 | 事件处理程序 | 描述 |
---|---|---|
open | ws.onopen | 就像你拨通电话后听到"嘟"声,表示你和服务器的通话(连接)已经接通了,现在可以开始聊天(发送消息)了。 |
message | ws.onmessage | 当你听到对方(服务器)说话(发送消息)时,这个事件就会提醒你,你就可以听到(接收)对方说了什么。 |
close | ws.onclose | 就像通话结束,电话挂断了。这个事件告诉你连接已经断开,你可以检查一下是不是要重新拨号(尝试重新连接)。 |
error | ws.onerror | 如果通话过程中出现了问题,比如信号不好或者电话坏了,这个事件就会告诉你出了什么问题,你可以尝试解决或者告诉别人电话出问题了。 |
4.2.3 对象所提供的方法
方法/事件 | 描述 |
---|---|
WebSocket(url, [protocol]) | 创建一个新的WebSocket连接到指定的服务器URL,可选地指定子协议。 |
close([code], [reason]) | 关闭WebSocket连接,可以指定一个状态码和关闭原因。 |
send(data) | 向服务器发送数据,数据可以是字符串、二进制数据或Blob对象。 |
onopen | 连接成功建立时触发的事件处理程序。 |
onmessage | 从服务器接收到消息时触发的事件处理程序。 |
onerror | WebSocket连接遇到错误时触发的事件处理程序。 |
onclose | WebSocket连接关闭时触发的事件处理程序。 |
在这之中最主要的方法有三个,分别是 : WebSocket(url,protocol) , Send(data) , Close(code,reason);
通俗来说就是:
(1)这就像是拿起电话,拨通服务器的号码。你需要告诉 WebSocket 你要连接的服务器地址(URL),有时候还需要指定你们通话用的语言(协议)。
(2)一旦电话接通,你就可以用这个方法来发送消息给服务器,就像你对着电话说话一样。
(3) 当你说完话,想要挂断电话时,这个方法就派上用场了。你可以告诉服务器你要挂断(关闭连接),有时候还可以给个理由(关闭原因)。
5 WebSocket使用
5.1 导入依赖
这边使用的是Spring Boot 开发的项目,使用的JDK版本是:1.8,首先先再pom.xml中添加依赖为:
XML
<dependencies>
<!--引入 WebSocket 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--为使用了注解的实体提供get,set.toString方法等-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--阿里提供的:用于JSON与字符串之间转换-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<!--这边使用的页面是JSP所以需要用到Web的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!--插件 -->
<build>
<finalName>chat</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
5.2 构造初始项目:
5.2.1 创建pojo
这里面有两个实体类分别是Result和User
java
@Data
public class Result {
private boolean flag;
private String message;
}
java
@Data
public class User {
private String userId;
private String username;
private String password;
}
5.2.2 创建工具包Utils
java
@RestController
@RequestMapping("user")
public class UserController {
/**
* 登陆
* @param user 提交的用户数据,包含用户名和密码
* @param session
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody User user, HttpSession session) {
Result result = new Result();
if(user != null && "123".equals(user.getPassword())) {
result.setFlag(true);
//将数据存储到session对象中
session.setAttribute("user",user.getUsername());
} else {
result.setFlag(false);
result.setMessage("登陆失败");
}
return result;
}
/**
* 获取用户名
* @param session
* @return
*/
@GetMapping("/getUsername")
public String getUsername(HttpSession session) {
String username = (String) session.getAttribute("user");
return username;
}
}
5.2.3 创建ws
ws就是WebSocket的缩写用来完成数据传输的;
里面也有一个pojo,实体类有两个分别是:Message与ResultMessage
java
@Data
public class Message {
private String toName;
private String message;
}
java
@Data
public class ResultMessage {
private boolean isSystem;
private String fromName;
private Object message;//如果是系统消息是数组
}
5.2.4 创建与编写Controller
在这我们创建一个controller包在这里面在创建一个UserControllerl里面包含登录与获取用户名的业务功能
java
@RestController
@RequestMapping("user")
public class UserController {
/**
* 登陆
* @param user 提交的用户数据,包含用户名和密码
* @param session
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody User user, HttpSession session) {
Result result = new Result();
if(user != null && "123".equals(user.getPassword())) {
result.setFlag(true);
//将数据存储到session对象中
session.setAttribute("user",user.getUsername());
} else {
result.setFlag(false);
result.setMessage("登陆失败");
}
return result;
}
/**
* 获取用户名
* @param session
* @return
*/
@GetMapping("/getUsername")
public String getUsername(HttpSession session) {
String username = (String) session.getAttribute("user");
return username;
}
}
5.2.5 创建Config配置类
配置类分为两个:1.GetHttpSessionConfig 2.WebSocketConfig
1.GetHttpSessionConfig :用来将登录就进来的用户以Session来保存起来
java
public class GetHttpSessionConfig extends
ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec
, HandshakeRequest request
,HandshakeResponse response) {
//获取HttpSession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
//将httpSession对象保存起来
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
2.WebSocketConfig:要来将使用了@ServerEndpoint 注解的类交给Spring 去管理
java
@CacheConfig
//表示这是一个配置类
public class WebSocketConfig {
@Bean
// 注入ServerEndpointExporter bean,用来自动扫描使用了 @ServerEndpoint 注解的类
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
这些是在WS文件包外面创建的,接着写完以上两个配置类我们还需要为WebSocket创建他的配置类就在WS文件夹内创建ChatEndpoint这个类是WebSocket的生命周期;
里面有建立连接onOpen,处理消息onMessage,以及关闭连接onClose
java
@ServerEndpoint("/chat")
@Component //交给Spring去管理
public class ChatEndpoint {
@OnOpen
// 连接建立
public void onOpen(Session session, EndpointConfig config) {
}
@OnMessage
// 接收消息
public void onMessage(String message) {
}
@OnClose
// 连接关闭
public void onClose(Session session) {
}
}
5.3 正式开始编写代码
5.3.1编写onOpen()
在onOpen中需要实现两个步骤,(1):将session进行保存 (2)广播消息,将登录的用户互相推送
首先编写一个用来存储的Map集合 :
定义为常量是因为需要将他们全部存入一个地方,如果不使用,则它每次都会创建一个新的Map集合
在继续编写onOpen方法:
步骤一:
我们说过了他步骤一是存储session 所以我们需要先使用上面的配置类 GetHttpSessionConfig
在本类的注解上添加:
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
在使用 EndpointConfig config 去获取我们之前存储的用户;
getUserProperties().get(HttpSession.class.getName());
在通过 httpSession.getAttribute("user") 去获取用户名;
具体代码如下:
java
//1,将session进行保存
this.httpSession = (HttpSession) config.getUserProperties()
.get(HttpSession.class.getName());
String user = (String) this.httpSession.getAttribute("user");
// user是在登录时,存储时的key
onlineUsers.put(user,session);
步骤二:
广播消息推送所有用户
首先在定义一个方法用来遍历Map集合,先使用 onlineUsers.entrySet(); 创建出一个Set集合在使用 .for 去增强循环,从中获取到所有用户对应的session对象 最后将传入的message为其发送出去;
具体代码如下:
java
private void broadcastAllUsers(String message) {
try {
//遍历map集合
Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
for (Map.Entry<String, Session> entry : entries) {
//获取到所有用户对应的session对象
Session session = entry.getValue();
//发送同步消息
session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
//记录日志
}
}
在到上面调用它, 然后在使用工具类去从中获取到message,
MessageUtils.getMessage(是不是系统消息,null,系统给的消息还需从别处获取)
/* 三个值的解释 1.表示这不是系统消息。2.发送消息的用户名。 3.实际的消息内容。*/
在为其编写一个方法 : getFriends()用来获取用户名称,在步骤一时我们已经存储了用户名称,就使用Set来存储一下他们的名字所以getFriends的代码就是:
public Set getFriends() {
Set<String> set = onlineUsers.keySet();
return set;
}
最后步骤二的代码如下:
java
//2:广播消息,将登录的用户互相推送
String message = MessageUtils.getMessage (true, null, getFriends () );
broadcastAllUsers(message);
onOpen建立连接就完成了!
5.3.2编写onMessage()
该方法需要实现的就是将消息发送给服务器,服务器再将该消息转发给另外一个浏览器,我们也分为以下几个步骤:
将消息具体推送给具体用户:在输消息时我们使用字符串不太方便所以我们需要转换JSON类型然后我们之前就已经注入了JSON之间转换的依赖了所以我们可以直接使用;
代码如下:
java
Message msg = JSON.parseObject(message, Message.class);
Message是一个实体里面包含了你要发送给谁,以及具体消息所以再这就是将message中的值为该实体赋值;
接下来我们再转出具体的你要发送给谁的那个名字以及具体的消息,通过上面赋值了的获取就好了:
String toName = msg.getToName(); //要发送给谁
String mess = msg.getMessage(); //具体消息
接下来我们再从之前存储Session的集合中获取toName接受方的session
Session session = onlineUsers.get(toName);
然后获取到了接收方的Session 这时候我们就可以将消息传输给服务器,再通过服务器传输给客户端,首先我们再通过MessageUtils这个工具类,去实现传输,首先我们需要获取到我们当前登录的用户名称,再onOpen方法中以及使用到了所以我们还是可以copy下来;
具体的代码如下:
java
String user = (String) this.httpSession.getAttribute("user");
//获取当前登录的用户名称
String msg1 = MessageUtils.getMessage(false, user, mess);
/* false:表示这不是系统消息。
user:发送消息的用户名。
mess:实际的消息内容。*/
最后我们再将他使用 session的.getBasicRemote().setdText()方法为其传输消息:
session.getBasicRemote().sendText(msg1);
5.3.3 编写 onClose()
在断开连接时调用的方法首先第一步:从onlineUsers中剔除当初用户的对象session; 第二步通知其他用户自己退出了连接;
下面来编写第一步剔除当前用户: onlineUsers.remove(用户名); 这时候的用户名之前在onOpen中就获取过了所以我们可以去copy下来 ;
代码如下:
//1.将当前用户下线,剔除;
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.remove(user);
步骤二:
通知其他用户的方法也在onOpen中写过了也可以copy下来:
//2,广播消息,将还在登录状态的用户互相推送
String message = MessageUtils.getMessage (true, null, getFriends () );
broadcastAllUsers(message);
步骤二的原理就等于是刷新了页面,然后将下线的用户删除掉,保留了还在线的用户
onClose方法也就实现完成了;
5.3.4 ChatEndpoint类完整代码
最后完整代码如下:
java
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {
private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();
private HttpSession httpSession;
/**
* 建立websocket连接后,被调用
* @param session
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
//1,将session进行保存
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.put(user,session);
//2,广播消息。需要将登陆的所有的用户推送给所有的用户
String message = MessageUtils.getMessage(true,null,getFriends());
broadcastAllUsers(message);
}
public Set getFriends() {
Set<String> set = onlineUsers.keySet();
return set;
}
private void broadcastAllUsers(String message) {
try {
//遍历map集合
Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
for (Map.Entry<String, Session> entry : entries) {
//获取到所有用户对应的session对象
Session session = entry.getValue();
//发送消息
session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
//记录日志
}
}
/**
* 浏览器发送消息到服务端,该方法被调用
* @param message
*/
@OnMessage
public void onMessage(String message) {
try {
//将消息推送给指定的用户
Message msg = JSON.parseObject(message, Message.class);
//获取 消息接收方的用户名
String toName = msg.getToName();
String mess = msg.getMessage();
//获取消息接收方用户对象的session对象
Session session = onlineUsers.get(toName);
String user = (String) this.httpSession.getAttribute("user");
String msg1 = MessageUtils.getMessage(false, user, mess);
session.getBasicRemote().sendText(msg1);
} catch (Exception e) {
//记录日志
}
}
/**
* 断开 websocket 连接时被调用
* @param session
*/
@OnClose
public void onClose(Session session) {
//1,从onlineUsers中剔除当前用户的session对象
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.remove(user);
//2,通知其他所有的用户,当前用户下线了
String message = MessageUtils.getMessage(true,null,getFriends());
broadcastAllUsers(message);
}
}