接触netty的源代码应该也有好几个月的时间了,一直觉得只是了解了一个框架的设计和原理,而不能熟练的运用这个框架也算是非常遗憾的事情了,尤其是以前读了nginx的部分代码,而我现在都还不会配置nginx服务器,好囧。。。可能也没有机会用nginx吧。。。不能强求。。。
netty不能够继续遗憾,其实它的运用无非就是用它定义的一些基础handler或者再定义自己的handler用于处理传输和接收数据,那么这篇就讲讲昨天搞的,用node.js将protobuf的数据发送给netty服务器。。。也就是着重要用一下netty自带的protobuf的handler。。。在这中间还遇到了一些坑。。。所也也觉得有必要写一下记录下来,以备以后有机会用到的时候不要再掉进坑里面。。。
这里先上netty主程序部分的代码吧:
package proto;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
public class Fjs {
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); //这个是用于serversocketchannel的eventloop
EventLoopGroup workerGroup = new NioEventLoopGroup(); //这个是用于处理accept到的channel
try {
ServerBootstrap b = new ServerBootstrap(); //构建serverbootstrap对象
b.group(bossGroup, workerGroup); //设置时间循环对象,前者用来处理accept事件,后者用于处理已经建立的连接的io
b.channel(NioServerSocketChannel.class); //用它来建立新accept的连接,用于构造serversocketchannel的工厂类
b.childHandler(new ChannelInitializer<SocketChannel>(){ //为accept channel的pipeline预添加的inboundhandler
@Override //当新连接accept的时候,这个方法会调用
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ReadTimeoutHandler(10)); //如果10秒钟都没有新的数据读取,那么自动关闭
ch.pipeline().addLast(new WriteTimeoutHandler(1)); //写的1秒钟超时
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder()); //这里需要将protobuf的数据封装成netty定义的帧,将帧完整的解析出来
ch.pipeline().addLast(new ProtobufDecoder(MyMessage.Message.getDefaultInstance())); //用于将二进制的protobuf数据转化为对象
ch.pipeline().addLast(new MyHandler()); //自己定义的handler
}
});
//bind方法会创建一个serverchannel,并且会将当前的channel注册到eventloop上面,
//会为其绑定本地端口,并对其进行初始化,为其的pipeline加一些默认的handler
ChannelFuture f = b.bind(8000).sync();
f.channel().closeFuture().sync(); //相当于在这里阻塞,直到serverchannel关闭
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String args[]) throws Exception {
new Fjs().run();
}
}
代码很简单,而且注释我觉得已经算是很详细了。。。
这里无非就是在pipeline上面添加了3个handler,而且前面的两个handler都是netty框架自带的。。分别用于将按照帧格式传送的数据按照一帧一帧的读取出来,然后接着另一个handler将二进制的的数据转化为对象。。这里详细说明一下吧:
(1)ProtobufVarint32FrameDecoder这个handler的作用就是将数据按照帧的格式读取出来,然后再将里面protobuf的数据取出来。。。
这里的帧是netty自己定义的一种格式,具体如下:
其实定义的帧格式本身还是很简单的,因为数据要在网络上面传输,而网络上传输的数据都是二进制的流数据,如果没有一定的格式包装我们是无法知道我们是否已经接收到,那么netty的做法就是在protobuf的数据前面加上一个头部,用于代表后面protobuf的数据的长度。。。也就是说这个头部是一个整形,但是这个整形又是采用的另外一种编码方式来存储的----google在protobuf里面提出来的Base 128 Varints
这种编码方式有很多的好处,而且可以方便按照字节的方式来解析数据,具体这种编码方式用另外一篇文章来说明吧。。
(2)ProtobufDecoder,这个也是netty自己的handler,它需要传进一个解码器,用于将前面的handler传递过来的protobuf二进制数据转化为对象。。。这个就不细讲了。。
(3)最后就是自己的handler了,很简单,因为直接接收到的就是protobuf的对象,那么就按照对象的方式操作一下就好了。。还是来看一下这个handler的代码吧:
public class MyHandler extends SimpleChannelInboundHandler<MyMessage.Message>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message message)
throws Exception {
// TODO Auto-generated method stub
System.out.println(message.getText().length());
System.out.println(message.getText());
ctx.close();
}
}
至于说前面两个netty的handler的源码,就不讲了,其实很简单,只要稍微看看就能看明白。
这个真的没啥意思,不过这里又要表扬一下新版本的netty,取消了byte与message之分,开发起来更清晰了。。。那么接下来就是node.js的代码了,它是用于将二进制的protobuf数据直接发给netty服务器。。
其实刚开始觉得这部分会非常的简单的,不就是将数据通过socket发过去么。。。刚开始就是这么直接弄的,后来发现出错。。。然后找了很久才知道netty需要将数据包装成定义好的帧格式。。
也就是说node.js在发送数据之前需要将二进制的数据转化一下,在前面添加一个采用Base 128 Varints编码的头部。。
那么接下来就直接贴上自己的代码了,里面就包含了如何在数据前面添加上头部。。。
var http = require("http");
var fs = require("fs");
var ProtoBuf = require("protobufjs");
//var Message = ProtoBuf.protoFromFile("MyMessage.proto").build("Message");
var data = fs.readFileSync("fjs.dat");
function computeRawVarint32Size (value) {
if ((value & (0xffffffff << 7)) == 0) return 1;
if ((value & (0xffffffff << 14)) == 0) return 2;
if ((value & (0xffffffff << 21)) == 0) return 3;
if ((value & (0xffffffff << 28)) == 0) return 4;
return 5;
}
function writeRawVarint32(value) {
var size = computeRawVarint32Size(value);
var buffer = new Buffer(size);
var i = 0;
while (true) {
if ((value & ~0x7F) == 0) {
buffer.writeUInt8(value, i);
break;
} else {
var temp = ((value & 0x7F) | 0x80);
buffer.writeUInt8(temp, i);
value >>>= 7;
}
i++;
}
return buffer;
}
function merge(a, b) {
return Buffer.concat([a, b], a.length + b.length);
}
function formFrame(data) {
var header = writeRawVarint32(data.length);
return merge(header, data);
}
var net = require('net');
var socket = new net.Socket();
socket.connect(8000, "192.168.2.181");
socket.on('connect', function(){
console.log("bbbb");
socket.write(formFrame(data));
});
代码还是很简单的,基本上很容易能明白。。。其实这里对Base 64 Varints的编码也都是从netty里面copy下来的。。这里就算是保存一下吧。。。说不定以后能用上。。。
毕竟通过protobuf来传输数据的话,可以实现跨平台跨语言的rpc调用。。。而且以后要是有机会的话还真想写一个这样子的rpc框架。。呵呵。。。希望有机会吧。。。