从零实现一个简易的聊天软件(Netty版)

从零实现一个简易的聊天软件(Netty版)

1、简介

官方的简介:

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

2、使用Netty的原因

为什么使用Netty,我们这里就不说java自带的socket,我们这里说为什么不使用Nio呢?

2.1 Nio的缺点

NIO的类库和API繁杂,学习成本高,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。

需要熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能写出高质量的NIO程序。

臭名昭著的epoll bug。它会导致Selector空轮询,最终导致CPU 100%。直到JDK1.7版本依然没得到根本性的解决。

Nio的简易代码

通过Nio简易代码和后面的Netty版实现比对后,这里的实现比较简易,但也能够发现使用Nio实现聊天软件复杂且繁琐。

**客户端代码实现 **

/**
 * @Description:
 * @author: zh
 * @Create : 2024/12/31
 * @Project_name : client
 * @Version :
 **/
public class NioClient {
    public static void main(String[] args) {
        String s = "zh";
        System.out.println(s.getBytes(Charset.forName("UTF-8")));
    }
    public void start() throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup(2);
        bootstrap.group(eventExecutors);
        //设置channel类型
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.remoteAddress("127.0.0.1",8888);

        bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        //pipeline和handler设置
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                socketChannel.pipeline().addLast(new NettyMsgCilentHandler());
            }
        });
        ChannelFuture connect = bootstrap.connect();
        connect.addListener((ChannelFuture listener) -> {
            if(listener.isSuccess()) {
                System.out.println("连接 成功!");
            } else {
                System.out.println("连接 失败! 可以进行后续的补救措施!");
            }
        });
        connect.sync();
        Channel channel = connect.channel();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入发送内容:");
        while(scanner.hasNext()) {
            String next = scanner.next();
            byte[] bytes = (new Date() + ">>" + next).getBytes();
            ByteBuf buffer = channel.alloc().buffer();
            buffer.writeBytes(bytes);
            channel.writeAndFlush(buffer);
            System.out.println("消息发送完成!");
        }
        eventExecutors.shutdownGracefully();


    }
}

服务端代码实现


