Chinaunix首页 | 论坛 | 博客
  • 博客访问: 7191060
  • 博文数量: 510
  • 博客积分: 12019
  • 博客等级: 上将
  • 技术积分: 6836
  • 用 户 组: 普通用户
  • 注册时间: 2005-08-01 16:46
文章分类

全部博文(510)

文章存档

2022年(2)

2021年(6)

2020年(59)

2019年(4)

2018年(10)

2017年(5)

2016年(2)

2015年(4)

2014年(4)

2013年(16)

2012年(47)

2011年(65)

2010年(46)

2009年(34)

2008年(52)

2007年(52)

2006年(80)

2005年(22)

分类: Java

2012-04-07 11:44:25

认识:

Apache MINA 2 是一个开发高性能和高可伸缩性网络应用程序的网络应用框架。它提供了一个抽象的事件驱动的异步 API,可以使用 TCP/IP、UDP/IP、串口和虚拟机内部的管道等传输方式。Apache MINA 2 可以作为开发网络应用程序的一个良好基础。

 

学习MINA的原因:

由 于项目中使用到了socket,并且使用的是传统阻塞式socket编程,项目二期的时候发现生产环境过一段时间就会内存溢出,性能也没有一个好的保证, 用检测工具发现线程很多阻塞了。以前公司都没有做过socket程序占大部分的这种项目,对于我们而言也只能网上找资料学习了。前一段时间对IM即时通讯 感兴趣就玩了下openfire,听说底层也是mina,当时也不知道MINA是个什么东东,没注意。后来随着我对我们项目框架的思考,发现我们开发人员 自己来写socket底层,一来socket这块比较生疏,不容易写好,弄不好哪里没关闭导致什么问题出现都说不定。二来技术人员很容易钻到技术实现这一 块来,而不是更多的去关注实现业务。这样非常不好,我在想是否有一种开源框架,帮我们封装好了socket底层,我们只需要用它就可以了呢,找了下,果然 有,不过普遍反应mina比较好,而且是非阻塞的(关于阻塞和非阻塞有什么区别,网上有很多资料),所以就选择学习mina 了,之所以学习它是因为想在后期优化项目中现有socket程序。

 

我自己实现的例子介绍(我使用自定义编解码): 

ProtocolCodecFilter 用 来在字节流和消息对象之间互相转换。当该过滤器接收到字节流的时候,需要首先判断消息的边界,然后把表示一条消息的字节提取出来,通过一定的逻辑转换成消 息对象,再把消息对象往后传递,交给 I/O 处理器来执行业务逻辑。这个过程称为“解码”。与“解码”对应的是“编码”过程。在“编码”的时候,过滤器接收到的是消息对象,通过与“解码”相反的逻 辑,把消息对象转换成字节,并反向传递,交给 I/O 服务来执行 I/O 操作。

在“编码”和“解码”中的一个重要问题是如何在字节流中判断消息的边界。通常来说,有三种办法解决这个问题:

  • 使用固定长度的消息。这种方式实现起来比较简单,只需要每次读取特定数量的字节即可。
  • 使用固定长度的消息头来指明消息主体的长度。比如每个消息开始的 4 个字节的值表示了后面紧跟的消息主体的长度。只需要首先读取该长度,再读取指定数量的字节即可。
  • 使用分隔符。消息之间通过特定模式的分隔符来分隔。每次只要遇到该模式的字节,就表示到了一个消息的末尾。

 

具体到示例应用来说,客户端和服务器之间的通信协议比较复杂,有不同种类的消息。每种消息的格式都不相同,同类消息的内容也不尽相同。因此,使用固定长度的消息头来指明消息主体的长度就成了最好的选择。

 

消息头

4个字节

消息体

不定长度

 

 

mina编解码流程:request->MyDecoder->MyHandler->MyEncode->response

 

---------------------自定义编解码代码实现----------------------------

 

解码:MyProtocalDecoder.java

 

