Netty多通道多对话管理
- 多通道:基于不同用途,服务器会建立多个socket监听,因此用户连接服务器的时候也会连接多个socket
- 多对话:一个用户连接服务器成功则建立一个对话,如果有多个用户则可能建立多个对话
需要解决的问题有3个:
数据隔离
先上结论:
- 线程隔离(不可行):同会话读取数据会出现线程切换,这是Netty基于事件驱动的模型的特性导致的
- 会话隔离(可行):对于单通道业务可行,但是对于多通道业务,无法实现同一用户的数据共享
- 身份隔离(可行):适配各种业务场景,但是身份传输和管理机制相对复杂
-
线程隔离(不可行)
使用ThreadLocal存储用户数据,当出现线程切换的时候,用户数据丢失
-
会话隔离(可行)
有两种实现方式:使用ChannelId、使用ChannelHandlerContext的属性Attribute
- 使用ChannelId
public class YourHandler extends ChannelHandlerAdapter {
private Map<ChannelId, YourSessionData> sessionDataMap = new ConcurrentHashMap<>();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ChannelId channelId = ctx.channel().id();
YourSessionData sessionData = sessionDataMap.computeIfAbsent(channelId, k -> new YourSessionData());
// 使用sessionData处理数据
}
}
- 使用ChannelHandlerContext的属性Attribute
public class YourHandler extends ChannelHandlerAdapter {
private AttributeKey<YourSessionData> sessionDataKey = AttributeKey.valueOf("attName");
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Attribute<YourSessionData> attr = ctx.attr(sessionDataKey);
YourSessionData sessionData = attr.get();
if (sessionData == null) {
sessionData = new YourSessionData();
attr.set(sessionData);
}
// 使用sessionData处理数据
}
}
- 身份隔离(可行)
由用户端发起身份认证获取身份ID,并将身份ID用于存储会话数据的标识。
- 搭建用户登录服务(这里依然使用Netty,但是实际上其他任意方式实现都可行,如使用Spring搭建Http接口)
public class LoginCenterHandler extends ChannelHandlerAdapter {
/**
* 为了简便,用户身份信息验证过程略,并使用UUID作为用户登录id
* 实际使用过程中需要在channelRead方法中接收用户身份信息并验证,验证通过后保存用户登录状态
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
UUID uuid = UUID.randomUUID();
byte[] bytes = UUIDUtils.toBytes(uuid);//uuid转16位字节数组,过程略
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
final ChannelFuture f = ctx.writeAndFlush(buf);
//保存用户登录状态略
//登录成功后关闭通道
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
});
}
}
用户身份识别
- 在需要验证身份的消息头增加身份ID,解析获取并验证身份是否合法,常见的数据结构:
消息头 (消息长度, 身份ID, etc.) |
---|
消息体 (actual data being sent) |
- 要注意身份ID的传输以及防窃取,避免"身份伪造攻击"(Identity Spoofing Attack),这里通过比较本次与上一次请求的身份ID,不一致则判定为非法
public class MessageHandler extends ChannelHandlerAdapter {
private AttributeKey<String> attrId = AttributeKey.valueOf("id");
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
byte[] bytes = new byte[16];
in.readBytes(bytes);
UUID id = UUIDUtils.toUUID(bytes);//16位字节数组转uuid,过程略
//验证身份ID是否有效过程略
//验证是否身份伪造
Attribute<String> prevId = ctx.attr(attrId);
if (prevId.get() != null && !prevId.get().equals(id)) {
//非法则关闭通道
ctx.close();
}
//记录上一次身份ID
if (prevId.get() == null) {
prevId.set(id);
}
//消息体处理
}
}
多通道交互
这里推荐使用观察者模式(事件驱动)搭建多通道之间的交互,该模式的核心思想是基于事件和事件监听器的机制,当特定事件发生时,系统会通知已注册的监听器,并由它们来处理事件。这种方式使得程序可以更加灵活地响应用户的操作和系统的状态变化。
在Java中,事件驱动设计模式通常通过以下几个关键组件来实现:
- 事件(Event):事件是程序中发生的某种特定行为或状态变化,如按钮点击、鼠标移动等。通常,事件是一个类,包含描述事件类型和相关数据的属性和方法。
- 事件源(Event Source):事件源是产生事件的对象,它会在特定的情况下触发事件。例如,按钮、文本框等用户界面组件通常充当事件源。
- 事件监听器(Event Listener):事件监听器是一个接口,包含用于处理特定事件的方法。程序员需要实现监听器接口,并在其中编写事件发生时的处理逻辑。
- 事件注册(Event Registration):程序需要将事件监听器注册到事件源上,以便在事件发生时能够及时地通知监听器。
用户身份验证交互演示(消息发送通道和用户登录通道之间的交互)
- 事件接口:定义事件
public interface Event {
}
- 事件监听接口:处理特定事件
public interface EventListener<T extends Event,S> {
S handle(T t);
}
- 事件注册中心:注册事件监听接口,同时接受并处理事件。这里使用reflect反射获取当前包中所有事件监听接口,实现自动注册,也支持手动注册。事件处理过程使用异步编程CompletableFuture,支持异步和同步获取事件处理结果。同一个事件支持注册多个监听接口
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.lang.reflect.Modifier;
import java.util.stream.Collectors;
import java.io.File;
import java.util.Enumeration;
public class EventPublisher {
private static ConcurrentMap<Class<? extends Event>, List<EventListener>> MAPPER = new ConcurrentHashMap<>();
static {
try {
// 获取当前类所在的包名
String packageName = EventPublisher.class.getPackage().getName();
// 获取当前包下指定接口的所有实现类
Set<Class<?>> implementations = getInterfaceImplementations(packageName, EventListener.class);
// 输出所有实现类的名称
for (Class<?> implementation : implementations) {
register((EventListener) implementation.newInstance());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void register(EventListener listener) {
// 获取父接口
Type[] genericInterfaces = listener.getClass().getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
// 判断是否是 EventListener 接口的实现
if (genericInterface instanceof ParameterizedType && ((ParameterizedType) genericInterface).getRawType() == EventListener.class) {
// 获取泛型参数 T 的具体类型
Type[] typeArguments = ((ParameterizedType) genericInterface).getActualTypeArguments();
if (typeArguments.length > 0) {
Class<? extends Event> eventType = (Class<? extends Event>) typeArguments[0];
if (!MAPPER.containsKey(eventType)) {
MAPPER.put(eventType, new ArrayList<>());
}
List<EventListener> listeners = MAPPER.get(eventType);
listeners.add(listener);
}
}
}
}
public static List<CompletableFuture> publish(Event event) {
Class<? extends Event> eventType=event.getClass();
List<CompletableFuture> completableFutures=new ArrayList<>();
if (!MAPPER.containsKey(eventType))
return completableFutures;
List<EventListener> listeners = MAPPER.get(eventType);
for (EventListener listener : listeners) {
CompletableFuture future = CompletableFuture.supplyAsync(() -> listener.handle(event));
completableFutures.add(future);
}
return completableFutures;
}
// 获取指定包下所有实现了指定接口的类
public static Set<Class<?>> getInterfaceImplementations(String packageName, Class<?> interfaceClass) throws Exception {
// 获取指定包下的所有类
List<Class<?>> classes = getClasses(packageName);
// 过滤出实现了指定接口的类
return classes.stream()
.filter(interfaceClass::isAssignableFrom)
.filter(clazz -> !Modifier.isInterface(clazz.getModifiers())) // 过滤掉接口自身
.collect(Collectors.toSet());
}
// 获取指定包下的所有类
private static List<Class<?>> getClasses(String packageName) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
List<File> dirs = new ArrayList<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
List<Class<?>> classes = new ArrayList<>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName));
}
return classes;
}
private static List<Class<?>> findClasses(File directory, String packageName) throws ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
if (files == null) {
return classes;
}
for (File file : files) {
if (file.isDirectory()) {
classes.addAll(findClasses(file, packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
classes.add(Class.forName(className));
}
}
return classes;
}
}
- 手动注册事件和触发事件
/**
* 注册事件(这里使用匿名监听接口,在能够访问用户登录通道数据的上下文中注册)
*/
EventPublisher.register(new EventListener<LoginEvent, Integer>() {
@Override
public Integer handle(LoginEvent loginEvent) {
loginCenter.addLogged(loginEvent.getUuid().toString());
log.info("用户上线{}", loginEvent.getUuid());
log.info("当前登录用户{}人", loginCenter.loggedNum());
return loginCenter.loggedNum();
}
});
/**
* 触发事件(任意位置)
*/
EventPublisher.publish(new LoginEvent(uuid));