说明:Springboot项目,页面是用 thymeleaf 整合的。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
spring:
thymeleaf:
cache: false
suffix: .html
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
head>
<body>
<div id="login" class="form-wrapper">
<div class="header">
登录
div>
div>
<div>
<span style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}">span>
div>
<form action="/user/login" method="post">
<div >
<div >
<input th:type="text" th:name="username" placeholder="username">
div>
<div >
<input th:type="password" th:name="password" placeholder="password" >
div>
div>
<div class="action" onclick="document.getElementById('lick1').click()">
<div class="btn">
确认
div>
div>
<input th:type="submit" id="lick1">
form>
div>
body>
html>
2)chat.html,聊天主页面
[1]、退出登录按钮,调用了logout接口,把session中的token值清除了
[2]、好友列表,在跳转到chat页面的时候,调用了getUserList并且把用户列表数据注入到模型中,界面展示出来
[3]、连接websocket,调用了connectWebSocket()函数,调用了后端 websocket端点的onOpen方法
[4]、断开连接,调用了后台的onClose方法
[5]、发送消息,调用了后台的onMessage方法
[6]、查看历史消息,调用了后台的/history方法
<!DOCTYPE html>
:th="http://www.thymeleaf.org">
>
>
-8">
>My WebSocket >
>
>
>
>
用户token值:: #ff0000" th:text="${session.token}">
>
>
我的好友列表:
>
>
>
>
>
:each="user:${users}" style="color: blue">
:οnclick="aClick([[${user}]]);" th:text="${user}" style="color: blue"> >
>
>
>
>
>
昵称::value="${session.token}"/>
>
>
>
>
消息:>
发送给谁: >
>
>>
历史消息:
>
>>
>
>
>
作用:把Http请求中的参数,传递到WebSocket中
public class GetHttpConfiguration extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// 通过getUserProperties()使得websocket连接类中可获取到配置类中得到的数据
Map<String, Object> userProperties = sec.getUserProperties();
HttpSession httpSession = (HttpSession) request.getHttpSession();
userProperties.put("token",httpSession.getAttribute("token").toString());
super.modifyHandshake(sec, request, response);
}
}
作用:登录拦截器,必须要在session中有token的值,否则跳转去登录。
public class LoginInterceptor implements HandlerInterceptor {
static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
HttpSession session = request.getSession();
Object token = session.getAttribute("token");
if (Objects.isNull(token)) {
response.sendRedirect("/login");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
logger.info("postHandle...");
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
logger.info("afterCompletion...");
}
}
作用:配置拦截器,配置不登录的路径
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) //可以把配置类加入bean 然后autowier得到 或者@Bean 返回值得到 JavaConfig 三种方法都可以
.addPathPatterns("/**") //拦截的路径 **代表所有
.excludePathPatterns("/login","/user/login"); //不拦截的路径
}
}
作用:开启websocket支持
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 扫描@ServerEndpoint,将@ServerEndpoint修饰的类注册为websocket
* 如果使用外置tomcat,则不需要此配置
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
作用:封装聊天消息历史类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class History {
private String from;
private String to;
private String time;
private String content;
}
作用:封装用户登录
@Data
public class User {
private String username;
private String password;
}
作用:封装消息发送类
public class SocketMsg {
private int type; //聊天类型0:群聊,1:单聊.
private String fromUser;//发送者.
private String toUser;//接受者.
private String msg;//消息
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getFromUser() {
return fromUser;
}
public void setFromUser(String fromUser) {
this.fromUser = fromUser;
}
public String getToUser() {
return toUser;
}
public void setToUser(String toUser) {
this.toUser = toUser;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
作用:配置login登录页面视图,配置chat页面视图,配置logout页面视图,配置登录逻辑
@Controller
@CrossOrigin
public class SsoController {
@Autowired
private UserController userController;
@RequestMapping(value = "login", method = {RequestMethod.POST, RequestMethod.GET})
public String login() {
return "login";
}
@RequestMapping(value = "chat", method = {RequestMethod.POST, RequestMethod.GET})
public String chat(HttpSession session,Model model) {
List<String> users = userController.getUserList(session);
model.addAttribute("a","123");
model.addAttribute("users",users);
return "chat";
}
@RequestMapping("logout")
public String logOut(HttpSession session){
session.removeAttribute("token");
return "login";
}
@PostMapping("/user/login")
public String login(HttpServletRequest request, HttpSession session, Map<String, Object> map, Model model) {
String name = request.getParameter("username");
String password = request.getParameter("password");
if (UserController.userMap.containsKey(name)) {
if (UserController.userMap.get(name).equals(password)) {
session.setAttribute("token", name);
return "redirect:/chat";
}
}
model.addAttribute("msg", "请输入正确的账号和密码");
return "login";
}
}
作用:模拟数据库用户,模拟用户的好友数据
@CrossOrigin
@RestController
public class UserController {
//所有的用户
public static Map<String, String> userMap = new HashMap<>();
//每个用户对应的好友
public static Map<String, List<String>> friendsMap = new HashMap<>();
static {
userMap.put("zhangsan", "123456");
userMap.put("lisi", "123456");
userMap.put("wangwu", "123456");
userMap.put("zhaoliu", "123456");
userMap.put("yangsilu", "123456");
userMap.put("ranqilin", "123456");
userMap.put("xuqiaodi", "123456");
userMap.put("luowengang", "123456");
friendsMap.put("zhangsan", List.of("lisi", "wangwu", "luowengang", "zhaoliu"));
friendsMap.put("lisi", List.of("zhangsan", "wangwu", "ranqilin"));
friendsMap.put("wangwu", List.of("zhangsan", "lisi", "xuqiaodi", "yangsilu"));
}
//获取我的好友
@GetMapping("user/list")
public List<String> getUserList(HttpSession session) {
Object token = session.getAttribute("token");
List<String> users = friendsMap.get(token.toString());
return users;
}
}
作用:1)获取用户聊天历史数据
2)提供了checkSocket接口,用于判断当前用户是否已经连接了webSocket。需要在前端连接websocket的时候做一个限制,目前没有实现。
@CrossOrigin
@RestController
public class HistoryController {
// 判断当前用户是否连接了webSocket
@GetMapping("/checkSocket")
public boolean checkSocket(HttpSession session) {
Object token = session.getAttribute("token");
String fromUser = token.toString();
return MyWebSocket.sessionIdNameMap.values().contains(fromUser);
}
@GetMapping("/history")
public List<History> getHistory(@RequestParam("toUser")String toUser, HttpSession session){
Object token = session.getAttribute("token");
String fromUser = token.toString();
List<String> datas = FileUtil.readFileLine();
List<History> historyList = new ArrayList<>();
for (String data : datas) {
historyList.add(JSON.parseObject(data,History.class));
}
//排序
List<History> collect = historyList.stream().filter(item -> {
String from = item.getFrom();
String to = item.getTo();
boolean flag = checkIn(from, to, fromUser, toUser);
return flag;
}).sorted((x1,x2)-> x2.getTime().compareTo(x1.getTime())).
collect(Collectors.toList());
return collect;
}
private boolean checkIn(String from, String to, String fromUser, String toUser) {
if(from.equals(fromUser) && to.equals(toUser)){
return true;
}
if(from.equals(toUser) && to.equals(fromUser)){
return true;
}
return false;
}
}
作用:1)onOpen方法,某个客户端的会话session存储到map中。每个客户端的socket对象存储在webSocketSet中。
2)onClose方法,连接关闭时候,把webSocket对象移除。
3)onError方法,如果连接发送异常会调用
4)OnMessage方法,客户端发送消息的方法。根据当前session的id,获取到发起方的Session对象,根据传递参数的用户名,获取到接收方的Session对象。判断接收方对象是否为空,不为空就可以发送消息。
@ServerEndpoint(value = "/websocket/{nickname}", configurator = GetHttpConfiguration.class)
@Component
public class MyWebSocket {
//用来记录sessionId和该session进行绑定
private static Map<String, Session> map = new ConcurrentHashMap<>();
// sessionId和username的映射
private static Map<String, String> sessionIdNameMap = new ConcurrentHashMap<>();
//用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String nickname;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("nickname") String nickname, EndpointConfig config) {
this.session = session;
this.nickname = nickname;
//在建立连接的时候,就保存频道号(这里使用的是session.getId()作为频道号)和session之间的对应关系
map.put(session.getId(), session);
Object token = config.getUserProperties().get("token");
sessionIdNameMap.put(session.getId(), token.toString());
webSocketSet.add(this); //加入set中
System.out.println("有新连接加入!当前在线人数为" + webSocketSet.size());
this.session.getAsyncRemote().sendText("恭喜" + nickname + "成功连接上WebSocket-->频道号是:" + nickname + "当前在线人数为:" + webSocketSet.size());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
String id = session.getId();
webSocketSet.remove(this); //从set中删除
sessionIdNameMap.remove(id);
map.remove(id);
System.out.println("有一连接关闭!当前在线人数为" + webSocketSet.size());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session, @PathParam("nickname") String nickname) {
System.out.println("来自客户端的消息-->" + nickname + ":" + message);
//群发消息
// broadcast(nickname + ":" +message);
//从客户端穿过来是json数据,转成SocketMsg对象,根据type判断是单聊还是群聊
ObjectMapper objectMapper = new ObjectMapper();
SocketMsg socketMsg;
try {
socketMsg = objectMapper.readValue(message, SocketMsg.class);
if (socketMsg.getType() == 1) {
//单聊,需要找到发送者和接受者
socketMsg.setFromUser(session.getId()); //发送者
Session fromSession = map.get(socketMsg.getFromUser());
Session toSession = map.get(getSessionId(sessionIdNameMap, socketMsg.getToUser()));
//发送给接受者
if (toSession != null) {
String from = nickname + ":" + socketMsg.getMsg();
String to = nickname + ":" + socketMsg.getMsg();
fromSession.getAsyncRemote().sendText(from);
toSession.getAsyncRemote().sendText(to);
//保存消息
String time = getTime();
History history = History.builder()
.from(sessionIdNameMap.get(session.getId()))
.to(socketMsg.getToUser())
.time(time)
.content(nickname + ":" + socketMsg.getMsg())
.build();
FileUtil.toFile(JSON.toJSONString(history));
} else {
fromSession.getAsyncRemote().sendText("系统消息:对方不在线或者您输入的频道号不对");
}
} else {
//群发消息
broadcast(nickname + ": " + socketMsg.getMsg());
// 保存消息
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 群发自定义消息
*/
public void broadcast(String message) {
for (MyWebSocket item : webSocketSet) {
//同步异步说明参考:http://blog.csdn.net/who_is_xiaoming/article/details/53287691
//this.session.getBasicRemote().sendText(message);
item.session.getAsyncRemote().sendText(message);//异步发送消息.
}
}
public String getTime(){
SimpleDateFormat format = new SimpleDateFormat("yyyy-dd-mm HH:mm:ss");
return format.format(new Date());
}
public String getSessionId(Map<String, String> map, String name) {
if (StringUtils.isBlank(name)) return null;
for (Map.Entry<String, String> entry : map.entrySet()) {
if (entry.getValue().equals(name)) {
return entry.getKey();
}
}
return null;
}
}
作用:把历史消息存储到文件中。
public class FileUtil {
private static final String file = "D:\\websocket-sse\\wechat\\history.txt";
public static void main(String[] args) {
readFileLine();
}
public static void toFile(String content) {
BufferedWriter out = null;
try {
out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, true)));
out.write(content + "\n");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static List> readFileLine() {
List> list = new ArrayList<>();
try (FileInputStream in = new FileInputStream(file)) {
Scanner sc = new Scanner(in, "UTF-8");
while (sc.hasNext()) {
String content = sc.nextLine();
if (StringUtils.isNotBlank(content)) {
list.add(content);
}
}
} catch (IOException e) {
}
return list;
}
}