[Netty实践] 简单聊天实现(一):基础部分

发布时间:2023年12月27日

目录

一、简介

二、结构

三、依赖

四、消息设计

五、序列化工具实现

六、编解码器实现

七、后续文章


一、简介

本章主要通过netty实现一个简单的聊天功能,主要分为三块:登录、单聊以及群聊功能,上诉功能会做的比较粗略,主打的就是一个学习,更完善的功能需要自行深入了解以及修改,也希望通过这个章节实践,能够给大家帮助。

该章节主要分为三部分,同时也是分为三篇博客:

第一部分,主要涉及服务端与客户端之间通信的消息设计,以及消息序列化相关实现

第二部分,主要涉及服务端的实现,主要包括服务端的创建、channel管理、组管理、对应各种Handler实现。

第三部分,主要涉及客户端的实现,主要包括客户端的创建、登录、单聊、群聊消息发送。

本篇博客,实现的是第一部分(基础部分)

二、结构

三、依赖

其他模块也一样

<dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.101.Final</version>
        </dependency>

        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.8.0</version>
        </dependency>

        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.8.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

    </dependencies>

四、消息设计

由于我们的聊天主要有以下三个功能:

登录,需要记录用户名对应的Channel

单聊,需要向对方用户发送消息

群聊,需要向群组发送消息

而以Request结尾的类是由客户端进行发送,服务端进行接收处理的。Response结尾的类是由服务端进行发送,客户端进行接收处理的。

继承了Message类的所有Request和Response,就是服务端与客户端之间交互的数据结构。

1、Message类,所有Request和Response类的父类,最关键的字段就是messageType,子类继承之后进行赋值,该值与类的类型进行绑定,用于byte字节数组反序列化时能够获取到需要反序列化的类型。

public abstract class Message {

    /**
     * 用于记录消息类型,序列化与反序列化时时候,用于找到指定类型
     */
    protected Byte messageType;

    /**
     * 发送时间
     */
    protected Date sendDate;

    public Message() {
        this.sendDate = new Date();
    }

}

2、LoginRequest和LoginResponse,这里登陆进行了简化,只需要用户传递一个username,就将username与channel进行绑定

@Data
@ToString
public class LoginRequest extends Message {

    private String username;

    public LoginRequest() {
        super.messageType = CommandConstant.loginRequest;
    }

    public LoginRequest(String username) {
        super.messageType = CommandConstant.loginRequest;
        this.username = username;
    }

}
@Data
@ToString
public class LoginResponse extends Message {

    private Boolean result;

    private String message;

    public LoginResponse() {
        super.messageType = CommandConstant.loginResponse;
    }

    public LoginResponse(Boolean result, String message) {
        super.messageType = CommandConstant.loginResponse;
        this.result = result;
        this.message = message;
    }

}

3、SingleMessageRequest和SingleMessageResponse,用于向另外一个用户发送消息

@Data
@ToString
public class SingleMessageRequest extends Message {

    /**
     * 发送人
     */
    private String sendFrom;

    /**
     * 接收人
     */
    private String sendTo;

    /**
     * 发送内容
     */
    private String content;

    public SingleMessageRequest() {
        super.messageType = CommandConstant.singleMessageRequest;
    }

    public SingleMessageRequest(String sendFrom, String sendTo, String content) {
        super.messageType = CommandConstant.singleMessageRequest;
        this.sendFrom = sendFrom;
        this.sendTo = sendTo;
        this.content = content;
    }
    
}
@Data
@ToString
public class SingleMessageResponse extends Message {

    /**
     * 发送人
     */
    private String sendFrom;

    /**
     * 发送内容
     */
    private String content;

    public SingleMessageResponse() {
        super.messageType = CommandConstant.singleMessageResponse;
    }

    public SingleMessageResponse(String sendFrom, String content) {
        super.messageType = CommandConstant.singleMessageResponse;
        this.sendFrom = sendFrom;
        this.content = content;
    }

}

4、GroupMessageRequest和GroupMessageResponse,用于向一个群组发送消息

@Data
@ToString
public class GroupMessageRequest extends Message {

    /**
     * 发送人
     */
    private String sendFrom;

    /**
     * 群组
     */
    private String group;

    /**
     * 发送内容
     */
    private String content;

    public GroupMessageRequest() {
        super.messageType = CommandConstant.groupMessageRequest;
    }

    public GroupMessageRequest(String sendFrom, String group, String content) {
        super.messageType = CommandConstant.groupMessageRequest;
        this.sendFrom = sendFrom;
        this.group = group;
        this.content = content;
    }


}
@Data
@ToString
public class GroupMessageResponse extends Message {

    /**
     * 发送人
     */
    private String sendFrom;

    /**
     * 群组
     */
    private String group;

    /**
     * 发送内容
     */
    private String content;

    public GroupMessageResponse() {
        super.messageType = CommandConstant.groupMessageResponse;
    }

