WebSocket实现简易聊天室
1 WebSocket介绍
- 网络通信协议
- 是HTML5开始提供的一个单个TCP连接上进行全双工通信协议
1.1 诞生原因(http无状态、无连接)
①HTTP协议:
由于HTTP协议是一种无状态、无连接、单向的应用层协议,通信请求只能由客户端发起。
服务器无法主动向客户端发送消息。(一问一答)
如果服务器由连续的状态变化,客户端要获知就非常麻烦,大多数web应用程序通过频繁的异步ajax请求实现长轮询,效率非常低(因为需要不停连接或HTTP连接始终打开)。
②WebSocket协议
协议包含两部分:握手和数据传输(握手:基于http)
服务端可以主动向客户端推送消息数据
1.2 websocket客户端与服务端
来自客户端的握手:
GET ws://localhost:/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGh1IHNhbXbsZSBub25jZQ==
Sec-WebSocket-Extentions: permessage-deflate
Sec-WebSocket-Version: 13
ws://localhost:/chat HTTP/1.1
ws是协议名称
来自服务端的握手:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: s3pPLMBiTxaQ9kYGzzhZRbx+Oo==
Sec-WebSocket-Extentions: permessage-deflate
头名称 | 说明 |
---|---|
Connection:graduation | 标识该HTTP请求是一个协议升级请求 |
Upgrade:WebSocket | 协议升级为WebSocket |
Sec-WebSocket-Version:13 | 客户端支持WebSocket的版本 |
Sec-WebSocket-Key: | 客户端采用Base64编码的24为随机字符序列,服务器接收客户端HTTP协议升级的证明。要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答 |
Sec-WebSocket-Extentison | 协议扩展类型 |
Sec-WebSocket-Key用于标识当前服务器与哪个客户端对应
①客户端(浏览器)实现
- websocket对象
实现websocket的web浏览器将通过websocket对象捅开所有必须的客户端功能(主要指支持HTML5的浏览器)
//创建websocket对象
//url: ws://ip地址:端口号/资源名称
var ws = new WebSocket(url);
- websocket事件
事件 | 事件处理程序 | 描述 |
---|---|---|
open | websocket对象.onopen | 连接建立时触发 |
message | websocket对象.onmessage | 客户端接收服务端数据时触发 |
error | websocket对象.onerror | 通信错误时触发 |
close | websocket对象.onclose | 连接关闭时触发 |
- websocket方法
WebSocket对象的相关方法:
send() 使用连接发送数据
②服务端实现
Tomcat 7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)
Java WebSocket应用由一些列的WebSocketEndpoint组成,Endpoint
是一个java对象,代表WebSocket连接的一端(处理websocket消息的接口
),对于服务端,我们可以视为处理WebSocket消息的接口,就像Servlet与http请求一样。
我们可以通过两种方式定义Endpoint:
- 编程式:继承javax.WebSocket.Endpoint并实现其方法
- 注解式:定义一个pojo,添加@ServerEndpoint相关注解
Endpoint实例再websocket握手时创建,并在客户端与服务端连接过程中有效,最后再连接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,方法如下:
方法 | 含义描述 | 注解 |
---|---|---|
onClose | 会话关闭时调用 | @OnClose |
onOpen | 当开启一个新会话时调用,该方法是客户端与服务端握手成功后调用的方法 | @OnOpen |
onError | 当连接异常时调用 | @OnError |
③服务端与客户端如何收发消息
- 服务端如何收消息
通过Session添加Messagehandler消息处理器来接收消息,此处的Session不是http中的session【注解方式:添加@OnMessage】
- 服务端如何推送消息
发送消息由RemoteEndpoint完成,其实例由Session维护。我们可以通过
Session.getBasicRemote
获取同步消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例。
2 项目开发
注意:此项目是通过session来保存用户数据的,因此如果要测试效果的话,一个用户就需要开启一个浏览器,否则session会覆盖用户数据,造成数据混乱
2.1 完整代码
链接:https://pan.baidu.com/s/1CioTDitIzInSSrNvNhQ8FA?pwd=zj8k
提取码:zj8k
-
为了避免跨域问题,可以直接手动将application.properties中的端口配置改为80,当然,也可以自己解决
固定页面: -
登录页面
-
登录成功页面
2.2 websocket开发
本教程主要是体验websocket的功能,因此很多东西都写死的,不太规范,比如没有使用数据库等等
2.2.1 系统广播消息推送 - onOpen
- 将ServerEndpointExporter注入到spring中
@Configuration
public class WebSocketConfig {
/*注入ServerEndpointExporter bean对象,自动注册使用了@ServerEndpoint注解的bean*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
- 存储httpSession
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
//将httpSession对象存储到配置对象中
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
- 编写ChatEndpoint
/**
* @author zhouYi
* @description TODO
* @date 2023/1/13 14:33
*/
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {
//使用线程安全的map来存储每一个客户端对应的chatEndpoint对象
private static Map<String, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
//声明websocket的session对象,通过该对象可以发送消息给指定用户
private Session session;
//声明一个httpSession对象,我们之前在HttpSession对象中存储了用户名
private HttpSession httpSession;
/**
* 连接建立时触发
* @param session
* @param config
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config){
//将局部的session对象赋值给成员session
this.session = session;
//获取httpSession对象
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
//从httpSession中获取用户名
String username = (String) httpSession.getAttribute("user");
//将当前对象存储到容器中
onlineUsers.put(username, this);
//将当前在线用户的用户名推送给所有的客户端
//1. 获取消息
String message = MessageUtils.getMessage(true, null, getNames());
//2. 调用方法进行系统消息的推送
broadcastAllUsers(message);
}
private void broadcastAllUsers(String message){
try{
//要将该消息推送给所有的客户端
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndpoint chatEndpoint = onlineUsers.get(name);
chatEndpoint.session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private Set<String> getNames(){
return onlineUsers.keySet();
}
}
2.2.2 聊天功能实现- onMessage
//接收到客户端发送的数据时被调用
@OnMessage
public void onMessage(String message, Session session){
try{
//将message转换成message对象
ObjectMapper mapper = new ObjectMapper();
Message msg = mapper.readValue(message, Message.class);
//获取要将数据发送给的用户
String toName = msg.getToName();
//获取消息数据
String data = msg.getMessage();
//获取当前登录的用户
String username = (String) httpSession.getAttribute("user");
//获取推送给指定用户的消息格式的数据
String resultMessage = MessageUtils.getMessage(false, username, data);
//发送数据
onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
2.2.3 聊天记录存储
2.2.4 效果
分别用edge、google、firefox登录张三、李四、王五账号
- 张三
- 李四
- 王五
①张三、李四互发消息
注意,页面右侧为自己的消息
②查看王五消息框
可以看到并没有收到消息,因为没有人给他发消息
现在李四,给王五发消息:
全部代码
package com.zi.demo.ws;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zi.demo.pojo.Message;
import com.zi.demo.util.MessageUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author zhouYi
* @description TODO
* @date 2023/1/13 14:33
*/
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {
//使用线程安全的map来存储每一个客户端对应的chatEndpoint对象
private static Map<String, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
//声明websocket的session对象,通过该对象可以发送消息给指定用户
private Session session;
//声明一个httpSession对象,我们之前在HttpSession对象中存储了用户名
private HttpSession httpSession;
/**
* 连接建立时触发
* @param session
* @param config
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config){
//将局部的session对象赋值给成员session
this.session = session;
//获取httpSession对象
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
//从httpSession中获取用户名
String username = (String) httpSession.getAttribute("user");
//将当前对象存储到容器中
onlineUsers.put(username, this);
//将当前在线用户的用户名推送给所有的客户端
//1. 获取消息
String message = MessageUtils.getMessage(true, null, getNames());
//2. 调用方法进行系统消息的推送
broadcastAllUsers(message);
}
private void broadcastAllUsers(String message){
try{
//要将该消息推送给所有的客户端
Set<String> names = onlineUsers.keySet();
for (String name : names) {
ChatEndpoint chatEndpoint = onlineUsers.get(name);
chatEndpoint.session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private Set<String> getNames(){
return onlineUsers.keySet();
}
//接收到客户端发送的数据时被调用
@OnMessage
public void onMessage(String message, Session session){
try{
//将message转换成message对象
ObjectMapper mapper = new ObjectMapper();
Message msg = mapper.readValue(message, Message.class);
//获取要将数据发送给的用户
String toName = msg.getToName();
//获取消息数据
String data = msg.getMessage();
//获取当前登录的用户
String username = (String) httpSession.getAttribute("user");
//获取推送给指定用户的消息格式的数据
String resultMessage = MessageUtils.getMessage(false, username, data);
//发送数据
onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 连接关闭时被调用
* @param session
*/
@OnClose
public void onClose(Session session){
String username = (String) httpSession.getAttribute("user");
//待优化
// //从容器中删除指定的用户
// onlineUsers.remove(username);
// //获取推送的消息
// String message = MessageUtils.getMessage(true, null, getNames());
// broadcastAllUsers(message);
}
}