Android开发之网络通讯详解

Android系统应用开发 实验五 网络通信 实验报告 实验目的: 1、 了解Android网络通信技术 2、 掌握在程序中嵌入浏览器的方法 3、掌握HTTP通信 4、掌握Socket通信 实验内容:(其中,1必做,2和3选一个) 1、 开发一个简单的浏览器程序,实现网址输入、前进、后退、刷新网页等功能。 2、 利用Socket协议,实现一个简单的聊天程序。 3、 利用HTTP协议,向服务器读取、发送数据。 立即下载

真正的才智是刚毅的志向。 —— 拿破仑

目前的App如果没有使用到网络技术,仅仅依靠本地是很难提升自己App的用户量的,21世纪是互联网时代,网络技术已经融入并改变了我们的生活。想象一下假如一周没有网络供你使用,你是不是有跟这个世界脱节了的感觉,哈哈,没有错,网络是如此的重要,因此我们就需要花点时间好好学习下这门技术。如有谬误,欢迎批评指正,如有疑问欢迎留言


通过本篇博客你将学到以下知识点:

①HTTP网络请求的方式

②Android中网络通讯实现的原理和步骤

③基于Http协议的访问网络的实例

④基于Socket套接字的服务器和android客户端交互的一个实例

⑤GET请求和POST请求的区别

⑥Socket与HTTP区别


一、HTTP网络请求的方式

   在Android中发送HTTP网络请求一般有三种方式HttpURLConnectoin、HttpClient、AndroidHttpClient(很少用)。其中AndroidHttpClient这种方式很少使用。接下来我们就来分别学习下这三种HTTP协议的网络请求。

1.1 HttpURLConnection

    Java.NET.*提供与联网有关的类,包括流和数据包套接字、Internet协议、常见HTTP处理,通过HttpURLConnection(继承自URLConnection),可用于向指定网站发送GET请求、POST请求。

HttpURLConnection访问网络的主要步骤:

首先需要创建一个URL对象,并传入目标的网络地址这样就得到了一个URL对象,得到此对象后调用URL的openConnection();方法即可得到HttpURLConnection对象,与之对应的代码如下:

[java]  view plain  copy
  1. URL url=new URL("http://www.baidu.com");  
  2. httpUrlConnection = (HttpURLConnection) url.openConnection();  

得到了HttpURLConnection的实例后,可以设置HTTP请求所使用的方法,在上面我们也提到主要有两个,GET和POST。GET表示希望从服务器那里获取数据,而POST则表示希望提交数据给服务器。首先来看下GET方法请求网络的操作,设置请求方式为GET的代码如下:

[java]  view plain  copy
  1. httpUrlConnection.setRequestMethod("GET");  

然后可以针对自己的业务需求去定制符合这个需求的代码,比如设置连接超时、读取超时的毫秒数、设置允许输出流,即允许上传数据到web服务、设置允许输入流,即允许下载、设置是否允许使用缓存等等。代码如下:

[java]  view plain  copy
  1. httpUrlConnection = (HttpURLConnection) url.openConnection();  
  2. httpUrlConnection.setRequestMethod("GET"); //设置请求方法  
  3. httpUrlConnection.setConnectTimeout(8000); //设置链接超时的时间  
  4. //将读超时设置为指定的超时值,以毫秒为单位。用一个非零值指定在建立到资源的连接后从input流读入时的超时时间。  
  5. //如果在数据可读取之前超时期满,则会引发一个 java.net.sockettimeoutexception。超时时间为零表示无穷大超时。  
  6. httpUrlConnection.setReadTimeout(8000);  
  7. httpUrlConnection.setDoInput(true); //允许输入流,即允许下载  
  8. httpUrlConnection.setDoOutput(true); //允许输出流,即允许上传  
  9. httpUrlConnection.setUseCaches(false); //设置是否使用缓存  

设置好这些之后,就可以调用相应的方法从服务器获得它返回的"流"了,这个返回的“流”中就包含了我们所想要的信息,所调用的方法就是getInputStream(),我们可以这样写代码

[java]  view plain  copy
  1. InputStream inputStream =httpUrlConnection.getInputStream();  
这样就得到了服务器给我返回的这个“流”,我们就可以从这个“流”中来读取数据了,在操作完后大家不要忘记在最后调用disconnect()这个方法将这个HTTP链接关闭掉。再了解了这些理论知识后,接着我们一起来完成一个案例,相信通过这个案例会加深你对HttpURLConnection的了解。
首先来看一下这个案例的运行效果,效果图如下:
 

 
这个界面的布局如下

[html]  view plain  copy
  1. <LinearLayout  
  2.     xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width= "match_parent"  
  5.     android:layout_height= "match_parent"  
  6.     android:orientation="vertical" >  
  7.      
  8.     <Button  
  9.         android:id= "@+id/btn_send_request"  
  10.         android:layout_width= "match_parent"  
  11.         android:layout_height= "wrap_content"  
  12.         android:text="请求网络" />  
  13.   
  14.     <ScrollView  
  15.         android:layout_width= "match_parent"  
  16.         android:layout_height= "match_parent" >  
  17.          
  18.      <TextView  
  19.         android:id= "@+id/tv_show_content"  
  20.             android:layout_width= "wrap_content"  
  21.             android:layout_height= "wrap_content" />  
  22.     </ScrollView >  
  23.   
  24. </LinearLayout>  

接着来看看MainActivity的代码