Java代码  收藏代码
  1. package com.ccic.mina.myprotocal;  
  2.   
  3. import java.nio.charset.Charset;  
  4. import java.nio.charset.CharsetDecoder;  
  5.   
  6. import org.apache.mina.core.buffer.IoBuffer;  
  7. import org.apache.mina.core.session.AttributeKey;  
  8. import org.apache.mina.core.session.IoSession;  
  9. import org.apache.mina.filter.codec.ProtocolDecoder;  
  10. import org.apache.mina.filter.codec.ProtocolDecoderOutput;  
  11.   
  12. public class MyProtocalDecoder implements ProtocolDecoder {  
  13.     private final AttributeKey CONTEXT = new AttributeKey(getClass(), "context");  
  14.     private final Charset charset;  
  15.     private int maxPackLength = 4000;  
  16.   
  17.     public MyProtocalDecoder() {  
  18.         this(Charset.defaultCharset());  
  19.     }  
  20.   
  21.     public MyProtocalDecoder(Charset charset) {  
  22.         this.charset = charset;  
  23.     }  
  24.   
  25.     public int getMaxLineLength() {  
  26.         return maxPackLength;  
  27.     }  
  28.   
  29.     public void setMaxLineLength(int maxLineLength) {  
  30.         if (maxLineLength <= 0) {  
  31.             throw new IllegalArgumentException("maxLineLength: " + maxLineLength);  
  32.         }  
  33.         this.maxPackLength = maxLineLength;  
  34.     }  
  35.   
  36.     private Context getContext(IoSession session) {  
  37.         Context ctx;  
  38.         ctx = (Context) session.getAttribute(CONTEXT);  
  39.         if (ctx == null) {  
  40.             ctx = new Context();  
  41.             session.setAttribute(CONTEXT, ctx);  
  42.         }  
  43.         return ctx;  
  44.     }  
  45.   
  46.     public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {  
  47.         final int packHeadLength = 8;  
  48.         // 先获取上次的处理上下文,其中可能有未处理完的数据  
  49.         Context ctx = getContext(session);  
  50.         // 先把当前buffer中的数据追加到Context的buffer当中  
  51.         ctx.append(in);  
  52.         // 把position指向0位置,把limit指向原来的position位置  
  53.         IoBuffer buf = ctx.getBuffer();  
  54.         buf.flip();  
  55.         // 然后按数据包的协议进行读取  
  56.         while (buf.remaining() >= packHeadLength) {  
  57.             buf.mark();  
  58.             // 读取消息头部分  
  59.             int length = buf.getInt();  
  60.   
  61.             // 检查读取是否正常,不正常的话清空buffer  
  62.             if (length < 0 || length > maxPackLength) {  
  63.                 System.out.println("长度[" + length + "] > maxPackLength or <0....");  
  64.                 buf.clear();  
  65.                 break;  
  66.             }  
  67.             // 读取正常的消息,并写入输出流中,以便IoHandler进行处理  
  68.             else if (length >= packHeadLength && length - packHeadLength <= buf.remaining()) {  
  69.                 int oldLimit2 = buf.limit();  
  70.                 buf.limit(buf.position() + length - packHeadLength);  
  71.                 String content = buf.getString(ctx.getDecoder());  
  72.                 buf.limit(oldLimit2);  
  73.                 out.write(content);  
  74.             } else {  
  75.                 // 如果消息包不完整  
  76.                 // 将指针重新移动消息头的起始位置  
  77.                 buf.reset();  
  78.                 break;  
  79.             }  
  80.         }  
  81.         if (buf.hasRemaining()) {  
  82.             // 将数据移到buffer的最前面  
  83.             IoBuffer temp = IoBuffer.allocate(maxPackLength).setAutoExpand(true);  
  84.             temp.put(buf);  
  85.             temp.flip();  
  86.             buf.clear();  
  87.             buf.put(temp);  
  88.   
  89.         } else {// 如果数据已经处理完毕,进行清空  
  90.             buf.clear();  
  91.         }  
  92.     }  
  93.   
  94.     public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {  
  95.     }  
  96.   
  97.     public void dispose(IoSession session) throws Exception {  
  98.         Context ctx = (Context) session.getAttribute(CONTEXT);  
  99.         if (ctx != null) {  
  100.             session.removeAttribute(CONTEXT);  
  101.         }  
  102.     }  
  103.   
  104.     // 记录上下文,因为数据触发没有规模,很可能只收到数据包的一半  
  105.     // 所以,需要上下文拼起来才能完整的处理  
  106.     private class Context {  
  107.         private final CharsetDecoder decoder;  
  108.         private IoBuffer buf;  
  109.         private int matchCount = 0;  
  110.         private int overflowPosition = 0;  
  111.   
  112.         private Context() {  
  113.             decoder = charset.newDecoder();  
  114.             buf = IoBuffer.allocate(3000).setAutoExpand(true);  
  115.         }  
  116.   
  117.         public CharsetDecoder getDecoder() {  
  118.             return decoder;  
  119.         }  
  120.   
  121.         public IoBuffer getBuffer() {  
  122.             return buf;  
  123.         }  
  124.   
  125.         public int getOverflowPosition() {  
  126.             return overflowPosition;  
  127.         }  
  128.   
  129.         public int getMatchCount() {  
  130.             return matchCount;  
  131.         }  
  132.   
  133.         public void setMatchCount(int matchCount) {  
  134.             this.matchCount = matchCount;  
  135.         }  
  136.   
  137.         public void reset() {  
  138.             overflowPosition = 0;  
  139.             matchCount = 0;  
  140.             decoder.reset();  
  141.         }  
  142.   
  143.         public void append(IoBuffer in) {  
  144.             getBuffer().put(in);  
  145.   
  146.         }  
  147.   
  148.     }  
  149. }  

 

 