public class NioServer {
    public static void main(String[] args) {
        start();
    }
    @SneakyThrows
    public static void start(){
        ServerBootstrap serverBootstrap = new ServerBootstrap();
//        绑定端口
        serverBootstrap.localAddress(8888);
//        绑定有多少个交换机
        EventLoopGroup main = new NioEventLoopGroup(2);
        EventLoopGroup sub = new NioEventLoopGroup(2);
        serverBootstrap.group(main,sub);
        //路由模式
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        serverBootstrap.option(ChannelOption.SO_KEEPALIVE, true);

        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel channel) throws Exception {
                channel.pipeline().addLast(null);
            }
        });
        serverBootstrap.clone();

    }
}
class  ChannleHander extends ChannelInboundHandlerAdapter{
//    重写读消息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf in = (ByteBuf) msg;
        try {
            while (in.isReadable()) {
//                这里是乱码,没有使用UTF-8格式读取
                byte b = in.readByte();
                //输出是采用UTF-8格式
                System.out.println((char) b);
            }
            System.out.println();
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

2.2 Netty的优点

Netty的优点有很多:

  • API使用简单,学习成本低。
  • 功能强大,内置了多种解码编码器,支持多种协议。
  • 性能高,对比其他主流的NIO框架,Netty的性能最优。
  • 社区活跃,发现BUG会及时修复,迭代版本周期短,不断加入新的功能。
  • Dubbo、Elasticsearch都采用了Netty,质量得到验证。

3、使用Netty

这里的开发环境是idea+maven+JDK1.8,spring boot的版本是

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.11</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

3.1、通信协议

我这的传输协议采用的是微软的protobuf序列化协议,为什么采用这个协议?

Protobuf 的优点:
  • 消息格式为二进制
  • 独立于平台和语言
  • JSONXML 等其他消息格式相比效率更
定义protoMsg
syntax = "proto3";
//这里定义的包名字,是java中的类的包名,可以在target中找到
//如果通过install 下载依赖的时候,下载不成功,建议将项目的目录移至没有中文的目录下重新构建一次
package com.zh.nettyServer.pojos;

/*消息的枚举类型*/
enum HeadType {
  LOGIN_REQUEST = 0;  //登录
  LOGIN_RESPONSE = 1;
  LOGOUT_REQUEST = 2;  //退出
  LOGOUT_RESPONSE = 3;
  HEART_BEAT = 4;      //心跳
  MESSAGE_REQUEST = 5;    //IM
  MESSAGE_RESPONSE = 6;   //消息转发
  MESSAGE_NOTIFICATION = 7;  //通知
}

/*登录信息*/
// LoginRequest对应的HeadType为LOGIN_REQUEST
// 消息名称去掉下划线,更加符合Java 的类名规范
message LoginRequest {
  string uid = 1;   // 用户唯一id
  string deviceId = 2;  // 设备ID
  string token = 3;       // 用户token
  uint32 platform = 4;  //客户端平台 windows、mac、android、ios、web
  string app_version = 5;   // APP版本号
}
/*登录响应*/
message LoginResponse {
  bool result = 1;  //true表示发送成功,false表示发送失败
  uint32 code = 2;  //错误码
  string info = 3;  //错误描述
  uint32 expose = 4;  //错误描述是否提示给用户:1 提示;0 不提示
}

/*聊天消息*/
message MessageRequest {
  uint64 msg_id = 1;
  string from = 2;
  string to = 3;
  uint64 time = 4;
  uint32 msg_type = 5;
  string content = 6;
  string url = 8;
  string property = 9;
  string from_nick = 10;
  string json = 11;
}

/*聊天响应*/
message MessageResponse {
  bool result = 1;
  uint32 code = 2;
  string info = 3;
  uint32 expose = 4;
}

/*通知*/
message MessageNotification {
  uint64 no_id = 1;
  string json = 2;
  string timestamp = 3;
}

/*心跳*/
message MessageHeartBeat {
  uint32   seq = 1;
  string   uid = 2;
  string   json = 3;
}


/*顶层消息*/
//顶层消息是一种嵌套消息,嵌套了各种类型消息
//逻辑上:根据消息类型 type的值,最多只有一个有效
message Message {
  HeadType       type = 1; //通用字段: 消息类型
  uint64         sequence = 2;  //通用字段:消息序列号
  string         session_id = 3;   //通用字段:会话id
  LoginRequest   loginRequest = 4;   //登录请求
  LoginResponse  loginResponse = 5;   //登录响应
  MessageRequest  messageRequest = 6;    //IM消息请求
  MessageResponse  messageResponse = 7;      //IM消息响应
  MessageNotification  notification = 8;        //系统通知
  MessageHeartBeat     heartBeat = 9;  //心跳
}

3.2、添加依赖

pom文件中添加上相关的依赖

<dependencies>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-devtools</artifactId>
       <optional>true</optional>
    </dependency>
    <dependency>
       <groupId>io.netty</groupId>
       <artifactId>netty-all</artifactId>
       <version>4.0.33.Final</version>
    </dependency>
    <dependency>
       <groupId>com.google.protobuf</groupId>
       <artifactId>protobuf-java</artifactId>
       <version>3.6.1</version>
    </dependency>
    <dependency>
       <groupId>commons-lang</groupId>
       <artifactId>commons-lang</artifactId>
       <version>2.6</version>
    </dependency>
    <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.16.10</version>
    </dependency>
    <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>17.0</version>
    </dependency>
</dependencies>

3.3、编写客户端代码

一、处理器

Netty提供了很多内置的处理器,高效地利用这些处理器,可以经过简单的配置就可以实现部分复杂功能,而不是自己花时间和精力去重复造“轮子”。

自定义编码器(Encoder)
public class ProtobufEncoder extends MessageToByteEncoder<ProtoMsg.Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ProtoMsg.Message msg, ByteBuf out) throws Exception {
        out.writeShort(ProtoInstant.MAGIC_CODE);
        out.writeShort(ProtoInstant.VERSION_CODE);
        byte[] bytes = msg.toByteArray();
        int length = bytes.length;
        out.writeInt(length);
        out.writeBytes(bytes);
    }
}
自定义解码器(Dncoder)
public class ProtobufDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        in.markReaderIndex(); // 将当前的 reader位置进行一个 标记
        if(in.readableBytes() < 8) {
            return;
        }
        short magic = in.readShort();
        if(magic!= ProtoInstant.MAGIC_CODE) {
            System.out.println("魔数有问题!");
            throw new UnsupportedOperationException();
        }
        short version = in.readShort();

        int length = in.readInt();
        if(length < 0) {
            ctx.close();
        }
        if(length > in.readableBytes()) {
            in.resetReaderIndex();
            return;
        }
        byte[] array;
        if(in.hasArray()) { //堆缓冲
            ByteBuf slice = in.slice();
            array = slice.array();
        } else { //直接缓冲
            array = new byte[length];
            in.readBytes(array, 0, length);
        }
        ProtoMsg.Message message = ProtoMsg.Message.parseFrom(array);
        if(message != null) {
            out.add(message);
        }
    }
}
自定义登录验证处理器(LoginResponseHandler)

本项目仅仅是一个学习使用的基于Netty的聊天软件,未使用前端和DB,所以任意用户都可以进行访问,如果有增加的需要请自行增加(后续有时间,会进行迭代升级)。

public class LoginResponseHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg == null || !(msg instanceof ProtoMsg.Message)) {
            super.channelRead(ctx, msg);
            return;
        }
        ProtoMsg.Message proto = (ProtoMsg.Message) msg;
        ProtoMsg.HeadType headType = proto.getType();
        if(!headType.equals(ProtoMsg.HeadType.LOGIN_RESPONSE)) {
            super.channelRead(ctx, msg);
            return;
        }

        ProtoMsg.LoginResponse loginResponse = proto.getLoginResponse();
        ProtoInstant.ResultCodeEnum result =
                ProtoInstant.ResultCodeEnum.values()[loginResponse.getCode()];
        if(!result.equals(ProtoInstant.ResultCodeEnum.SUCCESS)) {
            System.out.println("登录失败!");
            return;
        }

        ClientSession.loginSuccess(ctx, proto);
        //登录成功了,loginhandler还有用吗?还需要在pipline管道中占资源吗?
        ChannelPipeline p = ctx.pipeline();
        p.remove(this);
        p.addAfter("encoder","heartbeat",new HeartBeatClientHandler());
    }
}
自定义心跳处理器(HeartBeatClientHandler)