[java]  view plain  copy
  1. package com.example.httpcommunicatepractice;  
  2.   
  3.   
  4. import java.io.BufferedReader;  
  5. import java.io.InputStream;  
  6. import java.io.InputStreamReader;  
  7. import java.net.HttpURLConnection;  
  8. import java.net.URL;  
  9.   
  10.   
  11. import android.support.v7.app.ActionBarActivity;  
  12. import android.view.View;  
  13. import android.view.View.OnClickListener;  
  14. import android.widget.Button;  
  15. import android.widget.TextView;  
  16. import android.os.Bundle;  
  17. import android.os.Handler;  
  18. import android.os.Message;  
  19.   
  20.   
  21. public class MainActivity extends ActionBarActivity implements OnClickListener {  
  22.        
  23.      private Button btnSendRequest;  
  24.      private TextView tvShowContent;  
  25.      private static final String httpUrl="http://www.imooc.com/api/teacher?type=4&num=40" ;  
  26.      public static final int SHOW_RESPONSE = 0;  
  27.   
  28.      private Handler handler= new Handler(){  
  29.             public void handleMessage(Message msg) {  
  30.                  switch (msg. what) {  
  31.                  case SHOW_RESPONSE:  
  32.                      String result=(String) msg. obj;  
  33.                       tvShowContent.setText(result);  
  34.                       break;  
  35.                 }  
  36.            };  
  37.      };  
  38.        
  39.     @Override  
  40.     protected void onCreate(Bundle savedInstanceState) {  
  41.         super.onCreate(savedInstanceState);  
  42.         setContentView(R.layout. activity_main);  
  43.          
  44.         btnSendRequest=(Button) findViewById(R.id.btn_send_request );  
  45.         tvShowContent=(TextView) findViewById(R.id.tv_show_content );  
  46.          
  47.         btnSendRequest.setOnClickListener( this);  
  48.          
  49.     }  
  50.      
  51.     @Override  
  52.      public void onClick(View v) {  
  53.      if(v.getId()==R.id. btn_send_request){  
  54.            httpUrlConnection_get();  
  55.      }  
  56.      }  
  57.   
  58.      private void httpUrlConnection_get() {  
  59.             new Thread( new Runnable() {  
  60.                   
  61.                  @Override  
  62.                  public void run() {  
  63.                        
  64.                      InputStreamReader in= null;  
  65.                      HttpURLConnection httpUrlConnection= null;  
  66.                       try {  
  67.                              
  68.                           URL url= new URL("http://www.baidu.com" );  
  69.                           httpUrlConnection = (HttpURLConnection) url.openConnection();  
  70.                           httpUrlConnection.setRequestMethod( "GET"); //设置请求方法  
  71.                           httpUrlConnection.setConnectTimeout(8000); //设置链接超时的时间  
  72.                           //将读超时设置为指定的超时值,以毫秒为单位。用一个非零值指定在建立到资源的连接后从input流读入时的超时时间。  
  73.                           //如果在数据可读取之前超时期满,则会引发一个 java.net.sockettimeoutexception。超时时间为零表示无穷大超时。  
  74.                           httpUrlConnection.setReadTimeout(8000);  
  75.                           httpUrlConnection.setDoInput( true); //允许输入流,即允许下载  
  76.                           httpUrlConnection.setDoOutput( true); //允许输出流,即允许上传  
  77.                           httpUrlConnection.setUseCaches( false); //设置是否使用缓存  
  78.                             //建立连接,上面对urlConn的所有配置必须要在connect之前完,这里需要注意的是  
  79.                             //connect这个方法,在getInputStream()方法中会隐式的被调用,所以这里不写也没有问题  
  80.                            httpUrlConnection.connect();  
  81.                            InputStream inputStream=httpUrlConnection.getInputStream();  
  82.                            in= new InputStreamReader(inputStream);  
  83.                            BufferedReader bf= new BufferedReader(in);  
  84.                            StringBuffer sb= new StringBuffer();  
  85.                            String inputLine= null;  
  86.                              
  87.                            while((inputLine=bf.readLine())!= null){  
  88.                                 sb.append(inputLine);  
  89.                            }  
  90.                              
  91.                            Message message= new Message();  
  92.                            message. what= SHOW_RESPONSE;  
  93.                            message. obj=sb.toString();  
  94.                             handler.sendMessage(message);  
  95.                              
  96.                      } catch (Exception e) {  
  97.                            e.printStackTrace();  
  98.                      } finally{  
  99.                             if(httpUrlConnection!= null){  
  100.                                 httpUrlConnection.disconnect();  
  101.                            }  
  102.                      }  
  103.                 }  
  104.            }).start();  
  105.              
  106.      }  
  107.   
  108. }  

从代码中可以看到当点击“请求网络”这个按钮的时候,首先会去调用httpUrlConnection_get()这个方法,在这个方法中回去开启一个子线程去访问网络,这里我们访问的是百度首页,通过httpUrlConnection.getInputStream()这个方法获取百度给我们返回过来的"流",然后通过BufferedReader去读取这个流并将读取的结果放进StringBuffer中,通过Handler、Message的形式发送个主线程,更新TextView的内容。
在这里要注意三点:
①不要忘了添加访问网路的权限

[html]  view plain  copy
  1. <uses-permission android:name= "android.permission.INTERNET" />  

②更新控件的操作必须要放到主线程中,不能在子线程中更新控件
③可能看完后会有人问,一般我们在真正的开发中都是带有参数的,这里我提供了一个连接在上述MainActivity中大家可以看到有这么一行代码

[java]  view plain  copy
  1. private static final String httpUrl ="http://www.imooc.com/api/teacher?type=4&num=40" ;  

这个连接是慕课网的一个连接,大家可以把上述的"http://www.baidu.com",直接换成httpUrl,然后运行,可以发现它返回了相应的json。这里我这样做的主要目的就是要告诉大家,这个方式请求的参数是直接加载链接后面的。
那么如果是想要提交数据给服务器应该怎么办呢?其实也不复杂,只需要将HTTP 请求的方法改成POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键值对的形式存在,数据与数据之间用&符号隔开,比如说我们想要向服务器提交用户名和密码,就可以这样写:

[java]  view plain  copy
  1. httpUrlConnection.setRequestMethod( "POST");//设置请求方法  
  2. DataOutputStream dataOutputStream=new DataOutputStream(urlConnection.getOutputStream());  
  3.  //要提交的参数   
  4. String content = "username=admin&password=123456";  
  5. //将要上传的内容写入流中   
  6. dataOutputStream.writeBytes(content);    
  7. //刷新、关闭   
  8. dataOutputStream.flush();   
  9. dataOutputStream.close();  

