JAVA NIO实现服务端与客户端简单数据传输 JAVA NIO 之一

Java代码   收藏代码
  1. import java.io.IOException;  
  2. import java.net.InetSocketAddress;  
  3. import java.nio.ByteBuffer;  
  4. import java.nio.channels.ClosedChannelException;  
  5. import java.nio.channels.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.ServerSocketChannel;  
  8. import java.nio.channels.SocketChannel;  
  9. import java.util.Iterator;  
  10. /** 
  11. * 服务端 
  12. */  
  13. public class SocketServer {  
  14.   
  15.     /** 
  16.      * 服务器默认绑定端口 
  17.      */  
  18.     public static final int DEFAULT_PORT = 9999;  
  19.   
  20.     /** 
  21.      * 选择器 
  22.      */  
  23.     public Selector selector;  
  24.   
  25.     public SocketServer(String ip, int port) {  
  26.         ServerSocketChannel ssc = null;  
  27.         try {  
  28.             int _port = DEFAULT_PORT;  
  29.             if (port > 0)  
  30.                 _port = port;  
  31.             /* 获取通道 */  
  32.             ssc = ServerSocketChannel.open();  
  33.             /* 配置非阻塞 */  
  34.             ssc.configureBlocking(false);  
  35.             /** 
  36.              * 配置绑定端口 ServerSocketChannel没有bind()方法, 
  37.              * 因此有必要取出对等的ServerSocket并使用它来绑定到一 
  38.              * 个端口以开始监听连接 
  39.              */  
  40.             ssc.socket().bind(new InetSocketAddress(ip, _port));  
  41.             /* 获取选择器 */  
  42.             this.selector = Selector.open();  
  43.             /* 将通道注册到选择器 */  
  44.             ssc.register(this.selector, SelectionKey.OP_ACCEPT);  
  45.         }catch(ClosedChannelException e1){  
  46.             System.out.println("关闭的通道,无法注册到选择器");  
  47.             e1.printStackTrace();  
  48.         } catch (IOException e2) {  
  49.             try {  
  50.                 if(ssc != null) ssc.close();  
  51.             } catch (IOException e) {  
  52.                 e.printStackTrace();  
  53.             }  
  54.             System.out.println("服务器绑定端口冲突");  
  55.             e2.printStackTrace();  
  56.         }  
  57.     }  
  58.       
  59.     /** 
  60.      * 轮询选择器 
  61.      * @throws Exception 
  62.      */  
  63.     public void pollSelect() throws Exception {  
  64.         /* (阻塞)轮询选择器,直到有事件 */  
  65.         while (this.selector.select()>0) {  
  66.             /* 获取事件通知列表 */  
  67.             Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();  
  68.             while (it.hasNext()) {  
  69.                 SelectionKey selectKey = it.next();  
  70.                 it.remove();  
  71.                 try {  
  72.                     process(selectKey);  
  73.                 } catch (Exception e) {  
  74.                     e.printStackTrace();  
  75.                     continue;  
  76.                 }  
  77.             }  
  78.         }  
  79.     }  
  80.   
  81.     /** 
  82.      * 事件处理 
  83.      * @param selectKey 
  84.      */  
  85.     public void process(SelectionKey selectKey) throws Exception{  
  86.         if (selectKey.isAcceptable()) { /* 客户端连接事件 */  
  87.             accept(selectKey);  
  88.         } else if (selectKey.isReadable()) { /* 可读事件 */  
  89.             read(selectKey);  
  90.         }  
  91.     }  
  92.       
  93.     /** 
  94.      * 连接事件 
  95.      * @param selectKey 
  96.      */  
  97.     public void accept(SelectionKey selectKey) throws Exception {  
  98.         ServerSocketChannel ssc = null;  
  99.         try {  
  100.             ssc = (ServerSocketChannel) selectKey  
  101.                     .channel();  
  102.             SocketChannel sc = ssc.accept();  
  103.             sc.configureBlocking(false);  
  104.             /* 发送信息 */  
  105.             sc.write(ByteBuffer.wrap(new String("Hello World!")  
  106.                     .getBytes()));  
  107.             /* 注册读事件 */  
  108.             sc.register(this.selector, SelectionKey.OP_READ);  
  109.         } catch (ClosedChannelException e) {  
  110.             if(ssc!=null)   
  111.                 ssc.close();  
  112.             throw new IOException("关闭的通道,无法注册到选择器");  
  113.         } catch (IOException e) {  
  114.             if(ssc!=null)   
  115.                 ssc.close();  
  116.             throw new IOException("连接服务或配置失败!");  
  117.         }  
  118.     }  
  119.       
  120.     /** 
  121.      * 可读事件 
  122.      * @param selectKey 
  123.      */  
  124.     public void read(SelectionKey selectKey) throws Exception{  
  125.         SocketChannel channel = null;  
  126.         try {  
  127.             // 服务器可读取消息:得到事件发生的Socket通道  
  128.             channel = (SocketChannel) selectKey.channel();  
  129.             // 创建读取的缓冲区  
  130.             ByteBuffer buffer = ByteBuffer.allocate(100);  
  131.             channel.read(buffer);  
  132.             byte[] data = buffer.array();  
  133.             String msg = new String(data).trim();  
  134.             System.out.println("客户端:" + msg);  
  135.             // ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
  136.             // 将消息回送给客户端  
  137.             // channel.write(outBuffer);  
  138.         } catch (Exception e) {  
  139.             if(channel != null)   
  140.                 channel.close();  
  141.             throw new Exception("客户端将通道关闭,无法从通道读入缓冲或将缓冲数据写回通道!");  
  142.         }  
  143.     }  
  144.       
  145.       
  146.     public static void main(String[] args) {  
  147.         SocketServer ss = null;  
  148.         try {  
  149.             ss = new SocketServer("localhost"9999);  
  150.             ss.pollSelect();  
  151.         } catch (Exception e) {  
  152.             e.printStackTrace();  
  153.         }  
  154.     }  
  155. }  


