Netty教程—Part5—Server端返回日期数据

本文介绍如何在Netty客户端与服务器间实现双向通信。通过修改DateSender示例,新增DateModifier示例,使服务器能够修改日期并返回给客户端。客户端和服务器端管道中分别加入了ObjectEncoder和ObjectDecoder,确保数据正确交换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文由 ImportNew - 刘海波 翻译自 seeallhearall.blogspot。如需转载本文,请先参见文章末尾处的转载要求。

本文是Netty教程的第五篇。

客户端和服务器端都需要双向的读写操作。例如DateSender示 例,如果服务器端想将日期数据增加任意的天数,然后将修改后的日期返回,应该如何做呢?有个代码几乎和DateSender相 同的示例:DateModifier ,这个示例的服务器端 修改日期然后将修改后的数据返 回给客户端。为了实现这个功能,需要做如下修改:

  1. 服务器端需要在pipeline中添加一个ObjectEncoder, 才能返回一个日期数据给客户端。
  2. 客户端接收从服务器端返回的日期数据需要在pipeline中添加ObjectDecorder。
  3. 客户端需要添加一个额外的处理器来利用接收到的数据完成一些逻辑。

下面代码创建了新客户端的pipeline:

ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() {
  public ChannelPipeline getPipeline() throws Exception {
    return Channels.pipeline(
      new ObjectEncoder(),
      // Next 2 Lines are new
      new ObjectDecoder(ClassResolvers.cacheDisabled(getClass().getClassLoader())),
      new ClientDateHandler()
    );
  }
};

客户端的处理器DateHandler非常简单,当客户端接收数据并且将数据解码之后会调用这个处理器。

static class ClientDateHandler extends SimpleChannelHandler {
 public void messageReceived(ChannelHandlerContext ctx,MessageEvent e) throws Exception {
  Date date = (Date)e.getMessage();
  clog("Hey Guys !  I got back a modified date ! [" + date + "]");
 }
}

服务器端的pipeline几乎和客户端一样:

bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
 public ChannelPipeline getPipeline() throws Exception {
  return Channels.pipeline(
   new ObjectDecoder(ClassResolvers.cacheDisabled(getClass().getClassLoader())),
   // Next 2 Lines are new
   new ObjectEncoder(),
   new ServerDateHandler()
  );
 };
});

下面的代码是新的服务器端日期处理器DateHandler,里面有一些新的代码:

static class ServerDateHandler extends SimpleChannelHandler {
 Random random = new Random(System.nanoTime());
 public void messageReceived(ChannelHandlerContext ctx,MessageEvent e) throws Exception {
  Date date = (Date)e.getMessage();
  // Here's the REALLY important business service at the end of the pipeline
  long newTime = (date.getTime() + random.nextInt());
  Date newDate = new Date(newTime);
  slog("Hey Guys !  I got a date ! [" + date + "] and I modified it to [" + newDate + "]");
  // Send back the reponse
  Channel channel = e.getChannel();
  ChannelFuture channelFuture = Channels.future(e.getChannel());
  ChannelEvent responseEvent = new DownstreamMessageEvent(channel, channelFuture, newDate, channel.getRemoteAddress());
  ctx.sendDownstream(responseEvent);
  // But still send it upstream because there might be another handler
  super.messageReceived(ctx, e);
 }  
}

在第10-13行是一些新代码基于一个精妙的理念,主要用于将修改后的日期返回给调用的客户端。

  • 在第10行,从MessageEvent中获取了一个Channel的引用。
  • 日期数据即将被异步(会出 现重复调用写操作的风险)写回客户端,因此总会调用ChannelFuture。在第11行,使用Channels创建一个ChannelFuture对象,Channels有 很多有用的类似future的静态方法。
  • 第12行创建一个新MessageEvent:DownstreamMessageEvent。基本 上,返回值已经被打包准备回传给客户端。
  • 第13行调用ChannelHandlerContextsendDownstream方法将MessageEvent发送给客户端。