心跳处理器,用于服务端与客户端进行连接,保证服务的可用性,同时验证客户端的连通性。

public class HeartBeatClientHandler extends ChannelInboundHandlerAdapter {
    //还需要定义一个心跳发送的时间间隔 s为单位
    private static final int HEARTBEAT_INTERVAL = 60;

    //我们需要接收服务端的回包 ,所以需要用InboundHandler
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg == null || !(msg instanceof ProtoMsg.Message)) {
            super.channelRead(ctx, msg);
            return;
        }
        ProtoMsg.Message proto = (ProtoMsg.Message) msg;
        ProtoMsg.HeadType headType = proto.getType();
        if(headType.equals(ProtoMsg.HeadType.HEART_BEAT)) {
            System.out.println("收到服务器的心跳回包,连通性极好!");
        } else {
            super.channelRead(ctx, msg);
        }
    }

    //心跳什么时候开始发送呢? 回答:当我们的HeartBeatClientHandler加入到pipline管道的时候,就开始发送
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        ClientSession session = ClientSession.getSession(ctx);
        User user = session.getUser();
        HeartBeatMsgBuilder heartBeatMsgBuilder = new HeartBeatMsgBuilder(user, session);
        ProtoMsg.Message message = heartBeatMsgBuilder.buildMsg(); //消息拼装完成
        //发送 定时 每60s
        heartBeat(ctx, message);
    }

    private void heartBeat(ChannelHandlerContext ctx, ProtoMsg.Message message) {
        ctx.executor().schedule(() -> {
            if(ctx.channel().isActive()) {
                System.out.println("进行心跳包的发送!");
                ctx.writeAndFlush(message);
                heartBeat(ctx, message);
            }
        }, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
    }
}
消息接收处理器(ChatMsgHandler)
@Slf4j
public class ChatMsgHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (ObjectUtils.isEmpty(msg) || !(msg instanceof ProtoMsg.Message)) {
            log.info("消息不属于消息响应");
            super.channelRead(ctx, msg);
            return;
        }
        ProtoMsg.Message proto = (ProtoMsg.Message) msg;
        if (!proto.getType().equals(ProtoMsg.HeadType.MESSAGE_REQUEST)) {
            log.info("消息不属于消息响应");
            super.channelRead(ctx, msg);
            return;
        }
        ProtoMsg.MessageRequest response = proto.getMessageRequest();
        String content = response.getContent();
        String from = response.getFrom();
        System.err.println("收到来自"+from+"的消息:"+content);
    }
}
自定义异常处理器

public class ExceptionHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if(cause instanceof UnsupportedOperationException) {
            System.out.println("有未知异常!");
            ClientSession.getSession(ctx).closeSession();
        } else {
            System.out.println("有异常!");
            ctx.close();
        }
        CoreStart coreStart = CoreStart.getInstance();
        coreStart.setConnectFlag(false);
        coreStart.startCore();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
}

二、实体类和工具类
ProtoInstant

ProtoInstant类用于定义消息的类型,同时与服务端同时定义区分软件的魔术版本号

public class ProtoInstant {
    public static final short MAGIC_CODE = 0x86;
    public static final short VERSION_CODE = 0x01;
    public enum ResultCodeEnum {
        SUCCESS(0, "Success"),
        AUTH_FAILED(1, "登录失败"),
        NO_TOKEN(2, "没有授权码"),
        UNKNOW_ERROR(3, "未知错误");

        private Integer code;
        private String desc;

        ResultCodeEnum(Integer code, String desc) {
            this.code = code;
            this.desc = desc;
        }

        public Integer getCode() {
            return code;
        }

        public String getDesc() {
            return desc;
        }
    }
}
user实体类
@Slf4j
@Data
public class User {

    String uid;
    String devId;
    String token;
    String nickName = "nickName";
    PLATTYPE platform = PLATTYPE.WINDOWS;

    // windows,mac,android, ios, web , other
    public enum PLATTYPE {
        WINDOWS, MAC, ANDROID, IOS, WEB, OTHER;
    }

    private String sessionId;


    public void setPlatform(int platform) {
        PLATTYPE[] values = PLATTYPE.values();
        for (int i = 0; i < values.length; i++) {
            if (values[i].ordinal() == platform) {
                this.platform = values[i];
            }
        }
    }

    @Override
    public String toString() {
        return "User{" +
                "uid='" + uid + '\'' +
                ", devId='" + devId + '\'' +
                ", token='" + token + '\'' +
                ", nickName='" + nickName + '\'' +
                ", platform=" + platform +
                '}';
    }