Java代码   收藏代码
  1. import java.io.IOException;  
  2. import java.net.InetSocketAddress;  
  3. import java.nio.ByteBuffer;  
  4. import java.nio.channels.ClosedChannelException;  
  5. import java.nio.channels.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.SocketChannel;  
  8. import java.util.Iterator;  
  9. /** 
  10. * 客户端 
  11. */  
  12. public class SocketClient {  
  13.   
  14.     public Selector selector;  
  15.       
  16.     public SocketClient(String ip, int port){  
  17.         SocketChannel channel = null;  
  18.         try {  
  19.             //channel = SocketChannel.open(new InetSocketAddress(ip,port));  
  20.             channel = SocketChannel.open();  
  21.             // 设置通道为非阻塞    
  22.             channel.configureBlocking(false);  
  23.             // 获得一个通道管理器    
  24.             this.selector = Selector.open();  
  25.             // 客户端连接服务器,其实方法执行并没有实现连接   
  26.             channel.connect(new InetSocketAddress(ip, port));   
  27.             /**while(!channel.finishConnect()){ 
  28.                 System.out.println("尝试连接...."); 
  29.             }*/  
  30.             // 注册连接事件。    
  31.             channel.register(this.selector, SelectionKey.OP_CONNECT);    
  32.         } catch(ClosedChannelException e1){  
  33.             System.out.println("关闭的通道,无法注册到选择器");  
  34.             e1.printStackTrace();  
  35.         } catch (IOException e2) {  
  36.             System.out.println("连接异常!");  
  37.             try {  
  38.                 if(channel != null) channel.close();  
  39.             } catch (IOException e) {  
  40.                 e.printStackTrace();  
  41.             }  
  42.             e2.printStackTrace();  
  43.         }  
  44.     }  
  45.       
  46.     /** 
  47.      * 轮询选择器 
  48.      * @throws IOException 
  49.      */  
  50.     public void  pollSelect() throws Exception {    
  51.         /* (阻塞)轮询选择器,直到有事件 */  
  52.         while ( this.selector.select() > 0 ) {  
  53.             /* 获取事件通知列表 */  
  54.             Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();    
  55.             while (ite.hasNext()) {    
  56.                 SelectionKey selectKey = (SelectionKey) ite.next();    
  57.                 // 删除已选的key,以防重复处理    
  58.                 ite.remove();    
  59.                 process(selectKey);  
  60.             }    
  61.         }    
  62.     }  
  63.       
  64.     /** 
  65.      * 处理事件 
  66.      * @param selectKey 
  67.      */  
  68.     public void process(SelectionKey selectKey) throws Exception{  
  69.         if (selectKey.isConnectable()) {  
  70.             connect(selectKey);  
  71.         } else if (selectKey.isReadable()) {    
  72.             read(selectKey);  
  73.         }    
  74.     }  
  75.       
  76.     /** 
  77.      * 连接事件 
  78.      * @param selectKey 
  79.      * @throws Exception 
  80.      */  
  81.     public void connect(SelectionKey selectKey) throws Exception{  
  82.         try {  
  83.             SocketChannel channel = (SocketChannel) selectKey    
  84.                     .channel();    
  85.             /* 如果正在连接,则完成连接 */    
  86.             if(channel.isConnectionPending()){  
  87.                 /** 
  88.                  * connect()方法尚未被调用,调用finishConnect()方法, 
  89.                  * 那么将产生NoConnectionPendingException 
  90.                  */  
  91.                 channel.finishConnect();    
  92.             }  
  93.             /** 
  94.              * 在非阻塞模式下调用connect()方法之后,SocketChannel又被切换回了阻塞模式;那么如果 
  95.              * 有必要的话,调用线程会阻塞直到连接建立完成,finishConnect()方法接着就会返回true 
  96.              * 值。 
  97.              */  
  98.             /* 设置成非阻塞 */    
  99.             channel.configureBlocking(false);    
  100.             /* 给服务端发送信息 */  
  101.             channel.write(ByteBuffer.wrap(new String("编号001客户端连接成功!").getBytes()));    
  102.             /* 注册读事件 */    
  103.             channel.register(this.selector, SelectionKey.OP_READ);  
  104.         } catch (ClosedChannelException e) {  
  105.             throw new IOException("关闭的通道,无法注册到选择器");  
  106.         } catch (IOException e) {  
  107.             throw new IOException("连接服务或配置失败!");  
  108.         }  
  109.     }  
  110.       
  111.     /** 
  112.      * 读事件 
  113.      * @param selectKey 
  114.      * @throws Exception 
  115.      */  
  116.     public void read(SelectionKey selectKey) throws Exception{  
  117.         try {  
  118.             // 服务器可读通道  
  119.             SocketChannel channel = (SocketChannel) selectKey.channel();   
  120.             // 创建读取的缓冲区    
  121.             ByteBuffer buffer = ByteBuffer.allocate(100);    
  122.             channel.read(buffer);    
  123.             byte[] data = buffer.array();    
  124.             String msg = new String(data).trim();    
  125.             System.out.println(msg);    
  126.             ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
  127.             // 将消息回送给服务端    
  128.             //channel.write(outBuffer);  
  129.         } catch (Exception e) {  
  130.             throw new IOException("服务端将通道关闭,无法从通道读入缓冲或将缓冲数据写回通道!");  
  131.         }  
  132.     }  
  133.       
  134.     public static void main(String[] args) {  
  135.         SocketClient sc = null;  
  136.         try {  
  137.             sc = new SocketClient("localhost"9999);  
  138.             sc. pollSelect();  
  139.         } catch (Exception e) {  
  140.             e.printStackTrace();  
  141.         }  
  142.     }  
  143. }  

