ctx.writeAndFlush(loginRequestMessage);)
使用ctx发送,注意入站处理器调用写相关方法,会触发出站处理器(从最后向前找)。/**
* 在连接建立好之后 触发active事件
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//负责接收用户在控制台的输入,负责向服务器发送各种消息
//改线程可以和netty的线程不相关联(不使用event loop group中的线程),独立接收输入
new Thread(() -> {
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入密码");
String password = scanner.nextLine();
//应该校验用户名密码是否为空,这里省略
LoginRequestMessage loginRequestMessage = new LoginRequestMessage(username, password);
System.out.println(loginRequestMessage);
//发送消息
ctx.writeAndFlush(loginRequestMessage);//入站处理器调用写相关方法,会触发出站处理器(从最后向前找)
System.out.println("等待后续操作...");
},"system in");
}
ch.pipeline().addLast(new xxxHandler());
的方式代码看起来比较冗余。因此将验证用户名和密码的代码直接封装到一个Handler类中:LoginRequestMessageHandler
,在外部new出该实例,加入到pipeline中即可。该handler读取client传过来的消息(入栈处理器,解码),再写出登入成功、失败的消息(出栈处理器,编码)。注意:
SimpleChannelInboundHandler
该handler只处理LoginRequestMessage,意思就是client发送再多种消息,当前这个handler只处理LoginRequestMessage这一种请求登入的消息,进行用户名密码验证。这样好处就是,不需要将所有类型的消息接收到,在判断是不是请求登入的消息,再进行处理。
@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
String username = msg.getUsername();
String password = msg.getPassword();
boolean login = UserServiceFactory.getUserService().login(username, password);//后期可以优化到数据库中查询,这里简单在内存中查
LoginResponseMessage message;
if (login) {
SessionFactory.getSession().bind(ctx.channel(), username);
message = new LoginResponseMessage(true, "登录成功");
} else {
message = new LoginResponseMessage(false, "用户名或密码不正确");
}
ctx.writeAndFlush(message);
}
}
之后客户端new出这个类的实例,加入到pipeline中 socketChannel.pipeline().addLast(LOGIN_REQMSG_HANDLER);
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//对比学习,LoginResponseMessage发送的类对应的是SimpleChannelInboundHandler
// 其只对应LoginReqMeg一种消息进行处理,现在这个handler是对所有的msg都进行处理,所以要判断类型。
if (msg instanceof LoginResponseMessage){
LoginResponseMessage loginResponseMessage = (LoginResponseMessage) msg;
log.info("{}",loginResponseMessage);
if (loginResponseMessage.isSuccess()){
//多线程的原子性操作!
SUCCESS_LOGIN.set(true);
}
// 唤醒 system in 线程
WAIT_FOR_LOGIN.countDown();
}
}
CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1)
配合WAIT_FOR_LOGIN.countDown();
就可以唤醒线程;WAIT_FOR_LOGIN.await();
进入等待(await就是等待计数器变为0)。本项目中:
在client输入登入的用户名,密码后,线程要停下来await(释放了cpu),等待server验证登入结果。
如果server那边验证通过了,client线程放行,选择聊天室场景。
如果server验证失败,client这边线程也要继续,但是可以关闭管道。
上面就涉及两个线程之间的通信,一个netty的nio线程,一个自定义线程,需要一个线程等待。
之前完成了登入,现在完成登入之后,要进行聊天场景的配置,分别有:单聊、小组聊天、创建群聊、查看小组成员、退出群聊、加入群聊等。
// 如果登录失败
if (!LOGIN.get()) {
ctx.channel().close();
return;
}
while (true) {
System.out.println("==================================");
System.out.println("send [username] [content]");
System.out.println("gsend [group name] [content]");
System.out.println("gcreate [group name] [m1,m2,m3...]");
System.out.println("gmembers [group name]");
System.out.println("gjoin [group name]");
System.out.println("gquit [group name]");
System.out.println("quit");
System.out.println("==================================");
String command = null;
try {
command = scanner.nextLine();
} catch (Exception e) {
break;
}
if(EXIT.get()){
return;
}
下面站在client角度进行场景分析:
最关键的消息是从 当前用户 发送着,接收消息者,消息内容
从字符串中解析出的字段一,进行配置,write写出消息,触发出站操作,对该消息进行自定义协议封装。
// 在连接建立后触发 active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 负责接收用户在控制台的输入,负责向服务器发送各种消息
new Thread(() -> {
System.out.println("请输入用户名:");
String username = scanner.nextLine();
if(EXIT.get()){
return;
}
System.out.println("请输入密码:");
String password = scanner.nextLine();
if(EXIT.get()){
return;
}
// 构造消息对象
LoginRequestMessage message = new LoginRequestMessage(username, password);
System.out.println(message);
// 发送消息
ctx.writeAndFlush(message);
System.out.println("等待后续操作...");
try {
WAIT_FOR_LOGIN.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 如果登录失败
if (!LOGIN.get()) {
ctx.channel().close();
return;
}
while (true) {
System.out.println("==================================");
System.out.println("send [username] [content]");
System.out.println("gsend [group name] [content]");
System.out.println("gcreate [group name] [m1,m2,m3...]");
System.out.println("gmembers [group name]");
System.out.println("gjoin [group name]");
System.out.println("gquit [group name]");
System.out.println("quit");
System.out.println("==================================");
String command = null;
try {
command = scanner.nextLine();
} catch (Exception e) {
break;
}
if(EXIT.get()){
return;
}
String[] s = command.split(" ");
switch (s[0]){
case "send":
ctx.writeAndFlush(new ChatRequestMessage(username, s[1], s[2]));
break;
case "gsend":
ctx.writeAndFlush(new GroupChatRequestMessage(username, s[1], s[2]));
break;
case "gcreate":
Set<String> set = new HashSet<>(Arrays.asList(s[2].split(",")));
set.add(username); // 加入自己
ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));
break;
case "gmembers":
ctx.writeAndFlush(new GroupMembersRequestMessage(s[1]));
break;
case "gjoin":
ctx.writeAndFlush(new GroupJoinRequestMessage(username, s[1]));
break;
case "gquit":
ctx.writeAndFlush(new GroupQuitRequestMessage(username, s[1]));
break;
case "quit":
ctx.channel().close();
return;
}
}
}, "system in").start();
}
extends SimpleChannelInboundHandler
,这个每个client请求消息都能直接对应到目标的入站处理器,无需在channelRead方法之内做判断后分配。 @Override
public Set<String> getMembers(String name) {
return groupMap.getOrDefault(name, Group.EMPTY_GROUP).getMembers();
}
- 再通过每个组员名字的set集合拿到channel集合,使用了stream流进行操作
@Override
public List<Channel> getMembersChannel(String name) {
return getMembers(name).stream()
.map(member -> SessionFactory.getSession().getChannel(member))
.filter(Objects::nonNull) //处理还在线的
.collect(Collectors.toList());
}
原因
问题
client端发送心跳,如果没发送,说明连接假死
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
IdleStateEvent event = (IdleStateEvent) evt;
// 触发了写空闲事件
if (event.state() == IdleState.WRITER_IDLE) {
// log.debug("3s 没有写数据了,发送一个心跳包");
ctx.writeAndFlush(new PingMessage());
}
}
});
注意:读事件是入站,写是出站(从后向前传),对于触发IdleState这样的handler,可以是读写双向过期导致的,所以对应的handler要使用双向的:ChannelDuplexHandler. 但是这个handler 在这种情况下肯定不是去关心读写事件,这个handler只关系特殊事件(上一个IdleStateHandler产生的),重写对应的方法:userEventTrigger
服务器端解决,收上面的消息,比如5s内都没收到,那就说明client端假死了,以及不发心跳包了
如果能收到client端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
IdleStateEvent event = (IdleStateEvent) evt;
// 触发了读空闲事件
if (event.state() == IdleState.READER_IDLE) {
log.debug("已经 5s 没有读到数据了");
ctx.channel().close();
}
}
});
AtomicBoolean EXIT = new AtomicBoolean(false);