编码:MyProtocalEncoder.java

 

Java代码  收藏代码
  1. package com.ccic.mina.myprotocal;  
  2.   
  3. import java.nio.charset.Charset;  
  4.   
  5. import org.apache.mina.core.buffer.IoBuffer;  
  6. import org.apache.mina.core.session.IoSession;  
  7. import org.apache.mina.filter.codec.ProtocolEncoderAdapter;  
  8. import org.apache.mina.filter.codec.ProtocolEncoderOutput;  
  9.   
  10. public class MyProtocalEncoder extends ProtocolEncoderAdapter {  
  11.     private final Charset charset;  
  12.   
  13.     public MyProtocalEncoder(Charset charset) {  
  14.         this.charset = charset;  
  15.     }  
  16.   
  17.     // 在此处实现包的编码工作,并把它写入输出流中  
  18.     public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {  
  19.         String value = (String) message;  
  20.         IoBuffer buf = IoBuffer.allocate(value.getBytes().length);  
  21.         buf.setAutoExpand(true);  
  22.         if (value != null)  
  23.             buf.put(value.trim().getBytes());  
  24.         buf.flip();  
  25.         out.write(buf);  
  26.     }  
  27.   
  28.       
  29. }  

 编解码工厂类:MyProtocalCodecFactory.java

 

Java代码  收藏代码
  1. package com.ccic.mina.myprotocal;  
  2.   
  3. import java.nio.charset.Charset;  
  4.   
  5. import org.apache.mina.core.session.IoSession;  
  6. import org.apache.mina.filter.codec.ProtocolCodecFactory;  
  7. import org.apache.mina.filter.codec.ProtocolDecoder;  
  8. import org.apache.mina.filter.codec.ProtocolEncoder;  
  9.   
  10. public class MyProtocalCodecFactory implements ProtocolCodecFactory {  
  11.     private final MyProtocalEncoder encoder;  
  12.     private final MyProtocalDecoder decoder;  
  13.   
  14.     public MyProtocalCodecFactory(Charset charset) {  
  15.         encoder = new MyProtocalEncoder(charset);  
  16.         decoder = new MyProtocalDecoder(charset);  
  17.     }  
  18.   
  19.     public ProtocolEncoder getEncoder(IoSession session) {  
  20.         return encoder;  
  21.     }  
  22.   
  23.     public ProtocolDecoder getDecoder(IoSession session) {  
  24.         return decoder;  
  25.     }  
  26.   
  27. }  

 

 

Handler类:MyHandler.java