    public static User fromMsg(ProtoMsg.LoginRequest info) {
        User user = new User();
        user.uid = new String(info.getUid());
        user.devId = new String(info.getDeviceId());
        user.token = new String(info.getToken());
        user.setPlatform(info.getPlatform());
        log.info("登录中: {}", user.toString());
        return null;
    }
}
自定义消息类
@Data
public class ChatMsg {
    private User user;
    private long msgId;
    private String from;
    private String to;
    private long time;
    private String content;
    private MSGTYPE msgType;

    public ChatMsg(User user) {
        if(null == user) {
            return;
        }
        this.user = user;
        this.setTime(System.currentTimeMillis());
        this.setFrom(user.getUid());
    }

    public void fillMsg(ProtoMsg.MessageRequest.Builder cb) {
        if(msgId > 0) cb.setMsgId(msgId);
        if(StringUtils.isNotEmpty(from)) cb.setFrom(from);
        if(StringUtils.isNotEmpty(to)) cb.setTo(to);
        if(time > 0) cb.setTime(time);
        if(msgType != null) cb.setMsgType(msgType.ordinal());
        if(StringUtils.isNotEmpty(content)) cb.setContent(content);
    }

    public enum MSGTYPE {
        TEXT,VEDIO
    }
}
三、异步任务类

ExecuteTask接口用于执行任务

public interface ExecuteTask {
    void execute();
}

FutureTaskScheduler类用于完成任务的添加和删除,实现定时任务。

public class FutureTaskScheduler extends Thread{
    private ConcurrentLinkedQueue<ExecuteTask> executeTaskQueue =
            new ConcurrentLinkedQueue<>();
    private long sleepTime = 200;
    private ExecutorService pool = Executors.newFixedThreadPool(10);
    private static FutureTaskScheduler inst = new FutureTaskScheduler();
    public FutureTaskScheduler() {
        this.start();
    }
    //任务添加
    public static void add(ExecuteTask executeTask) {
        inst.executeTaskQueue.add(executeTask);
    }

    @Override
    public void run() {
        while (true) {
            handleTask();
            threadSleep(sleepTime);
        }
    }

    private void threadSleep(long sleepTime) {
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //执行任务
    private void handleTask() {
        ExecuteTask executeTask;
        while (executeTaskQueue.peek() != null) {
            executeTask = executeTaskQueue.poll();
            handleTask(executeTask);
        }
    }
    private void handleTask(ExecuteTask executeTask) {
        pool.execute(new ExecuteRunnable(executeTask));
    }

    class ExecuteRunnable implements Runnable {
        ExecuteTask executeTask;
        public ExecuteRunnable(ExecuteTask executeTask) {
            this.executeTask = executeTask;
        }
        @Override
        public void run() {
            executeTask.execute();
        }
    }
}
四、用户状态类

ClientSession用户存储用户使用状态、保存用户信息和保存服务端通信的Channel通道。

@Data
@Slf4j
public class ClientSession {
    public static final AttributeKey<ClientSession> SESSION_KEY = AttributeKey.valueOf("SESSION_KEY");
    private Channel channel;
    private User user;
    private String sessionId;
    private boolean isLogin = false;
    private boolean isConnected = false;
    //绑定channel
    public ClientSession(Channel channel) {
        this.channel = channel;
        this.sessionId = String.valueOf("-1");
        channel.attr(ClientSession.SESSION_KEY).set(this);
    }
    //登录成功后的一些逻辑处理
    public static void loginSuccess(ChannelHandlerContext ctx, ProtoMsg.Message proto) {
        Channel channel = ctx.channel();
        ClientSession session = channel.attr(ClientSession.SESSION_KEY).get();
        session.setSessionId(proto.getSessionId());
        session.setLogin(true);
        log.info("用户登录成功,用户信息为:{}", session.getUser());

    }
    //获取到clientSession
    public static ClientSession getSession(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        ClientSession session = channel.attr(ClientSession.SESSION_KEY).get();
        return session;
    }
    //关闭session
    public void closeSession() {
        isConnected = false;
        ChannelFuture future = channel.close();
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if(future.isSuccess()) {
                    System.out.println("链接断开!");
                }
            }
        });
    }

}
五、核心启动类

本机通过CoreStart类启动当前服务,并通过控制台的形式完成登录状态。

@Data
public class CoreStart {
    public static CoreStart instance = new CoreStart();
    public static CoreStart getInstance() {
        return instance;
    }
    private ClientSession session;
    private User user;
    private Channel channel;
    private NettyCilent nettyCilent = new NettyCilent();
    private LoginSender loginSender = new LoginSender();
    private ChatSender chatSender = new ChatSender();
    private boolean connectFlag = false;

    GenericFutureListener<ChannelFuture> closeListener = (ChannelFuture f) -> {
        System.out.println("连接异常断开....");
        channel = f.channel();
        ClientSession session = channel.attr(ClientSession.SESSION_KEY).get();
        session.closeSession();
    };

