Netty——部分优化以及搭建简单RPC框架(笔记)


代码接前文 Netty——搭建一个聊天室(笔记)

一、优化

1.1. 扩展序列化算法

抽象序列化算法

package com.yjx23332.netty.test.entity;

import java.io.*;

public interface Serializer {
    //反序列化算法
    <T> T deserialize(Class<T> clazz,byte[] bytes);
    //序列化算法
    <T> byte[] serialize(T object);

    enum Algorithm implements  Serializer{
        Java{
            @Override
            public <T> T deserialize(Class<T> clazz, byte[] bytes) {
                try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));){
                    return (T) objectInputStream.readObject();
                } catch (IOException | ClassNotFoundException e) {
                    throw new RuntimeException("反序列化失败",e);
                }
            }

            @Override
            public <T> byte[] serialize(T object) {
                try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                     ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);){
                    objectOutputStream.writeObject(object);
                    byte[] bytes = byteArrayOutputStream.toByteArray();
                    return bytes;
                } catch (IOException e) {
                    throw new RuntimeException("序列化失败",e);
                }
            }
        }
    }
}

修改编解码器

package com.yjx23332.netty.test.protocol;

import com.yjx23332.netty.test.entity.Message;
import com.yjx23332.netty.test.entity.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

/**
 * 必须确保收到的消息是完整的,才可以使用
 * */
@ChannelHandler.Sharable
@Slf4j
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf,Message> {
    /***
     * 编码
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
        ByteBuf outTemp = ctx.alloc().buffer();
        //1. 4 字节表示魔数
        outTemp.writeBytes(new byte[]{1,2,3,4});
        //2. 1 字节表示版本
        outTemp.writeByte(1);
        //3. 1 字节表示序列化方式,0 jdk,1 json
        outTemp.writeByte(0);
        //4. 1 字节表示指令类型
        outTemp.writeByte(msg.getMessageType());
        //5. 4 字节表示序列号
        outTemp.writeInt(msg.getSequenceId());

        // 上述和为15,为了满足2^n倍,让内存对齐。填入一个无意义字节
        outTemp.writeByte(0xff);

        //6. 获取内容的字节数组
        byte[] bytes = Serializer.Algorithm.Java.serialize(msg);

        //7. 4字节表示长度
        outTemp.writeInt(bytes.length);
        //8. 写入内容
        outTemp.writeBytes(bytes);
        out.add(outTemp);
    }

    /**
     * 解码
     * */
    @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);
        if(serializerType == 0){
            Message message = Serializer.Algorithm.Java.deserialize(Message.class,bytes);
            log.debug("{},{},{},{},{},{}",magicNum,version,serializerType,messageType,sequenceId,length);
            log.debug("{}",message);
            out.add(message);
        }
    }
}

1.2 加入JSON序列化

引入一个转json的包

		<dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.13</version>
        </dependency>
	JSON{
            @Override
            public <T> T deserialize(Class<T> clazz, byte[] bytes) {
                String json = new String(bytes,StandardCharsets.UTF_8);
                return JSONObject.parseObject(json,clazz);
            }

            @Override
            public <T> byte[] serialize(T object) {
                String json = JSONObject.toJSONString(object);
                return json.getBytes(StandardCharsets.UTF_8);
            }
        }

1.2.1 修改为从配置中读取设置

修改为读取配置文件模式(也可以直接用@value)

读取Properties
package com.yjx23332.netty.test.config;

import com.yjx23332.netty.test.entity.Serializer;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public abstract class Config {
    static Properties properties;
    static{
        try(InputStream in = Config.class.getResourceAsStream("/application.properties")){
            properties = new Properties();
            properties.load(in);
        }catch (IOException e){
            throw new ExceptionInInitializerError(e);
        }
    }
    public static int getServerPort(){
        String value = properties.getProperty("server.port");
        if(value == null){
            return 8080;
        }
        else{
            return Integer.parseInt(value);
        }
    }
    public static Serializer.Algorithm getSerializerAlgorithm(){
        String value = properties.getProperty("serializer.algorithm");
        if(value == null){
            return Serializer.Algorithm.Java;
        }
        else{
            return Serializer.Algorithm.valueOf(value);
        }
    }
}