之二:
引用

继上节利用JAVA NIO实现简单数据传,本节实现自定义对象传输,为了实现接收方构建完整对象,自定义对象实现Serializable接口,以便完成对象序列化与反序化。 
下一节我们将采用线程池来管理读写,期待... 

Java代码   收藏代码
  1. import java.io.Serializable;  
  2.   
  3. /** 
  4.  * 传输对象,利用ObjectOutputStream、ObjectInputStream 
  5.  * 传输,所以对象需要可序列化 
  6.  * @author oy 
  7.  * 
  8.  */  
  9. public class User implements Serializable{  
  10.       
  11.     private static final long serialVersionUID = 588789688009378496L;  
  12.   
  13.     private int age;  
  14.       
  15.     private String name;  
  16.       
  17.     /* Ignore this attribution */  
  18.     private transient String school;  
  19.   
  20.     public User(String name, int age, String school){  
  21.         this.name = name;  
  22.         this.age = age;  
  23.         this.school = school;  
  24.     }  
  25.   
  26.     public int getAge() {  
  27.         return age;  
  28.     }  
  29.     public void setAge(int age) {  
  30.         this.age = age;  
  31.     }  
  32.     public String getName() {  
  33.         return name;  
  34.     }  
  35.   
  36.     public void setName(String name) {  
  37.         this.name = name;  
  38.     }  
  39.   
  40.     public String getSchool() {  
  41.         return school;  
  42.     }  
  43.   
  44.     public void setSchool(String school) {  
  45.         this.school = school;  
  46.     }  
  47.   
  48.     @Override  
  49.     public String toString() {  
  50.         return "User [age=" + age + ", name=" + name + ", school=" + school  
  51.                 + "]";  
  52.     }  
  53. }  