关于HttpURLConnection的post请求大家可以找个自己公司的POST请求的接口,或者找一个POST请求的网站,自己去试试。我这里就不做演示了


1.2 HttpClient

     HttpClient是Apache提供的HTTP网络访问接口,从一开始的时候就被引入到了AndroidAPI中。它可以完成和HttpURLConnection几乎一模一样的效果,但是和HttpURLConnection不同的是HttpClient是org.apache.*下提供的类两者之间的用法也是有很大的差别的,HttpClient与HttpURLConnection一样也同样有GET和POS请求两种方式,同HttpURLConnection一样首先我们来学习一下HttpClient访问网络的主要步骤:
首先我们应该清楚HtppClient是一个接口,因此无法创建它的实例,通常情况下我们都会创建一个Apache提供给我们的一个默认的实例,代码如下:

[java]  view plain  copy
  1. //取得HttpClient对象  
  2. HttpClient httpClient= new DefaultHttpClient();  

假如我们想发送一条GET请求,我们需要创建一个HttpGet对象,将要访问的目标网络的地址传递过去,然后调用HttpClient的execute()方法并将HttpGet对象传入即可,与之对应的代码如下:

[java]  view plain  copy
  1. //HttpGet连接对象  
  2. HttpGet httpGet= new HttpGet("http://www.baiduc.com" );  
  3. //请求HttpClient,取得HttpResponse  
  4. HttpResponse httpResponse=httpClient.execute(httpGet);  

假如我们想发送一条POST请求,我们需要创建一个HttpPost请求,将要访问的目标网络的地址传递过去,代码如下:

[java]  view plain  copy
  1. HttpPost httpPost=new HttpPost( "http://www.baidu.com" );  

与GET方法不同的是,POST请求需要NameValuePair集合来存放待提交的参数,并将这个参数传到UrlEncodedFormEntity中,然后调用HttpPost的setEntity()方法将构建好的UrlEncodedFormEntity设置进去,与之对应的代码如下:

[java]  view plain  copy
  1. List<NameValuePair> params=new ArrayList<NameValuePair>();  
  2. params.add( new BasicNameValuePair("username" ,"admin" ));  
  3. params.add( new BasicNameValuePair("password" ,"123456" ));  
  4. HttpEntity httpEntity = new UrlEncodedFormEntity(params, "utf-8");  
  5. httpPost.setEntity(httpEntity);  

接下来的操作就与GET方法类似了,调用HttpClient的execute()方法,并将HttpPost对象传入即可,代码如下:

[java]  view plain  copy
  1. HttpResponse httpResponse = httpClient.execute(httpPost);  

从上面我们可以看到它返回了一个HttpResponse对象,服务器返回的所有信息都包含在了这个对象中,通常我们的做法是先取得服务器返回的状态码,如果返回的状态码为200就说明请求和响应都成功了,此时我们就可以按照业务需求去进行相应的代码的书写了,与之对应的代码如下:

[java]  view plain  copy
  1. if(httpResponse.getStatusLine().getStatusCode()==HttpStatus.SC_OK){  
  2.     //访问网络成功     
  3. }  

访问网络成功之后,可能有的同学会说,怎样拿到服务器给返回的数据呢?其实Apache已经给我们提供好方法了,我们可以调用HttpResponse这个类的getEntity()方法来获取一个HttpEntity对象,然后在调用EntityUtils.toString()这个方法将HttpEntity对象转换成字符串就可以了,与之对应的代码如下:

[java]  view plain  copy
  1. HttpEntity entity = httpResponse.getEntity();  
  2. String strResult = EntityUtils.toString(entity,"utf-8");  

可以看到在转换成字符串时,我们还可以指定相应的编码,防止转换后出现乱码的情况,至此HttpClient访问网络的内容就讨论的差不多了,接着来一个案例,这个案例HttpURLConnection的GET方法一样,只不过这里我们采用的是HttpClient的GET方法,因此我们只要稍作修改即可,修改过后的代码如下:

[java]  view plain  copy
  1. public class MainActivity extends ActionBarActivity implements OnClickListener {  
  2.        
  3.    。。。。。。。。。  
  4.     @Override  
  5.      public void onClick(View v) {  
  6.             switch (v.getId()) {  
  7.             case R.id. btn_send_request:  
  8.                 httpClient_get();  
  9.                  break;  
  10.            }  
  11.      }  
  12.      
  13.   
  14.      private void httpClient_get() {  
  15.              
  16.             new Thread( new Runnable() {  
  17.                   
  18.                  @Override  
  19.                  public void run() {  
  20.                        
  21.                       try {  
  22.                             //HttpGet连接对象  
  23.                            HttpGet httpGet= new HttpGet("http://www.baidu.com" );  
  24.                             //取得HttpClient对象  
  25.                            HttpClient httpClient= new DefaultHttpClient();  
  26.                             //请求HttpClient,取得HttpResponse  
  27.                            HttpResponse httpResponse=httpClient.execute(httpGet);  
  28.                            if(httpResponse.getStatusLine().getStatusCode()==HttpStatus. SC_OK){  
  29.                                  //取得返回的字符串  
  30.                                  resultData=EntityUtils. toString(httpResponse.getEntity(),"utf-8");  
  31.                                 Message message= new Message();  
  32.                                 message. what= SHOW_RESPONSE;  
  33.                                 message. obj= resultData;  
  34.                                  handler.sendMessage(message);  
  35.                            }  
  36.                      } catch (Exception e) {  
  37.                            e.printStackTrace();  
  38.                      }  
  39.                        
  40.                 }  
  41.            }).start();  
  42.              
  43.      }  
  44.        
  45. }  

