Java Socket实战之八 socket提升

Java Socket实战之八 socket提升

一直没时间继续写,这两天总算找了点时间把当时的一些想法简单实现了一下,比较初略,主要是记下自己的想法,下次有机会了再慢慢细化吧。

对于Socket编程来说,通常我们遇到的最大的麻烦就是要定义自己的协议,用来在server端和client端处理请求和响应,当socket处理的请求对象越来越多以后,如果规则定义不清楚就会导致代码急剧膨胀,并且维护性变差,所以这里我想了一个简单的方式来处理这种情况。

下面大概说一下我的想法

1. 首先会有几个和业务相关的类,User,MyUserService和MyUserServiceImpl。User就是我们通常的实体类;MyUserService是我们针对User实体类提供的业务逻辑接口,比较简单就写了三个方法;MyUserServiceImpl是业务逻辑实现类。

  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicclassUserimplementsjava.io.Serializable{
  3. privatestaticfinallongserialVersionUID=1L;
  4. privateStringname;
  5. privateStringpassword;
  6. publicUser(){
  7. }
  8. publicUser(Stringname,Stringpassword){
  9. this.name=name;
  10. this.password=password;
  11. }
  12. publicStringgetName(){
  13. returnname;
  14. }
  15. publicvoidsetName(Stringname){
  16. this.name=name;
  17. }
  18. publicStringgetPassword(){
  19. returnpassword;
  20. }
  21. publicvoidsetPassword(Stringpassword){
  22. this.password=password;
  23. }
  24. }

MyUserService.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importjava.util.List;
  3. publicinterfaceMyUserService{
  4. List<User>list(intsize);
  5. UserfindByName(Stringname);
  6. voidtest();
  7. }

MyUserServiceImpl.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importjava.util.ArrayList;
  3. importjava.util.List;
  4. publicclassMyUserServiceImplimplementsMyUserService{
  5. @Override
  6. publicList<User>list(intsize){
  7. List<User>users=newArrayList<User>();
  8. for(inti=0;i<size;i++){
  9. users.add(newUser("user_"+i,"password_"+i));
  10. }
  11. returnusers;
  12. }
  13. @Override
  14. publicUserfindByName(Stringname){
  15. returnnewUser(name,null);
  16. }
  17. @Override
  18. publicvoidtest(){
  19. //donothing
  20. }
  21. }

2. 服务器端类,主要有三个类MyServer,MyServerSimpleImpl和MyServerNIOImpl。MyServer是服务器端接口类,用来启动Socket server;MyServerSimpleImpl和MyServerNIOImpl是两个实现类,其中MyServerSimpleImpl是使用简单的Socket实现的,MyServerNIOImpl是使用java nio包里的类实现的,这个实现会有更好的性能。

MyServer.java

  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicinterfaceMyServer{
  3. publicvoidstartup()throwsException;
  4. publicvoidshutdown()throwsException;
  5. }

