这是我参与8月更文挑战的第22天,活动详情查看: 8月更文挑战
本文使用源码地址:simple-rpc
服务端代码详解
为了篇幅问题,可能会做代码上省略,并且删除了注释,建议现在源码查看。
代码如下:
public class NettyServer {
private static final NettyServer NETTY_SERVER = new NettyServer();
private EventLoopGroup bossGroup;
private EventLoopGroup workGroup;
private static final SerializeType SERIALIZE_TYPE = SerializeType.getByType(SimpleRpcPropertiesUtil.getSerializeType());
public void startServer(int port) {
synchronized (NettyServer.class) {
if (bossGroup != null || workGroup != null) {
log.debug("netty server is already start");
} else {
bossGroup = new NioEventLoopGroup(1);
workGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline().addLast(new NettyDecoderHandler(Request.class, SERIALIZE_TYPE));
socketChannel.pipeline().addLast(new NettyEncoderHandler(SERIALIZE_TYPE));
socketChannel.pipeline().addLast(new NettyServerBizHandler());
}
});
try {
bootstrap.bind(port).sync().channel();
log.info("NettyServer startServer start now!!!");
} catch (Exception e) {
log.error("NettyServer startServer error", e);
throw new SRpcException("NettyServer startServer error", e);
}
}
}
}
public static NettyServer getInstance() {
return NETTY_SERVER;
}
private NettyServer() {
}
}
首先SERIALIZE_TYPE是配置的服务端序列化方式,对应于前文中说到的序列化引擎。
bossGroup用于接受新连接线程,主要负责创建新连接,workGroup负责读取数据的线程,主要用于读取数据以及业务逻辑处理。
ChannelOption.SO_BACKLOG表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数。
childOption设置了两个参数:
ChannelOption.SO_KEEPALIVE:表示是否开启TCP底层心跳机制,true为开启ChannelOption.TCP_NODELAY:表示是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。
LoggingHandler设置日志级别,打印Netty的运行日志,方便问题排查。
childHandler完成的就是主要的业务数据逻辑处理了,首先是添加继承了ByteToMessageDecoder的解码器NettyDecoderHandler,实现了encode()方法。代码如下:
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
int readableLength = in.readableBytes();
if (readableLength < 4) {
return;
}
in.markReaderIndex();
int dataLength = in.readInt();
if (dataLength < 0) {
ctx.close();
}
if (readableLength < dataLength) {
in.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
in.readBytes(data);
Object obj = SerializerEngine.deserialize(data, genericClass, serializeType.getSerializeType());
out.add(obj);
}
接着添加继承了MessageToByteEncoder的编码器NettyEncoderHandler,实现了encode()方法。代码如下:
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, ByteBuf out) {
byte[] bytes = SerializerEngine.serialize(msg, serializeType.getSerializeType());
out.writeInt(bytes.length);
out.writeBytes(bytes);
}
在这里使用了一种简单的方式来解决Netty的粘包和半包问题,就是在报文的消息头中添加消息体的字节数组长度,如果当前可读长度小于目标长度,则返回,直到可以获取到的字节数组长度等于目标长度才继续执行。
最后添加的就是继承了SimpleChannelInboundHandler<Request>的业务处理器NettyServerBizHandler,代码如下:
public class NettyServerBizHandler extends SimpleChannelInboundHandler<Request> {
private static final Map<String, Semaphore> SERVICE_KEY_SEMAPHORE_MAP = Maps.newConcurrentMap();
@Override
protected void channelRead0(ChannelHandlerContext ctx, Request request) {
if (ctx.channel().isWritable()) {
Provider provider = this.getLocalCacheWithReq(request);
if (provider == null) {
log.error("can't find provider for request, request={}", request);
throw new SRpcException("illegal request, can't find provider for request, request={}" + request);
} else {
Provider requestProvider = request.getProvider();
Semaphore semaphore = this.initSemaphore(requestProvider.getServiceItf().getName(), requestProvider.getWorkerThreads());
Object result = this.invokeMethod(provider, request, semaphore);
Response response = Response.builder().invokeTimeout(request.getInvokeTimeout())
.result(result)
.uniqueKey(request.getUniqueKey())
.build();
ctx.writeAndFlush(response);
}
} else {
log.error("channel closed!");
}
}
// ...略
private Object invokeMethod(Provider provider, Request request, Semaphore semaphore) {
Object serviceObject = provider.getServiceObject();
Method serviceMethod = provider.getServiceMethod();
Object result = null;
boolean acquire = false;
try {
acquire = semaphore.tryAcquire(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
if (acquire) {
result = serviceMethod.invoke(serviceObject, request.getArgs());
}
} catch (Exception e) {
result = e;
log.error("NettyServerBizHandler invokeMethod error, provider={}, request={}", provider, request, e);
} finally {
if (acquire) {
semaphore.release();
}
}
return result;
}
// ....略
}
SERVICE_KEY_SEMAPHORE_MAP:根据前文中提到的服务端线程数workerThreads参数来初始化流控基础设施。key:服务接口权限类名,value:Semaphore(关于Semaphore如何完成限流可以参考之前并发编程中的文章:允许多个线程同时访问:信号量(Semaphore))。
代码执行逻辑如下:
- 我们通过
getLocalCacheWithReq()方法获取服务列表,根据request中的请求信息找到服务提供者。 - 在限流器
Semaphore的影响下发起反射调用:semaphore.tryAcquire()获取许可,获取成功执行反射调用。semaphore.release()方法释放许可。 - 通过
ctx.writeAndFlush()方法返回调用结果。
小结
Netty服务端接收客户端发起的请求字节数组,然后通过解码器NettyDecoderHandler将字节数组解码为对应的Java请求对象。然后通过解码得到的Java对象确定服务提供者接口和方法,然后通过使用Java反射来发起调用。为了控制服务端服务能力,使用Semaphore做了流控处理。
至此,我们完成了Netty服务端启动、数据序列化编码Handler、数据反序列化Handler、服务端服务调用及调用结果返回功能的实现。Netty服务端代码相对于客户端代码要简单很多,下篇我们将一起看下客户端端代码实现。