这与刚开始的运行效果是一样的。如果有参数对于GET请求直接加在链接后面就行了,你也可以跟HttpURLConnection一样试试访问这个链接"http://www.imooc.com/api/teacher?type=4&num=40";它的POST请求大家可以自己试验一下。


1.3 Android.net.*(Android网络接口)

常常使用此包下的类进行Android特有的网络编程,如:访问WiFi,访问Android联网信息,邮件等功能。


1.4 GET和POST请求方式的区别

 Get和Post提交的区别
 ①get提交,提交的信息都显示在地址栏中。post提交,提交的信息不显示在地址栏中
 ②get提交,对于敏感的数据信息不安全。post提交,对于敏感信息安全。
 ③get提交,对于大数据不行,因为地址栏存储体积有限post提交,可以提交大体积数据
 ④get提交,将信息封装到了请求消息的请求行中post提交,将消息封装到了请求体中

 在服务器的一个区别:
 如果出现将中文提交到tomcat服务器,服务器默认会用iso8859-1进行解码此时会出现乱码,通过iso8859-1进行编码,在用指定的中文码表解码,即可。这种方式get提交和post提交都有效,但是对于post提交方式提交的中文,还有另一种解决办法,就是直接使用服务端一个对象,request对象的setCharacterEncoding方法直接设置指定的中文码表就可以将中文数据解析出来。这个方法只对请求体中的数据进行解码。


1.5 HttpURLConnection和HttpClient应该使用哪种方式?

     目前网上的一些比较好的开源项目它们的网络请求基本都是基于HttpURLConnection或者HttpClient的,比如:Volley的Http请求在 android 2.3 版本之前是通过HttpClient ,在之后的版本是通过URLHttpConnection。xUtils都是通过HttpClient请求网(bitmap模块图片下载是通过URLHttpConnection),以及我们经常用的Universal-Image-Loader是采用HttpURLConnection等。URLHttpConnection默认支持GZIP压缩,api操作简单。那么究竟是选择HttpURLConnection呢?还是选择HttpClient呢?大家可以参考郭霖的这篇博客:http://blog.youkuaiyun.com/guolin_blog/article/details/12452307这里我只将他最后的总结的话跟大家分享下,如果你想详细的学习,就点击连接,最后的总结:

在Android 2.2版本之前,HttpClient拥有较少的bug,因此使用它是最好的选择。

而在Android 2.3版本及以后,HttpURLConnection则是最佳的选择。它的API简单,体积较小,因而非常适用于Android项目。压缩和缓存机制可以有效地减少网络访问的流量,在提升速度和省电方面也起到了较大的作用。对于新的应用程序应该更加偏向于使用HttpURLConnection,因为在以后的工作当中我们也会将更多的时间放在优化HttpURLConnection上面。


二、Socket(套接字)


2.1 简介及分类

    Socket(套接字)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
应用程序与服务器通信可以采用两种模式:TCP可靠通信 和UDP不可靠通信。


2.2 建立Socket链接

首先来看一点理论知识:
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。


服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。


客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。


连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。


接着我们来看一张来自于百度百科的TCP通讯模型


从图中我们可以看出要想建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务端称为ServerSocket。在建立服务端(ServerSocket)时需要使用ServertSocket对象,这个对象会自动对其构造函数中传入的端口号进行监听,并在接收到连接请求后,使用ServerSocket.accept()方法返回一个连接的Socket对象。而不管是建立客户端还是在进行其他数据交换方面的操作时,都需要使用Socket类,Socket类在进行初始化时需要传入Server端的Ip地址和端口号,代码如下:

[java]  view plain  copy
  1. Server socket = new Socket(HOST, PORT);   //host:为服务器的IP地址  port:为服务器的端口号  
并返回连接到Server端的一个Socket对象,如果是连接失败,那么将返回异常,同ServerSocket,它也是自动进行连接请求的。如果你的配置也就是服务器的Ip地址以及端口号配置好的话,这个连接就已经建立好了,但是仅仅是建立连接还没有用,进行数据的交互才是我们最终的目的,此时就需要用到IO流中的OutputStream和InputStream了,这一点从图中也可以看到,主要有两种情况:
当应用程序需要对流进行数据写操作时,可以使用Socket.getOutputStream()方法返回的数据流进行操作。
当应用程序要从流中取出数据时,可以使用Socket.getInputStream()方法返回的数据流进行操作。我们可以这样操

[java]  view plain  copy
  1. InputStream inputStream = socket.getInputStream();  
  2. InputStreamReader inputStreamReader = new InputStreamReader(inputStream);  
  3. BufferedReader bufferedReader = new BufferedReader(inputStreamReader);  

这样我们就可以从bufferedReader中读取数据了。与之对应的socket.getOutputStream()的操作与之类似。好了说了这么多理论知识,我们来一个小例子
这个例子要实现的效果的效果图如下:



从图中可以看出它实现了手机客户端通过Socket与服务器进行通讯这个功能,手机客户端发送给服务器一条消息后,服务器会把这条消息再发给客户端。它是怎么实现的呢?首先来理理思路:
通过上面的理论知识可以想到我们首要做的就是建立socket连接,这样才能保证客户端和服务端进行通讯,其次在连接建立后再安卓端会向服务端发送一条消息,通过Socket.getOutputStream()这个方法得到输出流,然后然后将这个输出的字符流转化为字节流转化的方法是用OutputStreamWriter然后再通过BufferWriter这个类的对象将要发送的这个消息内容通过write()方法写进去,通过调用BufferWriter.flush()或close()方法后,将其发送到服务器端。它的示例代码为:

[java]  view plain  copy
  1. BufferedWriter  mWriter = new BufferedWriter( new OutputStreamWriter(mSocket.getOutputStream(),"utf-8" ));  
  2. mWriter.write("你想向服务器端发送的内容"+“\n” );  
  3. mWriter.flush();  

而服务器端通过

[java]  view plain  copy
  1. BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream,"utf-8" ));  
通过这个方法就可以得到客户端向服务器发送过来的消息。
 