MyServerSimpleImpl.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importcom.googlecode.garbagecan.test.socket.IOUtil;
  3. importjava.io.ObjectInputStream;
  4. importjava.io.ObjectOutputStream;
  5. importjava.lang.reflect.Method;
  6. importjava.net.ServerSocket;
  7. importjava.net.Socket;
  8. publicclassMyServerSimpleImplimplementsMyServer{
  9. privateintport;
  10. publicMyServerSimpleImpl(intport){
  11. this.port=port;
  12. }
  13. publicvoidstartup()throwsException{
  14. newThread(newRunnable(){
  15. @Override
  16. publicvoidrun(){
  17. try{
  18. ServerSocketserver=newServerSocket(port);
  19. while(true){
  20. Socketsocket=server.accept();
  21. invoke(socket);
  22. }
  23. }catch(Exceptionex){
  24. ex.printStackTrace();
  25. }
  26. }
  27. }).start();
  28. }
  29. @Override
  30. publicvoidshutdown()throwsException{
  31. //Implementme
  32. }
  33. privatevoidinvoke(finalSocketsocket){
  34. newThread(newRunnable(){
  35. publicvoidrun(){
  36. ObjectInputStreamois=null;
  37. ObjectOutputStreamoos=null;
  38. try{
  39. ois=newObjectInputStream(socket.getInputStream());
  40. oos=newObjectOutputStream(socket.getOutputStream());
  41. Objectobj=ois.readObject();
  42. MyRequestrequest=(MyRequest)obj;
  43. MyResponseresponse=execute(request);
  44. oos.writeObject(response);
  45. oos.flush();
  46. }catch(Exceptionex){
  47. ex.printStackTrace();
  48. }finally{
  49. IOUtil.closeQuietly(ois);
  50. IOUtil.closeQuietly(oos);
  51. IOUtil.closeQuietly(socket);
  52. }
  53. }
  54. }).start();
  55. }
  56. privateMyResponseexecute(MyRequestrequest)throwsException{
  57. Classclazz=request.getRequestClass();
  58. StringmethodName=request.getRequestMethod();
  59. Class<?>[]parameterTypes=request.getRequestParameterTypes();
  60. Methodmethod=clazz.getDeclaredMethod(methodName,parameterTypes);
  61. Object[]parameterValues=request.getRequestParameterValues();
  62. finalObjectobj=method.invoke(clazz.newInstance(),parameterValues);
  63. returnnewMyGenericResponse(obj);
  64. }
  65. }

MyServerNIOImpl.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importcom.googlecode.garbagecan.test.socket.SerializableUtil;
  3. importjava.io.ByteArrayOutputStream;
  4. importjava.io.IOException;
  5. importjava.lang.reflect.Method;
  6. importjava.net.InetSocketAddress;
  7. importjava.nio.ByteBuffer;
  8. importjava.nio.channels.SelectionKey;
  9. importjava.nio.channels.Selector;
  10. importjava.nio.channels.ServerSocketChannel;
  11. importjava.nio.channels.SocketChannel;
  12. importjava.util.Iterator;
  13. importjava.util.logging.Level;
  14. importjava.util.logging.Logger;
  15. publicclassMyServerNIOImplimplementsMyServer{
  16. privatefinalstaticLoggerlogger=Logger.getLogger(MyServerNIOImpl.class.getName());
  17. privateintport;
  18. publicMyServerNIOImpl(intport){
  19. this.port=port;
  20. }
  21. publicvoidstartup()throwsException{
  22. newThread(newRunnable(){
  23. @Override
  24. publicvoidrun(){
  25. Selectorselector=null;
  26. ServerSocketChannelserverSocketChannel=null;
  27. try{
  28. selector=Selector.open();
  29. serverSocketChannel=ServerSocketChannel.open();
  30. serverSocketChannel.configureBlocking(false);
  31. serverSocketChannel.socket().setReuseAddress(true);
  32. serverSocketChannel.socket().bind(newInetSocketAddress(port));
  33. serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
  34. while(selector.select()>0){
  35. try{
  36. Iterator<SelectionKey>it=selector.selectedKeys().iterator();
  37. while(it.hasNext()){
  38. SelectionKeyreadyKey=it.next();
  39. it.remove();
  40. invoke((ServerSocketChannel)readyKey.channel());
  41. }
  42. }catch(Exceptionex){
  43. logger.log(Level.SEVERE,ex.getMessage(),ex);
  44. }
  45. }
  46. }catch(Exceptionex){
  47. logger.log(Level.SEVERE,ex.getMessage(),ex);
  48. }finally{
  49. try{
  50. selector.close();
  51. }catch(Exceptionex){}
  52. try{
  53. serverSocketChannel.close();
  54. }catch(Exceptionex){}
  55. }
  56. }
  57. }).start();
  58. }
  59. @Override
  60. publicvoidshutdown()throwsException{
  61. //Implementme
  62. }
  63. privatevoidinvoke(ServerSocketChannelserverSocketChannel)throwsException{
  64. SocketChannelsocketChannel=null;
  65. try{
  66. socketChannel=serverSocketChannel.accept();
  67. MyRequestmyRequest=receiveData(socketChannel);
  68. MyResponsemyResponse=execute(myRequest);
  69. sendData(socketChannel,myResponse);
  70. }finally{
  71. try{
  72. socketChannel.close();
  73. }catch(Exceptionex){}
  74. }
  75. }
  76. privateMyResponseexecute(MyRequestrequest)throwsException{
  77. Classclazz=request.getRequestClass();
  78. StringmethodName=request.getRequestMethod();
  79. Class<?>[]parameterTypes=request.getRequestParameterTypes();
  80. Methodmethod=clazz.getDeclaredMethod(methodName,parameterTypes);
  81. Object[]parameterValues=request.getRequestParameterValues();
  82. finalObjectobj=method.invoke(clazz.newInstance(),parameterValues);
  83. returnnewMyGenericResponse(obj);
  84. }
  85. privateMyRequestreceiveData(SocketChannelsocketChannel)throwsIOException{
  86. MyRequestmyRequest=null;
  87. ByteArrayOutputStreambaos=newByteArrayOutputStream();
  88. ByteBufferbuffer=ByteBuffer.allocate(1024);
  89. try{
  90. byte[]bytes;
  91. intsize=0;
  92. while((size=socketChannel.read(buffer))>=0){
  93. buffer.flip();
  94. bytes=newbyte[size];
  95. buffer.get(bytes);
  96. baos.write(bytes);
  97. buffer.clear();
  98. }
  99. bytes=baos.toByteArray();
  100. Objectobj=SerializableUtil.toObject(bytes);
  101. myRequest=(MyRequest)obj;
  102. }finally{
  103. try{
  104. baos.close();
  105. }catch(Exceptionex){}
  106. }
  107. returnmyRequest;
  108. }
  109. privatevoidsendData(SocketChannelsocketChannel,MyResponsemyResponse)throwsIOException{
  110. byte[]bytes=SerializableUtil.toBytes(myResponse);
  111. ByteBufferbuffer=ByteBuffer.wrap(bytes);
  112. socketChannel.write(buffer);
  113. }
  114. }