读取yml
package com.yjx23332.netty.test.config;

import com.yjx23332.netty.test.entity.Serializer;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;


import java.util.Properties;

public abstract class Config {
    static Properties properties;
    static{
        YamlPropertiesFactoryBean yamlProFb = new YamlPropertiesFactoryBean();
        yamlProFb.setResources(new ClassPathResource("application.yml"));
        properties = yamlProFb.getObject();
    }
	//余下同上

笔者用的是yml

server:
  port: 8080

serializer:
  algorithm: JSON

编码是从配置中读取
解码是从收到的消息中读取

package com.yjx23332.netty.test.protocol;

import com.yjx23332.netty.test.config.Config;
import com.yjx23332.netty.test.entity.Message;
import com.yjx23332.netty.test.entity.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

/**
 * 必须确保收到的消息是完整的,才可以使用
 * */
@ChannelHandler.Sharable
@Slf4j
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf,Message> {
    /***
     * 编码
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
        ByteBuf outTemp = ctx.alloc().buffer();
        //1. 4 字节表示魔数
        outTemp.writeBytes(new byte[]{1,2,3,4});
        //2. 1 字节表示版本
        outTemp.writeByte(1);
        //3. 1 字节表示序列化方式,0 jdk,1 json
        outTemp.writeByte(Config.getSerializerAlgorithm().ordinal());
        //4. 1 字节表示指令类型
        outTemp.writeByte(msg.getMessageType());
        //5. 4 字节表示序列号
        outTemp.writeInt(msg.getSequenceId());

        // 上述和为15,为了满足2^n倍,让内存对齐。填入一个无意义字节
        outTemp.writeByte(0xff);

        //6. 获取内容的字节数组
        byte[] bytes = Config.getSerializerAlgorithm().serialize(msg);

        //7. 4字节表示长度
        outTemp.writeInt(bytes.length);
        //8. 写入内容
        outTemp.writeBytes(bytes);
        out.add(outTemp);
    }

    /**
     * 解码
     * */
    @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);
        in.readBytes(bytes,0,length);
        //找到反序列化算法
        Serializer.Algorithm algorithm = Serializer.Algorithm.values()[serializerType];
        Class<?> messageClass = Message.getMessageClass(messageType);
        Object message = algorithm.deserialize(messageClass,bytes);
        log.debug("{},{},{},{},{},{}",magicNum,version,serializerType,messageType,sequenceId,length);
        log.debug("{}",message);
        out.add(message);
    }
}

1.2.2 测试

在test中创建如下代码,即可测试

package com.yjx23332.netty.test;

import com.yjx23332.netty.test.config.Config;
import com.yjx23332.netty.test.entity.Message;
import com.yjx23332.netty.test.entity.Serializer;
import com.yjx23332.netty.test.entity.vo.req.LoginRequestMessage;
import com.yjx23332.netty.test.protocol.MessageCodecSharable;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.logging.LoggingHandler;

public class TestSerializer {
    public static void main(String[] args) {
        MessageCodecSharable CODEC = new MessageCodecSharable();
        LoggingHandler LOGGING = new LoggingHandler();
        EmbeddedChannel channel = new EmbeddedChannel(LOGGING,CODEC,LOGGING);
        LoginRequestMessage message = new LoginRequestMessage("zhangsan","123");
        //出站测试
        //channel.writeOutbound(message);
        //入栈测试
        channel.writeInbound(messageToBytes(message));
    }

    public static ByteBuf messageToBytes(Message msg){
        int algorithm = Config.getSerializerAlgorithm().ordinal();
        ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
        out.writeBytes(new byte[]{1,2,3,4});
        out.writeByte(1);
        out.writeByte(algorithm);
        out.writeByte(msg.getMessageType());
        out.writeInt(msg.getSequenceId());
        out.writeByte(0xff);
        byte[] bytes = Serializer.Algorithm.values()[algorithm].serialize(msg);
        out.writeInt(bytes.length);
        out.writeBytes(bytes);
        return out;
    }
}

