前面小编两篇文章重点讲了dubbo协议以及registry协议,协议主要作用就是服务暴露引用,当我们的服务以及暴露和引用并且放到注册中心上去了,那么接下来就要发起远程的通讯。今天小编带来的就是远程通讯。进入正题
远程通信是客户端通过网络传输至服务端,并接收服务端的结果。其需要解决问题包括、连接的建立、数据流的编解码、IO线程的协议等。
假如咱们需要实现一个客户端与服务端的远程通信,那具体应该怎么做,有哪些子方案呢,小编这边罗列了一些方案,仅供参考。
那dubbo是如何解决远程通讯的?
Dubbo使用netty框架解决了大部分工作(IO模型,连接方式,线程调度中的IO线程以及数据交换中的粘包拆包),业务以及线程池调度、报文设计、编解码、序列化是Dubbo自己实现的。
小编使用代码给大家做个示例来解释上面的过程图。
public class NettyTransporterTest {
private final static String URL_TEXT = "dubbo://127.0.0.1:20880";
@Test
public void openServer() throws Exception {
NettyServer nettyServer = new NettyServer(URL.valueOf(URL_TEXT), new ChannelHandlerAdapter() {
@Override
public void received(Channel channel, Object message) throws RemotingException {
System.out.println("接受到数据:" + message);
if(Objects.nonNull(message)){
channel.send("我接受到你的数据了");
}
}
});
System.in.read();
}
@Test
public void openClient() throws Exception {
NettyClient nettyClient = new NettyClient(URL.valueOf(URL_TEXT), new ChannelHandlerAdapter() {
@Override
public void received(Channel channel, Object message) throws RemotingException {
System.out.println("接受到数据:" + message);
}
});
nettyClient.send("hello world");
//因为发送消息是异步的会立马结束,这边不让他结束
System.in.read();
}
}
两边的测试结果
//NettyServer端
接受到数据:hello world
//NettyClient端
接受到数据:我接受到你的数据了
注意
这边传输的时候使用string类型没问题,但如果是java对象的时候则编解码。需要配置dubbo的编解码器。
基本了解完通信方案后紧接着小编带大家了解一下dubbo的线程协作模型。
之前小编在Dubbo调用及容错机制详解中讲到过一些线程,如调用线程,IO线程,业务线程。这边小编通过debug将上面示例代码的客户端与服务端的线程截图一一说明。
服务端:
客户端:(业务线程为缓存线程,默认60s)
线程协作过程
上半部分为客户端,下半部分为服务端
虽然小编寥寥数语就差不多讲完了他的一个协作流程,但是里面细节还是很多的,尤其这边会涉及到netty的知识面,下面小编带大家稍作调试,希望小编能说清楚
小编从调用端开始解析,只挑重点代码(看注释)
首先是new NettyClient(final URL url, final ChannelHandler handler),NettyClient的构造方法。重要的是doOpen和doConnect方法
doOpen方法主要做初始化工作,封装所需要的一系列处理器和参数,doConnect主要是netty的连接(这边主要是为netty工作小编略过了)
@Override
protected void doOpen() throws Throwable {
//处理消息的处理器
final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
//都是netty相关内容 后面小编写netty文章的时候详细说明
bootstrap = new Bootstrap();
bootstrap.group(NIO_EVENT_LOOP_GROUP)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
//.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout())
.channel(socketChannelClass());
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(3000, getConnectTimeout()));
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
int heartbeatInterval = UrlUtils.getHeartbeat(getUrl());
if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
ch.pipeline().addLast("negotiation", SslHandlerInitializer.sslClientHandler(getUrl(), nettyClientHandler));
}
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
//添加编码解码器,心跳,以及用来处理消息的handler
ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("client-idle-handler", new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS))
.addLast("handler", nettyClientHandler);
String socksProxyHost = ConfigUtils.getProperty(SOCKS_PROXY_HOST);
if(socksProxyHost != null) {
int socksProxyPort = Integer.parseInt(ConfigUtils.getProperty(SOCKS_PROXY_PORT, DEFAULT_SOCKS_PROXY_PORT));
Socks5ProxyHandler socks5ProxyHandler = new Socks5ProxyHandler(new InetSocketAddress(socksProxyHost, socksProxyPort));
ch.pipeline().addFirst(socks5ProxyHandler);
}
}
});
}
NettyClientHandler数据结构
接着是client调用send方法
最终调用的是org.apache.dubbo.remoting.transport.netty4.NettyChannel#send
public void send(Object message, boolean sent) throws RemotingException {
// whether the channel is closed 主要判断管道是否被关闭
super.send(message, sent);
boolean success = true;
int timeout = 0;
try {
//最终调用是channel管道的writeAndFlush方法 这里会调用pipeline的writeAndFlush写到队列里面
//之后就交给服务端处理
ChannelFuture future = channel.writeAndFlush(message);
if (sent) {
// wait timeout ms
timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
success = future.await(timeout);
}
Throwable cause = future.cause();
if (cause != null) {
throw cause;
}
} catch (Throwable e) {
removeChannelIfDisconnected(channel);
throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
}
if (!success) {
throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress()
+ "in timeout(" + timeout + "ms) limit");
}
}
上面写完并且发送完消息后,服务端处理,之后返回到客户端,中间一些步骤先不管,直接从客户端IO线程到达客户端的业务线程。那么中间是如何到达AllChannelHandler,那我们看一下调用的栈堆信息:
org.apache.dubbo.remoting.transport.dispatcher.all.AllChannelHandler#received
public void received(Channel channel, Object message) throws RemotingException {
//返回一个缓存线程池中的一个
ExecutorService executor = getPreferredExecutorService(message);
try {
//缓存线程池提交任务然后就到业务线程池了,之后就交给我们业务处理器处理了,即示例代码中的打印数据那个。
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
} catch (Throwable t) {
if(message instanceof Request && t instanceof RejectedExecutionException){
sendFeedback(channel, (Request) message, t);
return;
}
throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);
}
}
public ExecutorService getPreferredExecutorService(Object msg) {
if (msg instanceof Response) {
Response response = (Response) msg;
DefaultFuture responseFuture = DefaultFuture.getFuture(response.getId());
// a typical scenario is the response returned after timeout, the timeout response may has completed the future
if (responseFuture == null) {
return getSharedExecutorService();
} else {
ExecutorService executor = responseFuture.getExecutor();
if (executor == null || executor.isShutdown()) {
executor = getSharedExecutorService();
}
return executor;
}
} else {
return getSharedExecutorService();
}
}
服务端其实很多跟客户端类似。但是服务端不是那么好调试第一是因为需要客户端发消息,而客户端和服务端的一些逻辑调用的类是在一起的,容易混淆,第二发消息的时候接受到断点调试直接是从服务端的IO线程跳到业务线程,只能通过堆栈信息来看。即同样在AllChannelHandler打断点调试。小编这边稍微说明一下,首先也是new NettyServer(URL url, ChannelHandler handler)这个构造方法。同样有doOpen方法
protected void doOpen() throws Throwable {
bootstrap = new ServerBootstrap();
bossGroup = NettyEventLoopFactory.eventLoopGroup(1, "NettyServerBoss");
workerGroup = NettyEventLoopFactory.eventLoopGroup(
getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
"NettyServerWorker");
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
bootstrap.group(bossGroup, workerGroup)
.channel(NettyEventLoopFactory.serverSocketChannelClass())
.option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// FIXME: should we use getTimeout()?
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
ch.pipeline().addLast("negotiation",
SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
}
//同样与客户端一样,只不过处理业务逻辑换成了NettyServerHandler
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
.addLast("handler", nettyServerHandler);
}
});
// bind
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
}
这边小编也贴出NettyServerHandler结构,与NettyClientHandler类似,小编不做赘述。
小结
就此dubbo是如何使用netty远程通信就完毕了,这边的话服务端或客户端读取的话,其实是一个死循环去管道里面读取数据。
上面服务端AllChannelHandler的堆栈信息中小编截到编解码的处理,那接下来讲一下编解码的机制以及他在哪个线程做了处理和发生的时机。
编码解码无非有四部分,即客户端发送request的时候进行编码,服务端接收到request则解码,服务端处理完业务返回response则需要编码,客户端拿到response进行解码。其编码过程如下图:
那编解码操作中哪个线程中执行的,大家可以启动一个dubbo服务,在客户端使用debug模式进行调试。具体编解码工具类为org.apache.dubbo.rpc.protocol.dubbo.DubboCodec,他主要是用来编码我们的请求报文体,他继承了ExchangeCodec类,父类的话获得序列化对象然后主要写了请求头。然后调用子类的方法进行报文体的编码。编码完成后写入流中。
这里编码request的是在IO线程里面操作的,如果大家打断点就会明白,线程名为NettyClientWorker。
具体源码如下(小编只贴一个客户端request编码)
org.apache.dubbo.remoting.exchange.code.ExchangeCodec
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
//获取Hessian2Serialization,序列化对象
Serialization serialization = getSerialization(channel);
// header.封装请求头
byte[] header = new byte[HEADER_LENGTH];
// set magic number. 设置魔数
Bytes.short2bytes(MAGIC, header);
// set request and serialization flag. 设置request 和 serialization 的标记
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
if (req.isEvent()) {
header[2] |= FLAG_EVENT;
}
// set request id.
Bytes.long2bytes(req.getId(), header, 4);
// encode request data.
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) {
encodeEventData(channel, out, req.getData());
} else {
//编码请求体
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
int len = bos.writtenBytes();
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);
// write 写入流
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
encodeRequestData(channel, out, req.getData(), req.getVersion());这代码为org.apache.dubbo.rpc.protocol.dubbo.DubboCodec
@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
//写入版本号
out.writeUTF(version);
// https://github.com/apache/dubbo/issues/6138
String serviceName = inv.getAttachment(INTERFACE_KEY);
if (serviceName == null) {
serviceName = inv.getAttachment(PATH_KEY);
}
//写入接口路径
out.writeUTF(serviceName);
//写入接口版本
out.writeUTF(inv.getAttachment(VERSION_KEY));
//写入调用的方法
out.writeUTF(inv.getMethodName());
//写入参数类型
out.writeUTF(inv.getParameterTypesDesc());
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
//写入参数值
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
out.writeAttachments(inv.getObjectAttachments());
}
报文格式以及报文格式说明在Dubbo远程传输协议详解,有详细讲解,这边小编贴个图,大家了解一下。
解释完了报文头之后,接着我们看一下报文体,即Body Content,报文体分为request的和response的(感觉是一种赘述)。
其实大家看到源码的时候也就一目了然了。
客户端request编码流程图:
代码同样在DubboCodec#decode和ExchangeCodec#decodeBody中,这边小编就不贴源代码了。这边的话记得服务端debug,客户端正常启动来调试。先告诉大家一个结论,然后使用流程图说明。
在解码的时候读取header信息实际在IO线程中调试的话线程名为NettyServerWorker,但是在requestBody解码在业务线程中
这里编码response的是在IO线程里面操作的,如果大家打断点就会明白,线程名为NettyServerWorker。
今天远程通讯的知识点不好消化,讲到线程协作的知识点,用到了很多netty的知识,小伙伴得自行去学习一下,当然小编后续也会继续学习,有缘的话写一下netty的学习总结。编解码机制的话小编觉得总体还可以,只不过调试代码的时候有点累,所以这边不贴源代码了,需要自己调试。再接再厉加油!