Netty 拆包粘包和服务启动流程分析

通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Channel。只有清楚这些,才能更好的了解和使用Netty。还在等什么,快来学习吧!

知识结构图:

技术:Netty,拆包粘包,服务启动流程

说明:若你对NIO有一定的了解,对于本章知识来说有很大的帮助!NIO教程

源码:https://github.com/ITDragonBlog/daydayup/tree/master/Netty/netty-stu

Netty 重要组件

这里让你清楚了解 ChannelPipeline,ChannelHandlerContext,ChannelHandler,Channel 四者之间的关系。

这里让你清楚了解 NioEventLoopGroup,NioEventLoop,Channel 三者之间的关系。

这里让你清楚了解 ServerBootstrap,Channel 两者之间的关系。

看懂了这块的理论知识,后面Netty拆包粘包的代码就非常的简单。

Channel

Channel : Netty最核心的接口。NIO通讯模式中通过Channel进行Socket套接字的读,写和同时读写操作。

ChannelHandler : 因为直接使用Channel会比较麻烦,所以在Netty编程中通过ChannelHandler间接操作Channel,从而简化开发。

ChannelPipeline : 可以理解为一个管理ChandlerHandler的链表。对Channel进行操作时,Pipeline负责从尾部依次调用每一个Handler进行处理。每个Channel都有一个属于自己的ChannelPipeline。

ChannelHandlerContext : ChannelPipeline通过ChannelHandlerContext间接管理每个ChannelHandler。

如下图所示,结合代码,在服务器初始化和客户端创建连接的过程中加了四个Handler,分别是日志事务,字符串分割解码器,接受参数转字符串解码器,处理任务的Handler。

NioEventLoopGroup

EventLoopGroup : 本质是个线程池,继承了ScheduledExecutorService 定时任务线程池。

NioEventLoopGroup : 是用来处理NIO通信模式的线程池。每个线程池有N个NioEventLoop来处理Channel事件,每一个NioEventLoop负责处理N个Channel。

NioEventLoop : 负责不停地轮询IO事件,处理IO事件和执行任务,类比多路复用器,细化分三件事。

1 轮询注册到Selector上所有的Channel的IO事件

2 处理产生网络IO事件的Channel

3 处理队列中的任务

ServerBootstrap