1.2 参数调整

1.2.1 CONNECT_TIMEOUT_MILLIS 连接超时

  • 属于SocketChannel参数
  • 用在客户端建立连接时,如果过在指定好秒内无法丽娜姐,会抛出timeout异常
  • SO_TIMEOUT主要用在阻塞IO,阻塞IO中accept,read等都是无限等待的,如果不希望永远阻塞,使用它调整时间
package com.yjx23332.netty.test;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestConnectionTimeout {
    public static void main(String[] args) {
        //1. 客户通过 .option() 方法配置参数 给SocketChannel 配置参数

        //2. 服务端
        // new ServerBootstrap().option() //是给 ServerSocketChannel
        // new ServerBootstrap().childOption() //给 SocketChannel
        NioEventLoopGroup group = new NioEventLoopGroup();
        try{
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
            		//单位毫秒
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,1000)
                    .channel(NioSocketChannel.class)
                    .handler(new LoggingHandler());
            ChannelFuture channelFuture = bootstrap.connect("localhost",8080);
            channelFuture.sync().channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
            log.debug("timeout");
        }finally {
            group.shutdownGracefully();
        }

    }
}

debug

设置如下断点
在这里插入图片描述
在超时报错的抛出异常的地方也加入断点
在这里插入图片描述

为channelFuture添加标记
在这里插入图片描述
切换至NIO线程
在这里插入图片描述

可以看到future来自这里
在这里插入图片描述
本质是一个定时线程,到时间后去检测是否成功,没有成功就抛出异常
通过Promise把结果返给主线程

				boolean wasActive = isActive();
                if (doConnect(remoteAddress, localAddress)) {
                    fulfillConnectPromise(promise, wasActive);
                } else {
                    connectPromise = promise;
                    requestedRemoteAddress = remoteAddress;

                    // Schedule connect timeout.
                    int connectTimeoutMillis = config().getConnectTimeoutMillis();
                    if (connectTimeoutMillis > 0) {
                        connectTimeoutFuture = eventLoop().schedule(new Runnable() {
                            @Override
                            public void run() {
                                ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                                if (connectPromise != null && !connectPromise.isDone()
                                        && connectPromise.tryFailure(new ConnectTimeoutException(
                                                "connection timed out: " + remoteAddress))) {
                                    close(voidPromise());
                                }
                            }
                        }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
                    }

                    promise.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (future.isCancelled()) {
                                if (connectTimeoutFuture != null) {
                                    connectTimeoutFuture.cancel(false);
                                }
                                connectPromise = null;
                                close(voidPromise());
                            }
                        }
                    });
                }

1.2.2 SO_BACKLOG 连接队列

client server syns queue accept queue bind() listen() connect() 1. SYN SYN_SEND put SYN_RCVD 2. SYN + ACK ESTABLISH 3. ACK put ESTABLISH accept() client server syns queue accept queue
  • syns queue:半连接队列
  • accept queue:全连接队列
  1. linux旧版本(2.2之前)中,backlog大小包括了两个队列的大小,之后分为了如下两个队列
  2. sync queue:通过/proc/sys/net/ipv4/tcp_max_syn_backlog指定,在syncookies启用的情况之下,逻辑上没有最大值限制,因此这个设置可以被忽略
  3. accept queue
    • 通过/proc/sys/net/core/somaxconn指定,在使用listen函数时,内核会根据传入的backlog参数与系统参数,取二者较小值
    • 如果accept queue队列满了,server将发送一个拒绝连接的错误信息到client

nio中,可以用bind(端口号,backlog参数)设置
在netty中,可以通过SO_BACKLOG来设置

package com.yjx23332.netty.test;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;

public class TestBacklogServer {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .option(ChannelOption.SO_BACKLOG,2) //全连接队列设置
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>(){
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler());
                    }
                }).bind(8080);
    }
}