3. 客户端类,主要有三个类MyClient,MyClientSimpleImpl和MyClientNIOImpl。MyClient是客户端接口类,用来向Server端发送请求并获取相应;MyClientSimpleImpl和MyClientNIOImpl是两个实现类,其中MyClientSimpleImpl是使用简单Socket实现的,MyClientNIOImpl是使用java nio包里的类实现的,这个实现会有更好的性能。

MyClient.java

  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicinterfaceMyClient{
  3. public<T>Texecute(MyRequestrequest,MyResponseHandler<T>handler);
  4. publicMyResponseexecute(MyRequestrequest);
  5. }

MyClientSimpleImpl.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importcom.googlecode.garbagecan.test.socket.IOUtil;
  3. importjava.io.IOException;
  4. importjava.io.ObjectInputStream;
  5. importjava.io.ObjectOutputStream;
  6. importjava.net.InetSocketAddress;
  7. importjava.net.Socket;
  8. importjava.net.SocketAddress;
  9. publicclassMyClientSimpleImplimplementsMyClient{
  10. privateStringhost;
  11. privateintport;
  12. publicMyClientSimpleImpl(Stringhost,intport){
  13. this.host=host;
  14. this.port=port;
  15. }
  16. public<T>Texecute(MyRequestrequest,MyResponseHandler<T>handler){
  17. MyResponseresponse=execute(request);
  18. returnhandler.handle(response);
  19. }
  20. publicMyResponseexecute(MyRequestrequest){
  21. MyResponseresponse=null;
  22. Socketsocket=null;
  23. ObjectOutputStreamoos=null;
  24. ObjectInputStreamois=null;
  25. try{
  26. socket=newSocket();
  27. SocketAddresssocketAddress=newInetSocketAddress(host,port);
  28. socket.connect(socketAddress,10*1000);
  29. oos=newObjectOutputStream(socket.getOutputStream());
  30. oos.writeObject(request);
  31. oos.flush();
  32. ois=newObjectInputStream(socket.getInputStream());
  33. Objectobj=ois.readObject();
  34. if(obj!=null){
  35. response=(MyResponse)obj;
  36. }
  37. }catch(IOExceptionex){
  38. ex.printStackTrace();
  39. }catch(ClassNotFoundExceptionex){
  40. ex.printStackTrace();
  41. }finally{
  42. IOUtil.closeQuietly(ois);
  43. IOUtil.closeQuietly(oos);
  44. IOUtil.closeQuietly(socket);
  45. }
  46. returnresponse;
  47. }
  48. }