本章重点,Netty是如何通过NIO辅助启动类来初始化Channel的?先看下面的源码。

  1. @Override
  2. void init(Channel channel) throws Exception {
  3. final Map<ChannelOption<?>, Object> options = options0();
  4. synchronized (options) {
  5. setChannelOptions(channel, options, logger);
  6. }
  7. final Map<AttributeKey<?>, Object> attrs = attrs0();
  8. synchronized (attrs) {
  9. for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
  10. @SuppressWarnings("unchecked")
  11. AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
  12. channel.attr(key).set(e.getValue());
  13. }
  14. }
  15. ChannelPipeline p = channel.pipeline();
  16. final EventLoopGroup currentChildGroup = childGroup;
  17. final ChannelHandler currentChildHandler = childHandler;
  18. final Entry<ChannelOption<?>, Object>[] currentChildOptions;
  19. final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
  20. synchronized (childOptions) {
  21. currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
  22. }
  23. synchronized (childAttrs) {
  24. currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
  25. }
  26. p.addLast(new ChannelInitializer<Channel>() {
  27. @Override
  28. public void initChannel(final Channel ch) throws Exception {
  29. final ChannelPipeline pipeline = ch.pipeline();
  30. ChannelHandler handler = config.handler();
  31. if (handler != null) {
  32. pipeline.addLast(handler);
  33. }
  34. ch.eventLoop().execute(new Runnable() {
  35. @Override
  36. public void run() {
  37. pipeline.addLast(new ServerBootstrapAcceptor(
  38. ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
  39. }
  40. });
  41. }
  42. });
  43. }

服务器启动和连接过程:

第一步:是给Channel设置options和attrs,

第二步:复制childGroup,childHandler,childOptions和childAttrs等待服务器和客户端连接,

第三步:实例化一个ChannelInitializer,添加到Pipeline的末尾。

第四步:当Channel注册到NioEventLoop时,ChannelInitializer触发initChannel方法,pipeline装入自定义的Handler,给Channel设置一下child配置。

小结:

1 group,options,attrs,handler,是在服务器端初始化时配置,是AbstractBootstrap的方法。

2 childGroup,childOption,childAttr,childHandler,是在服务器与客户端建立Channel后配置,是ServerBootstrap的方法。

3 Bootstrap 和 ServerBootstrap 都继承了AbstractBootstrap类。

4 若不设置childGroup,则默认取group值。

5 Bootstrap 和 ServerBootstrap 启动服务时,都会执行验证方法,判断必填参数是否都有配置。

Netty 拆包粘包

这里通过介绍Netty拆包粘包问题来对Netty进行入门学习。

在基于流的传输中,即便客户端发送独立的数据包,操作系统也会将其转换成一串字节队列,而服务端一次读取到的字节数又不确定。再加上网络传输的快慢。服务端很难完整的接收到数据。

常见的拆包粘包方法有三种

1 服务端设置一次接收字节的长度。若服务端接收的字节长度不满足要求则一直处于等待。客户端为满足传输的字节长度合格,可以考虑使用空格填充。

2 服务端设置特殊分隔符。客户端通过特殊分隔符粘包,服务端通过特殊分隔符拆包。

3 自定义协议。数据传输一般分消息头和消息体,消息头中包含了数据的长度。服务端先接收到消息头,得知需要接收N个数据,然后服务端接收直到数据为N个为止。

本章采用第二种,用特殊分隔符的方式。

创建服务端代码流程

第一步:准备两个线程池。一个用于接收事件的boss线程池,另一个用于处理事件的worker线程池。

第二步:服务端实例化ServerBootstrap NIO服务辅助启动类。用于简化提高开发效率。

第三步:配置服务器启动参数。比如channel的类型,接收channel的EventLoop,初始化的日志打印事件,建立连接后的事件(拆包,对象转字符串,自定义事件),初始化的配置和建立连接后的配置。

第四步:绑定端口,启动服务。Netty会根据第三步配置的参数启动服务。

第五步:关闭资源。

  1. package com.itdragon.delimiter;
  2. import io.netty.bootstrap.ServerBootstrap;
  3. import io.netty.buffer.ByteBuf;
  4. import io.netty.buffer.Unpooled;
  5. import io.netty.channel.ChannelFuture;
  6. import io.netty.channel.ChannelInitializer;
  7. import io.netty.channel.ChannelOption;
  8. import io.netty.channel.EventLoopGroup;
  9. import io.netty.channel.nio.NioEventLoopGroup;
  10. import io.netty.channel.socket.SocketChannel;
  11. import io.netty.channel.socket.nio.NioServerSocketChannel;
  12. import io.netty.handler.codec.DelimiterBasedFrameDecoder;
  13. import io.netty.handler.codec.FixedLengthFrameDecoder;
  14. import io.netty.handler.codec.string.StringDecoder;
  15. import io.netty.handler.logging.LogLevel;
  16. import io.netty.handler.logging.LoggingHandler;
  17. public class ITDragonServer {
  18. private static final Integer PORT = 8888; // 被监听端口号
  19. private static final String DELIMITER = "_$"; // 拆包分隔符
  20. public static void main(String[] args) {
  21. EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于接收进来的连接
  22. EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理进来的连接
  23. try {
  24. ServerBootstrap serverbootstrap = new ServerBootstrap(); // 启动NIO服务的辅助启动类
  25. serverbootstrap.group(bossGroup, workerGroup) // 分别设置bossGroup, workerGroup 顺序不能反
  26. .channel(NioServerSocketChannel.class) // Channel的创建工厂,启动服务时会通过反射的方式来创建一个NioServerSocketChannel对象
  27. .handler(new LoggingHandler(LogLevel.INFO)) // handler在初始化时就会执行,可以设置打印日志级别
  28. .childHandler(new ChannelInitializer<SocketChannel>() { // childHandler会在客户端成功connect后才执行,这里实例化ChannelInitializer
  29. @Override
  30. protected void initChannel(SocketChannel socketChannel) throws Exception { // initChannel方法执行后删除实例ChannelInitializer,添加以下内容
  31. ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER.getBytes()); // 获取特殊分隔符的ByteBuffer
  32. socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128, delimiter)); // 设置特殊分隔符用于拆包
  33. // socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8)); 设置指定长度分割
  34. socketChannel.pipeline().addLast(new StringDecoder()); // 设置字符串形式的解码
  35. socketChannel.pipeline().addLast(new ITDragonServerHandler()); // 自定义的服务器处理类,负责处理事件
  36. }
  37. })
  38. .option(ChannelOption.SO_BACKLOG, 128) // option在初始化时就会执行,设置tcp缓冲区
  39. .childOption(ChannelOption.SO_KEEPALIVE, true); // childOption会在客户端成功connect后才执行,设置保持连接
  40. ChannelFuture future = serverbootstrap.bind(PORT).sync(); // 绑定端口, 阻塞等待服务器启动完成,调用sync()方法会一直阻塞等待channel的停止
  41. future.channel().closeFuture().sync(); // 等待关闭 ,等待服务器套接字关闭
  42. } catch (Exception e) {
  43. e.printStackTrace();
  44. } finally {
  45. workerGroup.shutdownGracefully(); // 关闭线程组,先打开的后关闭
  46. bossGroup.shutdownGracefully();
  47. }
  48. }
  49. }

