平常项目中大部分还是hashmap,而在多线程环境下会出现线程安全的问题,因此这里采用concurrenthashmap
此代码只留作个人学习使用。
定义一个concurrenthashmap子类:
存储房间id和netty Channel对应关系
public class SessionGroup extends ConcurrentHashMap<String, Collection<Channel>> {
private static final Collection<Channel> EMPTY_LIST = new LinkedList();
private final transient ChannelFutureListener remover = new ChannelFutureListener() {
//ChannelFutureListener:监听 ChannelFuture 的结果。一旦通过调用 ChannelFuture.addListener(GenericFutureListener) 添加此侦听器,就会通知异步 Channel IO 操作的结果。将控制权快速返回给调用者 GenericFutureListener.operationComplete(Future) 直接由IO线程调用。因此,在处理程序方法中执行耗时任务或阻塞操作可能会导致 IO 期间出现意外暂停。
public void operationComplete(ChannelFuture future) {
future.removeListener(this);
SessionGroup.this.remove(future.channel());
}
};
public SessionGroup() {
}
protected String getKey(Channel channel) {
return (String)channel.attr(ChannelAttr.UID).get();
}
public void remove(Channel channel) {
String uid = this.getKey(channel);
if (uid != null) {
Collection<Channel> collections = (Collection)this.getOrDefault(uid, EMPTY_LIST);
collections.remove(channel);
if (collections.isEmpty()) {
this.remove(uid);
}
}
}
public void add(Channel channel) {
String uid = this.getKey(channel);
if (uid != null && channel.isActive()) {
channel.closeFuture().addListener(this.remover);
Collection<Channel> collections = (Collection)this.putIfAbsent(uid, new ConcurrentLinkedQueue(Collections.singleton(channel)));//Collections.singleton()方法是java.util.Collections类的方法。它在单个指定的元素上创建一个不可变的集合。
//ConcurrentLinkedQueue:线程安全的queue,适合多个线程访问一个队列使用
//putIfAbsent:ConcurrentHashMap特有的方法,防止出现线程安全问题
if (collections != null) {
collections.add(channel);
}
if (!channel.isActive()) {
this.remove(channel);
}
}
}
public void write(String key, Message message) {
this.find(key).forEach((channel) -> {
channel.writeAndFlush(message);//writeAndFlush:写队列并刷新,这里将消息遍历集合中的每一个channel然后写入消息,即转发消息给所有接受者
});
}
public void write(String key, Message message, Predicate<Channel> matcher) {
this.find(key).stream().filter(matcher).forEach((channel) -> {
channel.writeAndFlush(message);
});
}
//关于predicate函数式接口:主要是test(T t) 方法:
// test方法主要用于参数符不符合规则。返回值 boolean
//使用predicate接口,下面可以根据判断结果进行过滤
public void write(String key, Message message, Collection<String> excludedSet) {
this.find(key).stream().filter((channel) -> {
return excludedSet == null || !excludedSet.contains(channel.attr(ChannelAttr.UID).get());
}).forEach((channel) -> {
channel.writeAndFlush(message);
});
}
public void write(Message message) {
this.write(message.getReceiver(), message);
}
public Collection<Channel> find(String key) {
return (Collection)this.getOrDefault(key, EMPTY_LIST);
}
public Collection<Channel> find(String key, String... channel) {
List<String> channels = Arrays.asList(channel);
return (Collection)this.find(key).stream().filter((item) -> {
return channels.contains(item.attr(ChannelAttr.CHANNEL).get());
}).collect(Collectors.toList());
}
}
附:concurrenthashmap:线程安全的hashmap,与hashmap相比新增的方法:
putIfAbsent:与原有put⽅法不同的是,putIfAbsent⽅法中如果插⼊的key相同,则不替换原有的value值;
remove:与原有remove⽅法不同的是,新remove⽅法中增加了对value的判断,如果要删除的key-value不能与Map中原有的key-value对应上,则不会删除该元素;
get方法不加锁,put等方法具体是通过Unsafe#getXXXVolatile 和用 volatile 来修饰节点的 val 和 next 指针来实现的
public class TagSessionGroup extends SessionGroup {
public TagSessionGroup() {
}
protected String getKey(Channel channel) {
return (String)channel.attr(ChannelAttr.TAG).get();
}
}
推送消息类:
/**
* 推送群消息
*/
@Component
public class GroupMessagePusher {
@Resource
private TagSessionGroup tagSessionGroup;
public void push(final Message message) {
String roomId = message.getReceiver();
tagSessionGroup.write(roomId,message , channel -> !Objects.equals(message.getSender(),channel.attr(ChannelAttr.UID).get()));
}
public void push(final Message message,String uid) {
String roomId = message.getReceiver();
tagSessionGroup.write(roomId,message , channel -> Objects.equals(uid,channel.attr(ChannelAttr.UID).get()));//注意这里的房间id和channel的对应关系
}
}
handler:
public interface CIMRequestHandler {
void process(Channel var1, SentBody var2);
}
发送消息controller:
@RestController
@RequestMapping("/api/message")//对应room.html中的 function onMessageSend(){方法
public class MessageController {
@Resource
private GroupMessagePusher groupMessagePusher;
@PostMapping(value = "/send")
public @ResponseBody ResponseEntity<Long> send(@RequestParam long roomId,
@RequestParam String name,
@RequestParam String uid,
@RequestParam String icon,
@RequestParam String format,
@RequestParam String content) {
Message message = new Message();
message.setId(System.currentTimeMillis());
message.setAction(MessageAction.ACTION_CHAT);
message.setSender(uid);
message.setContent(content);
message.setReceiver(String.valueOf(roomId));
message.setFormat(format);
message.setExtra(name);
message.setTitle(String.valueOf(icon));
message.setTimestamp(System.currentTimeMillis());
groupMessagePusher.push(message);
return ResponseEntity.ok(message.getTimestamp());
}
}
handler处理器:
@Sharable
public class CIMNioSocketAcceptor extends SimpleChannelInboundHandler<SentBody> {
private static final Logger LOGGER = LoggerFactory.getLogger(CIMNioSocketAcceptor.class);
private static final int PONG_TIME_OUT_COUNT = 3;
private final ThreadFactory bossThreadFactory;
private final ThreadFactory workerThreadFactory;
private EventLoopGroup appBossGroup;
private EventLoopGroup appWorkerGroup;
private EventLoopGroup webBossGroup;
private EventLoopGroup webWorkerGroup;
private final Integer appPort;
private final Integer webPort;
private final CIMRequestHandler outerRequestHandler;
private final ChannelHandler loggingHandler = new LoggingHandler();
public final Duration writeIdle = Duration.ofSeconds(45L);
public final Duration readIdle = Duration.ofSeconds(60L);
public CIMNioSocketAcceptor(CIMNioSocketAcceptor.Builder builder) {
this.webPort = builder.webPort;
this.appPort = builder.appPort;
this.outerRequestHandler = builder.outerRequestHandler;
this.bossThreadFactory = (r) -> {
Thread thread = new Thread(r);
thread.setName("nio-boss-");
return thread;
};
this.workerThreadFactory = (r) -> {
Thread thread = new Thread(r);
thread.setName("nio-worker-");
return thread;
};
}
private void createWebEventGroup() {
if (this.isLinuxSystem()) {
this.webBossGroup = new EpollEventLoopGroup(this.bossThreadFactory);
this.webWorkerGroup = new EpollEventLoopGroup(this.workerThreadFactory);
} else {
this.webBossGroup = new NioEventLoopGroup(this.bossThreadFactory);
this.webWorkerGroup = new NioEventLoopGroup(this.workerThreadFactory);
}
}
private void createAppEventGroup() {
if (this.isLinuxSystem()) {
this.appBossGroup = new EpollEventLoopGroup(this.bossThreadFactory);
this.appWorkerGroup = new EpollEventLoopGroup(this.workerThreadFactory);
} else {
this.appBossGroup = new NioEventLoopGroup(this.bossThreadFactory);
this.appWorkerGroup = new NioEventLoopGroup(this.workerThreadFactory);
}
}
public void bind() {
if (this.appPort != null) {
this.bindAppPort();
}
if (this.webPort != null) {
this.bindWebPort();
}
}
public void destroy(EventLoopGroup bossGroup, EventLoopGroup workerGroup) {
if (bossGroup != null && !bossGroup.isShuttingDown() && !bossGroup.isShutdown()) {
try {
bossGroup.shutdownGracefully();
} catch (Exception var5) {
}
}
if (workerGroup != null && !workerGroup.isShuttingDown() && !workerGroup.isShutdown()) {
try {
workerGroup.shutdownGracefully();
} catch (Exception var4) {
}
}
}
public void destroy() {
this.destroy(this.appBossGroup, this.appWorkerGroup);
this.destroy(this.webBossGroup, this.webWorkerGroup);
}
private void bindAppPort() {
this.createAppEventGroup();
ServerBootstrap bootstrap = this.createServerBootstrap(this.appBossGroup, this.appWorkerGroup);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ChannelHandler[]{new AppMessageDecoder()});
ch.pipeline().addLast(new ChannelHandler[]{new AppMessageEncoder()});
ch.pipeline().addLast(new ChannelHandler[]{CIMNioSocketAcceptor.this.loggingHandler});
ch.pipeline().addLast(new ChannelHandler[]{new IdleStateHandler(CIMNioSocketAcceptor.this.readIdle.getSeconds(), CIMNioSocketAcceptor.this.writeIdle.getSeconds(), 0L, TimeUnit.SECONDS)});
ch.pipeline().addLast(new ChannelHandler[]{CIMNioSocketAcceptor.this});
}
});
ChannelFuture channelFuture = bootstrap.bind(this.appPort).syncUninterruptibly();
channelFuture.channel().newSucceededFuture().addListener((future) -> {
String logBanner = "\n\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n* *\n* *\n* App Socket Server started on port {}. *\n* *\n* *\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n";
(logBanner, this.appPort);
});
channelFuture.channel().closeFuture().addListener((future) -> {
this.destroy(this.appBossGroup, this.appWorkerGroup);
});
}
private void bindWebPort() {
this.createWebEventGroup();
ServerBootstrap bootstrap = this.createServerBootstrap(this.webBossGroup, this.webWorkerGroup);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ChannelHandler[]{new HttpServerCodec()});
ch.pipeline().addLast(new ChannelHandler[]{new ChunkedWriteHandler()});
ch.pipeline().addLast(new ChannelHandler[]{new HttpObjectAggregator(65536)});
ch.pipeline().addLast(new ChannelHandler[]{new WebSocketServerProtocolHandler("/", false)});
ch.pipeline().addLast(new ChannelHandler[]{new WebMessageDecoder()});
ch.pipeline().addLast(new ChannelHandler[]{new WebMessageEncoder()});
ch.pipeline().addLast(new ChannelHandler[]{CIMNioSocketAcceptor.this.loggingHandler});
ch.pipeline().addLast(new ChannelHandler[]{new IdleStateHandler(CIMNioSocketAcceptor.this.readIdle.getSeconds(), CIMNioSocketAcceptor.this.writeIdle.getSeconds(), 0L, TimeUnit.SECONDS)});
ch.pipeline().addLast(new ChannelHandler[]{CIMNioSocketAcceptor.this});
}
});
ChannelFuture channelFuture = bootstrap.bind(this.webPort).syncUninterruptibly();
channelFuture.channel().newSucceededFuture().addListener((future) -> {
String logBanner = "\n\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n* *\n* *\n* Websocket Server started on port {}. *\n* *\n* *\n* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n";
(logBanner, this.webPort);
});
channelFuture.channel().closeFuture().addListener((future) -> {
this.destroy(this.webBossGroup, this.webWorkerGroup);
});
}
private ServerBootstrap createServerBootstrap(EventLoopGroup bossGroup, EventLoopGroup workerGroup) {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
bootstrap.channel(this.isLinuxSystem() ? EpollServerSocketChannel.class : NioServerSocketChannel.class);
return bootstrap;
}
protected void channelRead0(ChannelHandlerContext ctx, SentBody body) {
this.outerRequestHandler.process(ctx.channel(), body);
}
public void channelActive(ChannelHandlerContext ctx) {
ctx.channel().attr().set(ctx.channel().id().asShortText());
}
public void channelInactive(ChannelHandlerContext ctx) {
if (ctx.channel().attr(ChannelAttr.UID) != null) {
SentBody body = new SentBody();
body.setKey("client_closed");
this.outerRequestHandler.process(ctx.channel(), body);
}
}
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleEvent = (IdleStateEvent)evt;
String uid = (String)ctx.channel().attr(ChannelAttr.UID).get();
if (idleEvent.state() == IdleState.WRITER_IDLE && uid == null) {
ctx.close();
} else {
Integer pingCount;
if (idleEvent.state() == IdleState.WRITER_IDLE && uid != null) {
pingCount = (Integer)ctx.channel().attr(ChannelAttr.PING_COUNT).get();
ctx.channel().attr(ChannelAttr.PING_COUNT).set(pingCount == null ? 1 : pingCount + 1);
ctx.channel().writeAndFlush(Ping.getInstance());
} else {
pingCount = (Integer)ctx.channel().attr(ChannelAttr.PING_COUNT).get();
if (idleEvent.state() == IdleState.READER_IDLE && pingCount != null && pingCount >= 3) {
ctx.close();
("{} pong timeout.", ctx.channel());
}
}
}
}
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOGGER.warn("EXCEPTION", cause);
}
private boolean isLinuxSystem() {
String osName = System.getProperty("").toLowerCase();
return osName.contains("linux");
}
public static class Builder {
private Integer appPort;
private Integer webPort;
private CIMRequestHandler outerRequestHandler;
public Builder() {
}
public CIMNioSocketAcceptor.Builder setAppPort(Integer appPort) {
this.appPort = appPort;
return this;
}
public CIMNioSocketAcceptor.Builder setWebsocketPort(Integer port) {
this.webPort = port;
return this;
}
public CIMNioSocketAcceptor.Builder setOuterRequestHandler(CIMRequestHandler outerRequestHandler) {
this.outerRequestHandler = outerRequestHandler;
return this;
}
public CIMNioSocketAcceptor build() {
return new CIMNioSocketAcceptor(this);
}
}
}
编码器和解码器:
public class AppMessageDecoder extends ByteToMessageDecoder {
public AppMessageDecoder() {
}
protected void decode(ChannelHandlerContext context, ByteBuf buffer, List<Object> queue) throws Exception {
context.channel().attr(ChannelAttr.PING_COUNT).set((Object)null);
if (buffer.readableBytes() >= 3) {
buffer.markReaderIndex();
byte type = buffer.readByte();
byte lv = buffer.readByte();
byte hv = buffer.readByte();
int length = this.getContentLength(lv, hv);
if (buffer.readableBytes() < length) {
buffer.resetReaderIndex();
} else {
byte[] dataBytes = new byte[length];
buffer.readBytes(dataBytes);
Object message = this.mappingMessageObject(dataBytes, type);
queue.add(message);
}
}
}
public Object mappingMessageObject(byte[] data, byte type) throws Exception {
if (0 == type) {
return Pong.getInstance();
} else {
Model bodyProto = Model.parseFrom(data);
SentBody body = new SentBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
body.putAll(bodyProto.getDataMap());
return body;
}
}
private int getContentLength(byte lv, byte hv) {
int l = lv & 255;
int h = hv & 255;
return l | h << 8;
}
}
public class AppMessageEncoder extends MessageToByteEncoder<Transportable> {
public AppMessageEncoder() {
}
protected void encode(ChannelHandlerContext ctx, Transportable data, ByteBuf out) {
byte[] body = data.getBody();
byte[] header = this.createHeader(data.getType(), body.length);
out.writeBytes(header);
out.writeBytes(body);
}
private byte[] createHeader(byte type, int length) {
byte[] header = new byte[]{type, (byte)(length & 255), (byte)(length >> 8 & 255)};
return header;
}
}
public interface Transportable {
byte[] getBody();
byte getType();
}
配置类:
public interface CIMRequestHandler {
void process(Channel var1, SentBody var2);
}
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(CIMHandler.class))
//ComponentScan的配置可以通知spring扫描拥有spring标准注解的类。这些标注大致是:@Component、@Controller、@Service、
// @Repository。但是我们也可以通过includeFilters属性配置通知spring扫描一些没有标准注解但是我们希望spring帮我们管理的类。
public class CIMConfiguration implements ApplicationListener<ApplicationStartedEvent> {
@Resource
private ApplicationContext applicationContext;
private final Map<String, CIMRequestHandler> handlerMap = new HashMap<>();
@Bean(destroyMethod = "destroy")
public CIMNioSocketAcceptor getNioSocketAcceptor(@Value("${cim.websocket.port}") int websocketPort) {
return new CIMNioSocketAcceptor.Builder()
.setWebsocketPort(websocketPort)
.setOuterRequestHandler(this)
.build();
}
@Bean
public TagSessionGroup tagSessionGroup() {
return new TagSessionGroup();
}
public void process(Channel channel, SentBody body) {
CIMRequestHandler handler = handlerMap.get(body.getKey());
if (handler != null){
handler.process(channel, body);
}
}
/**
* springboot启动完成之后再启动cim服务的,避免服务正在重启时,客户端会立即开始连接导致意外异常发生.
*/
@Override
public void onApplicationEvent(ApplicationStartedEvent startedEvent) {
Map<String, CIMRequestHandler> beans = applicationContext.getBeansOfType(CIMRequestHandler.class);
for (Map.Entry<String, CIMRequestHandler> entry : beans.entrySet()) {
CIMRequestHandler handler = entry.getValue();
CIMHandler annotation = handler.getClass().getAnnotation(CIMHandler.class);
if (annotation != null){
handlerMap.put(annotation.key(),handler);
}
}
applicationContext.getBean(CIMNioSocketAcceptor.class).bind();
}
}