MyClientNIOImpl.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importcom.googlecode.garbagecan.test.socket.SerializableUtil;
  3. importjava.io.ByteArrayOutputStream;
  4. importjava.io.IOException;
  5. importjava.net.InetSocketAddress;
  6. importjava.net.SocketAddress;
  7. importjava.nio.ByteBuffer;
  8. importjava.nio.channels.SocketChannel;
  9. importjava.util.logging.Level;
  10. importjava.util.logging.Logger;
  11. publicclassMyClientNIOImplimplementsMyClient{
  12. privatefinalstaticLoggerlogger=Logger.getLogger(MyClientNIOImpl.class.getName());
  13. privateStringhost;
  14. privateintport;
  15. publicMyClientNIOImpl(Stringhost,intport){
  16. this.host=host;
  17. this.port=port;
  18. }
  19. @Override
  20. public<T>Texecute(MyRequestrequest,MyResponseHandler<T>handler){
  21. MyResponseresponse=execute(request);
  22. returnhandler.handle(response);
  23. }
  24. @Override
  25. publicMyResponseexecute(MyRequestrequest){
  26. MyResponseresponse=null;
  27. SocketChannelsocketChannel=null;
  28. try{
  29. socketChannel=SocketChannel.open();
  30. SocketAddresssocketAddress=newInetSocketAddress(host,port);
  31. socketChannel.connect(socketAddress);
  32. sendData(socketChannel,request);
  33. response=receiveData(socketChannel);
  34. }catch(Exceptionex){
  35. logger.log(Level.SEVERE,null,ex);
  36. }finally{
  37. try{
  38. socketChannel.close();
  39. }catch(Exceptionex){}
  40. }
  41. returnresponse;
  42. }
  43. privatevoidsendData(SocketChannelsocketChannel,MyRequestmyRequest)throwsIOException{
  44. byte[]bytes=SerializableUtil.toBytes(myRequest);
  45. ByteBufferbuffer=ByteBuffer.wrap(bytes);
  46. socketChannel.write(buffer);
  47. socketChannel.socket().shutdownOutput();
  48. }
  49. privateMyResponsereceiveData(SocketChannelsocketChannel)throwsIOException{
  50. MyResponsemyResponse=null;
  51. ByteArrayOutputStreambaos=newByteArrayOutputStream();
  52. try{
  53. ByteBufferbuffer=ByteBuffer.allocateDirect(1024);
  54. byte[]bytes;
  55. intcount=0;
  56. while((count=socketChannel.read(buffer))>=0){
  57. buffer.flip();
  58. bytes=newbyte[count];
  59. buffer.get(bytes);
  60. baos.write(bytes);
  61. buffer.clear();
  62. }
  63. bytes=baos.toByteArray();
  64. Objectobj=SerializableUtil.toObject(bytes);
  65. myResponse=(MyResponse)obj;
  66. socketChannel.close();
  67. }finally{
  68. try{
  69. baos.close();
  70. }catch(Exceptionex){}
  71. }
  72. returnmyResponse;
  73. }
  74. }

4. 接下来是MyRequest和MyResponse和MyResponseHandler接口,其中MyRequest接口中定义了四个方法,分别用来获取远程业务逻辑实现类,方法名,参数类型列表和参数列表。MyResponse接口定义了一个方法用来从response类中获取结果。MyResponseHandler接口使用范型的方式来获取最终的结果对象。

MyRequest.java

  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importjava.io.Serializable;
  3. publicinterfaceMyRequestextendsSerializable{
  4. Class<?>getRequestClass();
  5. StringgetRequestMethod();
  6. Class<?>[]getRequestParameterTypes();
  7. Object[]getRequestParameterValues();
  8. }