核心参数说明

NioEventLoopGroup : 是用来处理I/O操作的多线程事件循环器。 Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。

ServerBootstrap : 启动NIO服务的辅助启动类。先配置Netty服务端启动参数,执行bind(PORT)方法才算真正启动服务。

group : 注册EventLoopGroup

channel : channelFactory,用于配置通道的类型。

handler : 服务器始化时就会执行的事件。

childHandler : 服务器在和客户端成功连接后会执行的事件。

initChannel : channelRegistered事件触发后执行,删除ChannelInitializer实例,添加该方法体中的handler。

option : 服务器始化的配置。

childOption : 服务器在和客户端成功连接后的配置。

SocketChannel : 继承了Channel,通过Channel可以对Socket进行各种操作。

ChannelHandler : 通过ChannelHandler来间接操纵Channel,简化了开发。

ChannelPipeline : 可以看成是一个ChandlerHandler的链表。

ChannelHandlerContext : ChannelPipeline通过ChannelHandlerContext来间接管理ChannelHandler。

自定义服务器处理类

第一步:继承 ChannelInboundHandlerAdapter,其父类已经实现了ChannelHandler接口,简化了开发。

第二步:覆盖 chanelRead()事件处理方法 ,每当服务器从客户端收到新的数据时,该方法会在收到消息时被调用。

第三步:释放 ByteBuffer,ByteBuf是一个引用计数对象,这个对象必须显示地调用release()方法来释放。

