注意:开发时关闭缓存不然没法看到实时页面
<dependency>
<groupId>com.corundumstudio.socketiogroupId>
<artifactId>netty-socketioartifactId>
<version>${netty-socketio.version}version>
dependency>
package com.example.config.socket;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
* 配置
* @date: 2019/12/2
* @author: 潘顾昌
* @version: 0.0.1
* @copyright Copyright (c) 2019, Hand
*/
@Configuration
public class SocketConfig {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(SocketConfig.class);
@Value("${socketio.port}")
private Integer port;
@Value("${socketio.workCount}")
private int workCount;
@Value("${socketio.allowCustomRequests}")
private boolean allowCustomRequests;
@Value("${socketio.upgradeTimeout}")
private int upgradeTimeout;
@Value("${socketio.pingTimeout}")
private int pingTimeout;
@Value("${socketio.pingInterval}")
private int pingInterval;
@Value("${socketio.maxFramePayloadLength}")
private int maxFramePayloadLength;
@Value("${socketio.maxHttpContentLength}")
private int maxHttpContentLength;
/**
* SocketIOServer配置
*
*/
@Bean("socketIOServer")
public SocketIOServer socketIOServer() {
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
// 配置端口
config.setPort(port);
// 开启Socket端口复用
com.corundumstudio.socketio.SocketConfig socketConfig = new com.corundumstudio.socketio.SocketConfig();
socketConfig.setReuseAddress(true);
config.setSocketConfig(socketConfig);
// 连接数大小
config.setWorkerThreads(workCount);
// 允许客户请求
config.setAllowCustomRequests(allowCustomRequests);
// 协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
config.setUpgradeTimeout(upgradeTimeout);
// Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
config.setPingTimeout(pingTimeout);
// Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
config.setPingInterval(pingInterval);
// 设置HTTP交互最大内容长度
config.setMaxHttpContentLength(maxHttpContentLength);
// 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
config.setMaxFramePayloadLength(maxFramePayloadLength);
return new SocketIOServer(config);
}
/**
* 开启SocketIOServer注解支持
*
*/
@Bean
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
return new SpringAnnotationScanner(socketServer);
}
}
package com.example.model;
import java.io.Serializable;
/*
* 消息实体类
* @date: 2019/12/2
* @author: 潘顾昌
* @version: 0.0.1
* @copyright Copyright (c) 2019, Hand
*/
public class MessageDto implements Serializable {
/**
* 源客户端用户名
*/
private String sourceUserName;
/**
* 目标客户端用户名
*/
private String targetUserName;
/**
* 消息类型
*/
private String msgType;
/**
* 消息内容
*/
private String msgContent;
/**
* 空构造方法
*/
public MessageDto() {
}
/**
* 构造方法
*
* @param sourceUserName
* @param targetUserName
* @param msgType
* @param msgContent
*/
public MessageDto(String sourceUserName, String targetUserName, String msgType, String msgContent) {
this.sourceUserName = sourceUserName;
this.targetUserName = targetUserName;
this.msgType = msgType;
this.msgContent = msgContent;
}
public String getSourceUserName() {
return sourceUserName;
}
public void setSourceUserName(String sourceUserName) {
this.sourceUserName = sourceUserName;
}
public String getTargetUserName() {
return targetUserName;
}
public void setTargetUserName(String targetUserName) {
this.targetUserName = targetUserName;
}
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
public String getMsgContent() {
return msgContent;
}
public void setMsgContent(String msgContent) {
this.msgContent = msgContent;
}
}
package com.example.controller;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.example.constant.MsgTypeEnum;
import com.example.exception.CustomException;
import com.example.model.MessageDto;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/*
* Socket处理器
* @date: 2019/12/2
* @author: 潘顾昌
* @version: 0.0.1
* @copyright Copyright (c) 2019, Hand
*/
@Component
public class SocketHandler {
/**
* logger
*/
private Logger logger = LoggerFactory.getLogger(SocketHandler.class);
/**
* ConcurrentHashMap保存当前SocketServer用户ID对应关系
*/
private Map<String, UUID> clientMap = new ConcurrentHashMap<>(16);
public Map<String, UUID> getClientMap() {
return clientMap;
}
public void setClientMap(Map<String, UUID> clientMap) {
this.clientMap = clientMap;
}
/**
* socketIOServer
*/
private final SocketIOServer socketIOServer;
// 构造注入
@Autowired
public SocketHandler(SocketIOServer socketIOServer) {
this.socketIOServer = socketIOServer;
}
/**
* 当客户端发起连接时调用
*
*/
@OnConnect
public void onConnect(SocketIOClient socketIOClient) {
String userName = socketIOClient.getHandshakeData().getSingleUrlParam("userName");
if (StringUtils.isNotBlank(userName)) {
logger.info("用户{}开启长连接通知, NettySocketSessionId: {}, NettySocketRemoteAddress: {}",
userName, socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
// 保存
clientMap.put(userName, socketIOClient.getSessionId());
// 发送上线通知
this.sendMsg(null, null,
new MessageDto(userName, null, MsgTypeEnum.ONLINE.getValue(), null));
}
}
/**
* 客户端断开连接时调用,刷新客户端信息
*/
@OnDisconnect
public void onDisConnect(SocketIOClient socketIOClient) {
String userName = socketIOClient.getHandshakeData().getSingleUrlParam("userName");
if (StringUtils.isNotBlank(userName)) {
logger.info("用户{}断开长连接通知, NettySocketSessionId: {}, NettySocketRemoteAddress: {}",
userName, socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
// 移除
clientMap.remove(userName);
// 发送下线通知
this.sendMsg(null, null,
new MessageDto(userName, null, MsgTypeEnum.OFFLINE.getValue(), null));
socketIOClient.disconnect();
}
}
/**
* sendMsg发送消息事件
*
*/
@OnEvent("sendMsg")
public void sendMsg(SocketIOClient socketIOClient, AckRequest ackRequest, MessageDto messageDto) {
String userName = messageDto.getSourceUserName();
UUID uuid = clientMap.get(userName);
if (uuid == null && !MsgTypeEnum.OFFLINE.getValue().equals(messageDto.getMsgType())){
throw new CustomException("该用户已下线");
}
Collection<SocketIOClient> allClients = socketIOServer.getAllClients();
if (messageDto != null) {
// 全部发送
clientMap.forEach((key, value) -> {
if (value != null) {
socketIOServer.getClient(value).sendEvent("receiveMsg", messageDto);
}
});
}
}
@OnEvent("offLine")
public void offline(SocketIOClient socketIOClient, AckRequest ackRequest, MessageDto messageDto){
logger.info("用户{}断开长连接通知, NettySocketSessionId: {}, NettySocketRemoteAddress: {}",
messageDto.getSourceUserName(), socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
UUID sessionId = socketIOClient.getSessionId();
String userName = messageDto.getSourceUserName();
String msgType = messageDto.getMsgType();
if (MsgTypeEnum.OFFLINE.getValue().equals(msgType)){
UUID uuid = clientMap.get(userName);
clientMap.remove(userName);
}
// 发送下线通知
this.sendMsg(null, null,
new MessageDto(userName, null, MsgTypeEnum.OFFLINE.getValue(), null));
}
}
package com.example.constant;
/*
* 消息类型
* @date: 2019/12/2
* @author: 潘顾昌
* @version: 0.0.1
* @copyright Copyright (c) 2019, Hand
*/
public enum MsgTypeEnum {
/**
* 全部发送
*/
ALL("00"),
/**
* 上线
*/
ONLINE("01"),
/**
* 下线
*/
OFFLINE("02"),
/**
* 指定发送
*/
SINGLE("03");
private String value;
MsgTypeEnum(String type) {
value = type;
}
public String getValue() {
return value;
}
}
package com.example.config.socket;
import ch.qos.logback.core.net.server.ServerRunner;
import com.corundumstudio.socketio.SocketIOServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/*
* SpringBoot启动之后执行
* @date: 2019/12/2
* @author: 潘顾昌
* @version: 0.0.1
* @copyright Copyright (c) 2019, Hand
*/
@Component
@Order(1)
public class SocketServer implements CommandLineRunner {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(ServerRunner.class);
/**
* socketIOServer
*/
private final SocketIOServer socketIOServer;
@Autowired
public SocketServer(SocketIOServer socketIOServer) {
this.socketIOServer = socketIOServer;
}
@Override
public void run(String... args) {
logger.info("---------- NettySocket通知服务开始启动 ----------");
socketIOServer.start();
logger.info("---------- NettySocket通知服务启动成功 ----------");
}
}
package com.example.config.socket;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.example.constant.MsgTypeEnum;
import com.example.controller.SocketHandler;
import com.example.model.MessageDto;
import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
/**
* @Author: 潘顾昌
* @Date: 2019/12/2 13:38
*/
@Component
@Order(2)
public class ThreadRunner implements CommandLineRunner {
/**
* logger
*/
private static final Logger logger = LoggerFactory.getLogger(ThreadRunner.class);
@Autowired
private SocketHandler socketHandler;
@Autowired
private SocketIOServer socketIOServer;
@Override
public void run(String... args) throws Exception {
logger.info("---------- 线程开始启动 ----------");
ThreadFactory threadFactory = ThreadUtil.createThreadFactoryBuilder().setNamePrefix("pigic").setDaemon(true).build();
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, threadFactory);
scheduledExecutorService.scheduleWithFixedDelay(() -> {
Map<String, UUID> clientMap = socketHandler.getClientMap();
UUID uuid = clientMap.get("pgc");
MessageDto messageDto = new MessageDto();
messageDto.setSourceUserName("系统管理员");
messageDto.setTargetUserName("pgc");
messageDto.setMsgType(MsgTypeEnum.SINGLE.getValue());
messageDto.setMsgContent(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
SocketIOClient client = socketIOServer.getClient(uuid);
if (ObjectUtil.isNotNull(client)){
client.sendEvent("receiveMsg", messageDto);
}
}, 10, 10, TimeUnit.SECONDS);
logger.info("---------- 线程开始启动 ----------");
}
}
<html xmlns:th="http://www.thymeleaf.org">
<head th:include="/common/common :: headJq('聊天室')">head>
<body>
<div id="app">
<input id="msgContent" name="msgContent" placeholder="请输入消息">
<select id="msgType" name="msgType">
<option value="00">全部option>
select>
<input type="button" onclick="sendMsg()" value="发送"/>
<input type="button" onclick="offline()" value="发送"/>
<input type="button" onclick="cleanMsg()" value="清空"/>
<input type="button" onclick="viewMsg()" value="Vue美化版"/>
<ul id="msgList">ul>
div>
<script type="text/javascript" th:inline="javascript">
var userName, socket;
// Socket连接
function initIm() {
userName = prompt("请输入用户名进入聊天室");
if ($.trim(userName)) {
socket = io.connect("localhost:9090", {
'query': 'userName=' + userName
});
// 成功连接事件
socket.on('connect', function () {});
// 断开连接事件
socket.on('disconnect', function () {});
// 监听receiveMsg接收消息事件
socket.on('receiveMsg', function (data) {
console.log(data);
var msgLi = "" + moment().format('HH:mm:ss') + " ";
if (data.msgType == '00') {
// 发送消息给全部人
msgLi = msgLi + data.sourceUserName + ": " + data.msgContent;
} else if (data.msgType == '01') {
// 上线通知
msgLi = msgLi + "" + data.sourceUserName + "进入了聊天室";
} else if (data.msgType == '02') {
// 下线通知
msgLi = msgLi + "" + data.sourceUserName + "离开了聊天室";
} else if (data.msgType == '03') {
// 下线通知
msgLi = msgLi + data.sourceUserName + "->"+data.targetUserName+": " + data.msgContent;
}
msgLi = msgLi + "";
$('#msgList').append(msgLi);
});
} else {
alert("非法用户名");
}
}
initIm();
// 发送消息
function sendMsg() {
console.log("111");
if ($.trim(userName)) {
var msgContent = $.trim($("#msgContent").val());
// 消息不能为空
if (msgContent) {
socket.emit('sendMsg', {
"sourceUserName": userName,
"msgType": $("#msgType").val(),
"msgContent": msgContent
});
} else {
alert("消息不能为空");
}
$("#msgContent").val('');
} else {
initIm();
}
}
// 清空消息
function cleanMsg() {
var result = confirm("你确定清空消息吗");
if (result) {
$('#msgList').html('');
}
}
// 下线
function offline() {
console.log("准备下线!");
socket.disconnect();
socket.close();
// socket.emit('offLine', {
// "sourceUserName": userName,
// "msgType": '02',
// "msgContent": null
// });
}
// Vue美化版
function viewMsg() {
window.location = "/view.shtml";
}
script>
body>
html>