MyResponse.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importjava.io.Serializable;
  3. publicinterfaceMyResponseextendsSerializable{
  4. ObjectgetResult();
  5. }

MyResponseHandler.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicinterfaceMyResponseHandler<T>{
  3. Thandle(MyResponseresponse);
  4. }

这几个接口的实现类分别对应MyGenericRequest,MyGenericResponse和MyGenericResponseHandler。

另外这里由于使用的反射类来在服务器端生成service实例,所以目前这里有个限制就是服务器端的Service实现类必须有默认构造函数。当然这是可以重构使其支持更多的方式。比如说支持从工厂方法获取实例,或者根据名字在服务器端直接获取已经创建好的,由于这个例子只是一个简单的原型,所以就不做具体深入的说明和实现了。

MyGenericRequest.java

  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicclassMyGenericRequestimplementsMyRequest{
  3. privatestaticfinallongserialVersionUID=1L;
  4. privateClass<?>requestClass;
  5. privateStringrequestMethod;
  6. privateClass<?>[]requestParameterTypes;
  7. privateObject[]requestParameterValues;
  8. publicMyGenericRequest(Class<?>requestClass,StringrequestMethod,Class<?>[]requestParameterTypes,Object[]requestParameterValues){
  9. this.requestClass=requestClass;
  10. this.requestMethod=requestMethod;
  11. this.requestParameterTypes=requestParameterTypes;
  12. this.requestParameterValues=requestParameterValues;
  13. }
  14. @Override
  15. publicClass<?>getRequestClass(){
  16. returnrequestClass;
  17. }
  18. @Override
  19. publicStringgetRequestMethod(){
  20. returnrequestMethod;
  21. }
  22. @Override
  23. publicClass<?>[]getRequestParameterTypes(){
  24. returnrequestParameterTypes;
  25. }
  26. @Override
  27. publicObject[]getRequestParameterValues(){
  28. returnrequestParameterValues;
  29. }
  30. }

MyGenericResponse.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicclassMyGenericResponseimplementsMyResponse{
  3. privateObjectobj=null;
  4. publicMyGenericResponse(Objectobj){
  5. this.obj=obj;
  6. }
  7. @Override
  8. publicObjectgetResult(){
  9. returnobj;
  10. }
  11. }

MyGenericResponseHandler.java
  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. publicclassMyGenericResponseHandler<T>implementsMyResponseHandler<T>{
  3. @Override
  4. publicThandle(MyResponseresponse){
  5. return(T)response.getResult();
  6. }
  7. }

5. 下面是两个辅助类

SerializableUtil.java

  1. packagecom.googlecode.garbagecan.test.socket;
  2. importjava.io.ByteArrayInputStream;
  3. importjava.io.ByteArrayOutputStream;
  4. importjava.io.IOException;
  5. importjava.io.ObjectInputStream;
  6. importjava.io.ObjectOutputStream;
  7. publicclassSerializableUtil{
  8. publicstaticbyte[]toBytes(Objectobject){
  9. ByteArrayOutputStreambaos=newByteArrayOutputStream();
  10. ObjectOutputStreamoos=null;
  11. try{
  12. oos=newObjectOutputStream(baos);
  13. oos.writeObject(object);
  14. byte[]bytes=baos.toByteArray();
  15. returnbytes;
  16. }catch(IOExceptionex){
  17. thrownewRuntimeException(ex.getMessage(),ex);
  18. }finally{
  19. try{
  20. oos.close();
  21. }catch(Exceptione){}
  22. }
  23. }
  24. publicstaticObjecttoObject(byte[]bytes){
  25. ByteArrayInputStreambais=newByteArrayInputStream(bytes);
  26. ObjectInputStreamois=null;
  27. try{
  28. ois=newObjectInputStream(bais);
  29. Objectobject=ois.readObject();
  30. returnobject;
  31. }catch(IOExceptionex){
  32. thrownewRuntimeException(ex.getMessage(),ex);
  33. }catch(ClassNotFoundExceptionex){
  34. thrownewRuntimeException(ex.getMessage(),ex);
  35. }finally{
  36. try{
  37. ois.close();
  38. }catch(Exceptione){}
  39. }
  40. }
  41. }

