背景:
目前手上正在开发一个xxx中心的项目,服务端需要和客户端进行即时通讯以确定客户端是否在线。认证这块通过用户登录方式使用http通道颁发token令牌,socket通道则使用这个token令牌进行建立通信链接。需求是如果令牌是非法的则不允许建立链接。
开始集成:
服务端
1、引入socket.io坐标:
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.7</version>
</dependency>
2、在配置文件中增加socket配置项
socketio:
#生产环境注意把127.0.0.1给换了
host: 127.0.0.1
port: 8188
maxFramePayloadLength: 1048576
maxHttpContentLength: 1048576
bossCount: 1
workCount: 100
allowCustomRequests: true
upgradeTimeout: 1000000
pingTimeout: 6000000
pingInterval: 25000
3、创建socket启动配置类,注意:jwt鉴权服务类你要换成自己的,在文章中我就不写了,代码太多了。
package com.back.web.core.config;
import com.back.framework.web.service.TokenService;
import com.back.system.service.ISysUserService;
import com.corundumstudio.socketio.*;
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;
@Configuration
public class SocketIOConfig {
/*
//这里换成你的签名验签服务
@Autowired
private TokenService tokenService;*/
@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);
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);
//下面屏蔽的是链接鉴权方法,由于我使用的jwt相关方法和类很多,没法往文章里写
/* config.setAuthorizationListener(new AuthorizationListener() {
@Override
public boolean isAuthorized(HandshakeData handshakeData) {
//获取socket链接发来的token参数
String tonken=handshakeData.getSingleUrlParam("token");
String userId=handshakeData.getSingleUrlParam("userId");
//这个isPass是我的验签token方法,如果验签通过就是true,否则false,false的话就不允许建立socket链接
return tokenService.isPass(tonken);
}
});*/
return new SocketIOServer(config);
}
}
备注:这里让大家看一下我的isPass方法,因为自定义程度较高,嵌套调用了很多我自己封装方法,并不通用这里只供参考,所以不需要复制到你的代码中。
public boolean isPass( String token) { // 获取请求携带的令牌 System.out.println("每次请求获得的token:"+token); if (StringUtils.isNotEmpty(token)) { try { Claims claims = parseToken(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); if(user!=null){ return true; }else { return false; } } catch (Exception e) { } } return false; }
4、创建服务层接口类
package com.back.system.service;
/**
* 即时通讯 服务层
*
* @author liujian
*/
public interface ISocketIOService {
/**
* 启动服务
*/
void start();
/**
* 停止服务
*/
void stop();
/**
* 推送信息给指定客户端
*
* @param userId: 客户端唯一标识
* @param msgContent: 消息内容
*/
void pushMessageToUser(String userId, String msgContent);
}
5、创建服务层实现类
package com.back.system.service.impl;
import com.back.common.utils.DateUtils;
import com.back.common.utils.http.HttpUtils;
import com.back.system.service.ISocketIOService;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service(value = "socketIOService")
public class SocketIOServiceImpl implements ISocketIOService {
private static final Logger log = LoggerFactory.getLogger(SocketIOServiceImpl.class);
/**
* 存放已连接的客户端
*/
private static Map<String, SocketIOClient> clientMap = new ConcurrentHashMap<>();
/**
* 自定义事件`push_data_event`,用于服务端与客户端通信
*/
private static final String PUSH_DATA_EVENT = "push_data_event";
@Autowired
private SocketIOServer socketIOServer;
/**
* Spring IoC容器创建之后,在加载SocketIOServiceImpl Bean之后启动
*/
@PostConstruct
private void autoStartup() {
start();
}
/**
* Spring IoC容器在销毁SocketIOServiceImpl Bean之前关闭,避免重启项目服务端口占用问题
*/
@PreDestroy
private void autoStop() {
stop();
}
@Override
public void start() {
// 监听客户端连接
socketIOServer.addConnectListener(client -> {
log.debug("客户端: " + getIpByClient(client) + " 已连接 ");
// 自定义事件`connected` -> 与客户端通信 (也可以使用内置事件,如:Socket.EVENT_CONNECT)
client.sendEvent("connected", "连接成功");
String userId = getParamsByClient(client);
if (userId != null) {
clientMap.put(userId, client);
}
});
// 监听客户端断开连接
socketIOServer.addDisconnectListener(client -> {
String clientIp = getIpByClient(client);
log.debug(clientIp + "客户端已断开连接");
String userId = getParamsByClient(client);
if (userId != null) {
clientMap.remove(userId);
client.disconnect();
}
});
// 自定义事件`client_info_event` -> 监听客户端消息
socketIOServer.addEventListener(PUSH_DATA_EVENT, String.class, (client, data, ackSender) -> {
// 客户端推送`client_info_event`事件时,onData接受数据,这里是string类型的json数据,还可以为Byte[],object其他类型
String clientIp = getIpByClient(client);
log.debug(clientIp + "客户端:" + data);
});
// 启动服务
socketIOServer.start();
// broadcast: 默认是向所有的socket连接进行广播,但是不包括发送者自身,如果自己也打算接收消息的话,需要给自己单独发送。
/* new Thread(() -> {
int i = 0;
while (true) {
try {
// 每5秒发送一次广播消息
Thread.sleep(5000);
socketIOServer.getBroadcastOperations().sendEvent("myBroadcast", "广播消息 " + DateUtils.dateTimeNow());
log.debug("服务端:"+"发送了第"+(i+1)+"条广播!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();*/
}
@Override
public void stop() {
if (socketIOServer != null) {
socketIOServer.stop();
socketIOServer = null;
}
}
@Override
public void pushMessageToUser(String userId, String msgContent) {
SocketIOClient client = clientMap.get(userId);
if (client != null) {
client.sendEvent(PUSH_DATA_EVENT, msgContent);
}
}
/**
* 获取客户端url中的userId参数(这里根据个人需求和客户端对应修改即可)
*
* @param client: 客户端
* @return: java.lang.String
*/
private String getParamsByClient(SocketIOClient client) {
// 获取客户端url参数(这里的userId是唯一标识)
Map<String, List<String>> params = client.getHandshakeData().getUrlParams();
List<String> userIdList = params.get("userId");
if (!CollectionUtils.isEmpty(userIdList)) {
return userIdList.get(0);
}
return null;
}
/**
* 获取连接的客户端ip地址
*
* @param client: 客户端
* @return: java.lang.String
*/
private String getIpByClient(SocketIOClient client) {
String sa = client.getRemoteAddress().toString();
String clientIp = sa.substring(1, sa.indexOf(":"));
return clientIp;
}
}
客户端
首先你的使用ide创建一个maven项目,在这个项目里进行下列操作:
1、引入socket.io客户端坐标以及一个工具类的坐标
<dependency>
<groupId>io.socket</groupId>
<artifactId>socket.io-client</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
2、客户端代码,主要是main函数里面的
public static void main(String[] args) {
//SpringApplication.run(SocketClientApplication.class, args);
// 服务端socket.io连接通信地址
String url = "http://127.0.0.1:8188";
try {
IO.Options options = new IO.Options();
options.transports = new String[]{"websocket"};
options.reconnectionAttempts = 2;
// 失败重连的时间间隔
options.reconnectionDelay = 1000;
// 连接超时时间(ms)
options.timeout = 500;
// userId: 唯一标识 传给服务端存储
final Socket socket = IO.socket(url + "?userId=1&token=eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImIxNmY3MDFhLTI0YTEtNDRkZi05Y2Y5LTE5ZTM2ZDAzOThmYSJ9.005IuyLNA5cxtMNK33zrWXi9ulGNnvy-t7px98BD2Y3s_-iXCnkqtfE5rYWW9AX4806lhwTZ4hrOPKZtOckAdA", options);
socket.on(Socket.EVENT_CONNECT, args1 -> socket.send("hello..."));
// 自定义事件`connected` -> 接收服务端成功连接消息
socket.on("connected", objects -> System.out.println("服务端:" + objects[0].toString()));
// 自定义事件`push_data_event` -> 接收服务端消息
socket.on("push_data_event", objects -> System.out.println("服务端:" + objects[0].toString()));
// 自定义事件`myBroadcast` -> 接收服务端广播消息
socket.on("myBroadcast", objects -> System.out.println("服务端:" + objects[0].toString()));
socket.connect();
while (true) {
Thread.sleep(5000);
// 自定义事件`push_data_event` -> 向服务端发送消息
socket.emit("push_data_event", "发送数据 " + DateUtils.dateTimeNow());
// 自定义事件`push_data_event` -> 接收服务端消息
socket.on("push_data_event", new Emitter.Listener() {
@Override
public void call(Object... objects) {
if (objects.length > 1) {
System.out.println("服务端:" + objects[0].toString());
}
}
});
// 自定义事件`push_data_event` -> 接收服务端消息
// socket.on("push_data_event", objects -> System.out.println("服务端:" + objects[0].toString()));
// 自定义事件`myBroadcast` -> 接收服务端广播消息
socket.on("myBroadcast", objects -> System.out.println("服务端:" + objects[0].toString()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
3、工具类,这个工具类用到了开头的那个long3的坐标。
package socket.io.client.socketClient;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.lang.management.ManagementFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 时间工具类
*
* @author liujian
*/
public class DateUtils extends org.apache.commons.lang3.time.DateUtils
{
public static String YYYY = "yyyy";
public static String YYYY_MM = "yyyy-MM";
public static String YYYY_MM_DD = "yyyy-MM-dd";
public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
private static String[] parsePatterns = {
"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
"yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
"yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
/**
* 获取当前Date型日期
*
* @return Date() 当前日期
*/
public static Date getNowDate()
{
return new Date();
}
/**
* 获取当前日期, 默认格式为yyyy-MM-dd
*
* @return String
*/
public static String getDate()
{
return dateTimeNow(YYYY_MM_DD);
}
public static final String getTime()
{
return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
}
public static final String dateTimeNow()
{
return dateTimeNow(YYYYMMDDHHMMSS);
}
public static final String dateTimeNow(final String format)
{
return parseDateToStr(format, new Date());
}
public static final String dateTime(final Date date)
{
return parseDateToStr(YYYY_MM_DD, date);
}
public static final String parseDateToStr(final String format, final Date date)
{
return new SimpleDateFormat(format).format(date);
}
public static final Date dateTime(final String format, final String ts)
{
try
{
return new SimpleDateFormat(format).parse(ts);
}
catch (ParseException e)
{
throw new RuntimeException(e);
}
}
/**
* 日期路径 即年/月/日 如2018/08/08
*/
public static final String datePath()
{
Date now = new Date();
return DateFormatUtils.format(now, "yyyy/MM/dd");
}
/**
* 日期路径 即年/月/日 如20180808
*/
public static final String dateTime()
{
Date now = new Date();
return DateFormatUtils.format(now, "yyyyMMdd");
}
/**
* 日期型字符串转化为日期 格式
*/
public static Date parseDate(Object str)
{
if (str == null)
{
return null;
}
try
{
return parseDate(str.toString(), parsePatterns);
}
catch (ParseException e)
{
return null;
}
}
/**
* 获取服务器启动时间
*/
public static Date getServerStartDate()
{
long time = ManagementFactory.getRuntimeMXBean().getStartTime();
return new Date(time);
}
/**
* 计算两个时间差
*/
public static String getDatePoor(Date endDate, Date nowDate)
{
long nd = 1000 * 24 * 60 * 60;
long nh = 1000 * 60 * 60;
long nm = 1000 * 60;
// long ns = 1000;
// 获得两个时间的毫秒时间差异
long diff = endDate.getTime() - nowDate.getTime();
// 计算差多少天
long day = diff / nd;
// 计算差多少小时
long hour = diff % nd / nh;
// 计算差多少分钟
long min = diff % nd % nh / nm;
// 计算差多少秒//输出结果
// long sec = diff % nd % nh % nm / ns;
return day + "天" + hour + "小时" + min + "分钟";
}
}
最后:一个socket通信服务,并且使用token鉴权完成了。提示一下,我遇到了鉴权失败的问题,因为客户端token传错了。注意下图
如果不注意上图,加了单引号,那么在这里获取token的时候,那个token值如下图解释。