这次了解Netty是想要尝试推送服务以及应对长连接的处理,由于Netty处理高并发NIO效率高,故先进行学习;方法是:先进行Demo操作,排除进展中的错误,再对问题进行分析,之后接着参考源码及他人经验去理解~
本篇幅环境:
linux:centos 7.0 IP:192.168.88.132
java: 1.8.0.152
tomcat 7
Eclipse:Mars.2 Release (4.5.2)
简单的C/S聊天(客户端发送,服务端固定回应)
注意: 在netty里,进出的都是ByteBuf
Customer端
初始化通道类,ClientChannelInitializer.java (主要是用于初始化客户端的逻辑与编/解码,通道是无法直接发信息的,ctx.writeAndFlush是不能直接写串类型的,需要编码器)
/**
* 客户端Channel通道初始化设置
*/
public class ClientChannelInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//这里引用默认的编码解码,默认是UTF-8的ByteBuf
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
//客户端的逻辑
pipeline.addLast("handler", new DemoClientHandler());
}
}
业务逻辑类 DemoClientHandler.java (这里可以填写你所需的业务逻辑处理返回数据)
/**
* 客户端业务逻辑
*/
public class DemoClientHandler extends SimpleChannelInboundHandler
@Override //处理异常用
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("exception is general");
}
/**
* 客户端启动逻辑
*/
public class DemoClient {
public static String host = "192.168.88.132"; //服务器IP地址
public static int port = 8000; //服务器端口
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ClientChannelInitializer());
//连接客户端
Channel channel = b.connect(host, port).sync().channel();
//控制台输入
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
for (;;) {
String line = in.readLine();
if (line == null) {
continue;
}else if(line.equals("close") || line.equals("关闭")){
channel.disconnect();
return;
}
//向服务端发送数据
channel.writeAndFlush(line);
}
} finally {
//优雅退出,释放线程池资源
group.shutdownGracefully();
}
}
}
Server端
初始化通道类 ServerChannelInitializer.java
/**
* 服务器Channel通道初始化设置
*/
public class ServerChannelInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//字符串解码和编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
//服务器的逻辑
//添加一个Hanlder用来处理各种Channel状态
pipeline.addLast("handlerIn", new DemoClientHandler());
//添加一个Handler用来接收监听IO操作的
pipeline.addLast("handlerOut", new OutHandler()); //公共处理类说明,可以不写
}
}
/**
* 服务器业务逻辑
*/
public class DemoServerHandler extends SimpleChannelInboundHandler
服务端启动类 :DemoServer.java
/**
* 服务器启动逻辑
*/
public class DemoServer {
public static void main(String[] args) throws Exception {
int port = 8000;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
//采用默认值
}
}
new DemoServer().bind(port);
}
public void bind(int port) throws Exception {
//配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new OutHandler()) //公共处理类说明用,可以不写
.childHandler(new ServerChannelInitializer());
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
Channel channel = f.channel();
//等待服务器监听端口关闭
f.channel().closeFuture().sync();
} finally {
//优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
公共处理类(可以不写,这个主要用来说明问题)
/**
* @ClassName: OutHandler
* @Description: 公共处理类
* @author linge
* @date 2018年1月8日 下午3:48:40
*
*/
public class OutHandler extends ChannelOutboundHandlerAdapter{
@Override
public void connect(ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
// TODO Auto-generated method stub
super.connect(ctx, remoteAddress, localAddress, promise);
System.out.println("<<<<<<<<<<<<<<< connect server success >>>>>>>>>>>>>>>>");
}
@Override
public void bind(ChannelHandlerContext ctx,
SocketAddress localAddress, ChannelPromise promise)
throws Exception {
// TODO Auto-generated method stub
super.bind(ctx, localAddress, promise);
System.out.println("<<<<<<<<<<<<<<< server bind success >>>>>>>>>>>>>>>>");
}
}
如果想进一步主动发信息给客户端,我之后会在文章之后附带github的链接,给你们实例。这个仅仅是一个入门级简例,用于理解基本的C/S通信;
看完第一个Demo后,有几个注意点
官网api : 官网API传送门
①一般用netty来发送和接收数据都会继承SimpleChannelInboundHandler和ChannelInboundHandlerAdapter这两个抽象类,那么这两个到底有什么区别呢?
其实用这两个抽象类是有讲究的,在客户端的业务Handler继承的是SimpleChannelInboundHandler,而在服务器端继承的是ChannelInboundHandlerAdapter。
最主要的区别就是SimpleChannelInboundHandler在接收到数据后会自动release掉数据占用的Bytebuffer资源(自动调用Bytebuffer.release())。而为何服务器端不能用呢,因为我们想让服务器把客户端请求的数据发送回去,而服务器端有可能在channelRead方法返回前还没有写完数据,因此不能让它自动release。
②关于启动类中的ServerBootstrap()方法
观看 .group() 可知,这里如果调用时,如果只传入了一个EventLoopGroup,最后也会调用group(EventLoopGroup parentGroup, EventLoopGroup childGroup)。
这里传入的两个EventLoopGroup分别叫做parentGroup和childGroup。其实我觉得更加好理解的方式应该叫boss和worker。boss这个EventLoopGroup作为一个acceptor负责接收来自客户端的请求,然后分发给worker这个EventLoopGroup来处理所有的事件event和channel的IO。
由父类AbstractBootstrap()重载方法group(EventLoopGroup parentGroup, EventLoopGroup childGroup)的super.(parentGroup)可得
/**
* The {@link EventLoopGroup} which is used to handle all the events for the to-be-created
* {@link Channel}
*/
public B group(EventLoopGroup group) {
if (group == null) {
throw new NullPointerException("group");
}
if (this.group != null) {
throw new IllegalStateException("group set already");
}
this.group = group;
return self();
}
③启动类中的引导程序所设置的信道差异
NioServerSocketChannel 是给server用的,程序由始至终只有一个NioServerSocketChannel
NioSocketChannel 是给客户端用的,每个连接生成一个NioSocketChannel 对象
最后NioServerSocketChannel中的doReadMessages()方法重载也会帮我们将每一个套接字请求转化为NioSocketChannel()对象进行处理
protected int doReadMessages(List
在服务端的ServerBootstrap中增加了一个方法childHandler,它的目的是添加handler,用来监听已经连接的客户端的Channel的动作和状态。
handler在初始化时就会执行,而childHandler会在客户端成功connect后才执行,这是两者的区别。
客户端需要加入的话也可以 在初始化处理类中加入监听,如上代码
//添加一个Handler用来接收监听IO操作的
pipeline.addLast("handlerOut", new OutHandler()); //公共处理类说明,可以不写
Tips: pipeline是伴随Channel的存在而存在的,交互信息通过它进行传递,我们可以addLast(或者addFirst)多个handler,第一个参数是名字,无具体要求,如果填写nul,系统会自动命名。
option() 、childOption() 方法
通用参数
=====================================================================================
MAX_MESSAGES_PER_READ
Netty参数,一次Loop读取的最大消息数,对于ServerChannel或者NioByteChannel,默认值为16,其他Channel默认值为1。默认值这样设置,是因为:ServerChannel需要接受足够多的连接,保证大吞吐量,NioByteChannel可以减少不必要的系统调用select。
WRITE_SPIN_COUNT
Netty参数,一个Loop写操作执行的最大次数,默认值为16。也就是说,对于大数据量的写操作至多进行16次,如果16次仍没有全部写完数据,此时会提交一个新的写任务给EventLoop,任务将在下次调度继续执行。这样,其他的写请求才能被响应不会因为单个大数据量写请求而耽误。
ALLOCATOR
Netty参数,ByteBuf的分配器,默认值为ByteBufAllocator.DEFAULT,4.0版本为UnpooledByteBufAllocator,4.1版本为PooledByteBufAllocator。该值也可以使用系统参数io.netty.allocator.type配置,使用字符串值:"unpooled","pooled"。
RCVBUF_ALLOCATOR
Netty参数,用于Channel分配接受Buffer的分配器,默认值为AdaptiveRecvByteBufAllocator.DEFAULT,是一个自适应的接受缓冲区分配器,能根据接受到的数据自动调节大小。可选值为FixedRecvByteBufAllocator,固定大小的接受缓冲区分配器。
AUTO_READ
Netty参数,自动读取,默认值为True。Netty只在必要的时候才设置关心相应的I/O事件。对于读操作,需要调用channel.read()设置关心的I/O事件为OP_READ,这样若有数据到达才能读取以供用户处理。该值为True时,每次读操作完毕后会自动调用channel.read(),从而有数据到达便能读取;否则,需要用户手动调用channel.read()。需要注意的是:当调用config.setAutoRead(boolean)方法时,如果状态由false变为true,将会调用channel.read()方法读取数据;由true变为false,将调用config.autoReadCleared()方法终止数据读取。
WRITE_BUFFER_HIGH_WATER_MARK
Netty参数,写高水位标记,默认值64KB。如果Netty的写缓冲区中的字节超过该值,Channel的isWritable()返回False。
WRITE_BUFFER_LOW_WATER_MARK
Netty参数,写低水位标记,默认值32KB。当Netty的写缓冲区中的字节超过高水位之后若下降到低水位,则Channel的isWritable()返回True。写高低水位标记使用户可以控制写入数据速度,从而实现流量控制。推荐做法是:每次调用channl.write(msg)方法首先调用channel.isWritable()判断是否可写。
MESSAGE_SIZE_ESTIMATOR
Netty参数,消息大小估算器,默认为DefaultMessageSizeEstimator.DEFAULT。估算ByteBuf、ByteBufHolder和FileRegion的大小,其中ByteBuf和ByteBufHolder为实际大小,FileRegion估算值为0。该值估算的字节数在计算水位时使用,FileRegion为0可知FileRegion不影响高低水位。
SocketChannel参数
====================================================================================
SO_RCVBUF
Socket参数,TCP数据接收缓冲区大小。该缓冲区即TCP接收滑动窗口,linux操作系统可使用命令:cat /proc/sys/net/ipv4/tcp_rmem查询其大小。一般情况下,该值可由用户在任意时刻设置,但当设置值超过64KB时,需要在连接到远端之前设置。
SO_SNDBUF
Socket参数,TCP数据发送缓冲区大小。该缓冲区即TCP发送滑动窗口,linux操作系统可使用命令:cat /proc/sys/net/ipv4/tcp_smem查询其大小。
TCP_NODELAY
TCP参数,立即发送数据,默认值为Ture(Netty默认为True而操作系统默认为False)。该值设置Nagle算法的启用,改算法将小的碎片数据连接成更大的报文来最小化所发送的报文的数量,如果需要发送一些较小的报文,则需要禁用该算法。Netty默认禁用该算法,从而最小化报文传输延时。
SO_KEEPALIVE
Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能.
SO_REUSEADDR
Socket参数,地址复用,默认值False。有四种情况可以使用:(1).当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你希望启动的程序的socket2要占用该地址和端口,比如重启服务且保持先前端口。(2).有多块网卡或用IP Alias技术的机器在同一端口启动多个进程,但每个进程绑定的本地IP地址不能相同。(3).单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。(4).完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。
SO_LINGER
Socket参数,关闭Socket的延迟时间,默认值为-1,表示禁用该功能。-1表示socket.close()方法立即返回,但OS底层会将发送缓冲区全部发送到对端。0表示socket.close()方法立即返回,OS放弃发送缓冲区的数据直接向对端发送RST包,对端收到复位错误。非0整数值表示调用socket.close()方法的线程被阻塞直到延迟时间到或发送缓冲区中的数据发送完毕,若超时,则对端会收到复位错误。
IP_TOS
IP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。
ALLOW_HALF_CLOSURE
Netty参数,一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。
DatagramChannel参数
===================================================================================
⑤ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter
根据名字的IN 与 Out可知,是对出入的操作,其实也是根据是否有数据或事件(event)来触发相应方法
前者 - 处理输入的数据且处理一切状态的改变
后者 - 处理输出数据,运行拦截一切的操作
这个channel的四种状态,我们从ChannelInboundHandler类中可以知道
ChannelUnregistered | Channel已经创建,但是还没有注册到EcentLoop上 |
ChannelRegistered | Channel已经注册到EventLoop |
ChannelActive | Channel已经激活了(已经连接到远程端),现在它已经准备好接受和发送信息 |
ChannelInactive | Channel没有连接到远程端 |
父类
一般常用方法如下:
ChannelInboundHandler.java
类型 | 描述 |
channelRegistered | 当一个Channel被注册到EventLoop上的时候并且能够处理IO的时候调用执行 |
channelUnregistered | 当一个Channel从EventLoop中注销的时候且不能再处理I/O的时候调用执行 |
channelActive | 当一个Channel被激活是调用执行 |
channelInactive | 当一个Channel已经处于非激活的状态且不再连接到远程端的时候被调用执行 |
channelReadComplete | 当一个Channel的读操作已经准备好的时候被调用执行 |
channelRead | 当数据已经从Channel读取的时候执行 |
channelWritabilityChanged | 当一个Channel的可写的状态发生改变的时候执行,用户可以保证写的操作不要太快,这样可以防止OOM,写的太快容易发生OOM,如果当发现Channel变得再次可写之后重新恢复写入的操作,Channel中的isWritable方法可以监控该channel的可写状态,可写状态的阀门直接通过Channel.config().setWriterHighWaterMark()和Channel.config().setWriteLowWaterMark()配置 |
userEventTriggered | 当ChannelInboundHandler的fireUserEventTriggered被调用的时候执行,因为一个POJO对象传输通过了ChannelPipeline,实际上userEventTriggered是调用fireUserEventTriggered()方法 |
ChannelOutboundHandler.java
类型 | 描述 |
bind(ChannelHandlerContext, SocketAddress,ChannelPromise) |
绑定到本地的地址的channel的请求被执行 |
connect(ChannelHandlerContext, SocketAddress,SocketAddress,ChannelPromise) |
连接到远程端的channel的请求被执行 |
disconnect(ChannelHandlerContext, ChannelPromise) |
当从远程端停止连接的时候执行 |
close(ChannelHandlerContext,ChannelPromise) | 请求关闭channel的时候执行 |
deregister(ChannelHandlerContext, ChannelPromise) |
请求当channel从EventLoop上注销的时候执行 |
read(ChannelHandlerContext) | 请求从channel中读取更多信息的时候执行 |
flush(ChannelHandlerContext) | 当从channel刷入队列信息到远程端的时候执行 |
write(ChannelHandlerContext,Object, ChannelPromise) |
当从channel中写数据到远程端的时候执行 |
当然一般我们都会利用适配器模式的适配类来重载自己需要的方法
我的gitHub地址 :文中例子及其他应用简单案例