IOUtil.java
  1. packagecom.googlecode.garbagecan.test.socket;
  2. importjava.io.InputStream;
  3. importjava.io.OutputStream;
  4. importjava.net.Socket;
  5. publicclassIOUtil{
  6. publicstaticvoidcloseQuietly(InputStreamis){
  7. try{
  8. is.close();
  9. }catch(Exceptione){
  10. }
  11. }
  12. publicstaticvoidcloseQuietly(OutputStreamos){
  13. try{
  14. os.close();
  15. }catch(Exceptione){
  16. }
  17. }
  18. publicstaticvoidcloseQuietly(Socketsocket){
  19. try{
  20. socket.close();
  21. }catch(Exceptione){
  22. }
  23. }
  24. }

6. 最后是一个测试类,其中包含了两种实现的测试test1()和test2()。其中只是创建server和client的部分不同。下面说一下客户端怎样发送请求并获取服务器端响应。

首先创建一个MyRequest的实例,在其中告诉服务器端希望服务器端使用那个类的那个方法来处理这个相应,然后是参数类型列表和参数列表。 然后使用MyClient的execute()方法来发送上面创建的请求,并获取服务器端响应。也可以使用MyGenericResponseHandler接口来直接获取相应中的结果。

  1. packagecom.googlecode.garbagecan.test.socket.sample10;
  2. importjava.util.List;
  3. publicclassTest{
  4. privatestaticintport=10000;
  5. publicstaticvoidmain(String[]args)throwsException{
  6. //test1();
  7. test2();
  8. }
  9. publicstaticvoidtest1()throwsException{
  10. MyServermyServer=newMyServerSimpleImpl(port);
  11. myServer.startup();
  12. Thread.sleep(3000);
  13. MyClientmyClient=newMyClientSimpleImpl("localhost",port);
  14. MyRequestrequest=null;
  15. MyResponseresponse=null;
  16. request=newMyGenericRequest(MyUserServiceImpl.class,"list",newClass<?>[]{int.class},newObject[]{2});
  17. response=myClient.execute(request);
  18. System.out.println(response.getResult());
  19. List<User>users=myClient.execute(request,newMyGenericResponseHandler<List<User>>());
  20. System.out.println(users);
  21. request=newMyGenericRequest(MyUserServiceImpl.class,"findByName",newClass<?>[]{String.class},newObject[]{"kongxx"});
  22. response=myClient.execute(request);
  23. System.out.println(response.getResult());
  24. Useruser=myClient.execute(request,newMyGenericResponseHandler<User>());
  25. System.out.println(user);
  26. response=myClient.execute(newMyGenericRequest(MyUserServiceImpl.class,"test",newClass<?>[]{},newObject[]{}));
  27. System.out.println(response.getResult());
  28. }
  29. publicstaticvoidtest2()throwsException{
  30. MyServermyServer=newMyServerNIOImpl(port);
  31. myServer.startup();
  32. Thread.sleep(3000);
  33. MyClientmyClient=newMyClientNIOImpl("localhost",port);
  34. MyRequestrequest=null;
  35. MyResponseresponse=null;
  36. request=newMyGenericRequest(MyUserServiceImpl.class,"list",newClass<?>[]{int.class},newObject[]{2});
  37. response=myClient.execute(request);
  38. System.out.println(response.getResult());
  39. List<User>users=myClient.execute(request,newMyGenericResponseHandler<List<User>>());
  40. System.out.println(users);
  41. request=newMyGenericRequest(MyUserServiceImpl.class,"findByName",newClass<?>[]{String.class},newObject[]{"kongxx"});
  42. response=myClient.execute(request);
  43. System.out.println(response.getResult());
  44. Useruser=myClient.execute(request,newMyGenericResponseHandler<User>());
  45. System.out.println(user);
  46. response=myClient.execute(newMyGenericRequest(MyUserServiceImpl.class,"test",newClass<?>[]{},newObject[]{}));
  47. System.out.println(response.getResult());
  48. }
  49. }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值