第四步:异常处理,即当Netty由于IO错误或者处理器在处理事件时抛出的异常时触发。在大部分情况下,捕获的异常应该被记录下来并且把关联的channel给关闭掉。

  1. package com.itdragon.delimiter;
  2. import com.itdragon.utils.ITDragonUtil;
  3. import io.netty.buffer.Unpooled;
  4. import io.netty.channel.ChannelHandlerContext;
  5. import io.netty.channel.ChannelInboundHandlerAdapter;
  6. import io.netty.util.ReferenceCountUtil;
  7. public class ITDragonServerHandler extends ChannelInboundHandlerAdapter{
  8. private static final String DELIMITER = "_$"; // 拆包分隔符
  9. @Override
  10. public void channelRead(ChannelHandlerContext chc, Object msg) {
  11. try {
  12. // 普通读写数据
  13. /* 设置字符串形式的解码 new StringDecoder() 后可以直接使用
  14. ByteBuf buf = (ByteBuf) msg;
  15. byte[] req = new byte[buf.readableBytes()];
  16. buf.readBytes(req);
  17. String body = new String(req, "utf-8");
  18. */
  19. System.out.println("Netty Server : " + msg.toString());
  20. // 分隔符拆包
  21. String response = ITDragonUtil.cal(msg.toString())+ DELIMITER;
  22. chc.channel().writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. } finally {
  26. ReferenceCountUtil.release(msg); // 写入方法writeAndFlush ,Netty已经释放了
  27. }
  28. }
  29. // 当出现Throwable对象才会被调用
  30. @Override
  31. public void exceptionCaught(ChannelHandlerContext chc, Throwable cause) {
  32. // 这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
  33. cause.printStackTrace();
  34. chc.close();
  35. }
  36. }

客户端启动流程

第一步:创建一个用于发送请求的线程池。

第二步:客户端实例化Bootstrap NIO服务启动辅助类,简化开发。

第三步:配置参数,粘包,发送请求。

第四步:关闭资源。

值得注意的是,和ServerBootstrap不同,它并没有childHandler和childOption方法。

  1. package com.itdragon.delimiter;
  2. import io.netty.bootstrap.Bootstrap;
  3. import io.netty.buffer.ByteBuf;
  4. import io.netty.buffer.Unpooled;
  5. import io.netty.channel.ChannelFuture;
  6. import io.netty.channel.ChannelInitializer;
  7. import io.netty.channel.ChannelOption;
  8. import io.netty.channel.nio.NioEventLoopGroup;
  9. import io.netty.channel.socket.SocketChannel;
  10. import io.netty.channel.socket.nio.NioSocketChannel;
  11. import io.netty.handler.codec.DelimiterBasedFrameDecoder;
  12. import io.netty.handler.codec.FixedLengthFrameDecoder;
  13. import io.netty.handler.codec.string.StringDecoder;
  14. public class ITDragonClient {
  15. private static final Integer PORT = 8888;
  16. private static final String HOST = "127.0.0.1";
  17. private static final String DELIMITER = "_$"; // 拆包分隔符
  18. public static void main(String[] args) {
  19. NioEventLoopGroup group = new NioEventLoopGroup();
  20. try {
  21. Bootstrap bootstrap = new Bootstrap();
  22. bootstrap.group(group)
  23. .channel(NioSocketChannel.class)
  24. .handler(new ChannelInitializer<SocketChannel>() {
  25. @Override
  26. protected void initChannel(SocketChannel socketChannel) throws Exception {
  27. ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER.getBytes());
  28. // 设置特殊分隔符
  29. socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128, delimiter));
  30. // 设置指定长度分割 不推荐,两者选其一
  31. // socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8));
  32. socketChannel.pipeline().addLast(new StringDecoder());
  33. socketChannel.pipeline().addLast(new ITDragonClientHandler());
  34. }
  35. })
  36. .option(ChannelOption.SO_KEEPALIVE, true);
  37. ChannelFuture future = bootstrap.connect(HOST, PORT).sync(); // 建立连接
  38. future.channel().writeAndFlush(Unpooled.copiedBuffer(("1+1"+DELIMITER).getBytes()));
  39. future.channel().writeAndFlush(Unpooled.copiedBuffer(("6+1"+DELIMITER).getBytes()));
  40. future.channel().closeFuture().sync();
  41. } catch (Exception e) {
  42. e.printStackTrace();
  43. } finally {
  44. group.shutdownGracefully();
  45. }
  46. }
  47. }

客户端请求接收类