    public GroupMessageResponse(String sendFrom, String group, String content) {
        super.messageType = CommandConstant.groupMessageResponse;
        this.sendFrom = sendFrom;
        this.group = group;
        this.content = content;
    }
    
}

5、CommandConstand,通过数值常量messageType绑定消息类型,在序列化对象时,会在数据中记录对象的messageType,在反序列化对象时,会从数据包中拿到messageType,将其转化为对应的消息类型进行处理

public class CommandConstant {

    public final static Byte singleMessageRequest = 1;
    public final static Byte singleMessageResponse = 2;

    public final static Byte groupMessageRequest = 3;
    public final static Byte groupMessageResponse = 4;

    public final static Byte loginRequest = 5;
    public final static Byte loginResponse = 6;

    public static Map<Byte, Class<? extends Message>> messageTypeMap = new ConcurrentHashMap<>();

    static {
        messageTypeMap.put(singleMessageRequest, SingleMessageRequest.class);
        messageTypeMap.put(singleMessageResponse, SingleMessageResponse.class);

        messageTypeMap.put(groupMessageRequest, GroupMessageRequest.class);
        messageTypeMap.put(groupMessageResponse, GroupMessageResponse.class);

        messageTypeMap.put(loginRequest, LoginRequest.class);
        messageTypeMap.put(loginResponse, LoginResponse.class);
    }

    public static Class<? extends Message> getMessageClass(Byte messageType){
        return messageTypeMap.get(messageType);
    }

}

五、序列化工具实现

该序列化工具主要用于将对象序列化为字节数组、以及将字节数组序列化为对象

public class SerializationUtil {

    private final static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();

    /**
     * 序列化
     */
    public static <T> byte[] serialize(T object){
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);

        try {
            Class<T> cls = (Class<T>) object.getClass();
            Schema<T> schema = getSchema(cls);

            return ProtostuffIOUtil.toByteArray(object, schema, buffer);
        } catch (Exception e) {
            throw e;
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化
     */
    public static <T> T deserialize(Class<T> cls, byte[] data) {
        Schema<T> schema = getSchema(cls);
        T message = schema.newMessage();
        ProtostuffIOUtil.mergeFrom(data, message, schema);
        return message;
    }

    public static <T> Schema<T> getSchema(Class<T> cls) {
        Schema<T> schema = (Schema<T>) schemaCache.get(cls);

        if(schema == null) {
            schema = RuntimeSchema.getSchema(cls);
            schemaCache.put(cls, schema);
        }
        return schema;
    }
    
}

六、编解码器实现

1、MessageEncode,用于将消息对象序列化为字节数组

字节数组主要包括三部分:

·有效数组长度,占4个字节,长度不包括自己,用于半包黏包判断

·消息的类型,占1个字节,用于反序列选择类型使用

·消息对象,占n个字节

public class MessageEncode extends MessageToByteEncoder<Message> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
        // 将对象进行序列化
        byte[] data = SerializationUtil.serialize(message);

        // 写数据长度,前4个字节用于记录数据总长度(对象 + 类型(1个字节))
        byteBuf.writeInt(data.length + 1);
        // 写记录消息类型,用于反序列选择类的类型
        byteBuf.writeByte(message.getMessageType());
        // 写对象
        byteBuf.writeBytes(data);
    }

}

2、MesageDecode,用于将字节数组反序列化为消息对象

反序列时会进行判断数据是否足够读取,足够的话就会读取到符合长度的字节数组进行序列化,否则的话等到下一个数据包到来再进行重新判断处理(解决半包黏包方案)

public class MessageDecode extends ByteToMessageDecoder {


    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {

        // 由于数据包的前4个字节用于记录总数据大小,如果数据不够4个字节,不进行读
        if(byteBuf.readableBytes() < 4) {
            return;
        }

        // 标记开始读的位置
        byteBuf.markReaderIndex();

        // 前四个字节记录了数据大小
        int dataSize = byteBuf.readInt();

        // 查看剩余可读字节是否足够,如果不是,重置读取位置,等待下一次解析
        if(byteBuf.readableBytes() < dataSize) {
            byteBuf.resetReaderIndex();
            return;
        }

        // 读取消息类型
        byte messageType = byteBuf.readByte();
        // 读取数据, 数组大小需要剔除1个字节的消息类型
        byte[] data = new byte[dataSize -1];

        byteBuf.readBytes(data);

        Message message = SerializationUtil.deserialize(CommandConstant.getMessageClass(messageType), data);

        list.add(message);
    }

}

七、后续文章

以上就是本节需要实现的所有内容,接下来关于server和client的实现,看以下文章:

https://blog.csdn.net/Staba/article/details/135061442

https://blog.csdn.net/Staba/article/details/135061661

文章来源:https://blog.csdn.net/Staba/article/details/135057482
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。