Java代码   收藏代码
  1.  /* 发送对象信息 */  
  2. ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  3. ObjectOutputStream oos = new ObjectOutputStream(baos);  
  4. oos.writeObject(new User("Test"30"xx大学"));  
  5. channel.write(ByteBuffer.wrap(baos.toByteArray()));  

Java代码   收藏代码
  1. /* 接收对象信息 */  
  2. ByteBuffer buffer = ByteBuffer.allocate(1024);  
  3. channel.read(buffer);  
  4. byte[] data = buffer.array();  
  5. ByteArrayInputStream bais = new ByteArrayInputStream(data);  
  6. ObjectInputStream ois = new ObjectInputStream(bais);  
  7. User user = (User)ois.readObject();  


之三:

   本节采用JDK1.5之后java.util.concurrent包的API服务端实现线程 

池读取信息,可以接采用Executors工具快速创建线程池,也可以ExecutorService子类自 

定义创建。 
  
   客端连接服务端发送信息后关闭连接SOCKET短连接(HTTP为短连接),若采用SOCKET长 

连接,需要增加"心跳检测",本节暂未实现长连接。 
  
  因Selector轮询可读事件时,存在重读问题,解决办法是在读的代码块中加下述代码 

selectKey.cancel()或selectKey.interestOps(selectKey.interestOps() & 

(~SelectionKey.OP_READ)) 


