如果你想搭建一个聊天服务,但是又不想用第三方服务,比如:环信,融云等其他,本篇文章可作为参考。文章采用springboot2.2.x构建,因为部分代码涉及业务,已移除,标识TODO都需要配合你自身的业务实现,如果遇到问题可以在评论区提问,或者私聊我也可以
1.导入maven引入依赖
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.18</version>
</dependency>
2.配置yml
socketio:
host: 192.168.0.120
port: 8887
# 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
maxFramePayloadLength: 1048576
# 设置http交互最大内容长度
maxHttpContentLength: 1048576
# socket连接数大小(如只监听一个端口boss线程组为1即可)
bossCount: 1
workCount: 100
allowCustomRequests: true
# 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间
upgradeTimeout: 1000000
# Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
pingTimeout: 6000000
# Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
pingInterval: 25000
3.创建socketIo的配置类
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.Transport;
import com.corundumstudio.socketio.listener.ExceptionListener;
import io.netty.channel.ChannelHandlerContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class SocketIOConfig {
@Value("${socketio.host}")
private String host;
@Value("${socketio.port}")
private Integer port;
@Value("${socketio.bossCount}")
private int bossCount;
@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;
@Bean
public SocketIOServer socketIOServer() {
SocketConfig socketConfig = new SocketConfig();
socketConfig.setTcpNoDelay(true);
socketConfig.setSoLinger(0);
// 解决重启端口占用问题,但因为是docker部署好像没有发现这个问题
// socketConfig.setReuseAddress(true);
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setSocketConfig(socketConfig);
// config.setHostname(host);
config.setPort(port);
config.setBossThreads(bossCount);
config.setWorkerThreads(workCount);
config.setAllowCustomRequests(allowCustomRequests);
config.setUpgradeTimeout(upgradeTimeout);
config.setPingTimeout(pingTimeout);
config.setPingInterval(pingInterval);
config.setTransports(Transport.WEBSOCKET);
config.setRandomSession(true); //default is false
//鉴权
config.setAuthorizationListener(data -> {
//socketio可以传参,可以获取到 链接后面的参数 ?username=1&pwd=2
String username= data.getSingleUrlParam("username");
String pwd= data.getSingleUrlParam("pwd");
//这里可以用作链接时的权限判断,返回false就是不允许链接,
return true;
});
// 异常
config.setExceptionListener(new ExceptionListener() {
@Override
public void onEventException(Exception e, List<Object> args, SocketIOClient client) {
e.printStackTrace();
}
@Override
public void onDisconnectException(Exception e, SocketIOClient client) {
e.printStackTrace();
}
@Override
public void onConnectException(Exception e, SocketIOClient client) {
e.printStackTrace();
}
@Override
public void onPingException(Exception e, SocketIOClient client) {
e.printStackTrace();
}
@Override
public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
return false;
}
});
return new SocketIOServer(config);
}
}
4.创建 ISocketIoService 用作socketio的接口服务
public interface ISocketIoService {
/**
* 启动服务
*/
void start();
/**
* 停止服务
*/
void stop();
}
5. 实现SocketIoServiceImpl
package com.yianjia.im.socketio;
import com.alibaba.fastjson.JSON;
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.yianjia.im.project.utils.JsonUtils;
import lombok.Data;
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.Service;
import javax.annotation.PreDestroy;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class SocketIoServiceImpl implements ISocketIoService {
private final static Logger logger = LoggerFactory.getLogger(SocketIoServiceImpl.class);
/**
* 存放已连接的客户端
*/
private static final Map<String, SocketIOClient> accountClientMap = new ConcurrentHashMap<>();
private static final Map<String, ImAccount> clientAccountMap = new ConcurrentHashMap<>();
/**
* 发送消息
*/
private static final String SEND_MSG_EVENT = "send_msg_event";
/**
* 客户端接收消息
*/
private static final String ON_RECEIVE_MSG = "on_receive_msg";
/**
* 未签收
*/
private static final String UNSIGN_MSG_EVENT = "unsign_msg_event";
@Autowired
private SocketIOServer socketIOServer;
/**
* Spring IoC容器在销毁SocketIOServiceImpl Bean之前关闭,避免重启项目服务端口占用问题
*/
@PreDestroy
private void autoStop() {
stop();
}
@Override
public void start() {
socketIOServer.start();
}
@Override
public void stop() {
if (socketIOServer != null) {
socketIOServer.stop();
socketIOServer = null;
}
}
/**
* 推送信息给指定客户端
*
* @param accountCode
* @param msgContent
* @param imMsg
*/
public void pushMessageToAccount(String accountCode, String msgContent, 发送消息体 imMsg) {
SocketIOClient client = accountClientMap.get(accountCode);
if (client != null && client.isChannelOpen()) {
client.sendEvent(ON_RECEIVE_MSG, msgContent);
logger.info("发送消息:" + accountCode);
}
}
/**
* 获取客户端url中的鉴权参数
*
* @param client: 客户端
*/
private Object getParamsByClient(SocketIOClient client) {
// 获取客户端url参数
String username = client.getHandshakeData().getSingleUrlParam("username");
String pwd = client.getHandshakeData().getSingleUrlParam("pwd");
//这里主要是返回用户信息 TODO
return null;
}
/**
* 获取连接的客户端ip地址
*
* @param client: 客户端
* @return: java.lang.String
*/
private String getIpByClient(SocketIOClient client) {
String sa = client.getRemoteAddress().toString();
if (client.getHandshakeData().getHttpHeaders().get("x-forwarded-for") != null) {
return client.getHandshakeData().getHttpHeaders().get("x-forwarded-for");
}
return sa.substring(1, sa.indexOf(":"));
}
/**
* 客户端链接
*
* @param client 客户端
*/
@OnConnect
private void connectListener(SocketIOClient client) {
Object user = getParamsByClient(client);
if (null == user) {
client.disconnect();
return;
}
logger.info("************ 客户端: " + getIpByClient(client) + " 已连接: " + user + " ************");
// TODO 获取用户的唯一标识,从上面user里面拿user.getImCode();
String userImCode = "";
SocketIOClient socketIOClient = accountClientMap.get(userImCode);
if (null != socketIOClient) {
if (clientAccountMap.containsKey(socketIOClient.getSessionId().toString())) {
clientAccountMap.remove(socketIOClient.getSessionId().toString());
}
if (socketIOClient.isChannelOpen()) {
socketIOClient.sendEvent("disconnect", "断开连接");
socketIOClient.disconnect();
}
}
accountClientMap.put(userImCode, client);
ImAccount imAccount = new ImAccount();
imAccount.setImCode(userImCode);
clientAccountMap.put(client.getSessionId().toString(), imAccount);
logger.info("当前在线人数:" + accountClientMap.keySet().size());
client.sendEvent("connected", "成功连接");
}
/**
* 链接断开
*
* @param client 客户端
*/
@OnDisconnect
private void disconnectListener(SocketIOClient client) {
logger.info("************ 客户端: " + getIpByClient(client) + " 已断开 ************");
client.disconnect();
clientAccountMap.remove(client.getSessionId().toString());
// 这里没有删除 account,是因为存在账户后登陆,然后上一个链接才断线的情况
logger.info("当前在线人数:" + clientAccountMap.keySet().size());
}
/**
* 未签收消息监听
*
* @param client 客户端
* @param data 数据
* @param ackRequest 回调函数
*/
@OnEvent(value = UNSIGN_MSG_EVENT)
private void unreadConversationEventListener(SocketIOClient client, String data, AckRequest ackRequest) {
ImAccount imAccount = clientAccountMap.get(client.getSessionId().toString());
String imCode = imAccount.getImCode();
if (StringUtils.isBlank(imCode)) {
client.disconnect();
return;
}
String clientIp = getIpByClient(client);
logger.info(clientIp + " 客户端 未签收消息");
/**
* TODO 根据imCode去数据库里面查询这个账户没有签收的消息
*/
List<消息db类> msgList = new ArrayList<>();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<消息db类> updateMsgList = msgList.stream().map(imMsg -> {
发送消息体 sendChatMsg = new 发送消息体();
// TODO 构建发送消息体,然后判断连接是否打开,打开的话,发过去
if (client.isChannelOpen()) {
client.sendEvent(ON_RECEIVE_MSG, JsonUtils.objectToJson(sendChatMsg));
消息db类.setSign("已签收");
return imMsg;
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList());
if (1 <= updateMsgList.size()) {
// TODO 更新数据库的消息状态
update(updateMsgList);
}
ackRequest.sendAckData("ok");
}
/**
* 消息发送监听
*
* @param client 客户端
* @param data 数据
* @param ackRequest 回调函数
*/
@OnEvent(value = SEND_MSG_EVENT)
private void sendMsgEventListener(SocketIOClient client, String data, AckRequest ackRequest) {
ImAccount imAccount = clientAccountMap.get(client.getSessionId().toString());
String imCode = imAccount.getImCode();
if (StringUtils.isBlank(imCode)) {
client.disconnect();
return;
}
String clientIp = getIpByClient(client);
logger.info(clientIp + " 客户端消息:" + data);
// TODO 将接受的消息字符串转成自己的对象
发送消息体 chatMsg = JSON.parseObject(data, 发送消息体.class);
// TODO 构建消息db类,并进行保存
消息db类 imMsg = new 消息db类();
save(imMsg);
// TODO 构建发送消息体,然后判断连接是否打开,打开的话,发过去
发送消息体 sendChatMsg = new 发送消息体();
pushMessageToAccount(acceptAccountCode, JsonUtils.objectToJson(sendChatMsg), imMsg);
ackRequest.sendAckData(data);
}
@Data
class ImAccount {
/**
* imCode,标识用户的唯一码
*/
private String imCode;
}
}
6.启动socketIo服务
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class SocketIoServer implements ApplicationRunner {
@Autowired
private ISocketIoService socketIOService;
@Override
public void run(ApplicationArguments args) throws Exception {
socketIOService.start();
}
}
7. 在Application里面注册配置
@Bean
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
return new SpringAnnotationScanner(socketServer);
}
8.Nginx配置参考