Java代码  收藏代码
  1. package com.ccic.mina.myprotocal;  
  2.   
  3. import org.apache.mina.core.service.IoHandlerAdapter;  
  4. import org.apache.mina.core.session.IdleStatus;  
  5. import org.apache.mina.core.session.IoSession;  
  6. import org.slf4j.Logger;  
  7. import org.slf4j.LoggerFactory;  
  8.   
  9. class MyHandler extends IoHandlerAdapter {  
  10.     private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);  
  11.   
  12.     @Override  
  13.     public void sessionOpened(IoSession session) throws Exception {  
  14.         LOGGER.info(" session is Opened..." + session.getId());  
  15.     }  
  16.   
  17.     @Override  
  18.     public void exceptionCaught(IoSession session, Throwable cause) throws Exception {  
  19.         // log.warn(" cause.getMessage()..." + cause.printStackTrace());  
  20.         session.close(true);  
  21.     }  
  22.   
  23.     @Override  
  24.     public void messageReceived(IoSession session, Object message) throws Exception {  
  25.         String pack = (String) message;  
  26.   
  27.         // 接收报文,进行业务处理  
  28.   
  29.         // 返回报文给客户端  
  30.         session.write(pack);  
  31.     }  
  32.   
  33.     @Override  
  34.     public void sessionIdle(IoSession session, IdleStatus status) throws Exception {  
  35.         LOGGER.info("IDLE " + session.getIdleCount(status));  
  36.     }  
  37. }  

 

 

 

 

客户端: MyProtocalClient.java

 