debug

在NioEventLoop方法中,打入如下断点,用来填满队列
在这里插入图片描述

随便找一个客户端即可,开启多实例模式
在这里插入图片描述
就可以出现报错了。
在这里插入图片描述
在此处为backlog的默认值
在这里插入图片描述
追踪可以看到,windows平台默认200,其余128
如果存在配置文件,读出,则以这个数字为准
在这里插入图片描述

			// Determine the default somaxconn (server socket backlog) value of the platform.
            // The known defaults:
            // - Windows NT Server 4.0+: 200
            // - Linux and Mac OS X: 128
            int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
            File file = new File("/proc/sys/net/core/somaxconn");
            BufferedReader in = null;
            try {
                // file.exists() may throw a SecurityException if a SecurityManager is used, so execute it in the
                // try / catch block.
                // See https://github.com/netty/netty/issues/4936
                if (file.exists()) {
                    in = new BufferedReader(new FileReader(file));
                    somaxconn = Integer.parseInt(in.readLine());
                    if (logger.isDebugEnabled()) {
                        logger.debug("{}: {}", file, somaxconn);
                    }
                } else {
                    // Try to get from sysctl
                    Integer tmp = null;
                    if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
                        tmp = sysctlGetInt("kern.ipc.somaxconn");
                        if (tmp == null) {
                            tmp = sysctlGetInt("kern.ipc.soacceptqueue");
                            if (tmp != null) {
                                somaxconn = tmp;
                            }
                        } else {
                            somaxconn = tmp;
                        }
                    }

                    if (tmp == null) {
                        logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
                                somaxconn);
                    }
                }

1.2.3 ulimit -n

  • 限制进程能够打开文件的数量
  • 属于操作系统参数,文件设置。

1.2.4 TCP_NODELAY

属于socketChannel,对于一些小的数据的发送,TCP可能不会及时发送,而是当累计到了一定量后,一起发出去。通过该参数可以设置是否要延迟发送。
默认是false。也就是开启了延迟发送(nagle算法)。

1.2.5 SO_SNDBUF & SO_RCVBUF

  • SO_SNDBUF 属于SocketChannel 参数
  • SO_RCVBUF 既可用于 SocketChannel 参数,也可以用于 ServerSocketChannel 参数(建议设置到 ServerSocketChannel 上)
    现在的TCP协议会在连接时,双方协商一个大小,一般不用自己调整。

1.2.6 ALLOCATOR分配器配置

  • 属于 SocketChannel 参数
  • 用于分配ByteBuf 与 ctx.alloc()

在这里插入图片描述
在这里插入图片描述
要修改的话如下即可(虚拟机参数)
在这里插入图片描述

如下为,是否首选直接内存
在这里插入图片描述

在这里插入图片描述

修改如下
在这里插入图片描述

package com.yjx23332.netty.test;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestByteBuf {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler());
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = ctx.alloc().buffer();
                                log.debug("alloc buf {}",buf);
                            }
                        });
                    }
                }).bind(8080);
    }
}

运行后,获得消息时,可得下列结果
在这里插入图片描述

1.2.7 RCVBUF_ALLOCATOR

  • 属于SocketChannel参数
  • 控制netty接受缓冲区大小
  • 负责入站数据的分配,决定缓冲区大小(可动态调整),
public class TestByteBuf {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler());
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf buf = ctx.alloc().buffer();
                                log.debug("alloc buf {}",buf);
                                log.debug("receive buf {}",msg);
                            }
                        });
                    }
                }).bind(8080);
    }
}

我们可以发现,即使设置了非池化和非直接内存,直接发送的IO消息,仍然是池化+直接内存。
在这里插入图片描述
allocHandle是RecvByteBufAllocator的内部类,allocate方法,会创建最初的byteBuf。决定了byteBuf的大小以及是否为直接内存。
allocator是分配器,决定池化还是非池化
在这里插入图片描述
强制使用ioBuffer(直接内存),guess猜测大小,依据于实际数据量