    GenericFutureListener<ChannelFuture> connectedListener = (ChannelFuture f) -> {
        // 这个eventloop是为了 链接失败后的 重连,每10s进行重连,重连的时候
        //需要用 eventLoop中的 schedule进行重连定时任务的触发
        EventLoop eventLoop = f.channel().eventLoop();
        if(!f.isSuccess()) {
            System.out.println("链接失败,开始重试,每10s一次...");
            eventLoop.schedule(()->nettyCilent.doConnect(), 10, TimeUnit.SECONDS);
            connectFlag = false;
        } else { //现在你连接成功了,这时候需要监听断开连接的消息
            connectFlag = true;
            System.out.println("连接成功,可以进行一系列后续操作了。。。");
            channel = f.channel();
            session = new ClientSession(channel);
            session.setConnected(true);
            channel.closeFuture().addListener(closeListener);
            notifyCommandThread();
        }
    };

    public void startCore() {
        Thread.currentThread().setName("Netty主线程");
        while(true) {
            //建立连接
            while(connectFlag == false) {
                startConnectSever();
                waitCommandThread();
            }

            //命名的处理 scanner system in
            while(null != session) {
                Scanner scanner = new Scanner(System.in);
                scanner.useDelimiter("\n");
                if(isLogin()) {
                    //已经登录了,可以直接发送消息
                    System.err.println("请输入消息,格式请参照:toUserId:content");
                    String[] info = null;
                    while(true) {
                        String input = scanner.next();

                        //消息发送是这样,比如说我给 id2 用户发送 helloworld,那么我的书写方式是
                        //id2:helloword
                        info = input.split(":");
                        if(info.length !=2) {
                            System.out.println("error: 消息格式有问题!");
                        } else {
                            break;
                        }
                    }
                    String toUserId = info[0];
                    String content = info[1];
                    startOneChat(toUserId, content);
                } else {
                    System.err.println("请登录,请输入指令 1");
                    String key = scanner.next();
                    if(!key.equals("1")) {
                        System.err.println("指令错误!");
                        continue;
                    }
                    System.err.println("请输入用户名和密码。格式为 id:pass");
                    String[] info = null;
                    while (true) {
                        String input = scanner.next();
                        info = input.split(":");
                        if(info.length !=2) {
                            System.out.println("error: 消息格式有问题!");
                        } else {
                            break;
                        }
                    }
                    String username = info[0];
                    String password = info[1];
                    startLogin(username, password);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    //启动我们的 nettyClient
    public void startConnectSever() {
        FutureTaskScheduler.add(() -> {
            nettyCilent.setConnectedListener(connectedListener);
            nettyCilent.doConnect();
        });
    }

    //线程的 wait和 notify
    private synchronized void notifyCommandThread() {
        this.notify();
    }
    private synchronized void   waitCommandThread() {
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //发送消息
    private void startOneChat(String toUserId, String message) {
        if(!isLogin()) {
            System.out.println("未登录,请先登录!");
            return;
        }
        chatSender.setSession(session);
        chatSender.setUser(user);
        chatSender.sendChatMsg(toUserId, message);
    }

    //登录消息的发送
    private void startLogin(String username, String password) {
        if(!isConnectFlag()) {
            System.out.println("连接异常,请先进行链接!");
            return;
        }
        User user = new User();
        user.setUid(username);
        user.setToken(password);
        user.setDevId("1111");
        this.user = user;
        session.setUser(user);
        loginSender.setUser(user);
        loginSender.setSession(session);
        loginSender.sendLoginMsg();
    }

    private boolean isLogin() {
        if(session == null) {
            return false;
        }
        return session.isLogin();
    }
}
六、客户端装载类

通过NettyCilent类完成EventLoophandler的组装,并且监听客户端与服务端的连接状态。

@Data
public class NettyCilent {
    private GenericFutureListener<ChannelFuture> connectedListener;
    public void doConnect() {
        try {
            Bootstrap bootstrap = new Bootstrap();
            EventLoopGroup g = new NioEventLoopGroup();
            bootstrap.group(g);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
            bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
            bootstrap.remoteAddress("127.0.0.1", 8086);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast("decoder", new ProtobufDecoder());
                    ch.pipeline().addLast("encoder", new ProtobufEncoder());
                    ch.pipeline().addLast(new LoginResponseHandler());
                    ch.pipeline().addLast(new ChatMsgHandler());
                    ch.pipeline().addLast(new ExceptionHandler());
                }
            });
            System.out.println("客户端开始链接....");
            ChannelFuture connect = bootstrap.connect();
            connect.addListener(connectedListener);
        } catch (Exception e) {
            System.out.println("链接失败!异常信息 = " + e);
        }
    }
}
七、启动客户端
@SpringBootApplication
public class ClientApplication {
    public static void main(String[] args) {
       CoreStart coreStart = CoreStart.getInstance();
       coreStart.startCore();
    }
}

3.4、编写服务端代码

一、处理器(handler)
自定义编码器(Encoder)

编码器与客户端的编码器一致。

自定义解码器(Dncoder)

解码器与客户端的解码器一致。

自定义登录请求处理器
public class   LoginRequestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg == null || !(msg instanceof ProtoMsg.Message)) {
            super.channelRead(ctx, msg);
            return;
        }
        ProtoMsg.Message pkg = (ProtoMsg.Message) msg;
        ProtoMsg.HeadType headType = pkg.getType();
        if(!headType.equals(ProtoMsg.HeadType.LOGIN_REQUEST)) {
            super.channelRead(ctx, msg);
            return;
        }
        //新的session的创建
        ServerSession session = new ServerSession(ctx.channel());
        //进行登录逻辑处理,异步进行处理。并且需要知道 处理的结果。 callbacktask就要
        //派上用场了
        CallbackTaskScheduler.add(new CallbackTask<Boolean>() {
            @Override
            public Boolean execute() throws Exception {
                //真的进行 login 逻辑的处理
                boolean result = action(session, pkg);
                return result;
            }
            //没有异常的话,我们进行处理
            @Override
            public void onBack(Boolean result) {
                if(result) {
                    ctx.pipeline().remove(LoginRequestHandler.class);
                    System.out.println("登录成功: user = " + session.getUser());
                } else {
                    session.closeSession(ctx);
                    System.out.println("登录失败: user = " + session.getUser());
                }
            }
            //有异常的话,我们进行处理
            @Override
            public void onException(Throwable t) {
                session.closeSession(ctx);
                System.out.println("登录时有未知异常: user = " + t);
            }
        });
    }

    private boolean action(ServerSession session, ProtoMsg.Message proto) {
        ProtoMsg.LoginRequest loginRequest = proto.getLoginRequest();
        long seqNo = proto.getSequence();
        User user = User.fromMsg(loginRequest);
        //user验证
        boolean isVaildUser = checkUser(user);
        if(!isVaildUser) {
            //组装失败的报文,发送给客户端失败的报文消息
            ProtoInstant.ResultCodeEnum resultCode = ProtoInstant.ResultCodeEnum.NO_TOKEN;
            ProtoMsg.Message response = loginResponse(resultCode, seqNo, "-1");
            session.writeAndFlush(response);
            return false;
        }
        //有效的 user,我们发送登录成功的报文给 客户端
        session.setUser(user); //第一个是set user信息;第二个是 创建sessionId
        session.bind();
        ProtoInstant.ResultCodeEnum resultCode = ProtoInstant.ResultCodeEnum.SUCCESS;
        ProtoMsg.Message response = loginResponse(resultCode, seqNo, session.getSessionId());
        session.writeAndFlush(response);
        return true;
    }

    private ProtoMsg.Message loginResponse(ProtoInstant.ResultCodeEnum resultCode, long seqNo, String sessionId) {
        ProtoMsg.Message.Builder mb = ProtoMsg.Message.newBuilder()
                .setType(ProtoMsg.HeadType.LOGIN_RESPONSE)
                .setSequence(seqNo)
                .setSessionId(sessionId);
        //你需要告诉我真实的 response 是什么. 所以我们需要构造 loginresponse
        ProtoMsg.LoginResponse.Builder res = ProtoMsg.LoginResponse.newBuilder()
                .setCode(resultCode.getCode())
                .setInfo(resultCode.getDesc())
                .setExpose(1);
        mb.setLoginResponse(res);
        return mb.build();
    }

    //校验合法user
    private boolean checkUser(User user) {
        //当前用户已经登录
        if(SessionMap.inst().hasLogin(user)) {
            return false;
        }
        return true;
    }
}
自定义心跳处理器