有什么需要注意的吗?ChannelHandlerContext基本上是pipeline功能上的引用。它能和pipeline一样 访问所有的处理器。更重要的是,它提供了一种和接收信息一样的路径来将数据发送回去。通过如下方法实现:

  1. 注意,在pipeline中除了ServerDateHandler之外可能还有其他的处理器,希望响应客户端将修改后的日期数据发送回去。然后,将当前接收的数据继续传送到pipeline中的下一个处理器。
  2. 如果直接将返回的日期数据直接写到Channel中,日期数据会从pipeline的上层开始推送到某些不期望被调用的处理器中。

所有这些东西引出了一 些额外的细节:pipeline中的处理器如何处理工作的,并解释了Upstream和Downstream。

<think>我们有一个明确的目标:使用Spring Boot整合Netty和Protostuff实现客户与服务通信。根据引用内容,我们可以构建如下步骤: 1. 项目结构:创建两个独立的模块(或项目),一个用于服务,一个用于客户。 2. 依赖管理:在Spring Boot项目中添加Netty和Protostuff的依赖。 3. 序列化/反序列化:使用Protostuff作为序列化工具,我们需要定义消息结构,并编写编解码器。 4. 服务实现:包括启动类、初始化类、处理类。 5. 客户实现:包括启动类、初始化类、处理类。 6. 测试:启动服务和客户,进行通信测试。 下面我们逐步展开: ### 步骤1: 创建项目 使用Spring Initializr创建两个Spring Boot项目(或一个多模块项目),分别命名为`netty-server`和`netty-client`。 ### 步骤2: 添加依赖 在`pom.xml`中添加依赖(两个项目都需要): ```xml <!-- Netty --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.68.Final</version> <!-- 使用最新稳定版本 --> </dependency> <!-- Protostuff --> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.7.2</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.7.2</version> </dependency> ``` ### 步骤3: 定义消息结构 我们需要定义一个消息类,用于客户和服务之间的通信。例如: ```java public class Message { private int id; private String content; // 省略getter和setter } ``` ### 步骤4: 编写Protostuff编解码器 我们需要实现Netty的`MessageToMessageDecoder`和`MessageToMessageEncoder`来处理Protostuff序列化和反序列化。 首先,创建一个工具类`ProtostuffUtil`: ```java import io.protostuff.LinkedBuffer; import io.protostuff.ProtostuffIOUtil; import io.protostuff.Schema; import io.protostuff.runtime.RuntimeSchema; public class ProtostuffUtil { private static final Schema<Message> schema = RuntimeSchema.getSchema(Message.class); public static byte[] serialize(Message message) { LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); try { return ProtostuffIOUtil.toByteArray(message, schema, buffer); } finally { buffer.clear(); } } public static Message deserialize(byte[] bytes) { Message message = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes, message, schema); return message; } } ``` 然后,编写编码器和解码器: ```java // 编码器 public class ProtostuffEncoder extends MessageToByteEncoder<Message> { @Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { byte[] bytes = ProtostuffUtil.serialize(msg); out.writeInt(bytes.length); // 写入消息长度 out.writeBytes(bytes); // 写入消息体 } } // 解码器 public class ProtostuffDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < 4) { return; // 长度字段不够 } in.markReaderIndex(); int length = in.readInt(); if (in.readableBytes() < length) { in.resetReaderIndex(); return; } byte[] bytes = new byte[length]; in.readBytes(bytes); Message message = ProtostuffUtil.deserialize(bytes); out.add(message); } } ``` ### 步骤5: 服务实现 #### 服务启动类(在Spring Boot启动时启动Netty服务) ```java import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; @Component public class NettyServer implements CommandLineRunner { @Value("${netty.port:8888}") private int port; @Override public void run(String... args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new NettyServerInitializer()); // 初始化类 ChannelFuture future = bootstrap.bind(port).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` #### 服务初始化类 ```java import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.codec.LengthFieldPrepender; public class NettyServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline() // 解决粘包拆包问题:长度字段为4个字节,在消息头 .addLast(new LengthFieldBasedFrameDecoder(1024*1024, 0, 4, 0, 4)) .addLast(new LengthFieldPrepender(4)) // 编解码器 .addLast(new ProtostuffDecoder()) .addLast(new ProtostuffEncoder()) // 业务处理器 .addLast(new NettyServerHandler()); } } ``` #### 服务处理器 ```java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class NettyServerHandler extends SimpleChannelInboundHandler<Message> { @Override protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception { System.out.println("Server received: " + msg); // 处理消息,并回复 Message response = new Message(); response.setId(msg.getId()); response.setContent("Server response for: " + msg.getContent()); ctx.writeAndFlush(response); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } ``` ### 步骤6: 客户实现 #### 客户启动类(模拟在Spring Boot中启动客户) ```java import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; @Component public class NettyClient implements CommandLineRunner { @Value("${netty.server.host:localhost}") private String host; @Value("${netty.server.port:8888}") private int port; @Override public void run(String... args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new NettyClientInitializer()); Channel channel = bootstrap.connect(host, port).sync().channel(); // 发送消息 Message message = new Message(); message.setId(1); message.setContent("Hello from client"); channel.writeAndFlush(message); // 等待连接关闭 channel.closeFuture().sync(); } finally { group.shutdownGracefully(); } } } ``` #### 客户初始化类 ```java import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.codec.LengthFieldPrepender; public class NettyClientInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline() .addLast(new LengthFieldBasedFrameDecoder(1024*1024, 0, 4, 0, 4)) .addLast(new LengthFieldPrepender(4)) .addLast(new ProtostuffDecoder()) .addLast(new ProtostuffEncoder()) .addLast(new NettyClientHandler()); } } ``` #### 客户处理器 ```java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class NettyClientHandler extends SimpleChannelInboundHandler<Message> { @Override protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception { System.out.println("Client received: " + msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } ``` ### 步骤7: 配置和测试 在服务和客户的`application.properties`中配置口和主机(客户需要配置服务的地址和口)。 启动服务项目,然后启动客户项目。观察控制台输出,应该能看到客户发送消息,服务接收并回复,客户再接收回复。 ### 注意事项 1. 我们在编解码器前添加了`LengthFieldBasedFrameDecoder`和`LengthFieldPrepender`,用于处理粘包拆包问题。 2. 序列化工具类`ProtostuffUtil`是针对`Message`类的,如果需要序列化其他对象,可以扩展。 3. 在实际项目中,可以将消息类型设计得更通用,例如包含消息类型字段,以便处理多种消息。 以上就是一个基于Spring Boot整合Netty和Protostuff实现客户与服务通信的简单示例。 ### 相关问题 1. 如何解决Netty中的粘包和拆包问题? 2. Protostuff序列化相比其他序列化方式(如JSON、Kryo)有什么优势? 3. 在Netty中如何实现心跳机制来保持长连接?</think>### 基于Spring Boot整合Netty和Protostuff实现通信 以下是完整的实现步骤,包含服务和客户代码: #### 1. 添加依赖(pom.xml) ```xml <dependencies> <!-- Netty --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.68.Final</version> </dependency> <!-- Protostuff --> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.7.2</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.7.2</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> ``` #### 2. 定义消息实体类 ```java public class Message { private int id; private String content; private long timestamp; // 省略getter/setter } ``` #### 3. Protostuff序列化工具类 ```java import io.protostuff.LinkedBuffer; import io.protostuff.ProtostuffIOUtil; import io.protostuff.Schema; import io.protostuff.runtime.RuntimeSchema; public class ProtostuffSerializer { private static final Schema<Message> schema = RuntimeSchema.getSchema(Message.class); public static byte[] serialize(Message message) { LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); try { return ProtostuffIOUtil.toByteArray(message, schema, buffer); } finally { buffer.clear(); } } public static Message deserialize(byte[] bytes) { Message message = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes, message, schema); return message; } } ``` #### 4. Netty服务实现 **启动类 (NettyServerStarter.java)** ```java import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; @Component public class NettyServerStarter implements CommandLineRunner { private static final int PORT = 8888; @Override public void run(String... args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new NettyServerInitializer()); ChannelFuture future = bootstrap.bind(PORT).sync(); System.out.println("Netty server started on port: " + PORT); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` **初始化类 (NettyServerInitializer.java)** ```java import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.codec.LengthFieldPrepender; public class NettyServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() // 处理粘包/拆包 .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4)) .addLast(new LengthFieldPrepender(4)) // 序列化处理器 .addLast(new ProtostuffDecoder()) .addLast(new ProtostuffEncoder()) // 业务处理器 .addLast(new NettyServerHandler()); } } ``` **处理器 (NettyServerHandler.java)** ```java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class NettyServerHandler extends SimpleChannelInboundHandler<Message> { @Override protected void channelRead0(ChannelHandlerContext ctx, Message msg) { System.out.println("Server received: " + msg.getContent()); // 构建响应 Message response = new Message(); response.setId(msg.getId()); response.setContent("Echo: " + msg.getContent()); response.setTimestamp(System.currentTimeMillis()); ctx.writeAndFlush(response); } } ``` #### 5. Netty客户实现 **启动类 (NettyClientStarter.java)** ```java @Component public class NettyClientStarter implements CommandLineRunner { private static final String HOST = "localhost"; private static final int PORT = 8888; @Override public void run(String... args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new NettyClientInitializer()); Channel channel = bootstrap.connect(HOST, PORT).sync().channel(); System.out.println("Connected to server"); // 发送测试消息 Message message = new Message(); message.setId(1); message.setContent("Hello from client"); message.setTimestamp(System.currentTimeMillis()); channel.writeAndFlush(message); channel.closeFuture().sync(); } finally { group.shutdownGracefully(); } } } ``` **初始化类 (NettyClientInitializer.java)** ```java public class NettyClientInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4)) .addLast(new LengthFieldPrepender(4)) .addLast(new ProtostuffDecoder()) .addLast(new ProtostuffEncoder()) .addLast(new NettyClientHandler()); } } ``` **处理器 (NettyClientHandler.java)** ```java public class NettyClientHandler extends SimpleChannelInboundHandler<Message> { @Override protected void channelRead0(ChannelHandlerContext ctx, Message msg) { System.out.println("Client received response: " + msg.getContent()); } } ``` #### 6. Protostuff编解码器 ```java // 编码器 public class ProtostuffEncoder extends MessageToByteEncoder<Message> { @Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) { byte[] bytes = ProtostuffSerializer.serialize(msg); out.writeBytes(bytes); } } // 解码器 public class ProtostuffDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { byte[] bytes = new byte[in.readableBytes()]; in.readBytes(bytes); out.add(ProtostuffSerializer.deserialize(bytes)); } } ``` ### 关键点说明 1. **粘包/拆包处理**:使用`LengthFieldBasedFrameDecoder`和`LengthFieldPrepender`解决TCP粘包问题 2. **序列化流程**: - 对象 → Protostuff序列化 → 字节数组 → Netty发送 - 接收字节数组 → Protostuff反序列化 → 对象 3. **通信流程**: ```mermaid sequenceDiagram Client->>Server: Protostuff编码消息 Server->>Client: Protostuff编码响应 ``` ### 测试方法 1. 启动服务应用 2. 启动客户应用 3. 观察控制台输出: ``` Server received: Hello from client Client received response: Echo: Hello from client ``` ### 相关问题 1. 如何处理Netty中的TCP粘包和拆包问题? 2. Protostuff相比JSON/Protobuf有哪些性能优势? 3. 如何在Netty中实现心跳机制保持长连接? 4. 如何设计支持多种消息类型的Netty通信协议? 5. 如何监控Netty服务的性能指标和连接状态?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值