在这里插入图片描述
自适应缓冲大小
在这里插入图片描述

二、简化的 RPC 框架

RPC:Remote Produce Call,远程调用请求。
基于上文聊天项目,搭建RPC请求和响应消息。

2.1 新增代码

Message

为Message类,新增

@Data
public abstract  class Message implements Serializable {
	...
	public static final int RPC_MESSAGE_TYPE_REQUEST = 101;

    public static final int RPC_MESSAGE_TYPE_RESPONSE = 102;

	static{
		...
		messageClasses.put(RPC_MESSAGE_TYPE_REQUEST,RpcRequestMessage.class);
        messageClasses.put(RPC_MESSAGE_TYPE_RESPONSE,RpcResponseMessage.class);
	}
}

RPCMessage

package com.yjx23332.netty.test.entity.vo.req;

import com.yjx23332.netty.test.entity.Message;

@Data
@ToString(callSuper = true)
public class RpcRequestMessage extends Message {
    /**
     * 调用的接口全限定名,服务端根据它找到实现
     * */
    private String interfaceName;
    /**
     * 调用接口中的方法名
     * */
    private String methodName;
    /**
     * 方法返回类型
     * */
    private Class<?> returnType;
    /**
     * 方法参数类型数组
     * */
    private Class[] parameterTypes;
    /**
     * 方法参数值数组
     * */
    private Object[] parameterValue;

    public RpcRequestMessage(int sequenceId,String interfaceName,String methodName,Class<?> returnType,Class[] parameterTypes,Object[] parameterValue){
        super.setSequenceId(sequenceId);
        this.interfaceName = interfaceName;
        this.methodName = methodName;
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.parameterValue = parameterValue;
    }



    @Override
    public int getMessageType() {
        return RPC_MESSAGE_TYPE_REQUEST;
    }
}

package com.yjx23332.netty.test.entity.vo.resp;

import com.yjx23332.netty.test.entity.Message;
import com.yjx23332.netty.test.entity.vo.req.RpcRequestMessage;
import lombok.Data;
import lombok.ToString;

@Data
@ToString(callSuper = true)
public class RpcResponseMessage extends Message {
    /**
     * 返回值
     * */
    private Object returnValue;

    /**
     * 异常值
     * */
    private Exception exceptionValue;

    @Override
    public int getMessageType() {
        return RPC_MESSAGE_TYPE_RESPONSE;
    }
}

RPCServer

package com.yjx23332.netty.test.server;


import com.yjx23332.netty.test.handler.RpcRequestMessageHandler;
import com.yjx23332.netty.test.protocol.MessageCodecSharable;
import com.yjx23332.netty.test.protocol.ProcotolFrameDecoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RPCServer {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler();
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();

        // rpc 请求消息处理
        RpcRequestMessageHandler RPC_HANDLER = new RpcRequestMessageHandler();
        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap
                    .channel(NioServerSocketChannel.class)
                    .group(boss,worker)
                    .childHandler(new ChannelInitializer<SocketChannel>(){
                        @Override
                        protected void initChannel(SocketChannel ch){
                            ch.pipeline().addLast(new ProcotolFrameDecoder());
                            ch.pipeline().addLast(LOGGING_HANDLER);
                            ch.pipeline().addLast(MESSAGE_CODEC);
                            ch.pipeline().addLast(RPC_HANDLER);
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (InterruptedException e){
            log.error("server error {}",e);
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}


RPCClient

package com.yjx23332.netty.test.client;

import com.yjx23332.netty.test.handler.RpcResponseMessageHandler;
import com.yjx23332.netty.test.protocol.MessageCodecSharable;
import com.yjx23332.netty.test.protocol.ProcotolFrameDecoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;


@Slf4j
public class RPCClient {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();

        //rpc响应消息处理器
        RpcResponseMessageHandler RPC_HANDLER = new RpcResponseMessageHandler();
        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap
                    .channel(NioSocketChannel.class)
                    .group(group)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new ProcotolFrameDecoder());
                            ch.pipeline().addLast(LOGGING_HANDLER);
                            ch.pipeline().addLast(MESSAGE_CODEC);
                            ch.pipeline().addLast(RPC_HANDLER);
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect("localhost",8080).sync();
            channelFuture.channel().writeAndFlush(new RpcRequestMessage(
                    1,
                    "com.yjx23332.netty.test.server.service.HelloService",
                    "sayHello",
                    String.class,
                    new  Class[]{String.class},
                    new Object[]{"张三"}
            ));
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.debug("client error {}",e);
        }finally {
            group.shutdownGracefully();
        }
    }
}


HelloService

这是我们远程调用的服务

package com.yjx23332.netty.test.server.service;

public interface HelloService {
    String sayHello(String userName);
}

package com.yjx23332.netty.test.server.service.Impl;

import com.yjx23332.netty.test.server.service.HelloService;

public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String userName) {
        return "你好," + userName;
    }
}

