第2章 第一款Netty应用程序
第2章 第一款Netty应用程序
-
- ChannelHandler用于构建应用业务逻辑。往往封装了为响应特定事件而编写的回调函数
-
- 本节主要讲解一个超级简单的Netty应用程序,回显服务: 客户端建立连接后,发送一个或多个消息。服务端收到后,将消息返回。
2.3 编写Echo服务器
Netty服务端至少需要两个部分: 一个ChannelHandler + 引导(Bootstrap)
2.3.1 ChannelHandler和业务逻辑
继承ChannelInboundHandlerAdapter类,感兴趣的入站方法:
-
- channelRead() - 对于每个传入的消息都要调用
-
- channelReadComplete() - 当前批量读取中的最后一条数据
-
- exceptionCaught() - 读取操作期间,有异常抛出时调用
代码:
/**
* 这里对于所有的客户端连接来说,都会使用同一个 EchoServerHandler,因为其被标注为@Sharable,
* 这将在后面的章节中讲到
*/
@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();
}
}
解释:
-
- channelRead和channelReadComplete理解:当批量消息后最后一条数据被channelRead(...)后触发channelReadComplete事件。
-
- ctx.write(...)只是将消息暂时存放在ChannelOutboundBuffer中,等待flush(...)操作
-
- @Sharable注解:本质是声明该ChannelHandler全局单例。可被多个Channel安全的共享。标注了@Sharable注解的ChannelHandler请注意不能有对应的状态
2.3.2 引导服务器
-
- 引导服务器主要打开Netty的Channel。并分配对应的EventLoop和ChannelPipeline。
-
- 一个Channel只有一个ChannelPipeline。ChannelPipeline是由一组ChannelHandler组成的责任链。
代码:
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
/**
*
* EchoServerHandler 实现了业务逻辑;
* main()方法引导了服务器;
*
* 引导过程中所需要的步骤如下:
* 创建一个 ServerBootstrap 的实例以引导和绑定服务器;
* 创建并分配一个 NioEventLoopGroup 实例以进行事件的处理,如接受新连接以及读/写数据;
* 指定服务器绑定的本地的 InetSocketAddress;
* 使用一个 EchoServerHandler 的实例初始化每一个新的 Channel;
* 调用 ServerBootstrap.bind()方法以绑定服务器。
*/
public static void main(String[] args)
throws Exception {
int port = 9080;
new EchoServer(port).start();
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//添加一个EchoServerHandler 到子Channel的 ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoServerHandler());
}
});
// 实际项目中,b.bind().sync()可以省略
//异步地绑定服务器调用 sync()方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
System.out.println(EchoServer.class.getName() +
" started and listening for connections on " + f.channel().localAddress());
// 实际项目中,f.channel().closeFuture().sync()可以省略
//获取 Channel 的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
2.4 编写Echo客户端
客户端将会:
-
- 建立连接
-
- 发送消息
-
- 关闭连接
2.4.1 ChannelHandler客户端逻辑
-
- Java是通过GC可达性分析来实现垃圾回收。对于Netty传输中的ByteBuf,使用的是引用计数算法。也就是说:如果你使用了Netty,需要你亲自考虑是否需要手动释放对象。判断方法,后文将会给出
-
- 扩展SimpleChannelInboundHandler类处理任务的Handler,无需手动释放对象。SimpleChannelInboundHandler.java中方法channelRead()中会负责释放引用。
-
- 客户端发送消息条数和服务端接收的消息条数是不对应的。除非处理了TCP的粘包黏包。
代码:
// SimpleChannelInboundHandler<T>中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中何时需要主动释放引用?
-
- 扩展的类不是: SimpleChannelInboundHandler,且该对象msg不会传给下一个ChannelHandler
-
- 扩展的类不是: SimpleChannelInboundHandler,且该对象msg不会被ctx.write(...)
2.4.2 引导客户端
引导客户端关键代码
代码:
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
/**
*
* EchoServerHandler 实现了业务逻辑;
* main()方法引导了服务器;
*
* 引导过程中所需要的步骤如下:
* 创建一个 ServerBootstrap 的实例以引导和绑定服务器;
* 创建并分配一个 NioEventLoopGroup 实例以进行事件的处理,如接受新连接以及读/写数据;
* 指定服务器绑定的本地的 InetSocketAddress;
* 使用一个 EchoServerHandler 的实例初始化每一个新的 Channel;
* 调用 ServerBootstrap.bind()方法以绑定服务器。
*/
public static void main(String[] args)
throws Exception {
int port = 9080;
new EchoServer(port).start();
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//添加一个EchoServerHandler 到子Channel的 ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoServerHandler());
}
});
// 实际项目中,b.bind().sync()可以省略
//异步地绑定服务器调用 sync()方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
System.out.println(EchoServer.class.getName() +
" started and listening for connections on " + f.channel().localAddress());
// 实际项目中,f.channel().closeFuture().sync()可以省略
//获取 Channel 的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
/**
* 这里对于所有的客户端连接来说,都会使用同一个 EchoServerHandler,因为其被标注为@Sharable,
* 这将在后面的章节中讲到
*/
@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();
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!