Java代码  收藏代码
  1. package com.ccic.mina.myprotocal;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.DataOutputStream;  
  5. import java.io.InputStreamReader;  
  6. import java.net.Socket;  
  7.   
  8. public class MyProtocalClient {  
  9.     // mina编解码流程:request->MyDecoder->MyHandler->MyEncode->response  
  10.     // 对于长度>1000字节的数据包采用分段发送。  
  11.     public static void main(String[] args) {  
  12.         try {  
  13.             Socket socket = new Socket("127.0.0.1"8080);  
  14.             DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
  15.             InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());  
  16.             BufferedReader in = new BufferedReader(inputStreamReader);  
  17.             for (int i = 0; i < 200; i++) {  
  18.                 long startTime = System.currentTimeMillis(); // 获取开始时间  
  19.                 String str = "2BC0A039329D8FF6F4CB8BAA71A45FED8E119BEB76F0F1B1AA99F73B867C1685AF0F903F10F65505A4D618E30C0A1A9046FF3301A368AD36A7ADC78761E6C4C758D2611F3F3E3C448E63CE1B2CA88ACF7C54F44AC3621D927C16E5C4876FFDF51392FF7BFE8392420B6125504F53AB5C5CB1C350EA67BE577B6DB689F63C47B5F5593C55B607667240A57DF820109A13194C42AA1F3205771AB9E3C359555BC973FF59DD752E19CB472B61D8FA910A0C820A2677A2032723A00E87079F0A79BB9ACA930EBF7F25BC31AA7E1DA70810FC2ABA508269185FC6306082E966D82464CBF07B77502962EE378629C74B26BD3B536CDAA049D3841B36B7B7A4E9EC95CDE330ABFCD594DF4FAE3DEEB99DBC8DDA054F4D6B0F0EAEE82A21771A92635CAD9FFE021A2A408BC7C40060CCA98C77FEB8A7F5BC8AD5A64CF58BC0D5C9ED56D1E2450A2A674E53093BC015E2CEFC1D9B6B8BC79B85868C47586632DDEA08ED9EEE31A4DC9D777C28AD65892E1C5E1260AFF9299E162FE3276CF1A0FAD89878FF89ACEFF48C2B0FCD07E272C834F0C07C1838A39F019D1184232688F40AC0AE2F302B8F20A12F7D7511042E7B68E0C07D3B94CC61609097B9B7062B9DEE91E73DCDD268CBD247DB2348311A4EC1A7CAA554287C464F751960624236313976D16B5C735F1083FD18CC1FB56614A3B9B7F32249A38FD1A6EF235369E2311D6C964F2D459C75EA732BD1F3095BB2DA5821655974E1A53E5EBDA47652000D563F52A48CA00F395EDAEC58EEBE8D46C57886F8C5811E6C8F5A979DE911249F07370FB8BEF8BB35E0E64608EC43581DFBFEABE6C11337FD05DCBDC9242EBFD42D82DD59D72682D3C94945FEB03051153863CD3556E6B87BABCF1A5B01698D459FD01CFB7C28C650E0A8FE575C8E380601D55E517DF5852F07A49CF982635FA1E4689AF1D9B94F02D8F00B4AED356401FF6FFC1FC73C99D3025C08096FEF6659F32EA4D53279FAC697D56339F1A5F689E806F0F900A09A8F4CDB64B5DACEF7D6DC06460217234794DB61CC5AD6E8534B4BA5FB3B38B8D195E3B71AEBA4983606E00ECDC644CFFC5696140C78767139B3806C68F4906810030BC23DFE91C1C8CD11F758CCB3E4BCFD254B5D3959EC7F7764CE76DB74E117E7F844ADA94F99B02AC961A87C3084358D6B64540898788BDAA8455F374B607C7ACAB489BE099AB7C7AD78EFF8C1405A90DDAF23EB5D454167178620458227922B8A8E9E1D8D980089FBFC8143BA8FFD96E49A18C32BAEFB088956A75192805D44C5C2BFB345D6FA54A74C8B8B6BD566F24DB3BAB58F71BDB03807A566DEFCAC57889F2CE4803B39D64AF61291D3C56A5C0B4F3C3E287CAD8540505E23A24EA2D4815A742ED7F9173F8C157735B8D7FA20727E013E5BEECEF3DAC27960E7BA025D5335952AC1C0CBB94523835574FCB1E5B5E1782FC1EFA9B0471F3D443F6104C13698929D2FCE5B2220BF9DCD125CA8F8918826A48F960CEE66E01AB5F066630E1793277838E0F997D8C8A01C086387BB2718C7CA1A1C3BB46223CE0B367DD86DCA768D50F41BB40188709103D2C854044DE2653002EEB78965D8141B5A6900B17A94A6846CA2595436E45DA841F6DC3165187DAF6499DB5B15233A7205D9CB70D8776F109C6B744A0DA3ADC4EABEFA6C83C28BC2BC4C3389CF792F1AC9B544290D1A5C0CCBD69E89183A08A5B2496398358A6005FD404EF89678BE00FA0034C192D5B089A92083F93C5E8A8E36367A9DA75B463A6566806FACB47E71C4276F45D294CE54E7292DBD092E8D6D74C474D49D94BA5594090EDD12AC06DE72DDEC2AB56AD94C";  
  20.                 String temp_ = "";  
  21.                 int SUB_COUNT = 1000;// 按1000个字节来截取字符串  
  22.                 // 发送数据包长度  
  23.                 out.writeInt(2608 + 8);  
  24.                 // 分段来发送数据  
  25.                 for (int c = 0; c < str.getBytes().length / SUB_COUNT; c++) {  
  26.                     if (temp_ == "" && temp_.length() == 0) {  
  27.                         temp_ = str.substring(c * SUB_COUNT, SUB_COUNT);  
  28.                         // 发送第一次也是第一段数据  
  29.                         out.write(temp_.getBytes());  
  30.                     } else {  
  31.                         // 按1000个字节分段发送数据  
  32.                         out.write(str.substring(c * SUB_COUNT, (c + 1) * SUB_COUNT).getBytes());  
  33.   
  34.                         // 发送剩余的不足1000个字节的数据串  
  35.                         int raim_ = str.substring((c + 1) * SUB_COUNT, str.getBytes().length).length();  
  36.                         if (raim_ < SUB_COUNT && raim_ != 0) {  
  37.                             out.write(str.substring((c + 1) * SUB_COUNT, str.getBytes().length).getBytes());  
  38.                         }  
  39.                     }  
  40.                 }  
  41.                 // 马上写入,释放缓存  
  42.                 out.flush();  
  43.                 System.out.println(i + " sended");  
  44.   
  45.                 char temp[] = new char[2700];  
  46.                 String backLine = "";  
  47.                 in.read(temp);  
  48.                 backLine = String.valueOf(temp).trim();  
  49.                 System.out.println("backLine==" + backLine);  
  50.                 long endTime = System.currentTimeMillis(); // 获取结束时间  
  51.                 System.out.println("程序运行时间(毫秒): " + (endTime - startTime) + "ms");  
  52.             }  
  53.             Thread.sleep(1000);  
  54.             out.close();  
  55.             in.close();  
  56.             socket.close();  
  57.         } catch (Exception e) {  
  58.             e.printStackTrace();  
  59.         }  
  60.     }  
  61. }  

 

 

 

