2、Netty环境搭建与第一个程序:Maven依赖配置,编写一个简单的Echo服务器与客户端
好,咱们正式开始动手了。这一章的目标很简单——把Netty跑起来。我见过太多人一上来就啃源码,结果连个Hello World都跑不通,信心直接崩了。所以咱们先搭环境,写一个Echo程序,让数据在客户端和服务器之间打个来回。你亲自跑通一次,后面学什么都踏实。
2.1 Maven依赖配置
我个人习惯用Maven管理项目。Netty的依赖其实不复杂,但版本选择有讲究。我早期踩过一个坑——用了4.0.x的旧版本,结果跟JDK8的新特性冲突,调试了一下午。所以这里我直接给你推荐稳定版本。
在 pom.xml 里加入以下依赖:
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
</dependencies>
为什么用 netty-all?说白了就是省事。它把Netty的核心模块、编解码器、传输层实现都打包在一起了。你刚开始学,没必要纠结哪个模块该引哪个不该引,一个 netty-all 搞定所有。
2.2 编写Echo服务器
Echo服务器,说白了就是客户端发什么,服务器原样返回什么。听起来简单,但它能帮你理解Netty最核心的「事件驱动」模型。
先看服务器端的启动代码:
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(port).sync();
System.out.println("服务器启动,端口:" + port);
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new EchoServer(8080).start();
}
}
这里有两个 EventLoopGroup,你可能会问:为什么需要两个?我刚开始学的时候也纳闷。其实 bossGroup 只负责接收新连接,workerGroup 负责处理已连接的读写操作。你想想看,如果只有一个线程组,既要接客又要干活,高并发下肯定忙不过来。
接下来是处理器:
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("收到客户端消息: " + in.toString(CharsetUtil.UTF_8));
ctx.write(in); // 把消息原样写回去
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush(); // 刷新缓冲区,把数据发出去
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
channelRead 里我直接调用了 ctx.write(in),注意这里没有释放 ByteBuf。为什么?因为Netty会在写入完成后自动帮你释放。如果你手动调了 ReferenceCountUtil.release(),反而会出问题。我曾经在这个细节上栽过跟头,排查了半天才发现是重复释放导致的。
2.3 编写Echo客户端
客户端跟服务器结构类似,但启动方式不同。客户端不需要 ServerBootstrap,而是用 Bootstrap:
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect(host, port).sync();
System.out.println("已连接到服务器");
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new EchoClient("127.0.0.1", 8080).start();
}
}
客户端的处理器稍微复杂一点,因为它要主动发消息:
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf message;
public EchoClientHandler() {
message = Unpooled.copiedBuffer("你好,Netty!", CharsetUtil.UTF_8);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("连接建立,发送消息...");
ctx.writeAndFlush(message);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("收到服务器回显: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
注意 channelActive 这个方法。它会在TCP连接建立成功后立即被调用。我习惯在这里发送第一条消息,这样能最快验证整个链路是否通畅。
2.4 运行与验证
先启动服务器,再启动客户端。你会看到控制台输出类似这样的内容:
服务器端:
服务器启动,端口:8080
收到客户端消息: 你好,Netty!
客户端:
已连接到服务器
连接建立,发送消息...
收到服务器回显: 你好,Netty!
java.net.BindException: Address already in use,说明端口被占用了。检查一下是不是之前启动的服务器没关掉。我遇到过最离谱的一次,是IDE里同时跑了两个实例,端口冲突了半小时才反应过来。
2.5 代码结构总结
咱们把整个流程梳理一下,方便你对照:
| 组件 | 作用 | 我的建议 |
|---|---|---|
| EventLoopGroup | 管理线程池,处理I/O事件 | 服务器用两个Group,客户端用一个 |
| ServerBootstrap / Bootstrap | 启动辅助类,配置参数 | 注意区分服务端和客户端的启动方式 |
| ChannelInitializer | 初始化管道,添加处理器 | 每个新连接都会执行一次 |
| ChannelHandler | 处理业务逻辑 | 注意ByteBuf的释放时机 |
嗯,到这里你的第一个Netty程序就跑通了。别看它简单,这个Echo模型是所有RPC框架的基础。后面咱们讲编解码、粘包拆包,都是在这个结构上做扩展。你先把这段代码敲一遍,跑通了再往下走。
下一章,咱们聊聊Netty的线程模型——为什么它能支撑百万级并发?这里面的门道,可比你想象的有意思多了。