前几天,我们认识了WebSocket,一个是客户端,一个是服务端,二者在连接的时候通过HTTP建立一次握手,后面就一直是Tcp传输的长连接。然而,在实际开发,几乎都是多客户端的形式,来连接。那么这次我们来用WebSocket技术做一下多客户端连接服务端。
多客户端和单客户端连接服务端的区别在于,应该在服务端对每个连接成功的客户端所有区分,即在单客户端连接成功时,保存识别该客户端的唯一标识(比如数据库中的主键,唯一标识)。这样就完成了多客户端的连接。
这里我们用SpringBoot整合WebSocket框架,实现一个多用户群聊,以及广播消息的功能。
这里,我用的是1.5.20版本,比较稳定,然后只需要导入Web模块和WebSocket模块。点击finish。
pom文件:
<dependencies>
<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>
<!-- thymeleaf -->
<!-- 必须加这个starter,不然访问页面会报错:没有配置thymeleaf模板的页面后缀。。。。-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
先写配置类:
@Configuration //表示这是个配置类,用法和 @Component 注解没有区别
public class WebSocketConfig {
/**
* @Bean 注解会把该方法的返回值当做一个JavaBean,存放在Spring上下文中,以供使用
* ServerEndpointExporter类的作用是,会扫描所有的服务器端点,把带有 @ServerEndpoint 注解的所有类都添加进来
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
写WebSocket服务端:
package com.xk.springboot.websocket.server;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* wevsocket服务端
* @author xiake
*
*/
@Component
@ServerEndpoint("/myws/{nickname}")
public class MyWebSocketServer {
private static final Logger LOGGER = LoggerFactory.getLogger(MyWebSocketServer.class);
/**
* 以用户的昵称为key,WebSocket为对象保存起来,因为是保存所有客户端对象的集合,所以必须是 static 的
*/
private static final Map<String, MyWebSocketServer> MORE_CHAT_USER_MAP = new ConcurrentHashMap<String, MyWebSocketServer>();
private static int onlineNumber = 0;//房间内人数
private Session session;//会话
private String nickname;//用户昵称
@OnOpen
public void onOpen(Session session,@PathParam("nickname")String nickname) {
this.session = session;
this.nickname = nickname;
//当客户端连接成功时,我们应该获取此客户端的唯一标识,然后保存到Map中
String sessionId = session.getId();
MORE_CHAT_USER_MAP.put(sessionId, this);
//房间内人数+1
onlineNumber++;
//通知所有已经在线的用户,xxx加入房间
sendMessageAll("["+nickname+"]"+"加入群聊", "系统消息");
}
@OnError
public void onError(Session session,Throwable error) {
LOGGER.info("服务端发生了错误 - {}",error.getMessage());
}
@OnClose
public void onClose() {
//此客户端断开连接后,在线人数-1,并且从集合中移除该客户端对象
onlineNumber--;
MORE_CHAT_USER_MAP.remove(nickname);
//通知其它客户端
sendMessageAll("["+nickname+"] 下线了", "系统消息");
}
@OnMessage
public void onMessage(String message,Session session) {
sendMessageAll(message, this.nickname);
}
/**
* 给其它人广播消息
* @param message
* @param nickName
*/
public void sendMessageOther(String message,String nickName) {
for (MyWebSocketServer item : MORE_CHAT_USER_MAP.values()) {
if(!item.nickname.equals(nickName)) {
item.session.getAsyncRemote().sendText("[" + nickName + "]: " + message);
}
}
}
/**
* 给某个人发送消息
* @param message
* @param toNickName
*/
public void sendMessageTo(String message, String fromNickName, String toNickName) {
for (MyWebSocketServer item : MORE_CHAT_USER_MAP.values()) {
if(item.nickname.equals(toNickName)) {
item.session.getAsyncRemote().sendText("[" + fromNickName + "]私聊了您: " + message);
break;
}
}
}
public void sendMessageToMe(String message,String nickname) {
this.session.getAsyncRemote().sendText("[" + nickname + "]:" + message);
}
/**
* 给所有人发送消息
* @param message
* @param fromNickName
*/
public void sendMessageAll(String message,String fromNickName) {
for (MyWebSocketServer item : MORE_CHAT_USER_MAP.values()) {
item.session.getAsyncRemote().sendText("[" + fromNickName + "]: " + message);
}
}
public static synchronized int getOnLineCount() {
return onlineNumber;
}
}
在这上面,相当于是在单客户端连接的基础上,增加了一些代码,是用来处理多客户端的,首先写代码之前,我们的思路是一定要正确的,在多客户端连接中,我们应该区分开每个客户端,所以需要保存每个客户端的唯一标识,幸好,WebSocket的Session会话中有唯一标识ID,把ID作为key,对象作为value,保存到Map集合中,需要的时候,再从Map集合中取出来,这样就形成了多客户端互不影响。
客户端:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>群聊功能实现</title>
<script type="text/javascript" src="js/jquery.min.js"></script>
</head>
<body>
<div>
<input type="text" id="nickname" placeholder="请输入昵称"/>
<button id="login">登录</button>
</div>
<div>
<input type="text" id="message" placeholder="输入消息"/>
<button id="send" disabled="disabled">发送</button>
</div>
<div id="show"></div>
</body>
<script type="text/javascript">
var ws = null;
var message = null;
$('#login').click(function(){
var nickname = $('#nickname').val();
if(nickname == null || nickname.length == 0){
alert("请输入昵称后再登录");
return;
}
var url = "ws://localhost:7777/myws/" + nickname;
ws = new WebSocket(url);
ws.onopen = function(){
$('#nicknme').attr("disabled","disabled");//登录成功后昵称不可更改
$('#login').attr("disabled","disabled");//登录成功后登录按钮不可点击
$('#send').removeAttr("disabled");//登录成功后发送按钮可以被点击
$('#show').append("<p>连接成功</p>");
}
ws.onmessage = function(event){
$('#show').append("<p>" + event.data + "</p>");
}
ws.onerror = function(){
alert("连接出错");
}
//发送消息
$('#send').click(function(){
var message = $('#message').val();
if(message == null || message.length == 0){
alert("消息不能为null");
return;
}
ws.send(message);
});
});
</script>
</html>
在这里,我用html页面充当客户端来和服务端连接。需要注意的是,我们后台WebSocket的服务端是相当于一个映射路径。然后和SpringBoot整合,在配置类里配置ServerEndpointExporter对象。把映射路径托管给Spring来处理。这样Spring就不会拦截这个请求。
效果图我就不放上来了,代码copy一下,就可以运行。不过博主还是建议,代码自己写一遍,毕竟咱们是学习技术的嘛。哈哈
如果有什么问题,可以在下方留言。我看到会一一回复。