前言
本文用于记录Android网络技术的使用, 包括我们如何发起一条HTTP请求、解析XML、JOSN格式的数据以及最好用的网络库Retrofit。
使用HTTP协议访问网络
关于HTTP协议的工作原理,我们只需要知道客户端向服务器发起一条HTTP请求,服务器接收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理。接下来我们将实现一下手动发送HTTP请求。
使用HttpURLConnection
我们先来大致了解一下使用HttpURLConnection发送HTTP请求的步骤:
- 获取HttpURLConnection实例,一般都是先new出一个URL对象并将目标网站传入,然后调用openConnection()方法即可获得实例
Url url=new URL("http://www.baidu.com");
HttpURLConnection connection=(HttpURLConnection) url.openConnection();
- 设置HTTP请求所使用的方法。常用方法有GET和POST。GET表示希望从服务器那获取数据,POST表示希望提交数据给服务器。
connection.setRequestMethod("GET");
- 一些其他设置,如设置连接超时、读取超时毫秒数、服务器希望得到的消息头等
- 调用getInputStream()方法获取服务器返回的输入流,接下来我们就可以对输入流进行读取
InputStream in=connection.getInputStream();
- 最后调用disconnect()方法将这个HTTP连接关闭
connection.disconnect();
实例:
向百度首页发起一条HTTP请求,返回它的HTML代码
- activity_main,包括发送请求的按钮和展示返回数据的文本框
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:id="@+id/send_request"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="Send Request"/><ScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/response_text"android:layout_width="match_parent"android:layout_height="wrap_content"/></ScrollView>
</LinearLayout>
- MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener {TextView responseText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button sendRequest=(Button) findViewById(R.id.send_request);responseText=(TextView) findViewById(R.id.response_text);sendRequest.setOnClickListener(this);}@Overridepublic void onClick(View view) {if(view.getId()==R.id.send_request){sendRequestWithHttpURLConnection();}}private void sendRequestWithHttpURLConnection(){//开启线程发送网络请求new Thread(new Runnable() {@Overridepublic void run() {HttpURLConnection connection=null;BufferedReader reader=null;try {//向百度的首页发起一条HTTP请求URL url=new URL("https://www.baidu.com");connection=(HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");connection.setConnectTimeout(8000);connection.setReadTimeout(8000);InputStream in=connection.getInputStream();//下面对获取到的输入流(服务器返回的流)进行读取reader=new BufferedReader(new InputStreamReader(in));StringBuilder response=new StringBuilder();String line;while((line=reader.readLine())!=null){response.append(line);}showResponse(response.toString());}catch (Exception e){e.printStackTrace();}finally {if(reader!=null){try {reader.close();}catch (IOException e){e.printStackTrace();}}if(connection!=null){//关闭HTTP连接connection.disconnect();}}}}).start();}private void showResponse(final String response){//因为Android不允许在子线程更新UI,所有我们使用runOnUiThread方法将线程切换为主线程runOnUiThread(new Runnable() {@Overridepublic void run() {//将结果显示到界面上responseText.setText(response);}});}
}
这里主要注意一下showResponse()方法中的runOnUiThread()方法,它可以将线程由子线程切换为主线程。
- 最后别忘了申请一下网络权限
<uses-permission android:name="android.permission.INTERNET"/>
POST请求
想要发送POST请求,只需将setRequestMethod的参数改为POST,并在获取输入流之前把要提交的数据写出即可。例如我们要向服务器提交用户名和密码,可以写成:
connection.setRequestMethod("POST");
DataOutputStream out=new DataOutputStream(connection.getOutputStream());
//数据与数据之间用&分隔开
out.writeBytes("username=admin&password=123456);
使用OkHttp
OkHttp是一个可以代替原生HttpURLConnection发送HTTP请求的网络通信库。可用于简化我们发送HTTP的步骤。下面我们来简单梳理一下其基本步骤:
- 首先使用前必须添加其依赖
implementation ("com.squareup.okhttp3:okhttp:4.9.0")
- 创建一个OkHttpClient实例
OkHttpClient client=new OkHttpClient();
- 要想发送HTTP请求,我们需要创建一个Request对象(目标网站为百度首页),注意此时请求并未发送
Request request=new Request.Builder().url("http://www.baidu.com").build();
- 调用OkHttpClient的newCall()方法来创建一个Call对象并调用它的execute()方法来发送请求并获取服务器返回的数据
Response response=client.newCall(request).execute();
- 获取具体的返回内容
String responseData=response.body().string();
POST请求
- 我们需要先构建出一个RequestBody对象来存放待提交的数据
RequestBody requestBody=new FormBody.Builder().add("username","admin").add("password","123456").build();
- 接着在构建出一个Request对象并将待提交的数据传入
Request request=new Request.Builder().url("http://www.baidu.com").post(requestBody).build();
最后就和GET请求一样调用execute()方法来发送请求并获取服务器返回的数据即可。
实例
这里我们就直接在上面的代码进行改造,将上面点击按钮的sendRequestWithHttpURLConnection()方法换为:
sendRequestWithOkHttp();
接着实现这个方法:
//使用OkHttp发送请求private void sendRequestWithOkHttp(){new Thread(new Runnable() {@Overridepublic void run() {try {//构建OkHttpClient实例OkHttpClient client=new OkHttpClient();//构建Request对象并将目标网址传入Request request=new Request.Builder().url("https://www.baidu.com").build();//调用execute()方法发送请求并接收返回的数据Response response=client.newCall(request).execute();//将返回的数据解析成字符串String responseData=response.body().string();//将返回的信息显示在屏幕上showResponse(responseData);}catch (Exception e){e.printStackTrace();}}}).start();}
解析XML格式数据
数据在网络上传输通常有两种方式XML和JSON,这两种数据分别具有其自己的结构规格和语义,当我们获取到这种数据时,我们要对其进行解析从而取出我们想要的那部分内容,接下来我们先来介绍Pull解析XML格式数据。
Pull解析方式
首先你要确保你已经下载好Tomcat并做好相关配置并将Tomcat启动起来,接下来我们来自定义一段XML格式的数据(这段数据我是定义在D:\Apache\apache-tomcat-8.5.95\webapps\AndroidStudy这个路径下的):
<apps><app><id>1</id><name>Google Maps</name><version>1.0</version></app><app><id>2</id><name>Chrome</name><version>2.1</version></app><app><id>3</id><name>Google Play</name><version>2.3</version></app>
</apps>
接下来我们可以打开浏览器看看是不是没问题,我的Tomcat端口是9000(可以在conf\server.xml文件中修改端口号),所以我输入localhost:9000/AndroidStudy/get_data.xml会显示出以下内容:
接下来我们就可以去对这些定义好的数据进行解析,我们接着在上面写好的代码上进行修改
- 首先构造Request对象时url我们要指定为访问本机IP地址,你打开控制台查一下本机电脑IP,如下所示
.url("http://192.168.??.???:9000/AndroidStudy/get_data.xml")
- 接着实现一个parseXMLWithPull对返回的数据进行解析
//Pull解析数据private void parseXMLWithPull(String xmlData){try {XmlPullParserFactory factory=XmlPullParserFactory.newInstance();//使用XmlPullParserFactory来创建出XmlPullParser实例XmlPullParser xmlPullParser=factory.newPullParser();//将服务器返回的数据设置进XmlPullParser就可以开始进行解析了xmlPullParser.setInput(new StringReader(xmlData));//通过getEventType()就可以得到当前的解析事件int eventType=xmlPullParser.getEventType();String id="";String name="";String version="";//!=XmlPullParser.END_DOCUMENT表示解析工作还未完成while(eventType!=XmlPullParser.END_DOCUMENT){String nodeName=xmlPullParser.getName();switch (eventType){//开始解析某个节点,将数据取出case XmlPullParser.START_TAG:{if("id".equals(nodeName)){id=xmlPullParser.nextText();} else if ("name".equals(nodeName)) {name=xmlPullParser.nextText();} else if ("version".equals(nodeName)) {version=xmlPullParser.nextText();}break;}//完成解析某个节点,将数据打印case XmlPullParser.END_TAG:{if("app".equals(nodeName)){Log.d("MainActivity","id is "+id);Log.d("MainActivity","name is "+name);Log.d("MainActivity","version is "+version);}break;}default:break;}eventType=xmlPullParser.next();}}catch (Exception e){e.printStackTrace();}}
注意:我们需要在AndroidManifest.xml的<application>标签中加入以下一行代码,不然程序会出现问题
android:usesCleartextTraffic="true"
接着启动程序点击按钮解析出来的数据就打印出来了。
解析JSON数据
接下来学习如何解析JSON格式的数据,首先说一下JSON相比XML的优势——JSON的体积更小,在网络传输的时候可以更省流量。但同时它的缺点是语义性较差,不如XML看起来直观。
在解析之前,我们和前面一样,新建一个get_data.json文件,接下来访问localhost:9000/AndroidStudy/get_data.json,如下所示:
使用JSONObject
- 和上面一样,构造url时传入自己数据源的url
.url("http://192.168.??.???:9000/AndroidStudy/get_data.json")
- 接着实现一个parseJSONWithJSONObject()方法对返回的数据进行解析
//使用JSONObject解析JSON格式的数据private void parseJSONWithJSONObject(String jsonData){try {JSONArray jsonArray=new JSONArray(jsonData);for(int i=0;i<jsonArray.length();i++){JSONObject jsonObject=jsonArray.getJSONObject(i);String id=jsonObject.getString("id");String version=jsonObject.getString("version");String name=jsonObject.getString("name");Log.d("MainActivity","id is "+id);Log.d("MainActivity","name is "+name);Log.d("MainActivity","version is "+version);}}catch (Exception e){e.printStackTrace();}}
我们前面定义的是一个JSON数组,所以这里我们先将服务器返回的数据传到一个JSONArray对象中,接着遍历JSONArray对象,这里我们使用JSONObject来接收遍历出来的元素,接着取出id、name、version这些数据。
接着启动程序点击按钮解析出来的数据就打印出来了。
使用GSON
可以看到上面使用JSONObject解析JSON数据已经非常方便了,但其实还有更加方便的,那就是Google提供的GSON开源库。
GSON的方便之处在于,它可以将一段JSON格式的数据自动映射成一个对象,例如有一段JSON格式数据如下所示:
{"name":"Tom","age":"20"}
我们就可以先定义一个Person类,并加入name和age属性,接下来只需要使用下面代码即可将JSON数据解析成一个Person对象:
Gson gson=new Gson();
Person person=gson.formJson(jsonData,Person.class);
如果是解析一个JSON数组的话,我们就要借助TypeToken将期望解析成的数据类型传入formJson()方法中:
List<Person> people=gson.formJson(jsonData,new TypeToken<List<Person>>(){}.getType());
下面我们来使用GSON解析JSON数据。
- 添加依赖
implementation ("com.google.code.gson:gson:2.9.0")
- 新建App类,添加id、name、version这三个属性
public class App {private String id;private String name;private String version;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getVersion() {return version;}public void setVersion(String version) {this.version = version;}
}
- 实现一个parseJSONWithGSON()方法对JSON数据进行解析
//使用GSON解析JSON数据private void parseJSONWithGSON(String jsonData){Gson gson=new Gson();List<App> appList=gson.fromJson(jsonData,new TypeToken<List<App>>(){}.getType());for (App app : appList) {Log.d("MainActivity","id is "+app.getId());Log.d("MainActivity","name is "+app.getName());Log.d("MainActivity","version is "+app.getVersion());}}
在这我们先new出一个Gson实例,接着调用formJson方法并将服务器返回的数据和我们期望解析成的数据格式传入,获得一个App集合,最后只需遍历集合获取数据打印即可。
Retrofit
Retrofit是在OkHttp的基础上进一步开发出来的应用层网络通信库,OkHttp注重的是底层通信的实现,而Retrofit注重的是上层接口的封装,Retrofit允许我们用更加面向对象的思维进行网络操作。
设计思想
- 首先,Retrofit允许我们先配置好一个根路径,在指定服务器接口地址时只需要使用相对路径即可,这样我们就不用每次去指定完整的URL地址了。
- 其次,Retrofit允许我们对服务器接口进行归类,将功能同属一类的服务器接口定义在同一个接口文件中,这样我们的代码结构会更加合理。
- 最后,我们完全不用关心网络通信的细节,只需在接口文件中声明一系列方法和返回值,然后通过注解的方式指定该方法对应哪个服务器接口,以及需要提供哪些参数。当我们的程序调用该方法时,Retrofit会自动向对应的服务器发起请求,并将响应的数据解析成返回值声明的类型。
这些设计思想就使得我们可以用更加面向对象的思维来进行网络操作了。
案例
重新新建一个项目RetrofitTest。
在这我们仍继续使用上面的get_data.json数据
- 添加依赖
implementation ("com.squareup.retrofit2:retrofit:2.6.1")
implementation ("com.squareup.retrofit2:converter-gson:2.6.1")
第一条依赖会将Retrofit、OkHttp、Okio这几个库一起下载,第二条依赖会将GSON库下载下来,所以Retrofit会将返回的JSON数据自动解析成对象。
- 新建App类
public class App {private String id;private String name;private String version;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getVersion() {return version;}public void setVersion(String version) {this.version = version;}
}
- 创建接口文件并在其内部定义具体的服务器接口方法。我们可以根据功能的不同对接口服务进行分类,例如在一个教务系统中,对老师的操作我们可以定义一个TeacherService,对学生的操作我们可以定义一个StudentService。
public interface AppService {@GET("get_data.json")public Call<List<App>> getAppData();}
在这我们就定义了一个getAppData()方法,返回值类型是Call类型,@GET注解表示发送的是一条GET请求,后面接的是具体路径。这里返回值类型我们需要注意一下,返回值类型我们必须声明成Retrofit内置的Call类型,然后通过泛型指定服务器返回的数据转要换成什么类型,这里我们转换成List<App>。
- 在布局中添加一个按钮用于发送请求
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:id="@+id/getAppDataBtn"android:text="Get App Data"android:layout_width="match_parent"android:layout_height="wrap_content"/></LinearLayout>
- 点击按钮发送请求并将返回的JSON数据解析出来打印到控制台
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button getData=(Button) findViewById(R.id.getAppDataBtn);getData.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {Retrofit retrofit= new Retrofit.Builder()//配置根路径->你自己主机的IP.baseUrl("http://192.168.??.???:9000/AndroidStudy/")//指定解析数据时使用的转换库.addConverterFactory(GsonConverterFactory.create()).build();//创建出AppService接口的动态代理对象,接下来就可以随意调用该接口中的方法了AppService appService=retrofit.create(AppService.class);//调用getAppData方法之后会返回一个Call<List<App>>对象,接着调用enqueue()方法,Retrofit就会根据注解中配置的服务器接口地址去进行网络请求,服务器响应的数据也会回调到enqueue()方法中的Callback实现里appService.getAppData().enqueue(new Callback<List<App>>() {@Overridepublic void onResponse(Call<List<App>> call, Response<List<App>> response) {//调用response.body()后我们会得到Retrofit解析后的对象List<App> appList = response.body();if(appList!=null){for (App app : appList) {Log.d("MainActivity","id is "+app.getId());Log.d("MainActivity","name is "+app.getName());Log.d("MainActivity","version is "+app.getVersion());}}}@Overridepublic void onFailure(Call<List<App>> call, Throwable t) {t.printStackTrace();}});}});}
}
注意:我们在构建Retrofit对象时上面两个方法是必须调用的。发起请求时,Retrofit会自动在内部开启子线程,当数据回调到Callback中之后又会自动切回主线程,整个过程我们都不需要考虑线程切换问题。
- 最后需要注意的是我们要在AndroidManifest.xml文件中需要加入网络权限以及在<application>标签中加入以下配置
<!-- 网络权限-->
<uses-permission android:name="android.permission.INTERNET"/><application>
...
android:networkSecurityConfig="@xml/network_config"
</application>
在res->xml文件下创建network_config,内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<network-security-config><base-config cleartextTrafficPermitted="true"><trust-anchors><certificates src="system"/></trust-anchors></base-config>
</network-security-config>
最后我们点击按钮发送网络请求返回的JSON数据就会被解析并将我们想要的数据打印在控制台了。
处理复杂的接口地址类型
- 上面我们的接口地址是静态的,但在很多场景下接口地址是动态变化的,如以下接口地址:
GET http://example.com/<page>/get_data.json
上面的<page>表示页数,我们传入不同的页数,显示的内容也是不一样的,服务器返回的数据也是不同的,像这种动态接口地址在Retrofit中应该怎么写呢(实体类为Data,创建了一个DataService接口):
public interface DataService{@GET("{page}/get_data.json")public Call<Data> getData(@Path("page") int page);}
在接口地址中,我们使用{page}来表示一个占位符,在getData()方法中添加了一个page参数,并使用@Path(“page”)注解来声明这个参数,这样当发送请求时,Retrofit会将page参数自动替换掉{page},这样不就实现了动态参数。
- 另外很多服务器的接口要求我们传入多个参数,如以下接口地址:
GET http://examlpe.com/get_data.json?u=<user>&t=<token>
带参数的GET请求格式如上,使用?来连接参数部分,每个参数都是使用=连接的键值对,多个参数之间使用&分隔,上面的示例就表示我们要传入user和token这两个参数,对于这种接口地址,我们可以使用@Path注解来解决,如下:
public interface DataService{@GET("get_data.json?u={user}&t={token}")public Call<Data> getData(@Path("user") String user,@Path("token") String token);}
也可以使用Retrofit针对这类GET请求提供的语法支持:
public interface DataService{@GET("get_data.json")public Call<Data> getData(@Query("u") String user,@Query("t") String token);}
这两种方式都可以将接口地址中的<user>和<token>用user和token替换。
其他注解
- @POST:提交数据
- @PUT、@PATCH:修改数据
- @DELETE:删除数据
DELETE http://example.com/data/<id>
上述接口地址表示要根据id删除一条数据,要想发出这样的请求我们可以写成下面这样
public interface DataService{@DELETE("data/{id}")public Call<ResponseBody> deleteData(@Path("id") String id);}
可以看到上面我们将Call的泛型指定为ResponseBody,这是因为@POST、@PUT、@PATCH、@DELETE注解与@GET注解不同,它们时操作服务器上的数据而不是获取数据,所以它们对服务器响应的数据并不关心。使用ResponseBody表示Retrofit能接收任何类型的响应数据,并且不会对响应数据进行解析。
提交数据
向服务器提交数据,如以下接口地址
POST http://example.com/data/create
使用POST来提交数据,需要我们将数据放到HTTP请求的body部分,Retrofit给我们提供了一个@Body注解来完成
public interface DataService{@POST("data/create")public Call<ResponseBody> postData(@Body Data data);//Data是你自己定义的要提交的数据}
这样当发送一个POST请求时,Retrofit会将Data对象中的数据转换成JSON格式的数据并放到HTTP请求的body部分。
关于Android网络技术的使用的分享到此结束,希望对您有帮助!