首先来看下服务端的代码
[java]  view plain  copy
  1. package com.beijing.edu;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.BufferedWriter;  
  5. import java.io.IOException;  
  6. import java.io.InputStream;  
  7. import java.io.InputStreamReader;  
  8. import java.io.OutputStreamWriter;  
  9. import java.net.ServerSocket;  
  10. import java.net.Socket;  
  11.   
  12. public class MySocketServer {  
  13.   
  14.      private static final int SOCKET_PORT = 1234;  
  15.      private ServerSocket serverSocket = null;  
  16.      private boolean flag = true;  
  17.      private BufferedReader reader;  
  18.      private BufferedWriter writer;  
  19.   
  20.      /** 
  21.       * @param args 
  22.       */  
  23.      public static void main(String[] args) {  
  24.            MySocketServer socketServer = new MySocketServer();  
  25.            socketServer.initSocket();  
  26.      }  
  27.   
  28.      private void initSocket() {  
  29.             try {  
  30.                  serverSocket = new ServerSocket( SOCKET_PORT);  
  31.                 System. out.println( "服务已经启动,端口号:" + SOCKET_PORT);  
  32.                  while ( flag) {  
  33.                      Socket clientSocket = serverSocket.accept();  
  34.                      System. out.println( "有客户端连接" );  
  35.   
  36.                      SocketThread socketThread = new SocketThread(clientSocket);  
  37.                      socketThread.start();  
  38.   
  39.                 }  
  40.   
  41.            } catch (IOException e) {  
  42.                 e.printStackTrace();  
  43.            } finally {  
  44.                  try {  
  45.                       writer.close();  
  46.                 } catch (IOException e) {  
  47.                      e.printStackTrace();  
  48.                 }  
  49.            }  
  50.      }  
  51.   
  52.      public class SocketThread extends Thread {  
  53.   
  54.             private Socket socket;  
  55.   
  56.             public SocketThread(Socket clientSocket) {  
  57.                  this. socket = clientSocket;  
  58.            }  
  59.   
  60.             @Override  
  61.             public void run() {  
  62.                  super.run();  
  63.   
  64.                 InputStream inputStream;  
  65.                  try {  
  66.                       //获取输入流  
  67.                      inputStream = socket.getInputStream();  
  68.                       //得到读取BufferedReader对象  
  69.                       reader = new BufferedReader( new InputStreamReader(inputStream,"utf-8" ));  
  70.                       writer = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(),"utf-8" ));  
  71.                       //循环读取客户端发过来的消息  
  72.                       while ( flag) {  
  73.                             if ( reader.ready()) {  
  74.                                 String result= reader.readLine();  
  75.                                 System. out.println( "客户端发过来的消息为:" + result);  
  76.                                  //将服务端发过来的消息,发送给客户端  
  77.                                  writer.write( "服务端发过来的消息:" + result+"\n" );  
  78.                                  writer.flush();  
  79.                            }  
  80.                      }  
  81.                 } catch (IOException e) {  
  82.                      e.printStackTrace();  
  83.                 }  
  84.   
  85.            }  
  86.      }  
  87. }  

其实服务端的代码非常简单,就是开启一个子线程来循环的从客户端那里获取消息,获取消息后将这个消息打印出来,并将这个消息再发送给客户端就,注意这里的服务端的代码我是运行在MyEclipse中的,是一个Java Project,你也可以在Eclipse中建一个Java Project。
接着来看看Android客户端的代码:

[java]  view plain  copy
  1. package com.example.socketclient1;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.BufferedWriter;  
  5. import java.io.IOException;  
  6. import java.io.InputStreamReader;  
  7. import java.io.OutputStreamWriter;  
  8. import java.net.Socket;  
  9. import java.text.SimpleDateFormat;  
  10. import java.util.Date;  
  11.   
  12. import android.support.v7.app.ActionBarActivity;  
  13. import android.os.AsyncTask;  
  14. import android.os.Bundle;  
  15. import android.os.Handler;  
  16. import android.view.View;  
  17. import android.view.View.OnClickListener;  
  18. import android.widget.Button;  
  19. import android.widget.EditText;  
  20. import android.widget.TextView;  
  21.   
  22.   
  23. public class MainActivity extends ActionBarActivity implements OnClickListener {  
  24.        
  25.      private EditText etIp;  
  26.      private EditText etPort;  
  27.      private Button btnStart;  
  28.      private TextView tvContent;  
  29.      private EditText etSendContent;  
  30.      private Button btnSendMessage;  
  31.        
  32.      private Socket clientSocket;  
  33.      private boolean isReceivingMsgReady;  
  34.      private BufferedReader mReader;  
  35.      private BufferedWriter mWriter;  
  36.      private StringBuffer sb= new StringBuffer();  
  37.        
  38.      private Handler handler= new Handler(){  
  39.            public void handleMessage(android.os.Message msg) {  
  40.                switch (msg. what) {  
  41.                case 0:  
  42.                     sb.append((String)msg. obj);  
  43.                     tvContent.setText( sb.toString());  
  44.                     break;  
  45.               }  
  46.           };  
  47.      };  
  48.   
  49.     @Override  
  50.     protected void onCreate(Bundle savedInstanceState) {  
  51.         super.onCreate(savedInstanceState);  
  52.         setContentView(R.layout. activity_main);  
  53.          
  54.         etIp=(EditText) findViewById(R.id. et_ip);  
  55.         etPort=(EditText) findViewById(R.id. et_port);  
  56.         btnStart=(Button) findViewById(R.id. btn_start);  
  57.         etSendContent=(EditText) findViewById(R.id.et_send_content );  
  58.         btnSendMessage=(Button) findViewById(R.id.btn_send );  
  59.         tvContent=(TextView) findViewById(R.id.tv_content );  
  60.         btnStart.setOnClickListener( this);  
  61.         btnSendMessage.setOnClickListener( this);  
  62.          
  63.     }  
  64.   
  65.      @Override  
  66.      public void onClick(View v) {  
  67.            switch (v.getId()) {  
  68.             
  69.            case R.id. btn_start:  
  70.                if(! isReceivingMsgReady){  
  71.                    initSocket();  
  72.               }  
  73.                break;  
  74.            case R.id. btn_send:  
  75.               send();  
  76.                break;  
  77.           }  
  78.      }  
  79.   
  80.      private void send() {  
  81.   
  82.            new AsyncTask<String, Integer, String>() {  
  83.   
  84.                @Override  
  85.                protected String doInBackground(String... params) {  
  86.                    sendMsg();  
  87.                     return null;  
  88.               }  
  89.   
  90.           }.execute();  
  91.        
  92.      }  
  93.   
  94.      /** 
  95.       * 向服务器发送消息 
  96.       */  
  97.      protected void sendMsg() {  
  98.            try {  
  99.               String msg=etSendContent.getText().toString();  
  100.                //通过BufferedWriter对象向服务器写数据  
  101.                mWriter.write(msg+ "\n");  
  102.                //一定要调用flush将缓存中的数据写到服务器  
  103.                mWriter.flush();  
  104.               String str= "\n"+ "我:" +msg+"   "+getTime(System.currentTimeMillis ())+"\n" ;  
  105.                 handler.obtainMessage(0,str).sendToTarget();  
  106.           } catch (IOException e) {  
  107.               e.printStackTrace();  
  108.           }  
  109.      }  
  110.   
  111.      private void initSocket() {  
  112.             
  113.            new Thread( new Runnable() {  
  114.                 
  115.                @Override  
  116.                public void run() {  
  117.                    String ip= etIp.getText().toString();  
  118.                     int port=Integer.parseInt(etPort.getText().toString());  
  119.                      
  120.                     try {  
  121.                          isReceivingMsgReady= true;  
  122.                          //在子线程中初始化Socket对象  
  123.                          clientSocket= new Socket(ip,port);  
  124.                          //根据clientSocket.getInputStream得到BufferedReader对象,从而从输入流中获取数据  
  125.                          mReader= new BufferedReader( new InputStreamReader(clientSocket.getInputStream(),"utf-8" ));  
  126.                          //根据clientSocket.getOutputStream得到BufferedWriter对象,从而从输出流中获取数据  
  127.                          mWriter= new BufferedWriter( new OutputStreamWriter(clientSocket.getOutputStream(),"utf-8" ));  
  128.                          while( isReceivingMsgReady){  
  129.                               if( mReader.ready()){  
  130.                                    handler.obtainMessage(0, mReader.readLine()).sendToTarget();  
  131.                              }  
  132.                              Thread. sleep(200);  
  133.                         }  
  134.                          mWriter.close();  
  135.                          mReader.close();  
  136.                          clientSocket.close();  
  137.                    } catch (Exception e) {  
  138.                         e.printStackTrace();  
  139.                    }  
  140.               }  
  141.           }).start();  
  142.             
  143.      }  
  144.        
  145.      /** 
  146.       * 得到自己定义的时间格式的样式 
  147.       * @param millTime 
  148.       * @return 
  149.       */  
  150.      private String getTime( long millTime) {  
  151.           Date d = new Date(millTime);  
  152.           SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" );  
  153.           System. out.println(sdf.format(d));  
  154.            return sdf.format(d);  
  155.      }  
  156. }  

可以看到服务器的代码和客户端的代码都很简单,这样就通过Socket实现了 客户端和服务器的通信。大家可以动手试试,在这里需要提醒大家注意的是:
①千万不要忘了添加权限。

[java]  view plain  copy
  1. <uses-permission android:name="android.permission.INTERNET" />  
  2. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />  
  3. <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />  
  4. <uses-permission android:name="android.permission.READ_PHONE_STATE" />  
  5. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  

② java socket编程时,常使用BufferedReader的readLine()函数来进行数据的读取。但是该函数是阻塞的,如果在接收的数据中不含有'\n','\r'或者结束符时,往往导致进程挂起,从而程序无法继续。所以在发送数据的另一端,一定要记得在最后加换行符,这一点在客户端的体现是mWriter.write(msg+ "\n");这一行代码,后面要加“\n”。


三、Socket与HTTP通讯的区别

           通常情况下Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。而HTTP连接使用的是“请求—响应”的方式,也就是说当需要数据的时候想服务器发送一条请求,服务器收到请求后返回相应的数据,请求过后这个连接就不在了,当下次需要数据时再发送请求,综上:HTTP这种方式连接不是一直存在的,而Socket一旦建立连接,这个连接是一直存在的,直到双方断开连接,这个连接才会消失。

         很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端需要定时向服务器端发送连接请求,这种在客户端的请求方式叫做“轮询”,顾名思义定时的去询问服务器是否有新数据,当服务器有新数据时,此时收到客户端发送的请求就会把这个数据发送给客户端,当服务器再有新消息时就要等到下次客户端的请求了。

         在开发中如果对数据的即时性要求比较高的情况下一般采用Socket通讯,例如聊天的功能。如果对数据的即时性要求不是特别高的情况下一般采用HTTP通讯, 比如在项目中,我们请求的接口,请求一次就给我们返回数据,很少会去不断的去请求服务器的数据。


关于Socket的讲解本节讲解了其理论知识,在下一节将会给大家带来利用Socket实现两个Android手机端的通讯,谢谢大家持续关注。

如有谬误,欢迎批评指正,如有疑问欢迎留言,大家一起讨论,共同进步。

 


源码猛戳这里。。。


原文链接:http://blog.youkuaiyun.com/dmk877/article/details/50893635

Android网络通讯——Socket通信 完善的Android通信Demo,内附网络调试助手供测试。代码可直接拿到项目中使用,无多余的代码。Android与服务端(网络调试助手或自己写的服务端)互相收发数据并打印出来,断线重连,内附测试教程。5个资源分就当是辛苦费了,现在下载啥都要分,小弟也是被逼无奈,各位伙伴捐点,确实没分的评论留邮箱。 立即下载

