基础逻辑
因为socket通信是阻塞通信,会导致线程资源浪费,因此采用同步NIO框架netty来代替原本的BIOsocket通信。二者之间的原理对比可以参考
其实区别就在于多个客户端同时向服务端发出请求时,服务端是开启多线程处理(宕机风险BIO)还是只开启一个线程处理(BIO)。
因此本次demo的改动就是用Netty通信代替socket通信,同时创建自己的序列化协议以及创建一个通用的序列化接口(可以后续采用多种序列化方式)来代替之前的Serializable接口。
知识点
- Netty:
ChannelHandlerContext:参考博客
.sync(); 返回一个ChannelFuture对象,参考博客
- 拆包粘包(了解概念)
- 缓存
- 写入和刷新方法
- 心跳机制:在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。
- json类, jackson序列化
实现步骤
首先再Maven工程下的pom.xml中添加依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.48.Final</version>
<scope>compile</scope>
</dependency>
- 因为换成了netty通信,所以要创建抽象的客户端接口RpcClient和服务端接口RpcServer,使得采用不同网络通信方式就是对这两个接口的不同实现类,例如NettyRpcServer和SocketRpcServer。
public interface RpcServer {
public void connect(int port);
}
public interface RpcClient {
public Object sendRequest(RpcClientRequest rpcClientRequest);
}
-
接下来是对服务端进行接口实现,但是因为对netty’服务端配置时,要对序列化后的二进制流进行传输和处理,所以要先定义序列化协议、序列化统用接口、以及反序列化后的信息处理。
自定义协议:定义序列化协议就像是can通讯协议一样,参考博客来定义,首先是协议的id,然后是传输的信息的类型,然后是序列化方式的类型编码、然后是传输信息的长度(防止粘包),上述四个都采用四个字节,也就是可以用writeInt方法直接服务,最后是传输的信息。
因为后续会采用jackson和kyro两种序列化方式,所以需要抽象出一个统用的序列化接口CommonSerializer,采用自定义编码的方式来识别序列化方式,接口的方法分别是序列化、反序列化、获取当前序列化方式的编码、根据编码选择序列化方式。
public interface CommonSerializer { // 序列化类型的编码单独列出,获取编码 int getCode(); // 序列化方法 byte[] serialize(Object obj); //反序列化 按照传输包的类型进行读取传输字节数组信息 Object deserialize(byte[] bytes,Class<?> clazz); // 通过编码获取序列化类 静态方法可以不实例化对象,直接引用,但是需要定义 static CommonSerializer getByCode(int code){ switch (code){ case 1: return new JsonSerializer(); default: return null; } }; }
其中编码是协议中包含的信息,自定义一个枚举数组SerializableCode来决定。
@AllArgsConstructor // 创建一个序列化类型的枚举,后续会加上kyro序列化 public enum SerializableCode { JSON_Serializable(1); @Getter private final int code; }
接下来是创建一个jackson序列化类JsonSerializer,对上述接口进行实现,需要注意的是jacson方式会导致传递参数的类型出错(因为jackson是转换成json字符串而不是字节数组),因此需要再RequestHandle方法中重新判断矫正(按照理想类型写、读)。但是首先要在pom.xml中导入依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.11.0</version> </dependency>
public class JsonSerializer implements CommonSerializer{ private static final Logger logger = LoggerFactory.getLogger(JsonSerializer.class); // 使用jackson序列器,其中ObjectMapper类是java对象和json结构之间转换的json工具类 private ObjectMapper objectMapper = new ObjectMapper(); @Override public int getCode() { return SerializableCode.valueOf("JSON_Serializable").getCode(); } // 序列化 @Override public byte[] serialize(Object obj) { try { return objectMapper.writeValueAsBytes(obj); } catch (JsonProcessingException e) { logger.error("序列化过程出错:{}",e.getMessage()); e.printStackTrace(); return null; } } // 反序列化 @Override public Object deserialize(byte[] bytes, Class<?> clazz) { try { Object read = objectMapper.readValue(bytes,clazz); if (read instanceof RpcClientRequest){ logger.info("服务端可以反序列化得到一个request对象"); Object obj = RequestHandle(read); // 这里不要忘了return!!!! return obj; }else if(read instanceof Response){ logger.info("客户端可以反序列化得到一个Response对象"); return (Response)read; } } catch (IOException e) { logger.error("反序列化中出错:{}",e.getMessage()); e.printStackTrace(); } return null; } /* 处理反序列化得到的信息,因为Json反序列化过程中,不会保障原先传递的参数还是之前的类型,因此需要验证,如果 不是就重新把参数信息转换成流,按照理想的参数类型重新写入。(参数类型是Class对象在反序列化中不会被修改)*/ public Object RequestHandle(Object read){ RpcClientRequest request = (RpcClientRequest)read; for (int i = 0; i < request.getParamTypes().length; i++ ){ // 理想参数类型 Class<?> paramType = request.getParamTypes()[i]; // 反序列化后的参数类型和理想的参数类型不匹配 if (!paramType.isAssignableFrom(request.getParameters()[i].getClass())){ try { // 重写、读参数 byte[] paramByte = objectMapper.writeValueAsBytes(request.getParameters()[i]); request.getParameters()[i] = objectMapper.readValue(paramByte,paramType); } catch ( IOException e) { logger.error("反序列化中参数重写出错:{}",e.getMessage()); e.printStackTrace(); } } } return request; } }
这里注意几个点:ObjectMapper类是java对象和json结构之间转换的json工具类,进行读取和写入;其次是反序列化再客户端和服务端都需要用到,因此需要判断反序列化得到信息的类型是请求还是响应;最后的注意点是及时return信息,尤其是trycatch代码块中,要不然会报空指针异常(别问我是怎么知道的)。。
-
定义好序列化方式,就进行编码器和解码器类的定义,通过ByteBuf类来写入读取。
编码器:定义一个通用编码器类CommonEncoder,继承netty自带的MessageToByteEncoder类。其实就是根据定义的协议进行数据写入,注意writeInt一次写入四个字节。实例化需要传输具体的序列化方法。
public class CommonEncoder extends MessageToByteEncoder { /* 协议包括编码协议id、传递数据包类型、序列化类型、信息长度以及传输信息 */ private static final Logger logger = LoggerFactory.getLogger(CommonEncoder.class); private static final int id = 0x00; private final CommonSerializer serializer; public CommonEncoder(CommonSerializer serializer) { this.serializer = serializer; } @Override protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception { // id byteBuf.writeInt(id); // 传输包类型 if(o instanceof RpcClientRequest){ byteBuf.writeInt(PackageType.RpcClientRequest.getCode()); }else if(o instanceof Response){ byteBuf.writeInt(PackageType.RpcServerResponse.getCode()); }else { byteBuf.writeInt(0); logger.error("传递的信息类型不在范围内"); } // 序列化方式类型 byteBuf.writeInt(serializer.getCode()); // 信息长度 byte[] msgBytes = serializer.serialize(o); byteBuf.writeInt(msgBytes.length); // 信息 byteBuf.writeBytes(msgBytes); } }
注意这里传输包的类型也是自定义一个枚举,包含客户端请求以及服务端的响应信息。
@AllArgsConstructor public enum PackageType { RpcClientRequest(1), RpcServerResponse(2); @Getter private final int code; }
解码器:继承ByteToMessageDecoder类,根据定义协议进行依次读取,需要注意的是注意当前读取次数和当前索引,确定当前读取数据,然后进行解码,解码之后的数据对象需要添加到 list 类参数中。
public class CommonDecoder extends ByteToMessageDecoder { private static final Logger logger = LoggerFactory.getLogger(CommonDecoder.class); // 这里的id要和编码器的id相同,协议才能对的上 private static final int id = 0x00; @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception { // id 解码 if (in.readInt() != id){ logger.error("协议的id没对上,未识别的协议包"); throw new RpcException(RpcError.Unknown_id); }; // 传输包类型解码,这里注意用in.readInt() == 判断两次相当于读取两次,应该预定义变量 int packageType = in.readInt(); Class<?> packageTypeClass; if (packageType == PackageType.RpcClientRequest.getCode() ){ packageTypeClass = RpcClientRequest.class; }else if(packageType == PackageType.RpcServerResponse.getCode()){ packageTypeClass = Response.class; }else{ logger.error("未识别的传输包类型"); throw new RpcException(RpcError.Unknown_packageType); } // 序列化解码 int serializableCode = in.readInt(); CommonSerializer serializer = CommonSerializer.getByCode(serializableCode); if (serializer == null){ logger.error("未识别的序列化方法"); throw new RpcException(RpcError.Unknown_serializable); } // 信息长度解码 int length = in.readInt(); // 信息解码 byte[] msg = new byte[length]; in.readBytes(msg); //读取到msg中 Object obj = serializer.deserialize(msg,packageTypeClass); if (obj == null){ logger.error("反序列化出错成为null"); } // !!!解码之后的数据对象需要添加到 list 对象里面 list.add(obj); } }
需要对demo2中的RpcError枚举进行扩充。
-
定义一个服务端的处理信息类NettyRpcServerHandler,注意父类是ChannelInboundHandlerAdapter。
和demo2中的处理逻辑类似,都是从注册表中取出服务,具体的反射调用流程再ThreadHandle中进行。因此需要工作现场和注册服务表两个属性,static代码块进行初始化,重写父类中的channelRead方法来处理channel中解码后传递的信息,exceptionCaught是异常处理。
public class NettyRpcServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(NettyRpcServerHandler.class);
private static ThreadHandle threadHandle;
private static ServiceRegistry serviceRegistry;
// 初始化工作线程和注册服务表
static{
threadHandle = new ThreadHandle();
serviceRegistry = new DefaultServiceRegistry();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try{
RpcClientRequest request = (RpcClientRequest)msg;
logger.info("服务器收到请求信息");
String interfaceName = request.getInterfacename();
Object service = serviceRegistry.getService(interfaceName);
Object result = threadHandle.handle(service,request);
// 写出数据
ChannelFuture future = ctx.writeAndFlush(result);
// 异步机制,添加监听确认消息发送完成后再关闭连接
future.addListener(ChannelFutureListener.CLOSE);
}finally {
// 自动释放msg对象,防止对象池溢出,必须执行,所以放在finally中。
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("服务端处理请求出错",cause);
cause.getMessage();
// 关闭通道
ctx.close();
}
}
注意其中netty中的操作是异步的,ChannelFuture future=ch.writeAndFlush(message)也是异步的,一个ChannelFuture对象代表尚未发生的IO操作,该方法只是把发送消息加入了任务队列,这时直接关闭连接会导致问题。所以我们需要在消息发送完毕后在去关闭连接。 future.addListener(ChannelFutureListener.CLOSE);就是添加监听,当future确实完成之后关闭后发出回调信息。
除此之外无论try成功与否都要及时释放对象,防止对象池内存溢出。所以在finally中加ReferenceCountUtil.release(msg);可以参考博客
ThreadHandle类和demo2中相同,逻辑都是接收请求,反射调用,赋值给response类对象。
public class ThreadHandle {
public Object handle(Object service, RpcClientRequest request) throws InvocationTargetException, IllegalAccessException {
private static final Logger logger = LoggerFactory.getLogger(ThreadHandle.class);
Method method;
Object result;
try{
method = service.getClass().getMethod(request.getMethodname(),request.getParamTypes());
}catch (Exception e){
Response response = new Response().fail();
return new RpcException(RpcError.Method_not_found);
}
// 参数是方法作用对象(服务端的实现类服务),以及参数
logger.info("服务器找到方法:"+method.getName());
try{
result = method.invoke(service,request.getParameters());
}catch (Exception e){
Response response = new Response().fail();
return new RpcException(RpcError.Method_not_used);
}
logger.info("服务器成功调用方法:"+method.getName());
//设置响应信息
Response response = new Response().success(result);
return response;
}
}
- 定义好上述服务端中需要的解码、编码、信息处理等类,就可以配置netty服务器,定义一个实现接口RpcServer的NettyRpcServer类。
public class NettyRpcServer implements RpcServer {
private static final Logger logger = LoggerFactory.getLogger(NettyRpcServer.class);
@Override
public void connect(int port) {
// 只有一个EventLoop,负责连接,如果连接成功,把channel转给workerGroup,EventLoopGroup相当于线程池
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 多个EventLoop,选择处理多个channel。
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 创建一个netty服务器
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup) //两个线程池作为参数
.channel(NioServerSocketChannel.class) //通道类型 定为非阻塞NIO
.handler(new LoggingHandler(LogLevel.INFO))
//表示请求线程全满时,系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数
.option(ChannelOption.SO_BACKLOG,1024)
// 是否开启 TCP 底层心跳机制
.childOption(ChannelOption.SO_KEEPALIVE,true)
// TCP默认开启了 Nagle 算法,该算法的作用是尽可能的发送大数据快,减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。
.childOption(ChannelOption.TCP_NODELAY,true)
// 增加对输入流的处理逻辑 !!
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline channelPipeline = socketChannel.pipeline();
channelPipeline.addLast(new CommonEncoder(new JsonSerializer()));
channelPipeline.addLast(new CommonDecoder());
channelPipeline.addLast(new NettyRpcServerHandler());
}
});
try {
// 监听客户端请求信息
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
netty服务端类是ServerBootstrap,创建对象后对其配置,传入参数分别是bossGroup以及workerGroup,对应连接以及处理信息线程。
通道为阻塞NIO通道,打印日志级别为info,option中的配置可以参考博客。Nagle算法是为了尽可能发送大的数据块,TCP_NODELAY就是用于启用或关于Nagle算法。如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false。
最重要的就是配置处理逻辑,对当前通道的channelpipeline配置2、3中定义的编码器、解码器、请求处理类。使用bind方法监听指定端口的请求,channelFuture.channel().closeFuture().sync();的作用:个人理解是现有两个线程,一个是服务器监听线程,一个是信息处理的main线程,异步同时进行,但是信息处理后关闭服务器需要确定通道断开,所以该语句就是暂时令main线程处于wait状态,监听线程监听到通道关闭后,main线程继续在进入finally语句块关闭两个EventLoopGroup,可以参考博客
最后关闭两个EventLoopGroup。
-
客户端类似,首先定义一个处理服务端发回信息的NettyRpcClientHandler类,继承ChannelInboundHandlerAdapter。含有信息处理和异常处理两个方法。
public class NettyRpcClientHandler extends ChannelInboundHandlerAdapter { private static final Logger logger = LoggerFactory.getLogger(NettyRpcClientHandler.class); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try{ logger.info(String.format("客户端收到服务端返回的响应信息:%s",msg.toString()) ); AttributeKey<Response> key = AttributeKey.valueOf("rpcResponse"); ctx.channel().attr(key).set((Response) msg); // 关闭通道 ctx.channel().close(); }finally { // 释放对象 ReferenceCountUtil.release(msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.error("客户端处理响应信息时发生错误:",cause); ctx.close(); } }
关键语句ctx.channel().attr(key).set((Response) msg);就是在通道中定义attr数组,使用返回的响应进行赋值。注意需要关闭通道以及释放对象,防止内存溢出。
-
创建Netty客户端NettyRpcClient类,需要配置通道,和服务端差不多,但是需要发送请求的sendRequest方法(逻辑和demo2一样)。
@AllArgsConstructor
public class NettyRpcClient implements RpcClient {
private static final Logger logger = LoggerFactory.getLogger(NettyRpcClient.class);
private String host;
private int port;
private static final Bootstrap bootstrap;
// 初始化过程,定义客户端的连接
static{
//只有一个非阻塞处理线程
EventLoopGroup eventGroup = new NioEventLoopGroup();
// 客户端定义及配置
bootstrap = new Bootstrap();
bootstrap.group(eventGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
// 对SocketChannel类型通道进行 逻辑处理初始化
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new CommonDecoder())
.addLast(new CommonEncoder(new JsonSerializer()))
.addLast(new NettyRpcClientHandler());
}
});
}
@Override
public Object sendRequest(RpcClientRequest rpcClientRequest) {
try {
// 客户端连接
ChannelFuture future = bootstrap.connect(host,port).sync();
logger.info("客户端连接到服务器{}:{}",host,port);
// 看未来客户端的通道情况
Channel channel = future.channel();
if (channel != null){
channel.writeAndFlush(rpcClientRequest)
// 监听发送过程
.addListener(future1 ->{
if(future1.isSuccess()){
logger.info(String.format("客户端发送请求:%s",rpcClientRequest.toString()));
}else{
logger.error("客户端发送信息失败",future1.cause());
}
} );
channel.closeFuture().sync();
AttributeKey<Response> key = AttributeKey.valueOf("rpcResponse");
Response response = channel.attr(key).get();
return response;
}
} catch (InterruptedException e) {
logger.error("客户端发送请求过程出错",e);
}
return null;
}
}
注意通道是NioSocketChannel.class,sendRequest方法包括发送请求以及返回响应(从attr中取出),和服务端一样需要增加一个监听器满足异步回调机制,channel.closeFuture().sync();满足发送完成接收关闭事件之前不中断连接。
-
测试
首先是服务端,和demo2逻辑一样(因为都是扩展的同一个接口),都是先创建服务对象,注册服务,再创建netty服务器对象,调用connect方法
public class TestServer { public static void main(String[] args) { Remote_game_interface Tx_net = new Player_info_find(); // 接口实现类,也就是服务 ServiceRegistry serviceRegistry = new DefaultServiceRegistry(); serviceRegistry.register(Tx_net); NettyRpcServer rpcServer = new NettyRpcServer(); //服务器 rpcServer.connect(9000); } }
客户端首先需要修正一下动态代理类的代码,测试代码中需要创建一个新的netty客户端,再创建一个动态代理类,传入动态代理的接口Class对象,最后调用服务端处的方法。
public class TestClient { public static void main(String[] args) { NettyRpcClient client = new NettyRpcClient("Localhost",9000); RpcClientProxy rpcClientProxy = new RpcClientProxy(client); Remote_game_interface client_proxy = rpcClientProxy.getProxy(Remote_game_interface.class); // 创建要查询的玩家 就是客户端向服务器传输的信息 Player player = new Player(2508,"倪粑粑"); // 使用客户端的代理调用服务器端的方法,信息由player提供 String result = client_proxy.playerinfo(player); // 输出官网回复结果 System.out.println("官网回复为:"+result); } }
结果太长就贴一下服务端和客户端的最后的info
查一查id为2508名字为倪粑粑的玩家,查封 INFO [nioEventLoopGroup-3-1] - 服务器成功调用方法:playerinfo
INFO [main] - 响应成功,服务器发回响应数据。。。 官网回复为:告诉你好消息:官网查封该外挂玩家
bug以及解决
-
slf4j日志输出的配置问题
使用slf4j来代替System.out.println();打印,首先是再pom.xml中增加依赖
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.28</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.28</version> </dependency>
然后再使用过程中发现报错:WARN No appenders could be found for logger。参考博客 ,新建resource目录(Maven结构),新建log4j.properties,代码为
log4j.rootLogger=INFO, stdout # Console output... log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
注意这里log4j.rootLogger=INFO, stdout,如果是WARN等级大于INFO,所以控制台显示不了info信息。
控制台如果没有显示info信息,在resource目录下新建logback.xml
<?xml version="1.0" encoding="UTF-8" ?> <configuration scan="true" scanPeriod="3 seconds"> <!--设置日志输出为控制台--> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{userId}] [%X{requestId}] %logger - %msg%n</pattern> </encoder> </appender> <!--设置日志输出为文件--> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <File>logFile.log</File> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>logFile.%d{yyyy-MM-dd_HH-mm}.log.zip</FileNamePattern> </rollingPolicy> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss,SSS} [%thread] %-5level %logger{32} - %msg%n</Pattern> </layout> </appender> <root> <level value="DEBUG"/> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> </configuration>
-
lombok注解的使用,不用再程序中自己写构造器和get方法,可以参考博客,本例中主要使用的就是无参构造@NoArgsConstructor、有参构造@AllArgsConstructor(类前)、参数get方法@Getter(成员变量前)
需要在idea中file setting plugin 中下载lombok插件,安装完成后再pom.xml导入依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency>
-
报错: jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of
jackson反序列化必须要存在一个无参构造函数,因此在本例中的Player类,也就是传入服务的参数类,需要加上一个无参构造@NoArgsConstructor。(之前为了传递信息只有一个全参构造@AllArgsConstructor)