本文主要介绍5块内容,同样也是本项目的核心代码。
1、服务端项目开发
2、客户端项目开发
3、客户端与服务端心跳机制
4、用户Session的管理
5、群组的Session的管理
开始上代码
1、服务端的开发
@Slf4j
public class ChatServer {
public static void main(String[] args) {
LoginRequestMessageHandler loginRequestMessageHandler = new LoginRequestMessageHandler();
ProcotolFrameDecoder procotolFrameDecoder = new ProcotolFrameDecoder();
MessageCodecSharable messageCodec = new MessageCodecSharable();
ChatRequestMessageHandler chatRequestMessageHandler = new ChatRequestMessageHandler();
GroupChatRequestHandler groupChatRequestHandler = new GroupChatRequestHandler();
GroupCreateRequestHandler groupCreateRequestHandler = new GroupCreateRequestHandler();
PingMessageHandler pingMessageHandler = new PingMessageHandler();
int port = getPort();
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(6);
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(boss,worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(procotolFrameDecoder);
ch.pipeline().addLast(new LoggingHandler());
//自定义 协议
ch.pipeline().addLast(messageCodec);
ch.pipeline().addLast(pingMessageHandler);
//优化 防止死链接 占用资源
// 服务器端检测读频率 超过5秒没读到客户端请求,则认定客户端掉线
ch.pipeline().addLast(new IdleStateHandler(10,0,0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
});
//自定义其它handler
ch.pipeline().addLast(loginRequestMessageHandler);
ch.pipeline().addLast(chatRequestMessageHandler);
ch.pipeline().addLast(groupChatRequestHandler);
ch.pipeline().addLast(groupCreateRequestHandler);
}
});
Channel channel = bootstrap.bind(8080).sync().channel();
channel.closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
2、客户端的开发
在接收客户端输入时,要新开一个线程,否则会阻塞。
@Slf4j
public class ChatClient {
public static void main(String[] args) {
int port = 8080;
MessageCodecSharable messageCodec = new MessageCodecSharable();
ProcotolFrameDecoder procotolFrameDecoder = new ProcotolFrameDecoder();
ChatRequestMessageHandler chatRequestMessageHandler = new ChatRequestMessageHandler();
// 通过判断true 或者 false 计算是否登录成功
AtomicBoolean EXIT = new AtomicBoolean(false);
// 让线程等待服务端返回登录结果
CountDownLatch WAIT_FOR_LOGIN = new CountDownLatch(1);
NioEventLoopGroup worker = new NioEventLoopGroup(6);
Bootstrap bootstrap = new Bootstrap();
Scanner scanner = new Scanner(System.in);
try {
bootstrap.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(procotolFrameDecoder);
ch.pipeline().addLast(messageCodec);
// 客户端检测写事件
ch.pipeline().addLast(new IdleStateHandler(0,5,0));
//
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.info("已经有2秒,客户端没有请求服务端了");
//ctx.channel().close();
ctx.writeAndFlush(new PingMessage(200,"心跳"));
}
}
});
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(chatRequestMessageHandler);
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
new Thread(()->{
System.out.println("请输入你的姓名:");
String username = scanner.nextLine();
System.out.println("请输入你的密码:");
String password = scanner.nextLine();
LoginRequestMessage login = new LoginRequestMessage(username,password);
System.out.println("发送的数据:"+login);
ctx.writeAndFlush(login);
try {
WAIT_FOR_LOGIN.await();
while (true){
System.out.println("欢迎进入聊天系统,请输入对应信息");
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 ;
command = scanner.nextLine();
String[] result = command.split(" ");
switch (result[0]){
case "send":
ctx.writeAndFlush(new ChatRequestMessage(result[2],result[1],username));
break;
case "gsend":
ctx.writeAndFlush(new GroupChatRequestMessage(username,result[1], result[2]));
break;
case "gcreate":
Set<String> userSet = Arrays.stream(result[2].split(",")).collect(Collectors.toSet());
userSet.add(username);
ctx.writeAndFlush(new GroupCreateRequestMessage(username,result[1],userSet));
break;
case "gmembers":
ctx.writeAndFlush("");
break;
case "gjoin":
ctx.writeAndFlush("");
break;
case "gquit":
ctx.writeAndFlush("");
break;
case "quit":
//退出
ctx.channel().close();
return;
}
}
} catch (InterruptedException e){
e.printStackTrace();
}
}).start();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof LoginResponse){
LoginResponse loginResponse = (LoginResponse)msg;
if (loginResponse.isSuccess()){
EXIT.set(true);
WAIT_FOR_LOGIN.countDown();
}else if (!loginResponse.isSuccess()){
ctx.channel().closeFuture();
}
}
if (msg instanceof ChatResponseMessage){
ChatResponseMessage message = (ChatResponseMessage) msg;
if (message.isSuccess()){
log.info("[聊天模块] 聊天失败,失败原因: {}",message.getReason());
}else{
log.info("[聊天模块] 聊天内容: {}",message.getContext());
}
}
if (msg instanceof GroupChatResponseMessage){
GroupChatResponseMessage message = (GroupChatResponseMessage) msg;
if (message.isSuccess()){
log.info("[群聊模块] 聊天失败,失败原因:{}",message.getReason());
}else{
log.info("[群聊模块] 聊天内容来自{},聊天内容{}",message.getFrom(),message.getContent());
}
}
if (msg instanceof GroupCreateResponseMessage){
GroupCreateResponseMessage message = (GroupCreateResponseMessage)msg;
if (message.isSuccess() && message.getReason() != null){
log.info("[群聊模块] 建群成功 群名{}",message.getReason());
}
if (!message.isSuccess()){
log.info("[群聊模块] 建群失败 群名{}",message.getReason());
}
if (message.getFrom() != null){
log.info("[群聊模块] 群主 {},拉你进群{}",message.getFrom(),message.getContent());
}
}
if (msg instanceof PongMessage){
PongMessage pongMessage = (PongMessage) msg;
}
}
// 在连接断开时触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("连接已经断开,按任意键退出..");
SessionFactory.getSession().unbind(ctx.channel());
EXIT.set(true);
}
// 在出现异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.debug("连接已经断开,按任意键退出..{}", cause.getMessage());
EXIT.set(true);
}
});
}
});
Channel channel = bootstrap.connect("localhost", port).sync().channel();
channel.closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
worker.shutdownGracefully();
}
}
}
3、服务端与客户端进行通信时,一定要确保对方是否存活。
心跳是检测对方是否在线的依据。心跳频率可以根据项目实际情况进行设定。
//客户端发起心跳请求
//
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.info("已经有2秒,客户端没有请求服务端了");
//ctx.channel().close();
ctx.writeAndFlush(new PingMessage(200,"探活"));
}
}
});
//服务端接收客户端的心跳请求,并响应
// 服务器端检测读频率 超过5秒没读到客户端请求,则认定客户端掉线
ch.pipeline().addLast(new IdleStateHandler(10,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.info("已经10秒没有读取到数据了");
//ctx.channel().close();
}
}
});
//响应客户端
@ChannelHandler.Sharable
public class PingMessageHandler extends SimpleChannelInboundHandler<PingMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, PingMessage msg) throws Exception {
ctx.writeAndFlush(new PongMessage(true,"OK"));
}
}
4、Session的管理。
通过管理session,可以方便服务端随时向客户端发送响应。
public class SessionMemoryImpl implements Session{
Map<Channel,String> channelBindUsername = new HashMap<>();
Map<String,Channel> usernameBindChannel = new HashMap<>();
//在内存中保留channel与用户的绑定关系,以及用户与channel的绑定。
//方便在业务中调用
@Override
public void bind(Channel channel, String username) {
channelBindUsername.put(channel,username);
usernameBindChannel.put(username,channel);
}
@Override
public void unbind(Channel channel) {
String user = channelBindUsername.get(channel);
channelBindUsername.remove(channel);
usernameBindChannel.remove(user);
}
@Override
public Object getAttribute(Channel channel, String name) {
return null;
}
@Override
public void setAttribute(Channel channel, String name, Object value) {
}
@Override
public Channel getChannel(String username) {
return usernameBindChannel.get(username);
}
}
5、群组的Session管理,通过实现接口,自定义逻辑。
public interface GroupSession {
//Map<String, Set<String>> GroupBindUsers = new HashMap<>();
boolean createGroup(String groupName,Set<String> users);
boolean cancelGroupMember(String groupName,String user);
boolean cancelGroup(String groupName);
List<Channel> getOnlineChannel(String groupName);
}