心跳处理器的逻辑是在客户端登录成功后,客户端将心跳处理器加入到pipeline中,与服务端进行心跳包的发送,用于验证客户端与服务端的连通性检测,保证客户端与服务端的连通性,可以提高用户的体验。

public class HeartBeatServerHandler extends IdleStateHandler {
    //多长时间没有心跳,就关闭连接,定义一个 时间啊
    private static final int READ_IDLE_GAP = 3000;
    public HeartBeatServerHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
        super(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds);
    }
    //这个构造函数,可以指定时间单位
    public HeartBeatServerHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
        super(readerIdleTime, writerIdleTime, allIdleTime, unit);
    }

    public HeartBeatServerHandler() {
        super(READ_IDLE_GAP, 0, 0, TimeUnit.SECONDS);
    }

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //心跳包不能为空; 心跳包的类型是HEART_BEAT
        if(msg == null || !(msg instanceof ProtoMsg.Message)) {
            super.channelRead(ctx, msg);
            return;
        }
        // 如果不为null我们就进行类型转化
        ProtoMsg.Message pkg = (ProtoMsg.Message) msg;
        ProtoMsg.HeadType headType = pkg.getType();
        if(headType.equals(ProtoMsg.HeadType.HEART_BEAT)) {
            //进行心跳包的处理
            FutureTaskScheduler.add(() -> {
                if(ctx.channel().isActive()) {
                    //直接将信息 回复给客户端
                    ctx.writeAndFlush(msg);
                }
            });
        }
        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        System.out.println(READ_IDLE_GAP + "秒内,没有收到数据,关闭连接!");
        ServerSession.closeSession(ctx);
    }
}
消息处理器

消息处理器目前的功能较为简易,本地由于没有使用DB和任何存储消息的数据库,所以没有实现对于离线用户的消息转发,仅仅只能对在线用户的消息发送,后续迭代后将上线,本消息处理器仅仅是一个对于Netty入门的一个写法。