相关推荐

谈谈Android的三种网络通信方式

Android平台有三种网络接口可以使用,他们分别是:java.net.*(标准Java接口)、Org.apache接口和Android.net.*(Android网络接口)。下面分别介绍这些接口的功能和作用。 1.标准Java接口 java.net.*提供与联网有关的类,包括流、数据包套接字(socket)、Internet协议、常见Http处理等。比如:创建URL,以及URLConnection/HttpURLConnection对象、设置链接参数、链接到服务器、向服务器写数据、从服务器读取数据等通信。这些在Java网络编程中均有涉及,我们看一个简单的socket编程,实现服务器回发客

Android手机实现通过网线直连设备通信

Android网线直连 Android设备通过网线直连

weixin_43743963的博客 4204

实战指南:基于汇川PLC与EtherNET/IP的无线以太网通信配置详解

本文提供了一份详细的实战指南,讲解如何配置汇川PLC基于EtherNET/IP协议的无线以太网通信。通过硬件选型(如达泰DTD419MB模块)、软件设置(使用AutoShop配置IP与I/O映射)及通信测试,实现PLC间稳定、灵活的数据交换,有效解决工业现场布线难题,提升部署与维护效率。

lake5的博客 614

安卓:UDP通信

UDP(User Datagram Protocol)是一种无连接的传输协议,它在网络通信中常用于实时应用程序,例如音频、视频流传输和在线游戏。与TCP(Transmission Control Protocol)相比,UDP具有更低的开销和更高的传输速度,但不提供可靠性和错误检测功能。网络通信的三要素:(1)、IPIP地址(Internet Protocol Address)是用于在网络中标识和定位设备的一组数字。它是互联网通信所必需的基本元素之一,用于确定数据包在网络中的传输路径。

A125679880的博客 4755

【TI毫米波雷达】IWR6843AOPEVM-G与DCA1000EVM实战:从硬件连接到数据流解析

本文详细解析了TI毫米波雷达开发套件IWR6843AOPEVM-G与DCA1000EVM的硬件连接、模式配置及数据流处理全流程。针对官方手册中的常见配置错误,提供了避坑指南和实测验证的正确设置方法,包括开关配置、网络参数等关键步骤。通过mmWave Studio软件配置雷达参数,并深入解析数据采集与处理流程,帮助开发者高效实现毫米波雷达应用开发。

weixin_30653023的博客 368

基于NXP1778的CAN自动波特率检测

一、概念 NXP1778芯片内部自带一个CAN控制器。CAN模块由两部分组成:控制器和验收滤波器。所有的寄存器和RAM都以32位字宽度来访问。 CAN自动波特率检测主要用于以一种CAN的转换的产品中。产品中CAN部分的波特率以前都是通过网页中修改然后配置下去的,依据实际情况的考虑,如果CAN部分的波特率能够根据客户设备的改变而自动改变,将会增加其灵活性。   二、原理 根据CAN控制器的

菜鸟的痕迹专栏 5273

app与服务器对接的http 协议与socket协议区别

app与后台

4978

数据库迁移策略实战:MySQL到PostgreSQL

数据库迁移是一项复杂且细致的任务,需要充分的准备工作、合适的迁移策略和严格的验证与优化步骤。通过选择合适的迁移工具、进行手动迁移和应用程序适配、以及迁移后的验证与优化,可以确保数据迁移的成功和业务的连续性。希望本文的分享能够为大家在MySQL到PostgreSQL的数据库迁移实战中提供一些有用的参考和启示。

shejizuopin的博客 1733

(七) 安卓之网络通信

在安卓应用开发中,网络通信是实现与服务器交互、数据同步和实时通信的重要部分。掌握不同的网络通信方法,可以帮助开发者构建功能丰富且响应迅速的应用。本文将介绍安卓开发中常用的网络通信方法、关键概念、实现方式以及一些实践示例,帮助你全面掌握安卓网络通信技术。掌握安卓网络通信技术是开发现代应用的关键。通过本文的介绍和实践示例,希望你对安卓网络通信方法有一个全面的了解,并在实际开发中灵活应用。持续学习和实践是掌握网络通信技术的关键,祝你在安卓开发的道路上取得成功!

小新的博客 1182

EtherCAT APWR报文实战:从站地址配置(0x0010)的初始化流程与报文解析

本文详细解析了EtherCAT APWR报文在从站地址配置(0x0010寄存器)中的初始化流程与实战应用。通过类比物业分配门牌号,生动解释了APWR报文如何高效批量配置从站地址,并深入探讨了0x0010寄存器的特殊作用、初始化步骤及常见问题排查方法。文章结合IgH主站的三从站配置案例,提供了报文解析技巧和调试建议,帮助工程师快速掌握EtherCAT网络配置的核心技术。

weixin_28744423的博客 412

Unity嵌套ScrollView滑动冲突解决方案:ExecuteEvents.Execute事件透传

在Unity UGUI中,竖向与横向ScrollView嵌套时出现滑动失效,本质是EventSystem事件分发机制的拦截限制。其原理在于PointerDown被外层捕获后,后续PointerMove默认不向下透传;ExecuteEvents.Execute提供了一条绕过常规冒泡流程的官方事件旁路,支持低延迟、方向自适应的精准透传。该技术具备高兼容性与零侵入特性,广泛应用于电商商品页、内容流卡片组、多级导航等需‘列表中嵌套轮播/标签’的真实场景,是解决UGUI嵌套滚动问题的核心工程实践方案。

weixin_33164198的博客 633

徐松亮android教学-杂项笔记

版权声明:本文为博主徐松亮的原创作品,未经允许不得转载,多谢支持!QQ:5387603 推荐点击此链接:欢迎进入徐松亮博客一站式导航搜索(随时更新) 既然叫杂项笔记,就是说杂乱无章的笔记,此笔记的主要目的不是为了教学,而是用于本人学习过程中的点滴记录,之所以开放,就是希望能给新手及爱好者些许点滴概念性帮助。 目录 1,开发环境搭建: 2,模拟器:Genymotion 3,手机开启开发模...

