Netty的强大,我也不多说了(主要是还没有用到多强大的功能,不知道到底有多强大,哈哈哈)
想要熟练掌握一个框架的使用,阅读源码和多敲代码多测试才是正道,看太多的介绍都是虚的。
话不多说,直奔主题,上代码!
首先,新建一个SpringBoot项目(SpringBoot不是必需,任意新建一个Java项目都可以,主要是本人习惯了用SpringBoot)
NettyDemoApplication--启动类
@SpringBootApplication
public class NettyDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NettyDemoApplication.class, args);
new Server().start(5000);
}
}
启动类很简单,直接创建一个Server,绑定5000端口并启动,下面是Server的代码
Server--netty服务端
public class Server {
public void start(int port) {
new Thread("netty-server-thread") {
@Override
public void run() {
super.run();
NioEventLoopGroup bossGroup = null;
NioEventLoopGroup workerGroup = null;
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// ch.pipeline().addLast(new StringDecoder());
// ch.pipeline().addLast(new ServerHandler());
ch.pipeline().addLast(new WriteHandler1());
ch.pipeline().addLast(new WriteHandler2());
ch.pipeline().addLast(new ReadHandler1());
ch.pipeline().addLast(new ReadHandler2());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
System.out.println("[" + Thread.currentThread().getName() + "]" + Server.class.getSimpleName() + ": server start");
bootstrap.bind(port).sync().channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
}
}.start();
}
}
ServerBootstrap可以说是Netty服务的一个启动辅助类,一般需要给他设置两个线程组,其中bossGroup用于绑定端口后监听客户端的连接和读写事件,监听到事件后,使用workerGroup线程组里的线程接收客户端发起的请求和回应请求结果给客户端。
.channel(NioServerSocketChannel.class),指定该服务使用的Channel为NIO类型的,说到NIO,就要提到NIO之前的BIO,BIO全称为Blocking IO,即阻塞式IO,比如服务启动后,监听端口的方法accept()会一直阻塞,直到有新的连接创建,程序才会继续往下执行,然后监听是否接收到客户端发来的请求信息,又会调用read()方法,一直阻塞,直到读取到客户端有数据过来。这势必对线程造成大大的资源浪费。之后,Java官方设计出了NIO,全称为Non-blocking IO,即非阻塞式IO,它引入了Selector模式,即选择器或者叫多路复用器模式,所有的连接、读、写事件都注册到Selector,它自身不断轮询,检测到有事件发生后,回调相应的事件方法处理业务。关于这一块内容,感兴趣的自行问度娘吧,作者实在太懒了。。
然后就是给Channel添加ChannelHandler了,这里添加了四个ChannelHandler。ChannelHandler是用于处理连接相关的触发事件的,比如通道激活,取消激活,通道注册,通道注销,通道读写事件等等。
ReadHandler1
public class ReadHandler1 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// super.channelRead(ctx, msg);
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String str = new String(bytes);
buf.release();
String str1 = " ReadHandler1 channelRead: " + str;
System.out.println(ctx.toString() + str1);
ctx.fireChannelRead(str1);
}
}
ReadHandler2
public class ReadHandler2 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// super.channelRead(ctx, msg);
System.out.println(ctx.toString() + " ReadHandler2 channelRead: " + msg);
String response = "hello";
ByteBuf buf = ctx.alloc().buffer(response.length());
buf.writeBytes(response.getBytes());
ctx.write(buf);
}
}
WriteHandler1
public class WriteHandler1 extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
// super.write(ctx, msg, promise);
System.out.println(ctx.toString() + " WriteHandler1 write");
String response = "WriteHandler1 write ";
ByteBuf buf = ctx.alloc().buffer(response.length());
buf.writeBytes(response.getBytes());
buf.writeBytes((ByteBuf)msg);
ctx.write(buf);
ctx.flush();
}
}
WriteHandler2
public class WriteHandler2 extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
// super.write(ctx, msg, promise);
System.out.println(ctx.toString() + " WriteHandler2 write");
String response = "WriteHandler2 write ";
ByteBuf buf = ctx.alloc().buffer(response.length() * 2);
buf.writeBytes(response.getBytes());
buf.writeBytes((ByteBuf)msg);
ctx.write(buf);
}
}
ChannelHandler又可以分为ChannelInboundHandler和ChannelOutboundHandler,这里的In和Out是从服务器的角度看的,外面进来的(客户端请求)是Inbound,里面出去的(发往客户端)是Outbound,我这里增加了四个Handler,其中两个WriteHandler继承自OutboundHandler,两个ReadHandler继承自InboundHandler,Channel引入了一个叫Pipeline的东西,顾名思义就是管道的意思,它的内部实现机制也非常的“管道”,往管道pipeline里添加Handler(addLast()方法),即把Handler按照顺序一个个塞进管道里,WriteHandler1在最前面(从服务端角度看是最外面),ReadHandler2在最后面(从服务端角度看是最里面),外面的客户端发送数据进来时,通过管道最外面,逐个逐个地“流经”每个Handler,当然Handler的类型不同,只有InboundHandler才会关注进来的数据,所以数据先后会经过ReadHandler1和ReadHandler2,而响应客户端时,OutboundHandler才关心发送出去的数据,数据会先后经过WriteHandler2和WriteHandler1,这跟往管道添加Handler的顺序是反过来的。
我们在各个Handler里面打印一些信息,然后运行起来,验证上面的说法
使用SocketTool工具,创建一个TCP Client客户端,然后连接5000端口,如上图所示,接下来,我们向服务器打个招呼,发送“hi”,然后观察服务端的打印信息,以及服务端回应客户端的信息
可以看到,服务端接收数据时先经过ReadHandler1,然后经过ReadHandler2,发送数据时先经过WriteHandler2,然后经过 WriteHandler1,和上面的说法一致
这里客户端收到的是WriteHandler1在前面?有读者可能疑惑了,注意看上面四个Handler的代码,回应客户端时,我是从ReadHandler2发送了“hello”,然后先经过 WriteHandler2,在hello前拼接了一段字符串,再经过WriteHandler1,在WriteHandler2拼接的基础上,再拼接了WriteHandler1的字符串。
至此,入门示例就讲解完了,关于ChannelInboundHandler,它有很多的子类,比如经常用到的有:
SimpleChannelInboundHandler,简单易用的类,内部重写了channelRead方法,主要是自动释放了ByteBuf的引用(ByteBuf的引用计数器是为了控制垃圾回收的,以后有时间再讨论ByteBuf吧)
MessageToMessageDecoder,消息体转换的解码器,举个简单的例子,客户端发过来一串由数字组成的字符串,你想把它们转换成int数值然后再传给下一个Handler处理这个数值(假设下一个Handler只能处理int数值),你就可以使用这个类把String类型转成Integer
DelimiterBasedFrameDecoder,基于分隔符的解码器,创建时给定一个分隔符,比如美元符号“$”,它会不断接收数据,直到接收到$,才会把前面接收到的数据丢给下一个Handler处理
LineBasedFrameDecoder,其实就是特殊的DelimiterBasedFrameDecoder,它是基于行的解码器(即分隔符就是\r\n或者\n),它会不断接收数据,直到接收到换行,才会把前面接收到的数据丢给下一个Handler处理
StringDecoder,字符串解码器,非常的简单,就是把收到的数据(网络层传输的其实都是字节数组byte[])直接转成String,如果你设计的系统只使用明文ascii字符串作为唯一通信的数据类型,在通道的最开始添加使用这个解码器,可以大大减少你后面Handler需要把Object msg强转为String的操作
第一次写博客,有不足之处,请多多指教!