public class ChatRedirectHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg == null || !(msg instanceof ProtoMsg.Message)) {
            super.channelRead(ctx, msg);
            return;
        }
        ProtoMsg.Message proto = (ProtoMsg.Message) msg;
        ProtoMsg.HeadType headType = proto.getType();
        if(!headType.equals(ProtoMsg.HeadType.MESSAGE_REQUEST)) {
            super.channelRead(ctx, msg);
            return;
        }
        ServerSession session = ServerSession.getSession(ctx);
        if(session == null || !session.isLogin()) {
            System.out.println("用户没有登录!");
            return;
        }
        // 进行消息的转发,异步转发
        FutureTaskScheduler.add(() -> {
            action(session, proto);
        });
    }

    private void action(ServerSession session, ProtoMsg.Message proto) {
        ProtoMsg.MessageRequest messageRequest = proto.getMessageRequest();
        System.out.println("消息转发,from = " + messageRequest.getFrom() +
                ". To = " + messageRequest.getTo() + ". 内容为 = " + messageRequest.getContent());
        String to = messageRequest.getTo();
        //我们需要判断 接收者 是否在线
        List<ServerSession> toSession = SessionMap.inst().getSessionBy(to);
        if(toSession == null || toSession.size() == 0) {
            System.out.println("用户离线,无法发送!");
            //将离线消息进行保存,等用户上线再次发送给用户
        } else {
            toSession.forEach(s -> {
                s.writeAndFlush(proto);
            });
        }
    }
}
服务端异常处理器

异常处理器是用于处理在pipeline管道中出现的异常处理器。

public class ServerExceptionHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ServerSession.closeSession(ctx);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if(cause instanceof UnsupportedOperationException) {
            ServerSession.closeSession(ctx);
        } else {
            System.out.println("获得已知异常!");
            ctx.close();
        }
    }
}
二、实体类和工具类

实体类与工具类与客户端的实体类和工具类保持一致,只需要cv过来就可以。

三、异步任务类和回调方法类
回调接口
public interface CallbackTask<T> {
    T execute() throws Exception;
    void onBack(T t); // 执行没有 异常的情况下的 返回值
    void onException(Throwable t);
}
回调接口实现类
public class CallbackTaskScheduler extends Thread {
    private ConcurrentLinkedQueue<CallbackTask> executeTaskQueue =
            new ConcurrentLinkedQueue<>();
    private long sleepTime = 200;
    private ExecutorService pool = Executors.newCachedThreadPool();
    ListeningExecutorService lpool = MoreExecutors.listeningDecorator(pool);
    private static CallbackTaskScheduler inst = new CallbackTaskScheduler();
    private CallbackTaskScheduler() {
        this.start();
    }
    //add task
    public static <T> void add(CallbackTask<T> executeTask) {
        inst.executeTaskQueue.add(executeTask);
    }

    @Override
    public void run() {
        while (true) {
            handleTask();
            threadSleep(sleepTime);
        }
    }