ServicesFactory

package com.yjx23332.netty.test.server.factory;

import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public abstract class ServicesFactory{
    static Properties properties;
    static Map<Class<?>,Object> map = new ConcurrentHashMap<>();
    static{
        try{
            YamlPropertiesFactoryBean yamlProFb = new YamlPropertiesFactoryBean();
            yamlProFb.setResources(new ClassPathResource("application.yml"));
            properties = yamlProFb.getObject();
            Set<String> names = properties.stringPropertyNames();
            for(String name : names){
                if(name.endsWith("Service")){
                    Class<?> interfaceClass = Class.forName(name);
                    Class<?> instanceClass = Class.forName(properties.getProperty(name));
                    map.put(interfaceClass,instanceClass.getDeclaredConstructor().newInstance());
                }
            }
        } catch (ClassNotFoundException | InstantiationError|InstantiationException|IllegalAccessException|InvocationTargetException|NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
    public static <T> T getService(Class<T> interfaceClass){ return (T) map.get(interfaceClass);}
}



server:
  port: 8080

serializer:
  algorithm: JSON

com:
  yjx23332:
    netty:
      test:
        server:
          service:
            HelloService: com.yjx23332.netty.test.server.service.Impl.HelloServiceImpl

Handler

package com.yjx23332.netty.test.handler;

import com.yjx23332.netty.test.entity.vo.req.RpcRequestMessage;
import com.yjx23332.netty.test.entity.vo.resp.RpcResponseMessage;
import com.yjx23332.netty.test.server.factory.ServicesFactory;
import com.yjx23332.netty.test.server.service.HelloService;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


@Slf4j
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage msg) throws Exception {
    	RpcResponseMessage rpcResponseMessage = new RpcResponseMessage();
    	rpcResponseMessage.setSequenceId(msg.getSequenceId());
        try {
            HelloService helloService = (HelloService) ServicesFactory.getService(Class.forName(msg.getInterfaceName()));
            Method method = helloService.getClass().getMethod(msg.getMethodName(),msg.getParameterTypes());
            Object invoke = method.invoke(helloService,msg.getParameterValue());
            rpcResponseMessage.setReturnValue(invoke);
        }catch (Exception e) {
            log.debug("{}", e);
            rpcResponseMessage.setExceptionValue(new Exception("远程调用出错" + e.getCause().getMessage()));
        }finally {
            ctx.writeAndFlush(rpcResponseMessage);
        }
    }
    /**
     * 测试用
     * */
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        RpcRequestMessage rpcRequestMessage = new RpcRequestMessage(
                1,
                "com.yjx23332.netty.test.server.service.HelloService",
                "sayHello",
                String.class,
                new  Class[]{String.class},
                new Object[]{"张三"}
        );
        HelloService helloService = (HelloService)
                ServicesFactory.getService(Class.forName(rpcRequestMessage.getInterfaceName()));
        Method method = helloService.getClass().getMethod(rpcRequestMessage.getMethodName(),rpcRequestMessage.getParameterTypes());
        Object invoke = method.invoke(helloService,rpcRequestMessage.getParameterValue());
        System.out.println(invoke);
    }
}