和服务器处理类一样,这里只负责打印数据。

  1. package com.itdragon.delimiter;
  2. import io.netty.channel.ChannelHandlerContext;
  3. import io.netty.channel.ChannelInboundHandlerAdapter;
  4. import io.netty.util.ReferenceCountUtil;
  5. public class ITDragonClientHandler extends ChannelInboundHandlerAdapter{
  6. @Override
  7. public void channelRead(ChannelHandlerContext chc, Object msg) {
  8. try {
  9. /* 设置字符串形式的解码 new StringDecoder() 后可以直接使用
  10. ByteBuf buf = (ByteBuf) msg;
  11. byte[] req = new byte[buf.readableBytes()];
  12. buf.readBytes(req);
  13. String body = new String(req, "utf-8");
  14. */
  15. System.out.println("Netty Client :" + msg);
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. } finally {
  19. ReferenceCountUtil.release(msg);
  20. }
  21. }
  22. public void exceptionCaught(ChannelHandlerContext chc, Throwable cause) {
  23. cause.printStackTrace();
  24. chc.close();
  25. }
  26. }

打印结果

  1. 一月 29, 2018 11:31:10 上午 io.netty.handler.logging.LoggingHandler channelRegistered
  2. 信息: [id: 0xcf3a3ac1] REGISTERED
  3. 一月 29, 2018 11:31:11 上午 io.netty.handler.logging.LoggingHandler bind
  4. 信息: [id: 0xcf3a3ac1] BIND: 0.0.0.0/0.0.0.0:8888
  5. 一月 29, 2018 11:31:11 上午 io.netty.handler.logging.LoggingHandler channelActive
  6. 信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] ACTIVE
  7. 一月 29, 2018 11:31:18 上午 io.netty.handler.logging.LoggingHandler channelRead
  8. 信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0xf1b8096b, L:/127.0.0.1:8888 - R:/127.0.0.1:4777]
  9. 一月 29, 2018 11:31:18 上午 io.netty.handler.logging.LoggingHandler channelReadComplete
  10. 信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
  11. Netty Server : 1+1
  12. Netty Server : 6+1
  13. Netty Client :2
  14. Netty Client :7

从日志中可以看出Channel的状态从REGISTERED ---> ACTIVE ---> READ ---> READ COMPLETE。服务端也是按照特殊分割符拆包。

总结

看完本章,你必须要掌握的三个知识点:NioEventLoopGroup,ServerBootstrap,ChannelHandlerAdapter

1 NioEventLoopGroup 本质就是一个线程池,管理多个NioEventLoop,一个NioEventLoop管理多个Channel。

2 NioEventLoop 负责不停地轮询IO事件,处理IO事件和执行任务。

3 ServerBootstrap 是NIO服务的辅助启动类,先配置服务参数,后执行bind方法启动服务。

4 Bootstrap 是NIO客户端的辅助启动类,用法和ServerBootstrap类似。

5 Netty 使用FixedLengthFrameDecoder 固定长度拆包,DelimiterBasedFrameDecoder 分隔符拆包。

到这里,Netty的拆包粘包,以及Netty的重要组件,服务器启动流程到这里就结束了,如果觉得不错可以点一个** "推荐" ** ,也可以** "关注" **我哦。

优质文章

http://blog.csdn.net/spiderdog/article/category/1800249

https://www.jianshu.com/p/c5068caab217