    private void threadSleep(long sleepTime) {
        try {
            Thread.sleep(sleepTime);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    //任务执行
    private void handleTask() {
        CallbackTask executeTask = null;
        while (executeTaskQueue.peek() != null) {
            executeTask = executeTaskQueue.poll();
            handleTask(executeTask);
        }
    }
    private <T> void handleTask(CallbackTask<T> executeTask) {
        ListenableFuture<T> future = lpool.submit(new Callable<T>() {
            public T call() throws Exception {
                return executeTask.execute();
            }
        });
        Futures.addCallback(future, new FutureCallback<T>() {
            @Override
            public void onSuccess(T t) {
                executeTask.onBack(t);
            }

            @Override
            public void onFailure(Throwable throwable) {
                executeTask.onException(throwable);
            }
        });
    }
}
任务接口
//不需要知道异步线程的 返回值
public interface ExecuteTask {
    void execute();
}
任务执行实现类
public class FutureTaskScheduler extends Thread{
    private ConcurrentLinkedQueue<ExecuteTask> executeTaskQueue =
            new ConcurrentLinkedQueue<>();
    private long sleepTime = 200;
    private ExecutorService pool = Executors.newFixedThreadPool(10);
    private static FutureTaskScheduler inst = new FutureTaskScheduler();
    public FutureTaskScheduler() {
        this.start();
    }
    //任务添加
    public static void add(ExecuteTask executeTask) {
        inst.executeTaskQueue.add(executeTask);
    }

    @Override
    public void run() {
        while (true) {
            handleTask();
            threadSleep(sleepTime);
        }
    }

    private void threadSleep(long sleepTime) {
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //执行任务
    private void handleTask() {
        ExecuteTask executeTask;
        while (executeTaskQueue.peek() != null) {
            executeTask = executeTaskQueue.poll();
            handleTask(executeTask);
        }
    }
    private void handleTask(ExecuteTask executeTask) {
        pool.execute(new ExecuteRunnable(executeTask));
    }

    class ExecuteRunnable implements Runnable {
        ExecuteTask executeTask;
        public ExecuteRunnable(ExecuteTask executeTask) {
            this.executeTask = executeTask;
        }
        @Override
        public void run() {
            executeTask.execute();
        }
    }
}
四、用户状态存储类
@Data
public class SessionMap {
    //用单例模式进行sessionMap的创建
    private SessionMap(){}

    private static SessionMap singleInstance = new SessionMap();

    public static SessionMap inst() {
        return singleInstance;
    }

    //进行会话的保存
    //key 我们使用 sessionId;value 需要是 serverSession
    private ConcurrentHashMap<String, ServerSession> map = new ConcurrentHashMap<>(256);
    //添加session
    public void addSession(String sessionId, ServerSession s) {
        map.put(sessionId, s);
        System.out.println("用户id: = " + s.getUser().toString() + "" +
                ".在线人数:" + map.size() );
    }

    //删除session
    public void removeSession(String sessionId) {
        if(map.contains(sessionId)) {
            ServerSession s = map.get(sessionId);
            map.remove(sessionId);
            System.out.println("用户id下线: = " + s.getUser().toString() + "" +
                    "。在线人数:" + map.size() );
        }
        return;
    }

    public boolean hasLogin(User user) {
        Iterator<Map.Entry<String, ServerSession>> iterator = map.entrySet().iterator();
        while(iterator.hasNext()) {
            Map.Entry<String, ServerSession> next = iterator.next();
            User userExist = next.getValue().getUser();
            if(userExist.getUid().equals(user.getUid())) {
                return true;
            }
        }
        return false;
    }

    //如果user在线,肯定有sessionMap里保存的 serverSession
    //如果user 不在线,serverSession也没有。用这个来判断 to user是否在线
    public List<ServerSession> getSessionBy(String userId) {
        return map.values().stream().
                filter(s -> s.getUser().getUid().equals(userId)).
                collect(Collectors.toList());
    }
}
五、服务端服务类

服务端服务类(ServerSession)用于实现客户端与服务端Channel进行绑定类,从而实现数据的传输。

@Data
public class ServerSession {
    public static final AttributeKey<ServerSession> SESSION_KEY =
            AttributeKey.valueOf("SESSION_KEY");
    //通道
    private Channel channel;
    private User user;
    private final String sessionId;
    private boolean isLogin = false;

    public ServerSession(Channel channel){
        this.channel = channel;
        this.sessionId = buildNewSessionId();
    }

    private String buildNewSessionId() {
        return UUID.randomUUID().toString().replaceAll("-","");
    }

    //session需要和通道进行一定的关联,他是在构造函数中关联上的;
    //session还需要通过sessionkey和channel进行再次的关联;channel.attr方法.set当前的
    // serverSession
    //session需要被添加到我们的SessionMap中
    public ServerSession bind(){
        System.out.println("server Session 会话进行绑定 :" + channel.remoteAddress());
        channel.attr(SESSION_KEY).set(this);
        SessionMap.inst().addSession(sessionId, this);
        isLogin = true;
        return this;
    }

    //通过channel获取session
    public static ServerSession getSession(ChannelHandlerContext ctx){
        Channel channel = ctx.channel();
        return channel.attr(SESSION_KEY).get();
    }

    //关闭session
    public static void closeSession(ChannelHandlerContext ctx){
        ServerSession serverSession = ctx.channel().attr(SESSION_KEY).get();
        if(serverSession != null && serverSession.getUser() != null) {
            ChannelFuture future = serverSession.channel.close();
            future.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if(!future.isSuccess()) {
                        System.out.println("Channel close error!");
                    }
                }
            });
            SessionMap.inst().removeSession(serverSession.sessionId);
        }
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
        user.setSessionId(sessionId);
    }

    //写消息
    public void writeAndFlush(Object msg) {
        channel.writeAndFlush(msg);
    }

}
六、服务端装载类
public class ChatServer {
    public void run() {
        EventLoopGroup bg = new NioEventLoopGroup();
        EventLoopGroup wg = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        try {
            b.group(bg,wg);
            b.channel(NioServerSocketChannel.class);
            b.localAddress(new InetSocketAddress(8086));
            b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProtobufDecoder());
                    ch.pipeline().addLast(new ProtobufEncoder());
                    ch.pipeline().addLast(new HeartBeatServerHandler());
                    ch.pipeline().addLast(new LoginRequestHandler());
                    ch.pipeline().addLast(new ChatRedirectHandler());
                    ch.pipeline().addLast(new ServerExceptionHandler());
                }
            });
            //绑定进行异步绑定
            ChannelFuture channelFuture = b.bind().sync();
            System.out.println("开始进行客户端的链接 监听... ...");
            //客户端关闭进行异步关闭
            ChannelFuture closeFuture = channelFuture.channel().closeFuture();
            closeFuture.sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            wg.shutdownGracefully();
            bg.shutdownGracefully();
        }
    }
}
七、服务端启动类
@SpringBootApplication
public class NettyServerApplication {

    public static void main(String[] args) {
       ChatServer cs = new ChatServer();
       cs.run();
    }

}

4、系统实现效果

客户端

登录

image-20250206111908000

消息发送

image-20250206111948357

心跳包接收

image-20250206112015308

掉线重连

image-20250206112105006

服务端

异常接收

这个异常接收是客户端掉线重连的异常

image-20250206112233388

客户端服务注册

image-20250206112303922

消息转发

image-20250206112345436

心跳包发送

image-20250206112416048

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值