Netty实现简易聊天室
文章目录
在Netty 的学习结束之后就很想去实现一个简单的聊天室,在参考黑马视频之外,添加自己的理解和处理实现了一个建议的聊天室,本聊天室将实现简单的几个技术需求:
- 定制协议
- 协议格式定制
- 协议的编解码器
- 用户Session 会话功能
- 群聊Session 会话功能
- 实时聊天
在产品需求或者说业务需求的角度实现了以下几个功能:
- 用户在线登录
- 单对单实时聊天
- 群聊
- 群创建
- 获取群成员列表
- 群聊天
- 退出群
- 客户端退出
在下面的内容中将会分别按照以上基本内容进行梳理和实现:
代码仓库:Netty_sourceopen_project: Netty 开源项目 (gitee.com)——chartSimple
1.协议定制
Socket 或是ServerSocket是运输层传输的门户,之所以说是门户而不说是API,是因为都是基于运输层协议进行的封装,在运输层协议之上可以使用现成通用的协议,但是一些需求的情况下,通用协议无法满足实际的业务需求或者控制起来过于繁琐,这样就可以根据实际业务情况进行协议的定制。
自定义协议主要关注:
- 魔数
- 用来第一时间判定是否是无效数据包(就是用于判定是否是基于此协议的数据包)
- 版本号
- 可以支持协议的升级
- 序列化算法(消息正文的编解码)
- 消息正文采用哪种序列化反序列化方式,可以由此扩展,例如:json,protobuf,hessian,jdk
- 序列化的概念可以理解为将对象转为字节数组,使用jdk 的序列化方式就是实现Serializable接口
- 指令类型
- 是登录,注册…跟业务相关,用于区分消息的种类
- 请求序号
- 为双工通信时,提供异步能力,将接收到序列的消息按照正确的序号组合到一起
- 正文长度
- 消息正文
1.1协议格式:
魔数 | 版本号 | 序列化方式 | 指令类型 | 序列化id | 对齐填充 |
---|---|---|---|---|---|
4byte | 1byte | 1byte | 1byte | 4byte | 1byte |
1,2,3,4 | 1 | 0jdk 1json | Message | 请求连接序号 | 0xff |
1.2编解码器:
Netty中常用的编解码器有两种实现方式:
- 继承ByteToMessageCodec<目的消息类型>
- 继承 MessageToMessageCodec<ByteBuf, 目的消息类型>
两种的区别主要在于第一种默认没有进行黏包半包的处理,而第二种默认情况下进行了黏包半包的处理,因为我们这里要和Netty 提供的帧解码器LengthFieldBasedFrameDecoder使用(Netty 提供的解决黏包半包问题的Handler),所以选择通过第二种方式进行处理
@ChannelHandler.Sharable
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
/**
* 编码器
* @param ctx 流水线上下文
* @param msg 消息
* @param outList 输出
* @throws Exception
*/
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
/*这里需要自己创建ByteBuf*/
ByteBuf out = ctx.alloc().buffer();
/*1. 魔数 4Byte*/
out.writeBytes(new byte[]{1,2,3,4});
/*2. 版本号 1Byte*/
out.writeByte(1);
/*3. 序列化方式 jdk 0 json 1 1byte*/
out.writeByte(0);
/*4. 指令类型 1byte*/
out.writeByte(msg.getMessageType());
/*5. 请求序号 4byte*/
out.writeInt(msg.getSequenceId());
/*---------------首部有效部分 11Byte--------------------*/
/*6. 无效填充,对齐 1byte*/
out.writeByte(0xff);
ByteArrayOutputStream bos= new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();
/*7. 数据长度,4byte*/
out.writeInt(bytes.length);
/*8. 写入数据*/
out.writeBytes(bytes);
/*数据传入下一个处理器*/
outList.add(out);
}
/**
*
* 解码器
* @param ctx 流水线上下文
* @param in 这里的ByteBuf 不会有线程安全的问题,因为我们可以确定,
* 这里自定义编解码器的上一个一定是帧解码器,帧解码器可以将
* 一个完整的ByteBuf 传递给这个自定义编解码器,所以这里就不用
* 去考虑状态
* @param out 输出
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int magicNum = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId =in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes = new byte[length];
in.readBytes(bytes,0,length);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
out.add(message);
}
}
2.黏包半包问题
在应用层网络的传输过程中因为有收发缓冲区,Nagle算法等等一系列原因的存在,所以很容易就会出现黏包半包问题,而核心思路就是要确定消息的边界,而常见的解决方式有以下几种:
- 短连接:
- 每发送一条消息之后断开连接,以断开连接作为消息边界
- 连接的建立和断开都是非常消耗资源的显然这种方式不合适
- 定长解码器:
- 接收方和发送方约定消息的长度
- 对于一些固定长度的协议类型是非常方便的方式,但是对于我们的协议显然是不合适的
- Netty->FixedLengthFrameDecoder
- 分隔符界定符
- 双方约定分隔符的方式作为消息的边界
- 可以解决消息的边界问题,但是无论是发送方还是接收方,接收到数据之后都需要对数据进行遍历一个个的查找分隔符的位置,显然这种方式效率也不高。
- Netty->
- lineBaseFrameDecoder 以换行符作为界定符
- DelimiterBaseFrameDecoder 自定义界定符
- LTC解码器
- LengthFieldBasedFrameDecoder
- Netty 提供通过指定参数,对于消息字节流进行分割,单位都是byte
- maxFrameLength
- 消息最大长度
- lengthFieldOffset
- 长度字段开始的偏移量
- lengthFieldLength
- 长度字段长度
- lengthAdjustment
- initialBytesToStrip
- 组合控制截取数据字段,指定要截取数据开始的偏移,以及截取部分的数据大小
- maxFrameLength
这里直接使用Netty 提供的LTC解码器,对其进行包装:
package com.wang.demo.protocol.codec;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
/**
* @author: Jeffrey
* @date: 2022/01/18/9:20
* @description: 封装器 直接指定参数(作为引入外部配置的预留类)
*/
public class ProcotoFrameDecoder extends LengthFieldBasedFrameDecoder {
public ProcotoFrameDecoder(){
this(1024,
12,
4,
0,0);
}
public ProcotoFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip);
}
}
3.封装消息
3.1Message
Message 类是所有消息类型的父类,实现了序列化接口,封装消息类型,以及序号ID
package com.wang.demo.protocol.message;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* <p>用于客户端和服务端通讯消息的顶层抽象,其他通讯消息都继承自此类
* <p>实现JDK序列化接口
* @author: Jeffrey
* @date: 2022/01/20/10:14
* @description: 消息封装类
*
*/
@Data
public abstract class Message implements Serializable {
/**
* 请求序号ID
*/
private int sequenceId;
/**
* 消息类型
*/
private int messageType;
/**
* 获取消息ID的方法,由子类实现
* @return messageType
*/
public abstract int getMessageType();
/**
* 静态Map 存储消息类型码和消息类型类对象的映射关系
*/
private static final Map<Integer, Class<? extends Message>> messageClasses = new HashMap<>();
/**
* 根据消息类型,获得对应的消息的类对象
* @param messageType 消息类型字节
* @return 消息 class
*/
public static Class<? extends Message> getMessageClass(int messageType) {
return messageClasses.get(messageType);
}
/**
* 定义指令类型,在protocol中占1字节,8位二进制,两位十六进制
*/
public static final int LoginRequestMessage = 0;
public static final int LoginResponseMessage = 1;
public static final int ChatRequestMessage = 2;
public static final int ChatResponseMessage = 3;
public static final int GroupCreateRequestMessage = 4;
public static final int GroupCreateResponseMessage = 5;
public static final int GroupJoinRequestMessage = 6;
public static final int GroupJoinResponseMessage = 7;
public static final int GroupQuitRequestMessage = 8;
public static final int GroupQuitResponseMessage = 9;
public static final int GroupChatRequestMessage = 10;
public static final int GroupChatResponseMessage = 11;
public static final int GroupMembersRequestMessage = 12;
public static final int GroupMembersResponseMessage = 13;
public static final int PingMessage = 14;
public static final int PongMessage = 15;
static {
messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
messageClasses.put(ChatRequestMessage, ChatRequestMessage.class);
messageClasses.put(ChatResponseMessage, ChatResponseMessage.class);
messageClasses.put(GroupCreateRequestMessage, GroupCreateRequestMessage.class);
messageClasses.put(GroupCreateResponseMessage, GroupCreateResponseMessage.class);
messageClasses.put(GroupJoinRequestMessage, GroupJoinRequestMessage.class);
messageClasses.put(GroupJoinResponseMessage, GroupJoinResponseMessage.class);
messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
messageClasses.put(GroupChatRequestMessage, GroupChatRequestMessage.class);
messageClasses.put(GroupChatResponseMessage, GroupChatResponseMessage.class);
messageClasses.put(GroupMembersRequestMessage, GroupMembersRequestMessage.class);
messageClasses.put(GroupMembersResponseMessage, GroupMembersResponseMessage.class);
}
}
3.2AbstractResponseMessage
作为所有响应消息的抽象父类,封装消息的 succes布尔属性,用于判断操作是否成功,其次就是封装操作成功或失败的原因
package com.wang.demo.protocol.message;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* <p>响应消息的抽象父类
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public abstract class AbstractResponseMessage extends Message {
/**
* 封装操作结果是否成功
*/
private boolean success;
/**
* 封装传递的信息
*/
private String reason;
public AbstractResponseMessage() {
}
public AbstractResponseMessage(boolean success, String reason) {
this.success = success;
this.reason = reason;
}
}
3.3其他消息
- LoginRequestMessage
- LoginResponseMessage
- 登录客户端请求和服务器响应消息
- ChatRequestMessage
- ChatResponseMessage
- 聊天客户端请求和服务端转发以及相应消息
- GroupCreateRequestMessage
- GroupCreateResponseMessage
- 创建群聊客户端请求和服务端响应消息
- GroupChatRequestMessage
- GroupChatResponseMessage
- 群聊天客户端请求和服务端响应消息
- GroupJoinRequestMessage
- GroupJoinResponseMessage
- 群加入客户端请求和服务端响应消息
- GroupMemberRequestMessage
- GroupMemberResponseMessage
- 群成员列表客户端请求和服务端响应消息
- GroupQuitRequestMessage
- GroupQuitResponseMessage
- 退出群聊客户端请求和服务端响应消息
4.处理连接断开
对于客户端来说,关闭的方式有两种,分别是直接关闭客户端(异常断开)以及优雅关闭客户端(正常关闭,正常释放资源)
而对于服务器来说要对这两种方式都进行处理,直接封装为一个Handler
ChatClient
.connect("localhost", 8080)
.sync()
.channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
ChatServer
package com.wang.demo.application.handler;
import com.wang.demo.service.factory.SessionFactory;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* <p>两种退出方式
* <ul>
* <li>正常断开连接退出 channelInactive</li>
* <li>异常断开(直接关闭客户端)exceptionCaught</li>
* </ul></p>
* @author: Jeffrey
* @date: 2022/01/21/11:13
* @description:
*/
@ChannelHandler.Sharable
@Slf4j
public class QuitHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
/*把Channel 从会话管理器中移除*/
SessionFactory.getSession().unbind(ctx.channel());
log.debug("{}已经断开",ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
SessionFactory.getSession().unbind(ctx.channel());
log.debug("{}异常断开 异常是{}",ctx.channel(),cause.getMessage());
}
}
5.实现会话
Session 是客户存储在服务器上的用户信息,而对于Session这个整体的生命周期来说是整个项目的运行,所以可以通过下面静态的方式实现Session,并用户信息存入其中。
Session
package com.wang.demo.service;
import io.netty.channel.Channel;
/**
* @author: Jeffrey
* @date: 2022/01/21/9:43
* @description:
*/
public interface Session {
/**
* 绑定会话
* @param channel 哪个 channel 要绑定会话
* @param username 会话绑定用户
*/
void bind(Channel channel, String username);
/**
* 解绑会话
* @param channel 哪个 channel 要解绑会话
*/
void unbind(Channel channel);
/**
* 获取属性
* @param channel 哪个 channel
* @param name 属性名
* @return 属性值
*/
Object getAttribute(Channel channel, String name);
/**
* 设置属性
* @param channel 哪个 channel
* @param name 属性名
* @param value 属性值
*/
void setAttribute(Channel channel, String name, Object value);
/**
* 根据用户名获取 channel
* @param username 用户名
* @return channel
*/
Channel getChannel(String username);
}
SessionImpl
package com.wang.demo.service.impl;
import com.wang.demo.service.Session;
import io.netty.channel.Channel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author: Jeffrey
* @date: 2022/01/21/10:09
* @description: 整个application 的会话
*/
public class SessionMemoryImpl implements Session {
/**
* username:channel
*/
private final Map<String, Channel> usernameChannelMap = new ConcurrentHashMap<>();
/**
* channel:username
*/
private final Map<Channel, String> channelUsernameMap = new ConcurrentHashMap<>();
/**
* channel:(key:value)
*/
private final Map<Channel,Map<String,Object>> channelAttributesMap = new ConcurrentHashMap<>();
@Override
public void bind(Channel channel, String username) {
usernameChannelMap.put(username, channel);
channelUsernameMap.put(channel, username);
/*给每一个连接,配置一个attribute 空间*/
channelAttributesMap.put(channel, new ConcurrentHashMap<>());
}
@Override
public void unbind(Channel channel) {
String username = channelUsernameMap.remove(channel);
usernameChannelMap.remove(username);
channelAttributesMap.remove(channel);
}
@Override
public Object getAttribute(Channel channel, String key) {
return channelAttributesMap.get(channel).get(key);
}
@Override
public void setAttribute(Channel channel, String key, Object value) {
channelAttributesMap.get(channel).put(key, value);
}
@Override
public Channel getChannel(String username) {
return usernameChannelMap.get(username);
}
}
SessionFactory
package com.wang.demo.service.factory;
import com.wang.demo.service.Session;
import com.wang.demo.service.impl.SessionMemoryImpl;
/**
* @author: Jeffrey
* @date: 2022/01/21/10:15
* @description:
*/
public class SessionFactory {
private static Session session =new SessionMemoryImpl();
public static Session getSession() {
return session;
}
}
同样可以类似的将GroupSession 所实现
6.具体业务的实现
上面我们实现的可以说都是技术需求,不仅是能在简易聊天室中使用,更是可以通用到很多的业务内,而针对于聊天室的业务:
这里以两个为具体示例进行说明:
6.1单对单聊天
实现用户与用户之间进行消息传递方式可以理解为是通过服务器做为转发件,这样可以避免用户和用户的客户端之间要建立Socket 连接(有想法的可以考虑使用P2P方式),所有的用户都连接到了服务器上,所以服务器是持有所有用户的channel的,而反映到代码上就是username 和 channel 之间的映射:
当一个用户向另一个用户发送消息时,可以发送一个封装好的协议请求数据报,发送给服务器,由服务器进行转发,通过to:username 获取目的用户的channel,并将数据写到channel 中,这是服务器的思路。
而体现到代码上就是一个继承了SimpleChannelInboundHandler< 消息类型泛型> 类的Handler
这里封装的消息类型是:ChatRequestMessage
package com.wang.demo.protocol.message;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* <p>聊天建立请求消息
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class ChatRequestMessage extends Message {
/**
* 聊天内容
*/
private String content;
/**
* 目的用户
*/
private String to;
/**
* 源用户
*/
private String from;
public ChatRequestMessage() {
}
public ChatRequestMessage(String from, String to, String content) {
this.from = from;
this.to = to;
this.content = content;
}
/**
* @return 返回本消息的消息类型
*/
@Override
public int getMessageType() {
return ChatRequestMessage;
}
}
可以看到他是继承了Message 类,并添加自己的实例变量,来源,目的地,和消息内容,从MessageCodecSharable 中解析出来的内容是Message 类型,当设置泛型为ChatRequestMessage,就会将Message 类型的比对,如果是ChatRequestMessage 类型才会进入到Handler 的代码中进行处理,如果不是则交给其他的Handler,通过这一特性,我们就能根据相同父类Message 最终支持多种信息请求的处理:
PersonChatHandler
package com.wang.demo.application.handler;
import com.wang.demo.protocol.message.ChatRequestMessage;
import com.wang.demo.protocol.message.ChatResponseMessage;
import com.wang.demo.service.factory.SessionFactory;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author: Jeffrey
* @date: 2022/01/21/11:40
* @description:
*/
@ChannelHandler.Sharable
public class PersonChatHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {
/*消息目的地*/
String to = msg.getTo();
/*获取目的地与服务器建立的Channel*/
Channel channel = SessionFactory.getSession().getChannel(to);
if (channel != null){
/*目的地在线,向目的地Channel 发送消息*/
channel.writeAndFlush(new ChatResponseMessage(true,msg.getFrom(),msg.getContent()));
}else{
/*目的地channel 断开,不在线,向发送者发送响应消息*/
ctx.writeAndFlush(new ChatResponseMessage(false,"对方用户不在线!"));
}
}
}
其他类型的数据可以根据对应的消息分别编写handler 并add到server中
这里实现功能其实就是实时聊天的功能,两个用户必须同时在线才能进行通讯,如果目的用户不在线,就会因为channel 连接断开而找不到用户,无法进行通讯,服务器是无法和用户主动建立联系的(),而对于QQ和微信进行聊天其实如何建立还需呀以下两个服务:
- 静默状态下消息提醒
- 有的时候即使关闭了QQ的主程序,但是他也会接收消息,这个我的想法是通过守护线程进行实现,当主程序关闭时隐藏时,可以通过守护线程维持channel 连接,实时检查channel 上的消息,而给用户的主观感受是,虽然程序已经关闭了,但有消息来的时候还能唤醒主程序进行提醒
- 一个客户端可以和多个用户甚至多个群聊进行通信
- 这个我考虑的是一种收发和分发的机制,通过维护一个收分缓冲区,服务接收到消息根据目的用户唯一标识进行分发,而整体发送时则由一个端口发送,这个其实和运输层协议的收发分发观念相同
- 收到大量消息并保持数据顺序不变
- to 发给 from 的消息可以维护成同一个队列
- 多个to 发送给fram 的消息也维护成队列,接收时进行根据时间排序
6.2客户端退出
就是上面的连接断开的具体业务体现,再客户端的体现就是:
case "quit":
/*正常退出会触发inActive 事件
* 退出 正常退出和异常退出
* */
ctx.channel().close();
return;
如果输入了命令quit,就会触发一个inActive 时间到服务,这样服务器就能顺利将channel 信息进行移除,如果直接关闭了客户端也是会触发一个异常到服务器,这样服务器也能进行后续资源的释放
7.假死与心跳
在一些情况下,即使客户端已经断开了连接,但是服务端可能没有检测到连接断开,这种情况称为假死,这显然是非常浪费服务器资源,所以可以通过交换心跳包的形式,互相告知,我还活着:
服务器通过配置IdleStateHandler 配置对读消息进行检测,如果5秒内还没有读到消息,就会触发ChannelDuplexHandler 的处理,认定客户端已经假死,从映射存储中移除
pipeline.addLast(new IdleStateHandler(5,0,0))
.addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE){
/*对超时假死用户进行处理*/
SessionFactory.getSession().unbind(ctx.channel());
}
super.userEventTriggered(ctx, evt);
}
})
客户端同样要进行检测,但是客户端进行的是写检测,如果超过指定时间没有主动向服务器发送消息,就主动向服务器发送一个心跳包:
pipeline.addLast(new IdleStateHandler(0,3,0))
.addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent stateEvent = (IdleStateEvent) evt;
if (stateEvent.state() == IdleState.WRITER_IDLE){
/*每3秒向服务器发送心跳包*/
ctx.writeAndFlush(new PingMessage());
}
}
})
而心跳包的本质,其实就是一个没有任何消息意义的Message
package com.wang.demo.protocol.message;
/**
* <p>心跳数据包,定期发送解决连接假死
*/
public class PingMessage extends Message {
@Override
public int getMessageType() {
return PingMessage;
}
}
综上,我们就实现了实时通讯的所有功能
8.核心代码
ChatClient
package com.wang.demo.application.client;
import com.wang.demo.application.handler.PersonChatHandler;
import com.wang.demo.protocol.codec.MessageCodecSharable;
import com.wang.demo.protocol.codec.ProcotoFrameDecoder;
import com.wang.demo.protocol.message.*;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author: Jeffrey
* @date: 2022/01/21/10:26
* @description:
*/
@Slf4j
public class ChatClient {
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
Scanner scanner = new Scanner(System.in);
MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
LoggingHandler LOGGING_HANDLER = new LoggingHandler();
/*多线程倒计时锁 计数器*/
CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
AtomicBoolean LOGIN = new AtomicBoolean(false);
try {
Channel channel = new Bootstrap()
.channel(NioSocketChannel.class)
.group(group)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(0,3,0))
.addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent stateEvent = (IdleStateEvent) evt;
if (stateEvent.state() == IdleState.WRITER_IDLE){
/*每3秒向服务器发送心跳包*/
ctx.writeAndFlush(new PingMessage());
}
}
})
.addLast(new ProcotoFrameDecoder())
.addLast(LOGGING_HANDLER)
.addLast(MESSAGE_CODEC)
.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object message) throws Exception {
if (message instanceof LoginResponseMessage) {
LoginResponseMessage loginResponseMessage = (LoginResponseMessage) message;
if (loginResponseMessage.isSuccess()) {
/*登录成功*/
LOGIN.set(true);
}
/*计数-1 使得active继续执行,唤醒Sysin 线程*/
}
if (message instanceof ChatResponseMessage) {
ChatResponseMessage msg = (ChatResponseMessage) message;
if (!msg.isSuccess()){
System.out.println(msg.getReason());
}else{
System.out.println(msg.getFrom() + "::" + msg.getContent());
}
}
if (message instanceof GroupChatResponseMessage) {
GroupChatResponseMessage msg = (GroupChatResponseMessage) message;
System.out.println("[" + msg.getGroupName() + "]::" + msg.getFrom() + "::" + msg.getContent());
}
if (message instanceof GroupCreateResponseMessage){
GroupCreateResponseMessage msg = (GroupCreateResponseMessage) message;
if (msg.isSuccess()){
System.out.println(msg.getReason());
}else{
System.out.println("创建失败,原因:"+msg.getReason());
}
}
if (message instanceof GroupJoinResponseMessage) {
GroupJoinResponseMessage msg = (GroupJoinResponseMessage) message;
System.out.println(msg.getReason());
}
if (message instanceof GroupMembersResponseMessage){
GroupMembersResponseMessage msg = (GroupMembersResponseMessage) message;
if (msg.getMembers() == null){
System.out.println("群聊不存在!");
}else{
Set<String> members = msg.getMembers();
int i = 1;
for (String temp : members){
System.out.println("[User"+(i)+"]"+temp);
i++;
}
}
}
if (message instanceof GroupQuitResponseMessage){
GroupQuitResponseMessage msg = (GroupQuitResponseMessage) message;
System.out.println(msg.getReason());
}
/*计数-1 使得active继续执行,唤醒Sysin 线程*/
WAIT_FOR_LOGIN.countDown();
}
/**
* 在连接建立后触发active 事件,发送一个LoginRequestMessage
* 这里相当于是通过监听器(配置处理器)的方式进行
* 监听active 事件,如果发生则发送
* 也可以在连接建立后通过channel 进行发送
* @param ctx 流水线上下文
* @throws Exception e
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
/*1)创建LoginRequestMessage 发送服务器处理*/
System.out.println("username:");
String lin1 = scanner.nextLine();
System.out.println("password:");
String lin2 = scanner.nextLine();
/*构造消息对象*/
LoginRequestMessage message = new LoginRequestMessage(lin1, lin2);
/**
* 发送消息
* 这里会触发出站操作 ->MESSAGE_CODEC ->lOGGING_HANDLER
*/
ctx.writeAndFlush(message);
try {
/*计数器等待
* 等待客户端的回复,计数+1 等待read唤醒此线程
* */
System.out.println("WAIT LOGIN---------");
WAIT_FOR_LOGIN.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
/*线程被唤醒,线程通信 判断登录登录是否成功*/
if (!LOGIN.get()) {
/*channel 关闭会触发channelCloseFuture 然后会进行优雅关闭*/
System.out.println("LOGIN FAILURE! PLEASE CHECK YOUR username AND PASSWORD!");
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 = scanner.nextLine();
/*解析命令*/
String[] s = command.split(" ");
switch (s[0]) {
case "send":
ctx.writeAndFlush(new ChatRequestMessage(lin1, s[1], s[2]));
break;
case "gsend":
ctx.writeAndFlush(new GroupChatRequestMessage(lin1, s[1], s[2]));
break;
case "gcreate":
/*分组内的用户名*/
String[] split = s[2].split(",");
List<String> list = Arrays.asList(split);
Set<String> set = new HashSet<>(list);
/*加入自己*/
set.add(lin1);
ctx.writeAndFlush(new GroupCreateRequestMessage(s[1], set));
break;
case "gmembers":
ctx.writeAndFlush(new GroupMembersRequestMessage(s[1]));
break;
case "gjoin":
ctx.writeAndFlush(new GroupJoinRequestMessage(lin1, s[1]));
break;
case "gquit":
ctx.writeAndFlush(new GroupQuitRequestMessage(lin1, s[1]));
break;
case "quit":
/*正常退出会触发inActive 事件
* 退出 正常退出和异常退出
* */
ctx.channel().close();
return;
}
}
}
}, "Sysin").start();
}
})
.addLast();
}
})
.connect("localhost", 8080)
.sync()
.channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
ChatServer
package com.wang.demo.application.server;
import com.wang.demo.application.handler.*;
import com.wang.demo.protocol.codec.MessageCodec;
import com.wang.demo.protocol.codec.MessageCodecSharable;
import com.wang.demo.protocol.codec.ProcotoFrameDecoder;
import com.wang.demo.service.Session;
import com.wang.demo.service.factory.SessionFactory;
import com.wang.demo.service.pojo.Group;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
/**
* @author: Jeffrey
* @date: 2022/01/21/9:21
* @description:
*/
@Slf4j
public class ChatServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
LoggingHandler LOGGING_HANDLER = new LoggingHandler();
MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
LoginHandler LOGIN_HANDLER = new LoginHandler();
PersonChatHandler PERSON_CHAT_HANDLER = new PersonChatHandler();
QuitHandler QUIT_HANDLER =new QuitHandler();
GroupCreateRequestMessageHandler CREATE_GROUP_HANDLER = new GroupCreateRequestMessageHandler();
GroupChatRequestMessageHandler CHAT_GROUP_HANDLER = new GroupChatRequestMessageHandler();
GroupJoinRequestMessageHandler JOIN_GROUP_HANDLER = new GroupJoinRequestMessageHandler();
GroupQuitHandler GROUP_QUIT_HANDLER = new GroupQuitHandler();
GroupMemberRequestMessageHandler GROUP_MEMBERS_HANDLER =new GroupMemberRequestMessageHandler();
Channel channel = new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(5,0,0))
.addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE){
/*对超时假死用户进行处理*/
SessionFactory.getSession().unbind(ctx.channel());
}
super.userEventTriggered(ctx, evt);
}
})
.addLast(new ProcotoFrameDecoder())
.addLast(LOGGING_HANDLER)
.addLast(MESSAGE_CODEC)
.addLast(LOGIN_HANDLER)
.addLast(PERSON_CHAT_HANDLER)
.addLast(QUIT_HANDLER)
.addLast(CREATE_GROUP_HANDLER)
.addLast(CHAT_GROUP_HANDLER)
.addLast(JOIN_GROUP_HANDLER)
.addLast(GROUP_QUIT_HANDLER)
.addLast(GROUP_MEMBERS_HANDLER);
}
}).bind("localhost",8080).sync().channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
log.error("Server error{}",e.getMessage());
}finally {
/*发送完缓存中的数据后关闭资源*/
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}