Netty 拆包粘包和服务启动流程分析的更多相关文章

  1. 【转】Netty 拆包粘包和服务启动流程分析

    原文:https://www.cnblogs.com/itdragon/archive/2018/01/29/8365694.html Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你 ...

  2. netty拆包粘包

    客户端 tcp udp socket网络编程接口 http/webservice mqtt/xmpp 自定义RPC (dubbo) 应用层 服务端 ServerSocket ss = new serv ...

  3. Cinder Volume 服务启动流程分析和周期性任务分析

    1.cinder-volume服务的程序入口 #!/usr/bin/python2 # PBR Generated from u'console_scripts' import sys from ci ...

  4. Netty入门教程:Netty拆包粘包技术讲解

    Netty编解码技术是什么意思呢?所谓的编解码技术,说白了就是java序列化技术.序列化有两个目的: 1.进行网络传输2.对象持久化 虽然我们可以使用java进行序列化,Netty去传输.但是java ...

  5. 服务端NETTY 客户端非NETTY处理粘包和拆包的问题

    之前为了调式和方便一直没有处理粘包的问题,今天专门花了时间来搞NETTY的粘包处理,要知道在高并发下,不处理粘包是不可能的,数据流的混乱会造成业务的崩溃什么的我就不说了.所以这个问题 在我心里一直是个 ...

  6. Netty中粘包和拆包的解决方案

    粘包和拆包是TCP网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包和拆包 TCP是个“流”协议,所谓流,就是没有界限的一串 ...

  7. Netty入门系列(2) --使用Netty解决粘包和拆包问题

    前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络 ...

  8. 使用Netty如何解决拆包粘包的问题

    首先,我们通过一个DEMO来模拟TCP的拆包粘包的情况:客户端连续向服务端发送100个相同消息.服务端的代码如下: AtomicLong count = new AtomicLong(0); NioE ...

  9. cassandra 服务启动流程

    cassandra 服务启动流程 1.  setup 1)   CassandraDaemon ->main publicstaticvoidmain(String[]args) { insta ...

随机推荐

  1. python2中的__init__.py文件的作用

    python2中的__init__.py文件的作用: 1.python的每个模块的包中,都必须有一个__init__.py文件,有了这个文件,我们才能导入这个目录下的module. 2.__init_ ...

  2. Python的伪私有属性

    什么是伪私有属性? 在Python中,没有类似 private 之类的关键字来声明私有方法或属性. Python中要声明私有属性,需要在属性前加上双下划线(但是结尾处不能有双下划线),如:self._ ...

  3. CSS Content 属性

    content 属性,用在after,before伪元素上,用于在选中的元素前后插入内容. 插入的内容多种多样, 纯文字 h1::after{ content:"h1后插入内容" ...

  4. 前端自动化(三) 合并压缩css、压缩js、添加时间戳、打包上线操作

    前端自动化(三)   合并压缩css.压缩js.添加时间戳.打包上线操作 此文在前两篇基础上对比参考,会很方便理解 gulp.task("delete",function(){ r ...

  5. MySQL并发复制系列二:多线程复制 2016

    并发复制(Parallel Replication) 系列二: Enhanced Multi-threaded Slaves作者:沃趣科技MySQL数据库工程师  麻鹏飞 首先梳理下传统MySQL/M ...

  6. 编写Qt Designer自定义控件

    一)流程概述 在使用Qt Designer设计窗体界面时,我们可以使用Widget Box里的窗体控件非常方便的绘制界面,比如拖进去一个按钮,一个文本编辑器等.虽然Qt Designer里的控件可以满 ...

  7. Python 解析构建数据大杂烩 -- csv、xml、json、excel

    Python 可以通过各种库去解析我们常见的数据.其中 csv 文件以纯文本形式存储表格数据,以某字符作为分隔值,通常为逗号:xml 可拓展标记语言,很像超文本标记语言 Html ,但主要对文档和数据 ...

  8. Mac 安装 Gradle

    首先下载 Gradle 通过官网进行下载 https://gradle.org 下载的文件名可能是 gradle-3.3-bin.zip 解压 将此文件解压到任意位置,如解压到 /usr/local ...

  9. A. Vasya and Football

    A. Vasya and Football time limit per test 2 seconds memory limit per test 256 megabytes input standa ...

  10. UEP-保存

    uep的保存操作分为ajaxgrid和ajaxform两种方式 1.ajaxgrid public void storeInfoSave(){ try { //两个dataWrap 一个dataWra ...