https://www.jianshu.com/p/e58674eb4c7a
1. Netty 能够帮助搭建允许系统能够扩展到支持150000名并发用户。
2. Netty 设计关键: 异步 + 事件驱动
典型的BIO服务端:
1. 一个主线程在某个port监听,等待客户端连接。
2. 当接收到客户端发起的连接时,创建一个新的线程去处理客户端请求。
3. 主线程重新回到port监听,等待下一个客户端连接。
缺点:
1. 每个新的客户端Socket都需要创建一个新的Thread处理,将会导致大量的线程处于休眠状态。
2. 每个线程都有调用栈的内存分配,连接数非常多时,耗费较多内存。
3. 连接数比较多时,创建大量线程,上下文切换所带来的开销较大。
代码:
-
public void serve(int port) throws IOException {
-
// 创建Socket
-
ServerSocket serverSocket =
new ServerSocket(port);
-
// 等待客户端连接
-
Socket clientSocket = serverSocket.accept();
-
// 创建输入流
-
BufferedReader in =
new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
-
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(),
true);
-
String request, response;
-
while((request = in.readLine()) !=
null) {
-
if(
"Done".equals(request)) {
-
break;
-
}
-
response = processRequest(request);
-
out.println(response);
-
}
-
}
1. 使用Selector来实现Java的非阻塞I/O操作。将多个Socket的读写状态绑定到Selector上,允许在任何时间检查任意的读操作/写操作的完成状态。
2. 允许单个线程处理多个并发的连接。
Netty的主要构件块:
1. Channel
2. 回调
3. Future
4. 事件和ChannelHandler
Channel是传入(入站)或者传出(出站)数据的载体(如一个文件、一个Socket或一个硬件设备)。可以被打开或者被关闭,连接或断开连接。
回调只是:先写一段代码,该段代码在将来某个适当的时候会被执行。Netty大量使用了回调,比如:某ChannelHandler中的channelActive()方法则是一个回调,表示连接建立时,请执行该段回调代码。
异步操作占位符。在操作完成时,提供结果的访问。
JDK提供的Future和ChannelFuture对比:
1. JDK提供的Future需要手动检查对应的操作是否完成,或一直阻塞直到它完成
2. ChannelFuture能够注册Listener监听器,监听器的回调函数operationComplete()能异步的在操作完成时被调用。
代码:
-
public static void connect() {
-
Channel channel = CHANNEL_FROM_SOMEWHERE;
-
-
ChannelFuture future = channel.connect(
new InetSocketAddress(
"127.0.0.1",
9080));
-
future.addListener(
new ChannelFutureListener() {
-
@Override
-
public void operationComplete(ChannelFuture future) throws Exception {
-
if(future.isSuccess()) {
-
ByteBuf buf = Unpooled.copiedBuffer(
"hello", Charset.defaultCharset());
-
ChannelFuture wf = future.channel().writeAndFlush(buf);
-
// ...
-
}
else {
-
// 失败后可尝试重连/切换链路
-
future.cause().printStackTrace();
-
}
-
}
-
})
-
}
1. 事件:发生某种事件触发适当的动作。比如入站触发事件: 链路激活(channelActive)/数据可读(channelRead)/发生异常(exceptionCaught)/...
2. Channelhandler:一组为了响应特定事件而被执行的回调函数。如:ChannelInboundHanderAdapter.java是一个入站事件
Channel和EventLoop都是Netty核心概念,而且有一些约定俗成的规定,能帮助编程和理解:
1. 单个Channel只会映射到单个EventLoop
2. 单个EventLoop可以处理多个Channel(1:n关系)
3. 一个EventLoop在其生命周期内只能绑定到一个线程上4. 由于单个Channel在其生命周期中只会有一个I/O线程,所以ChannelPipeline中多个ChannelHandler无需关心同步互斥问题
1. ChannelHandler用于构建应用业务逻辑。往往封装了为响应特定事件而编写的回调函数
2. 本节主要讲解一个超级简单的Netty应用程序,回显服务: 客户端建立连接后,发送一个或多个消息。服务端收到后,将消息返回。
Netty服务端至少需要两个部分: 一个ChannelHandler + 引导(Bootstrap)
继承ChannelInboundHandlerAdapter类,感兴趣的入站方法:
1. channelRead() - 对于每个传入的消息都要调用
2. channelReadComplete() - 当前批量读取中的最后一条数据
3. exceptionCaught() - 读取操作期间,有异常抛出时调用
代码:
-
@ChannelHandler.Sharable
-
public
class EchoServerHandler extends ChannelInboundHandlerAdapter{
-
-
/**
-
* 每次传入的消息都要调用
-
*/
-
@Override
-
public void channelRead(ChannelHandlerContext ctx, Object msg) {
-
ByteBuf in = (ByteBuf) msg;
-
System.out.println(
-
"Server received: " + in.toString(CharsetUtil.UTF_8));
-
ctx.write(in);
-
}
-
-
/**
-
* 读完当前批量中的最后一条数据后,触发channelReadComplete(...)方法
-
*/
-
@Override
-
public void channelReadComplete(ChannelHandlerContext ctx)
-
throws Exception {
-
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
-
.addListener(ChannelFutureListener.CLOSE);
-
}
-
-
/**
-
* 异常捕获
-
*/
-
@Override
-
public void exceptionCaught(ChannelHandlerContext ctx,
-
Throwable cause) {
-
cause.printStackTrace();
-
ctx.close();
-
}
-
}
解释:
1. channelRead和channelReadComplete理解:当批量消息后最后一条数据被channelRead(...)后触发channelReadComplete事件。
2. ctx.write(...)只是将消息暂时存放在ChannelOutboundBuffer中,等待flush(...)操作
3. @Sharable注解:本质是声明该ChannelHandler全局单例。可被多个Channel安全的共享。标注了@Sharable注解的ChannelHandler请注意不能有对应的状态
4. 完整代码地址
1. 引导服务器主要打开Netty的Channel。并分配对应的EventLoop和ChannelPipeline。
2. 一个Channel只有一个ChannelPipeline。ChannelPipeline是由一组ChannelHandler组成的责任链。
代码:
-
EventLoopGroup group =
new NioEventLoopGroup();
-
try {
-
ServerBootstrap b =
new ServerBootstrap();
-
b.group(group)
-
.channel(NioServerSocketChannel.class)
-
.localAddress(
new InetSocketAddress(port))
-
.childHandler(
new ChannelInitializer
() {
-
@Override
-
public void initChannel(SocketChannel ch) throws Exception {
-
ch.pipeline().addLast(
new EchoServerHandler());
-
}
-
});
-
}
finally {
-
group.shutdownGracefully().sync();
-
}
客户端将会:
1. 建立连接
2. 发送消息
3. 关闭连接
1. Java是通过GC可达性分析来实现垃圾回收。对于Netty传输中的ByteBuf,使用的是引用计数算法。也就是说:如果你使用了Netty,需要你亲自考虑是否需要手动释放对象。判断方法,后文将会给出
2. 扩展SimpleChannelInboundHandler类处理任务的Handler,无需手动释放对象。SimpleChannelInboundHandler.java中方法channelRead()中会负责释放引用。
3. 客户端发送消息条数和服务端接收的消息条数是不对应的。除非处理了TCP的粘包黏包。
代码:
-
// SimpleChannelInboundHandler
中channelRead方法负责释放对象msg引用
-
public
abstract
class SimpleChannelInboundHandler<I> ...{
-
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
-
boolean release =
true;
-
try {
-
// ...
-
}
finally {
-
if (autoRelease && release) {
-
// 减少对象msg引用计数
-
ReferenceCountUtil.release(msg);
-
}
-
}
-
}
-
}
问:ChannelHandler中何时需要主动释放引用?
1. 扩展的类不是: SimpleChannelInboundHandler,且该对象msg不会传给下一个ChannelHandler
2. 扩展的类不是: SimpleChannelInboundHandler,且该对象msg不会被ctx.write(...)
给出引导客户端关键代码,完整代码请参考地址
代码:
-
EventLoopGroup group =
new NioEventLoopGroup();
-
try {
-
Bootstrap b =
new Bootstrap();
-
b.group(group)
-
.channel(NioSocketChannel.class)
-
.remoteAddress(
new InetSocketAddress(host, port))
-
.handler(
new ChannelInitializer
() {
-
@Override
-
public void initChannel(SocketChannel ch)
-
throws Exception {
-
ch.pipeline().addLast(
-
new EchoClientHandler());
-
}
-
});
-
// 下面两行代码可以删除
-
ChannelFuture f = b.connect().sync();
-
f.channel().closeFuture().sync();
-
}
finally {
-
group.shutdownGracefully().sync();
-
}
1.完整代码地址
2.netty-in-action下载地址