控制台实现聊天室功能
参考一个厉害的教学up:孙哥
https://space.bilibili.com/284638819
功能分析
:::info
Spring ---->工厂
SpringBoot应用---->启动类main—>服务端启动
- 服务端一定是一个Bean
- Handler注入
- bean启动—>生命周期init()
:::
代码实现
Session:存已登录的用户名和用户管道
//存储结构 从设计角度全局唯一map
//SessionManager
public class Session{
private static final Map<String,Channel>usernameChannelMap
private static final Map<Channel,String>channelUsernameMap
//方法的五要素:修饰值 返回值 方法名(参数表) 异常
// 1.多个用户信息 多个channel 多个数据
// 2. key value map
public void bind (Channel channel,String username){
usernameChannelMap.put(username,channel);
channelUsernameMap.put(channel,username);
}
public void unbind (Channel channel){
String username=channelUsernameMap.remove(channel);
if(username!=null){
usernameChannelMap.remove(username);
}
}
public Channel getChannel(String username){
}
}
群对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Group {
private String groupName;
private Set<String> members;
}
群会话session
public class GroupSession {
//1 存储聊天室的信息
//key string 聊天室的名字
private static final Map<String,Group> groupMap=new HashMap<>();
public Group createGroup(String name, Set<String> members){
Group group=new Group(name,members);
/**
* 向map添加key value
* 如果添加key value 不存在 返回null
*/
return groupMap.putIfAbsent(name,group);
}
//获取聊天室的成员
public Set<String> getMembers(String name){
return groupMap.get(name).getMembers();
}
//获取聊天室成员的channel
public List<Channel> getMembersChannel(String name){
List<Channel> retList=new ArrayList<>();
Set<String> members = getMembers(name);
for (String member : members) {
Channel channel = Session.getChannel(member);
retList.add(channel);
}
return retList;
}
}
信息枚举类
/**
* 信息枚举类
*/
public enum MessageType {
LOGIN_REQUEST_MESSAGE ((byte) 1, LoginRequestMessage.class),
LOGIN_RESPONSE_MESSAGE((byte) 2, LoginResponseMessage.class),
CHAT_REQUEST_MESSAGE((byte) 3, ChatRequestMessage.class),
CHAT_RESPONSE_MESSAGE((byte) 4,ChatResponseMessage.class),
GROUP_CREATE_REQUEST_MESSAGE((byte) 5, GroupCreateRequestMessage.class),
GROUP_CREATE_RESPONSE_MESSAGE((byte) 6,GroupCreateResponseMessage.class),
GROUP_CHAT_REQUEST_MESSAGE((byte) 7,GroupChatRequestMessage.class),
GROUP_CHAT_RESPONSE_MESSAGE((byte) 8,GroupChatResponseMessage.class),
PING_MESSAGE((byte) 9,PingMessage.class),
PONG_MESSAGE((byte) 10,PongMessage.class);
private final byte value;
private final Class<?> classType;
MessageType(byte value, Class<?> classType) {
this.value = value;
this.classType = classType;
}
public byte getValue() {
return value;
}
public Class<?> getClassType() {
return classType;
}
public static Class<?> getClass(byte value){
switch (value){
case 1: return LoginRequestMessage.class;
case 2:return LoginResponseMessage.class;
case 3:return ChatRequestMessage.class;
case 4:return ChatResponseMessage.class;
case 5:return GroupCreateRequestMessage.class;
case 6:return GroupCreateResponseMessage.class;
case 7:return GroupChatRequestMessage.class;
case 8:return GroupChatResponseMessage.class;
case 9:return PingMessage.class;
case 10:return PongMessage.class;
default:return null;
}
}
}
分别封装请求消息和效应消息
解码器:
@Slf4j
public class ChatByteToMessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//1.幻术 suns
ByteBuf buf = in.readBytes(4);
//2. 协议版本 一个字节
byte protoVersion = in.readByte();
//3.序列化方式
byte serializableNo = in.readByte();
//4.功能指令
byte funcNo = in.readByte();
//5.正文长度
int contentLength = in.readInt();
//6.正文
log.debug("解码"+in);
Message message=null;
String str = (String) in.readCharSequence(contentLength,Charset.forName("UTF-8"));
message = (Message) JSONUtil.toBean(str, MessageType.getClass(funcNo));
log.debug("解码"+message);
out.add(message);
}
}
编码器:
public class ChatMessageToByteEncoder extends MessageToByteEncoder<Message> {
private static final Logger log = LoggerFactory.getLogger(ChatMessageToByteEncoder.class);
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
log.debug("encode method invoke");
//协议头
//1. 幻术 四个字节 suns
out.writeBytes("suns".getBytes());
//2. 一个字节协议版本
out.writeByte(1);
//3. 序列化方式 1. json 2. protobuf 3. hession
out.writeByte(1);
//4. 功能指令 0.登录 1. 聊天
out.writeByte(msg.gainMessageType());
// 正文长度
String jsonContent = JSONUtil.toJsonStr(msg);
out.writeInt(jsonContent.length());
out.writeCharSequence(jsonContent, Charset.forName("UTF-8"));
}
}
handler
ChatRequestMessageHandler
@Slf4j
public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {
String from = msg.getFrom();
String to = msg.getTo();
String content = msg.getContent();
Channel channel = Session.getChannel(to);
log.debug("from:{} to:{} content:{}",from,to,content);
if(channel!=null){
log.debug("发送成功");
ChatResponseMessage chatResponseMessage =new ChatResponseMessage("200","chat ok",from,content);
channel.writeAndFlush(chatResponseMessage);
ctx.writeAndFlush(new ChatResponseMessage("200","send is OK"));
}else {
log.debug("发送失败");
ctx.writeAndFlush(new ChatResponseMessage("500","error"));
}
}
}
LoginRequestMessageHandler
@Slf4j
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
private static Map<String, String> userDB = new HashMap<>();
static {
userDB.put("suns1", "123456");
userDB.put("suns2", "123456");
userDB.put("suns3", "123456");
userDB.put("suns4", "123456");
userDB.put("suns5", "123456");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
log.debug("login method invoke....");
String username= msg.getUsername();
String password = msg.getPassword();
boolean isLogin=login(username,password);
if(isLogin){
log.debug("login is ok....");
Session.bind(ctx.channel(),username);
ctx.writeAndFlush(new LoginResponseMessage("200","login is ok"));
}else {
log.debug("login is error");
ctx.writeAndFlush(new LoginResponseMessage("500","Check you name or password"));
}
}
private boolean login(String username, String password) {
String storePassword = userDB.get(username);
if (storePassword == null || !password.equals(storePassword)) {
return false;
}
return true;
}
}
等handler
客户端
package com.suns.netty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.suns.netty.codec.ChatByteToMessageDecoder;
import com.suns.netty.codec.ChatMessageToByteEncoder;
import com.suns.netty.message.*;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
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.net.InetSocketAddress;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class MyNettyClient {
public static void main(String[] args) throws InterruptedException, JsonProcessingException {
Scanner scanner = new Scanner(System.in);
CountDownLatch WAIT_LOGIN = new CountDownLatch(1);
AtomicBoolean LOGIN = new AtomicBoolean(false);
AtomicBoolean SERVER_ERROR = new AtomicBoolean(false);
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
LoggingHandler LOGGING_HANDLER = new LoggingHandler();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 50);
Bootstrap group = bootstrap.group(eventLoopGroup);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 7, 4, 0, 0));
ch.pipeline().addLast(LOGGING_HANDLER);
ch.pipeline().addLast(new ChatMessageToByteEncoder());
ch.pipeline().addLast(new ChatByteToMessageDecoder());
ch.pipeline().addLast(new IdleStateHandler(8, 3, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.WRITER_IDLE) {
ctx.writeAndFlush(new PingMessage("client"));
} else if (idleStateEvent.state() == IdleState.READER_IDLE) {
log.debug("服务器端 已经8秒没有响应数据了。。。");
log.debug("关闭channel");
ctx.channel().close();
SERVER_ERROR.set(true);
/*
close.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
//进行重连
}
});
close.addListener(promise->{
});
*/
}
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("recive data {}", msg);
if (msg instanceof LoginResponseMessage) {
LoginResponseMessage loginResponseMessage = (LoginResponseMessage) msg;
if (loginResponseMessage.getCode().equals("200")) {
LOGIN.set(true);
}
WAIT_LOGIN.countDown();
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (SERVER_ERROR.get()) {
/*
当前执行这行代码的线程 ,后续会被netty回收,所以在这里直接重连不好
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.closeFuture().sync();
*/
log.debug("重连...");
//等价于从EventLoopGroup中 从新获得了
EventLoop eventLoop = ctx.channel().eventLoop();
eventLoop.submit(() -> {
Channel channel = null;
try {
ChannelFuture connect = bootstrap.connect(new InetSocketAddress(8000));
connect.addListener(promise -> {
//promise.isSuccess();
log.debug("{} ", promise.cause());
});
Channel channel1 = connect.sync().channel();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
channel.closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
} else {
log.debug("client close ...");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.debug("client close....");
super.exceptionCaught(ctx, cause);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//用户名 密码 让用户输入 Scanner 输入
//用户的输入 菜单的展示 UI相关的内容
new Thread(() -> {
System.out.println("请输入用户名: ");
String username = scanner.nextLine();
System.out.println("请输入密码: ");
String password = scanner.nextLine();
//发送登录操作
LoginRequestMessage loginRequestMessage = new LoginRequestMessage(username, password);
ctx.writeAndFlush(loginRequestMessage);
try {
WAIT_LOGIN.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!LOGIN.get()) {
ctx.channel().close();
return;
}
while (true) {
System.out.println("========================================");
System.out.println("send [username] [content]");
System.out.println("gcreate [group name] [m1,m2,m3...]");
System.out.println("gsend [group name] [content]");
System.out.println("quit");
System.out.println("========================================");
String command = scanner.nextLine();
String[] args = command.split(" ");
switch (args[0]) {
case "send":
ctx.writeAndFlush(new ChatRequestMessage(username, args[1], args[2]));
break;
case "gcreate":
String groupName = args[1];
String[] membersString = args[2].split(",");
Set<String> members = new HashSet<>(Arrays.asList(membersString));
members.add(username);
ctx.writeAndFlush(new GroupCreateRequestMessage(groupName, members));
break;
case "gsend":
String gName = args[1];
String content = args[2];
ctx.writeAndFlush(new GroupChatRequestMessage(username, gName, content));
break;
case "quit":
//服务器端 unbind
ctx.channel().close();
return;
}
}
}, "Client UI").start();
}
});
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.closeFuture().sync();
/* ChannelFuture connect = bootstrap.connect(new InetSocketAddress(8000));
connect.addListener(promise -> {
if(!promise.isSuccess()){
//重试 3次
}else{
Channel channel = connect.channel();
channel.closeFuture().sync();
}
});*/
} catch (InterruptedException e) {
log.error("client error ", e);
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
服务端
package com.suns.netty;
import com.suns.netty.codec.ChatByteToMessageDecoder;
import com.suns.netty.codec.ChatMessageToByteEncoder;
import com.suns.netty.handler.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
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.concurrent.TimeUnit;
/*
RPC ---> dubbo
Spring ---> 工厂的
SpringBoot应用 ---> 启动类 main ---> 服务端启动
1. 服务器 一定是一个 Bean
1. Handler注入
2. bean启动 ---> 生命周期 init()
@Import(Bean.class)--->spi
启动器 starter
1. 导入jar---> @Import()
2. AutoConfigure
*/
@Slf4j
public class MyNettyServer {
public static void main(String[] args) {
LoggingHandler LOGGING_HANDLER = new LoggingHandler();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
LoginRequestMessageHandler LOGINREQUESTMESSAGEHANDLER = new LoginRequestMessageHandler();
ChatRequestMessageHandler CHATREQUESTMESSAGEHANDLER = new ChatRequestMessageHandler();
GroupCreateMessageHandler GROUPCREATEMESSAGEHANDLER = new GroupCreateMessageHandler();
GroupChatRequestMessasgeHandler GROUPCHATREQUESTMESSASGEHANDLER = new GroupChatRequestMessasgeHandler();
QuitHandler QUITHANDLER = new QuitHandler();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 7, 4, 0, 0));
pipeline.addLast(LOGGING_HANDLER);
pipeline.addLast(new ChatByteToMessageDecoder());
pipeline.addLast(new ChatMessageToByteEncoder());
pipeline.addLast(new IdleStateHandler(8, 3, 0, TimeUnit.SECONDS));
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.READER_IDLE) {
log.debug("client已经8s没有与服务端通信..... ");
log.debug("关闭channel");
ctx.channel().close();
}else if(idleStateEvent.state() == IdleState.WRITER_IDLE){
//代码在实际开发中应该应用,但是为了模拟client的处理 暂时注释
//ctx.writeAndFlush(new PongMessage("server"));
}
}
});
pipeline.addLast(LOGINREQUESTMESSAGEHANDLER);
pipeline.addLast(CHATREQUESTMESSAGEHANDLER);
pipeline.addLast(GROUPCREATEMESSAGEHANDLER);
pipeline.addLast(GROUPCHATREQUESTMESSASGEHANDLER);
pipeline.addLast(QUITHANDLER);
}
});
Channel channel = serverBootstrap.bind(8000).sync().channel();
channel.closeFuture().sync();
} catch (Exception e) {
log.error("server error", e);
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}