一、示例概述
本示例代码简单,用来研究websocket内部方法及它们的调用关系。示例前端做了一个很丑的页面,用于展示聊天界面,其中用到了富文本编辑器wangEditor,后端还是常规的代码
二、代码结构目录
代码目录.png三、代码展示
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>mxyz.xiongzelin</groupId>
<artifactId>websocket2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>websocket2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、application.properties
server.port=1111
# 定位模板的目录
#当引入模板引擎jar包时,默认的静态根路径变成了templates
#但如果是html静态文件,默认资源根目录为static下
spring.mvc.view.prefix=/html/
# 给返回的页面添加后缀名
spring.mvc.view.suffix=.html
spring.devtools.restart.enabled=true
#设置重启的目录
spring.devtools.restart.additional-paths=src/main/java
#classpath目录下的static文件夹内容修改不重启
spring.devtools.restart.exclude=static/**
#页面不加载缓存,修改即时生效
spring.freemarker.cache=false
spring.thymeleaf.cache=false
3、WebsocketServer.java代码
package mxyz.xiongzelin.websocket2.component;
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.lang.reflect.Array;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @description:
*
* @author: xiongzelin
*
* @create: 2019/06/10
**/
@Component
@ServerEndpoint("/web/{username}")
public class WebsocketServer {
private static int count = 0;
private static ConcurrentHashMap<String,WebsocketServer> clients = new ConcurrentHashMap<>();
private String guid;
private Session session;
private String username;
//在线人数增加方法
private static synchronized void addOnlineCount(){
WebsocketServer.count++;
}
//在线人数减少方法
private static synchronized void subOnlineCount(){
WebsocketServer.count--;
}
//获取在线人数方法
public static synchronized int getOnlineCount(){
return WebsocketServer.count;
}
//消息群发
public void sendMessageAll(String message,String username) throws IOException {
Collection<WebsocketServer> wss = getClients().values();
// wss.forEach(ws ->{
// synchronized (ws){
// if (!ws.getUsername().equals(guid)){
// ws.session.getAsyncRemote().sendText(message);
// }
// }
// });
Iterator<WebsocketServer> iterator = wss.iterator();
while (iterator.hasNext()){
WebsocketServer ws = iterator.next();
if (!ws.getUsername().equals(username)){
//注意getBasicRemote()和getAsyncRemote()的区别,用错会报错,报错异常信息:java.lang.IllegalStateException: The remote endpoint was in state [TEXT_FULL
ws.session.getBasicRemote().sendText(message);
}
}
}
//消息单发
public void sendMessageForOne(String message,String guid)throws IOException{
clients.get(guid).session.getBasicRemote().sendText(message);
}
/**
* 用户建立连接时调用此方法
*/
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) throws IOException{
this.guid = UUID.randomUUID().toString();
this.username = username;
this.session = session;
//当用户账号在另一台设备登陆时,当前账号被挤出来,类似qq
for(Map.Entry<String,WebsocketServer> entry : getClients().entrySet()){
if (entry.getValue().getUsername().equals(username) && !entry.getValue().getGuid().equals(getGuid())){
sendMessageForOne("refuse",entry.getKey());
System.out.println("退出人:"+entry.getKey());
getClients().remove(entry.getKey());
sendMessageAll("用户 "+username+" 已离开!",username);
subOnlineCount();
System.out.println("用户 "+ username+" 已离开!,用户 id 是: "+ entry.getKey() +" ,当前人数 "+getCount()+" 人 ,剩余聊友:" + getGuids());
break;
}
}
addOnlineCount();
WebsocketServer.clients.put(guid,this);
System.out.println("用户 "+username+" 已上线!,用户 id 是: "+ getGuid() +" ,当前人数 "+getCount()+" 人,剩余聊友:" + getGuids());
sendMessageAll("用户 "+username+" 已上线!",getUsername());
}
/**
* 接收到客户端消息时调用
* @param message
* @param session
* @throws IOException
*/
@OnMessage
public synchronized void onMessage(String message,Session session)throws IOException{
System.out.println("来自 "+getUsername()+" 的消息:" + message + ",用户 id 是: "+ getGuid());
sendMessageAll("来自 "+getUsername()+" 的消息: --" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().getTime()) + message,getUsername());
}
//关闭连接时调用,当客户端关闭连接时,服务端也会调用此方法
@OnClose
public void onClose() throws IOException{
//前端页面刷新时会再次调用此方法
if (getGuids().contains(getGuid())){
getClients().remove(getGuid());
sendMessageAll("用户 "+getUsername()+" 已离开!",getUsername());
subOnlineCount();
System.out.println("用户 "+getUsername()+" 已离开!,用户 id 是: "+ getGuid() +" ,当前人数 "+getCount()+" 人 ,剩余聊友:" + getGuids());
}
}
//发生错误时调用
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
//获取所有连接
public static synchronized Map<String, WebsocketServer> getClients() {
return clients;
}
public static int getCount() {
return count;
}
public Session getSession() {
return session;
}
public String getUsername() {
return username;
}
public String getGuid() {
return guid;
}
public String getGuids(){
return JSON.toJSON(getClients().keySet()).toString();
}
}
4、WebsocketConfig.java代码
package mxyz.xiongzelin.websocket2.conf;
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();
}
}
上面是配置文件代码,不能少,不然也会出问题
5、WebsocketControllor.java代码
package mxyz.xiongzelin.websocket2.controllor;
import mxyz.xiongzelin.websocket2.component.WebsocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @description: 用户通过此接口进入聊天界面
*
* @author: xiongzelin
*
* @create: 2019/06/11
**/
@Controller
@RequestMapping("/websocket")
public class WebsocketControllor {
@Autowired
private WebsocketServer websocketServer;
@RequestMapping("/send")
public String sendMsg(){
return "websocket3";
}
}
四、效果展示
浏览器地址栏输入 http://localhost:1111/websocket/send ,会出现如下界面:
这个输入框的限制就是用户名为空或者空格
登陆以后界面如下:
登陆后界面.png可以多浏览器、多窗口打开此页面,然后就可以在编辑器中输入内容了,效果如下:
聊天界面.png如果大家想看一下全部代码,可以从 github 拉取代码
五、参考资料
WebSocket详解教程
Java中Spring WebSocket详解
SpringBoot2.0集成WebSocket,实现后台向前端推送信息
SpringBoot集成WebSocket实现群聊,后台消息推送
网友评论