Java代码   收藏代码
  1. import java.io.IOException;  
  2. import java.net.InetSocketAddress;  
  3. import java.nio.channels.SelectionKey;  
  4. import java.nio.channels.Selector;  
  5. import java.nio.channels.ServerSocketChannel;  
  6. import java.nio.channels.SocketChannel;  
  7. import java.util.Iterator;  
  8. import java.util.concurrent.ExecutorService;  
  9. import java.util.concurrent.LinkedBlockingQueue;  
  10. import java.util.concurrent.ThreadPoolExecutor;  
  11. import java.util.concurrent.TimeUnit;  
  12. /** 
  13. * 服务端 
  14. */  
  15. public class SocketServer {  
  16.   
  17.     /** 
  18.      * 服务器默认绑定端口 
  19.      */  
  20.     public static final int DEFAULT_PORT = 9999;  
  21.   
  22.     /** 
  23.      * 选择器 
  24.      */  
  25.     private Selector selector;  
  26.     /** 
  27.      * 读取线程池 
  28.      */  
  29.     private ExecutorService pool;  
  30.       
  31.     public SocketServer(String ip, int port) {  
  32.         ServerSocketChannel ssc = null;  
  33.         try {  
  34.             int _port = DEFAULT_PORT;  
  35.             if (port > 0)  
  36.                 _port = port;  
  37.             /* 获取通道 */  
  38.             ssc = ServerSocketChannel.open();  
  39.             /* 配置非阻塞 */  
  40.             ssc.configureBlocking(false);  
  41.             /** 
  42.              * 配置绑定端口 ServerSocketChannel没有bind()方法, 
  43.              * 因此有必要取出对等的ServerSocket并使用它来绑定到一 
  44.              * 个端口以开始监听连接 
  45.              */  
  46.             ssc.socket().bind(new InetSocketAddress(ip, _port));  
  47.             /* 获取选择器 */  
  48.             this.selector = Selector.open();  
  49.             /* 将通道注册到选择器 */  
  50.             ssc.register(this.selector, SelectionKey.OP_ACCEPT);  
  51.               
  52.             /** 
  53.              * 可以使用Executors,快速创建线程池,但是如果综合考虑 
  54.              * CPU、内存等资及并发情况,可以采用自定方式创建池( 
  55.              *  拒绝处理等问题 
  56.              * ),此处简单自定义创建读取池 
  57.              * */  
  58.             pool = new ThreadPoolExecutor(12, 2000L,  
  59.                     TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(5));  
  60.         } catch (IOException e2) {  
  61.             System.out.println("服务器启动失败!");  
  62.             e2.printStackTrace();  
  63.             try {  
  64.                 if(ssc != null) ssc.close();  
  65.             } catch (IOException e) {  
  66.                 e.printStackTrace();  
  67.             }  
  68.         }  
  69.     }  
  70.       
  71.     /** 
  72.      * 轮询选择器 
  73.      * @throws Exception 
  74.      */  
  75.     public void pollSelect() throws Exception {  
  76.         /* (阻塞)轮询选择器,直到有事件 */  
  77.         while (true) {  
  78.             int readyChannels = 0;  
  79.             /*选择器是否关闭*/  
  80.             if(this.selector.isOpen()){  
  81.                 readyChannels = this.selector.select();  
  82.             }  
  83.             if(readyChannels == 0continue;  
  84.             /* 获取事件通知列表 */  
  85.             Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();  
  86.             while (it.hasNext()) {  
  87.                 SelectionKey selectKey = it.next();  
  88.                 it.remove();  
  89.                 try {  
  90.                     process(selectKey);  
  91.                 } catch (Exception e) {  
  92.                     e.printStackTrace();  
  93.                 }  
  94.             }  
  95.         }  
  96.     }  
  97.   
  98.     /** 
  99.      * 事件处理 
  100.      * @param selectKey 
  101.      */  
  102.     public void process(SelectionKey selectKey) throws Exception{  
  103.         /* 客户端连接事件 */  
  104.         if (selectKey.isAcceptable()) {   
  105.             accept(selectKey);  
  106.         } else if (selectKey.isReadable()) { /* 可读事件 */  
  107.             read(selectKey);  
  108.             /** 
  109.              * 解决重复读 
  110.              * 或设置不关注读事件来解决重复 
  111.              * selectKey.interestOps(selectKey.interestOps() & (~SelectionKey.OP_READ)); 
  112.              * */  
  113.             selectKey.cancel();  
  114.         }  
  115.     }  
  116.       
  117.     /** 
  118.      * 连接事件 
  119.      * @param selectKey 
  120.      */  
  121.     public void accept(SelectionKey selectKey) throws Exception {  
  122.         SocketChannel sc = null;  
  123.         try {  
  124.             ServerSocketChannel ssc = (ServerSocketChannel) selectKey  
  125.                     .channel();  
  126.             sc = ssc.accept();  
  127.             sc.configureBlocking(false);  
  128.             System.out.println("客户端:"  
  129.                     + sc.socket().getInetAddress().getHostAddress()+"端口"+sc.socket().getPort()  
  130.                     + " 已连接");  
  131.             sc.register(this.selector, SelectionKey.OP_READ);  
  132.         } catch (Exception e) {  
  133.             if(sc!=null)   
  134.                 sc.close();  
  135.             throw new IOException("客户端已断开连接!");  
  136.         }  
  137.     }  
  138.       
  139.     /** 
  140.      * 可读事件 
  141.      * @param selectKey 
  142.      */  
  143.     public void read(SelectionKey selectKey) throws Exception{  
  144.         SocketChannel sc = (SocketChannel) selectKey  
  145.                 .channel();  
  146.         /*提交线程池处理*/  
  147.         pool.submit(new SocketServerReadThread(sc));  
  148.     }  
  149.       
  150.       
  151.     public static void main(String[] args) {  
  152.         SocketServer ss = null;  
  153.         try {  
  154.             ss = new SocketServer("localhost"9999);  
  155.             ss.pollSelect();  
  156.         } catch (Exception e) {  
  157.             e.printStackTrace();  
  158.         }  
  159.     }  
  160. }  

Java代码   收藏代码
  1. import java.io.ByteArrayInputStream;  
  2. import java.io.IOException;  
  3. import java.io.ObjectInputStream;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.SocketChannel;  
  6.   
  7. /** 
  8.  * 从线程中读信息 
  9.  * @author oy 
  10.  * 
  11.  */  
  12. public class SocketServerReadThread implements Runnable{  
  13.       
  14.     private SocketChannel channel;  
  15.       
  16.     public SocketServerReadThread(SocketChannel channel){  
  17.         this.channel = channel;  
  18.     }  
  19.       
  20.     @Override  
  21.     public void run() {  
  22.         try {  
  23.             // 创建读取的缓冲区  
  24.             ByteBuffer buffer = ByteBuffer.allocate(1024);  
  25.             channel.read(buffer);  
  26.             byte[] data = buffer.array();  
  27.             ByteArrayInputStream bais = new ByteArrayInputStream(data);  
  28.             ObjectInputStream ois = new ObjectInputStream(bais);  
  29.             User user = (User)ois.readObject();  
  30.             System.out.println("客户端:"  
  31.                     + channel.socket().getInetAddress().getHostAddress()+"端口"+channel.socket().getPort() + " 消息: " + user.toString());  
  32.         } catch (Exception e) {  
  33.             System.out.println("客户端已断开连接!");  
  34.             try {  
  35.                 if(channel != null)   
  36.                     channel.close();  
  37.             } catch (IOException e1) {  
  38.                 e1.printStackTrace();  
  39.             }  
  40.         }  
  41.     }  

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
【项目介绍】 Java开发基于多线程和NIO实现聊天室源码+项目说明(含服务端+客户端).zip 涉及到的技术点 - 线程池ThreadPoolExecutor - 阻塞队列BlockingQueue,生产者消费者模式 - Selector - Channel - ByteBuffer - ProtoStuff 高性能序列化 - HttpClient连接池 - Spring依赖注入 - lombok简化POJO开发 - 原子变量 - 内置锁 - CompletionService - log4j+slf4j日志 - 实现的功能 - 登录注销 - 单聊 - 群聊 - 客户端提交任务,下载图片并显示 - 上线下线公告 - 在线用户记录 - 批量下载豆瓣电影的图片,并打为压缩包传输给客户端 - 客户端使用方式: - 登录:默认用户名是user1-user5,密码分别是pwd1-pwd5 - 例:打开客户端后输入用户名为user1,密码为pwd1 - 注销:关闭客户端即可 - 单聊:@username:message - 例:@user2:hello - 群聊:message - 例:hello,everyone - 提交任务:task.file:图片的URL / task.crawl_image:豆瓣电影的id[?imageSize=n] 可以加请求参数 - 例1:task.file:https://img1.doubanio.com/view/movie_poster_cover/lpst/public/p2107289058.webp 下载完毕后会弹出一个框,输入想将其保存到的路径,比如E:/img.webp - 例2:task.crawl_image:1292371?imageSize=2 下载完毕后在弹出的框中输入E:/images.zip 【备注】 1.项目代码均经过功能验证ok,确保稳定可靠运行。欢迎下载食用体验! 2.主要针对各个计算机相关专业,包括计算机科学、信息安全、数据科学与大数据技术、人工智能、通信、物联网等领域的在校学生、专业教师、企业员工。 3.项目具有丰富的拓展空间,不仅可作为入门进阶,也可直接作为毕设、课程设计、大作业、初期项目立项演示等用途。 4.当然也鼓励大家基于此进行二次开发。在使用过程中,如有问题或建议,请及时沟通。 5.期待你能在项目中找到乐趣和灵感,也欢迎你的分享和反馈!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值