xslxhn的专栏 1633

利用GPAC生成MP4文件

GPAC主要针对学生和内容创作者,代表了一个跨平台的多媒体框架,开发人员可以使用它在 LGPL 许可下制作开源媒体。GPAC多媒体框架兼容范围广泛的流行文件类型,从常见格式(如 AVI、MPEG 和 MOV)到复杂格式(如 MPEG-4 系统或 VRML/X3D)和 360 电影。

深之JohnChen的专栏 1772

android知识点(好)

1. android单实例运行方法 我们都知道Android平台没有任务管理器,而内部App维护者一个Activity history stack来实现窗口显示和销毁,对于常规从快捷方式运行来看都是startActivity可能会使用FLAG_ACTIVITY_NEW_TASK标记来打开一个新窗口,比如Launcher,所以考虑单任务的实现方法比较简单,首先Android123纠正下大家一种错误

小范屯 2069

Cesium教程(十七):淹没分析

淹没分析是根据某片区域的地形及洪水流量速度,动态模拟该地形区域水位逐渐上涨的淹没过程。该功能可适用于山区、丘陵等地形起伏较大区域,模拟洪水涨到安全限定高度的淹没过程,为防洪水救灾提供一定的参考。此外,还可以为河谷地带修建水利工程设计与选址提供参考依据。

WwLK123的博客 3008

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

【Android】网络通讯

看完了Android存储方面的内容,由于之前自己对数据存储方面有自己写过小封装,Android提供的api也都比较简单,所以就不打算再重新写了。今天看了下关于网络通讯方面的内容,进步加深对网络方面的一个理解,写下自己的理解,和一些基本的操作,这样方便以后在遇到一些基础的问题的时候,不需要自己谷歌,通过博客就可以很好地找到,提升效率。网络通讯 1.网络通讯流程 2.WebView

hehedd23 831

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

易安卓E4A多线程大数据网络通讯TCPb1.4下载仓库:打造高效稳定的网络通讯

易安卓E4A多线程大数据网络通讯TCPb1.4下载仓库:打造高效稳定的网络通讯 去发现同类优质开源项目:https://gitcode.com/ 易安卓E4A多线程大数据网络通讯TCPb1.4下载仓库,是专注于大数据网络通讯的顶级模块,为开发者提供稳定、高效的网络通讯解决方案。 项目介绍 易安卓E4A多线程大数据网络通讯TCPb1.4下载仓库,是一款基于E4A框架的网络通讯模块。它凭借卓越的性能和...

gitblog_06788的博客 750

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Android网络通讯原理解析

虽然标题写的是《Android网络通讯原理解析》,实际上,发不管是安卓还是linux系统或者是塞班,Windows,Mac OS还是其他各种各样的操作系统,其内部的网络通讯原理基本一致;有些小伙伴想知道安卓两个应用之间胡这是从安卓应用到服务主机之间数据传输是怎么实现的,但是搜到的大部分讲的比较抽象,因此我决定写一篇文章把这这些知识点串联起来,让大家有一个更加全面的认识; 我们先来看一个场景吧; 我写了一个Http请求图片的应用,然后我把这个应用安装在了我的小米手机上。我打开应用,程序从后台服务器请求到了图

zhonglunshun的专栏 2568

安卓设备的Socket网络通讯例程 (A1) - ESP8266应用

安卓设备的Socket网络通讯例程的开发 (A) 本博文系JGB联合商务组的原创作品,引用请标明出处 此通讯例程实际名称为【Socket网络调试器】,是在Android Studio 4.1环境下开发的安卓设备与一台JGB物联设备的通讯例子。 按惯例,先上调试场景图,不感兴趣的直接忽略之。 安卓设备(这里是P9手机)的【拍照】功能按钮点击后的截图呈示如下: 从图中可看到此时拍下的照片文件已被存放到指定的文件夹中。 为了让读者对其中的核心部件JGB02开发板有个总体印象,我这里展示了它的正视图(有一个低分

JGB联合商务组的博客 634

android网络通讯,Android网络请求通讯

一个多月没写博客了,天天忙到凌晨.具体内容是准备近期的培训资料.这种生活要持续到春节之前.经常感慨上年纪了记忆力衰退的很厉害.许多事情回头就忘的一干二净,额米豆腐.正好昨天刚整理了一下网络通讯的内容,今天写写吧,算是给2014年留下一个尾巴的脚步.Android通讯和java普通通讯没什么两样,不是tcp就是udp,通讯方式也无就是socket、http和soap.最节省资源的方式个人建议还是ht...

weixin_39885683的博客 215

用c#开发安卓程序 (xamarin.android)

作为c#程序员,没有精力和激情去学习java了,又遇到一些项目需要开发手机端,于是我们的networkcomms2.3.1网络通讯框架又要出场了,是的,这是一款来自英国的网络通讯框架,由c#语言编写,其在编写时根据不用的应用环境,写了不同的代码,支持安卓,平果,winphone等平台开发。 ...

chvft44866的专栏 1410

一个基于Unity开发的ANDROID网游模板

该文章将简述我的一个基于Unity开发的ANDROID网游模板的大致开发路程和开发时遇到的关键节点。可能很少涉及具体代码,后续将可通过github获取项目资源。 目录 开发节点简述 核心一:网络通讯实现 核心二:通讯字符串的处理 核心三:Unity上的开发关键点 效果展示 github 开发节点简述 本次开发耗时四天,开发的过程中遇到的问题主要是网络通讯,字符串处理和客户端内的线程状态与服务端线程状态的协调。 服务端: 通过否是否是否是断开错误开始监听端口8080验证连接是否在table中打印正常信.

qq_35465882的博客 601
上一篇: Android中运用OpenGL
下一篇: android 之实现手机震动功能
混魔MJM
博客等级 码龄9年 57粉丝 0原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值