package com.yjx23332.netty.test.handler;

import com.yjx23332.netty.test.entity.vo.resp.RpcResponseMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
        log.debug("{}",msg);
    }
}

修改序列化方法

我们这个地方传的是className,而我们原来的代码需要的是类的反射。因此我们要么设置一个转换器,让它自动处理class的名字与反射内容之间的转换。要么 ,就使用支持类名模式。
笔者图方便,使用了支持类名模式。

package com.yjx23332.netty.test.entity;

import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONReader;

import java.io.*;
import java.nio.charset.StandardCharsets;

public interface Serializer {
    //反序列化算法
    <T> T deserialize(Class<T> clazz,byte[] bytes);
    //序列化算法
    <T> byte[] serialize(T object);

    enum Algorithm implements  Serializer{
        Java{
            @Override
            public <T> T deserialize(Class<T> clazz, byte[] bytes) {
                try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));){
                    return (T) objectInputStream.readObject();
                } catch (IOException | ClassNotFoundException e) {
                    throw new RuntimeException("反序列化失败",e);
                }
            }

            @Override
            public <T> byte[] serialize(T object) {
                try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                     ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);){
                    objectOutputStream.writeObject(object);
                    byte[] bytes = byteArrayOutputStream.toByteArray();
                    return bytes;
                } catch (IOException e) {
                    throw new RuntimeException("序列化失败",e);
                }
            }
        },
        JSON{
            @Override
            public <T> T deserialize(Class<T> clazz, byte[] bytes) {
                String json = new String(bytes,StandardCharsets.UTF_8);
                return JSONObject.parseObject(json,clazz,JSONReader.Feature.SupportClassForName);
            }

            @Override
            public <T> byte[] serialize(T object) {
                String json = JSONObject.toJSONString(object);
                return json.getBytes(StandardCharsets.UTF_8);
            }
        }
    }
}

在这里插入图片描述

2.2 让它更符合使用习惯

改造RPCResponseHandler

package com.yjx23332.netty.test.handler;

import com.yjx23332.netty.test.entity.vo.resp.RpcResponseMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.Promise;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Data
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
    /***
     * Integer - 序列号
     * Promise - 接收结果
     */
    public static final Map<Integer, Promise<Object>> PROMISE_MAP = new ConcurrentHashMap<>();
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
        log.debug("{}",msg);
        Promise<Object> promise = PROMISE_MAP.remove(msg.getSequenceId());
        if(promise != null){
            Object returnValue = msg.getReturnValue();
            Exception exceptionValue = msg.getExceptionValue();
            if(exceptionValue != null){
                promise.setFailure(exceptionValue);
            }
            else{
                promise.setSuccess(returnValue);
            }
        }
    }
}

改造客户端

package com.yjx23332.netty.test.client;

import com.yjx23332.netty.test.entity.vo.req.RpcRequestMessage;
import com.yjx23332.netty.test.handler.RpcResponseMessageHandler;
import com.yjx23332.netty.test.protocol.MessageCodecSharable;
import com.yjx23332.netty.test.protocol.ProcotolFrameDecoder;
import com.yjx23332.netty.test.server.service.HelloService;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Proxy;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;