服务端类:MyProtocalServer.java

 

Java代码  收藏代码
  1. package com.ccic.mina.myprotocal;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.InetSocketAddress;  
  5. import java.nio.charset.Charset;  
  6. import java.util.concurrent.Executors;  
  7.   
  8. import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;  
  9. import org.apache.mina.core.service.IoAcceptor;  
  10. import org.apache.mina.core.session.IdleStatus;  
  11. import org.apache.mina.filter.codec.ProtocolCodecFilter;  
  12. import org.apache.mina.filter.executor.ExecutorFilter;  
  13. import org.apache.mina.filter.logging.LogLevel;  
  14. import org.apache.mina.filter.logging.LoggingFilter;  
  15. import org.apache.mina.transport.socket.nio.NioSocketAcceptor;  
  16.   
  17. /** 
  18.  * mina编解码流程:request->MyDecoder->MyHandler->MyEncode->response 
  19.  *  
  20.  * @author 
  21.  *  
  22.  */  
  23. public class MyProtocalServer {  
  24.     private static final int PORT = 8080;  
  25.   
  26.     // static Logger logger = Logger.getLogger(MyProtocalServer.class);  
  27.   
  28.     public static void main(String[] args) throws IOException {  
  29.         // PropertyConfigurator.configure("conf\\log4j.properties");  
  30.         IoAcceptor acceptor = new NioSocketAcceptor();  
  31.         // Log4jFilter lf = new Log4jFilter(logger);  
  32.   
  33.         // 定义SLF4J 日志级别 Look: http://riddickbryant.iteye.com/blog/564330  
  34.         LoggingFilter loggingFilter = new LoggingFilter();  
  35.         loggingFilter.setSessionCreatedLogLevel(LogLevel.NONE);// 一个新的session被创建时触发  
  36.         loggingFilter.setSessionOpenedLogLevel(LogLevel.NONE);// 一个新的session打开时触发  
  37.         loggingFilter.setSessionClosedLogLevel(LogLevel.NONE);// 一个session被关闭时触发  
  38.         loggingFilter.setMessageReceivedLogLevel(LogLevel.NONE);// 接收到数据时触发  
  39.         loggingFilter.setMessageSentLogLevel(LogLevel.NONE);// 数据被发送后触发  
  40.         loggingFilter.setSessionIdleLogLevel(LogLevel.INFO);// 一个session空闲了一定时间后触发  
  41.         loggingFilter.setExceptionCaughtLogLevel(LogLevel.INFO);// 当有异常抛出时触发  
  42.   
  43.         acceptor.getFilterChain().addLast("logger", loggingFilter);  
  44.         // 过滤器(自定义协议)  
  45.         acceptor.getFilterChain().addLast("codec",  
  46.                 new ProtocolCodecFilter(new MyProtocalCodecFactory(Charset.forName("UTF-8"))));  
  47.         // 设置数据将被读取的缓冲区大小  
  48.         acceptor.getSessionConfig().setReadBufferSize(3000);  
  49.         // 10秒内没有读写就设置为空闲通道  
  50.         acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);  
  51.   
  52.         // 连接池设置  
  53.         // get a reference to the filter chain from the acceptor  
  54.         DefaultIoFilterChainBuilder filterChainBuilder = acceptor.getFilterChain();  
  55.         filterChainBuilder.addLast("threadPool"new ExecutorFilter(Executors.newCachedThreadPool()));  
  56.   
  57.         acceptor.setHandler(new MyHandler());  
  58.         acceptor.bind(new InetSocketAddress(PORT));  
  59.         System.out.println("Start server is listenig at port " + PORT);  
  60.     }  
  61. }  

 

 

运行:

启动服务端server: MyProtocalServer->Main方法

客户端:MyProtocalClient->Main方法

 

阅读(13243) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~