Introduction
在使用netty向指定客户端发送消息的时候,应该需要怎么做呢?
方案
首先我们需要认识Netty的主要构件:
- Channel
- 回调
- Future
- 事件和ChannelHandle
在不考虑分布式netty的情况下,我们知道
netty的处理模型是存在一组IO线程,去处理IO事件,如read,connect,write等等,对于服务端接收到的每个channel,都会将该channel映射到一条IO线程。当一个channel被建立之后,需要将其初始化,包含给他创建pipleline并填充channelhandler;给channel附以channelOptions和channelAttrs等
换句话说,在没有加上@ChannelHandler.Sharable的情况下,每个handler都是channel独享的,这就不会发生线程安全问题
如何绑定客户端
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
通过阅读netty源码,我们可以知道每个用户channel都是由ChannelGroup进行管理的,而ChannelGroup的具体实现类是DefaultChannelGroup
public class DefaultChannelGroup extends AbstractSet<Channel> implements ChannelGroup {
private static final AtomicInteger nextId = new AtomicInteger();
private final String name;
private final EventExecutor executor;
private final ConcurrentMap<ChannelId, Channel> serverChannels;
private final ConcurrentMap<ChannelId, Channel> nonServerChannels;
private final ChannelFutureListener remover;
private final VoidChannelGroupFuture voidFuture;
private final boolean stayClosed;
private volatile boolean closed;
我们可以看到父类ChannelGroup该接口继承Set接口,因此可以通过ChannelGroup可管理服务器端所有的连接的Channel,然后对所有的连接Channel广播消息,而其子类是利用一个ConcurentMap来存储channel和对应的channelId的关系
在创建channel的时候,ChannelId接口及其实现类为Channel实现了一个全局唯一的标识Id,在channelActive时,将channel加入channelGroup中
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启!");
//添加到channelGroup通道组
MyChannelHandlerPool.channelGroup.add(ctx.channel());
}
public interface ChannelId extends Serializable, Comparable<ChannelId> {
String asShortText();
String asLongText();
}
因此,我们很容易想到的方法是在用户和channel之间创建绑定关系,最简单的方法是利用hashmap,将用户id作为key,channelId作为value定义在ChannelHandler中,维持一个用户的在线状态,而我这里使用的是redis,利用用户传来的jwt token进行在线状态的处理。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg.getClass());
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
Map paramMap=getUrlParams(uri);
System.out.println("接收到的参数是:"+ JSON.toJSONString(paramMap));
//如果url包含参数,需要处理
String token = (String)paramMap.get("token");
if(token==null){
ctx.close();
return;
}
Optional.ofNullable(token).ifPresent(u->{
this.userId = messageHandler.getUserInfo(token,ctx.channel().id().asLongText());
if(userId==null){
ctx.writeAndFlush(new TextWebSocketFrame("还未登录"));
ctx.close();
return;
}
});
if(uri.contains("?")){
String newUri=uri.substring(0,uri.indexOf("?"));
System.out.println(newUri);
request.setUri(newUri);
}
}else if(msg instanceof TextWebSocketFrame){
//正常的TEXT消息类型
TextWebSocketFrame frame=(TextWebSocketFrame)msg;
System.out.println("客户端收到服务器数据:" +frame.text());
// Object o = JSON.parse(frame.text());
JSONObject jsonObject = JSONObject.parseObject(frame.text());
messageHandler.handleMsg(frame.text());
// ctx.writeAndFlush(new TextWebSocketFrame(frame.text()));
// sendAllMessage(frame.text());
}
super.channelRead(ctx, msg);
}
下面是我的MessageHandler处理模块,这里我们可以看到在用户验证token成功(未过期)之后,将用户id作为key,channelid作为value存储到redis中,在连接关闭的时候调用offline方法作为下线状态
/**
* @program: cloud
* @description: 聊天业务处理
* @author: Mr.Wang
* @create: 2021-04-06 22:55
**/
@Getter
@Setter
@Slf4j
public class MessageHandler {
/**
* Authorization认证开头是"bearer "
*/
private static final String BEARER = "Bearer ";
private String signingKey = "123456";
RedisTemplate redisTemplate;
public MessageHandler(){
this.redisTemplate = SpringContextHolder.getBean("redisTemplate");
}
public void online(boolean flag,String token){
}
public String getUserInfo(String token,String contextId) {
String userId = null;
boolean invalid = Boolean.TRUE;
try {
Claims claims = getJwt(token).getBody();
userId = claims.get("id",String.class);
if(userId!=null){
redisTemplate.opsForValue().set("USERONLINE::"+ userId,contextId);
}
invalid = Boolean.FALSE;
return userId;
} catch (SignatureException | ExpiredJwtException | MalformedJwtException ex) {
log.error("user token error :{}", ex.getMessage());
return null;
} catch (Exception e){
e.printStackTrace();
}
return userId;
}
public void offLine(String userId){
redisTemplate.delete("USERONLINE::"+userId);
}
public Jws<Claims> getJwt(String jwtToken) {
if (jwtToken.startsWith(BEARER)) {
jwtToken = StringUtils.substring(jwtToken, BEARER.length());
}
return Jwts.parser() //得到DefaultJwtParser
.setSigningKey(signingKey.getBytes()) //设置签名的秘钥
.parseClaimsJws(jwtToken);
}
public void handleMsg(String msg){
JSONObject jsonObject = JSONObject.parseObject(msg);
Msg myMsg = new Msg();
String receiverId = (String)jsonObject.get("receiver");
myMsg.setContent((String)jsonObject.get("content"));
myMsg.setReceiver(receiverId);
myMsg.setSendDate(new Date());
myMsg.setMsgId(String.valueOf(redisTemplate.opsForValue().increment("MSG::uuid")));
if(receiverId!=null){
System.out.println("发送!");
String contextId =(String) redisTemplate.opsForValue().get("USERONLINE::"+receiverId);
System.out.println("id 是"+contextId);
MyChannelHandlerPool.channelGroup.writeAndFlush(new TextWebSocketFrame("你好"),new MyMacher(contextId));
System.out.println();
}
}
}
获取了channelid后,如何给指定id的channel发消息呢?
MyChannelHandlerPool.channelGroup.writeAndFlush(new TextWebSocketFrame("你好"),new MyMacher(contextId));
我们看到channelGroup的writeAndFlush方法
ChannelGroupFuture writeAndFlush(Object var1, ChannelMatcher var2);
ChannelGroupFuture writeAndFlush(Object var1, ChannelMatcher var2, boolean var3);
public ChannelGroupFuture writeAndFlush(Object message, ChannelMatcher matcher, boolean voidPromise) {
if (message == null) {
throw new NullPointerException("message");
} else {
Object future;
if (voidPromise) {
Iterator var5 = this.nonServerChannels.values().iterator();
while(var5.hasNext()) {
Channel c = (Channel)var5.next();
if (matcher.matches(c)) {
c.writeAndFlush(safeDuplicate(message), c.voidPromise());
}
}
future = this.voidFuture;
} else {
Map<Channel, ChannelFuture> futures = new LinkedHashMap(this.size());
Iterator var9 = this.nonServerChannels.values().iterator();
while(var9.hasNext()) {
Channel c = (Channel)var9.next();
if (matcher.matches(c)) {
futures.put(c, c.writeAndFlush(safeDuplicate(message)));
}
}
future = new DefaultChannelGroupFuture(this, futures, this.executor);
}
ReferenceCountUtil.release(message);
return (ChannelGroupFuture)future;
}
}
我们可以看到这里是利用hashmap的双向链表对channelgroup中持有的channel与对应macher调用matches方法进行判断,如果返回true则向对应channel发送消息
看到Matchs方法
public interface ChannelMatcher {
boolean matches(Channel var1);
}
这里我们自定义一个MyMatcher,定义构造函数重写方法
public class MyMacher implements ChannelMatcher {
String id;
public MyMacher(String id) {
this.id = id;
}
@Override
public boolean matches(Channel channel) {
return channel.id().asLongText().equals(this.id);
}
}
传入的id即为接收方的channelid,我们这里注意有asLongText()与asShortText()两种静态方法,在存储和获取的时候需要是相同的类型。
在matches中调用equals方法判断两者是否相同
最后
MyChannelHandlerPool.channelGroup.writeAndFlush(new TextWebSocketFrame("你好"),new MyMacher(contextId));
System.out.println();
这样就可以实现向指定channel发送消息了