@Slf4j
public class RPCClientManager {
    private static volatile Channel channel = null;
    /**
     * 计数器
     * */
    private  static  final AtomicInteger ID = new AtomicInteger();
    /**
     * 锁
     * */
    private static final Object LOCK = new Object();
    /**
     * 初始化channel
     * */
    private static void initChannel(){
        NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();

        //rpc响应消息处理器
        RpcResponseMessageHandler RPC_HANDLER = new RpcResponseMessageHandler();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                .channel(NioSocketChannel.class)
                .group(group)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ProcotolFrameDecoder());
                        ch.pipeline().addLast(LOGGING_HANDLER);
                        ch.pipeline().addLast(MESSAGE_CODEC);
                        ch.pipeline().addLast(RPC_HANDLER);
                    }
                });
        try{
            channel = bootstrap.connect("localhost",8080).sync().channel();
            channel.closeFuture().addListener(promise->{
                group.shutdownGracefully();
            });
        } catch (InterruptedException e) {
            log.debug("client error {}",e);
        }
    }

     public static Channel getChannel(){
        if(channel == null){
            synchronized (LOCK){
                if(channel == null){
                    initChannel();
                }
            }
        }
        return channel;
     }

    /**
     * 代理类,将方法调用转换为消息
     * 在这里我们就不调用原方法,而是直接替换为发送消息的逻辑
     * 然后,我们在后面等待结果即可
     * */
    public static <T> T getProxyService(Class<T> serviceClass){
        ClassLoader loader = serviceClass.getClassLoader();
        Class<?>[] interfaces = new Class[]{serviceClass};
        int sequenceId = ID.getAndAdd(1);
        Object o = Proxy.newProxyInstance(loader,interfaces,(proxy,method,args)->{
            //1. 将方法的调用,转换成消息对象
            RpcRequestMessage rpcRequestMessage = new RpcRequestMessage(
                    sequenceId,
                    serviceClass.getName(),
                    method.getName(),
                    method.getReturnType(),
                    method.getParameterTypes(),
                    args
            );

            //2. 发送消息对象
            getChannel().writeAndFlush(rpcRequestMessage);

            //3. 准备一个空 Promise 对象 ,来接受结果
            //4. 用 eventLoop 线程来异步接收结果,当我们addListener时,就由它来接收
            //此处我们时同步接收
            DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
            RpcResponseMessageHandler.PROMISE_MAP.put(sequenceId,promise);
            promise.await();
            if(promise.isSuccess()){
                return promise.getNow();
            }
            else{
                throw new RuntimeException(promise.cause());
            }
        });
        return (T) o;
    }

   public static void main(String[] args) {
        HelloService service = getProxyService(HelloService.class);
        System.out.println(service.sayHello("zhangsan"));
        System.out.println(service.sayHello("lisi"));
    }
}

笔记中的工厂和代理都是自己实现,而不是用Spring的方式,实际开发中,可以用SpringBoot来帮助我们快速开发。

参考文献

[1]黑马程序员Netty全套教程

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用Netty编写一个RPC框架可以分为以下几个步骤: 1. 定义通信协议:首先需要定义客户端和服务端之间的通信协议,包括消息的格式、编码方式、数据传输方式等。可以选择使用自定义协议或者现有的协议,如HTTP、TCP等。 2. 设计服务接口:定义服务接口,包括方法名、参数列表、返回值等。可以使用Java接口或者其他IDL(接口描述语言)工具来定义服务接口。 3. 实现服务端:使用Netty构建服务端,监听指定的端口,接收客户端的请求。当有请求到达时,根据协议解析请求数据,并根据请求调用相应的业务逻辑处理方法,最后将结果封装成响应数据发送给客户端。 4. 实现客户端:使用Netty构建客户端,连接到服务端的IP和端口。当需要调用远程服务时,根据协议封装请求数据,并发送给服务端。然后等待服务端返回响应数据,解析响应数据并返回给调用方。 5. 进行序列化和反序列化:在客户端和服务端之间进行数据传输时,需要对请求参数和响应结果进行序列化和反序列化。可以使用现有的序列化框架,如Protobuf、JSON等。 6. 管理连接和负载均衡:在实际应用中,可能存在多个服务提供者和多个消费者,需要管理客户端和服务端之间的连接,以及实现负载均衡策略,选择合适的服务提供者进行调用。 7. 异常处理和容错机制:在RPC框架中,需要考虑异常处理和容错机制。当服务端出现异常或不可用时,需要进行相应的处理,如重试、降级、熔断等。 这些步骤只是一个大致的框架,具体的实现细节和代码编写会根据具体需求而有所不同。通过以上步骤,你可以使用Netty构建一个简单RPC框架

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值