参考书作者:郭霖
我会将所学的知识简单记录下来以便于大家快速查阅资料,另外郭霖大侠没有提到的东西我也会作出补充
我们
通常情况下,每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器
上获取数据。在网络上传输数据时最常用的格式用两种:XML和JSON
解析XML格式数据
我们就搭建一个最简单的Web服务器,在这个服务器上提供一段XML文本,然后我们程序访问这个服务器,再对得到的XML文本
进行解析
大家先下载好Apache服务器(百度搜索或者直接在官网上下载),一直默认安装就行了,
下面打开浏览器验证一下
接下来在你的安装目录Apache\htdocs目录下新建一个名为get_data.xml的文件,编辑这个文件
1
Google Maps
1.0
2
Chrome
2.1
3
Google Play
2.3
在浏览器访问http://127.0.0.1/get_data.xml可以看到内容 现在我们就解析这个返回的XML文件
为了方便起见,我们在下面这个项目上进行解析XML
HttpURLConnection与OkHttp的使用
比较常用的解析XML方法有Pull解析和SAX解析当然还有DOM解析,这里我们只介绍前两种解析
Pull和SAX解析方式
Pull解析我们只需要添加一个私有方法,然后在sendRequestWithOkHttp方法中调用parserXMLWithPull方法
SAX解析我们需要创建一个新的类ContentHandler,这个类继承自DefaultHandler类,并重写父类的5个方法
我们修改MainActivity中的代码
package com.gougoucompany.clarence.networktest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.xml.parsers.SAXParserFactory;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
TextView responseText;
@Override
protected 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);
}
@Override
public void onClick(View v) {
if(v.getId() == R.id.send_request) {
//sendRequestWithURLConnection();
/*今天,我们有许多出色的网络通信库都可以替代原生的HttpURLConnection,而其中OkHttp是比较出色的一个
* 现在已经成为了广大安卓开发者首选的网络通信库 OkHttp的项目主页地址是: http://github.com/square/okhttp
* 可以查看最新的版本
* 我们要现在app/build.gradle文件中加入OkHttp库的依赖,会自动下载两个库OkHttp和Okio库
* 我们来看OkHttp的使用步骤
* 1. 创建OkHttpClient实例
* 2. 创建一个Request对象
* 3. 使用OkHttpClient的newCall()方法创建一个Call对象,并调用它的execute()方法来发送请求和接受服务器返回的数据
* 4. 使用Response对象接受服务器返回的数据 然后使用response.body().string()方法获得具体的内容
* 这种是使用"GET"方法提交请求
*
* 下来看如何使用"POST"方法提交请求
* 先构建一个RequestBody对象来存放待提交的数据
* RequestBody requestBody = new FormBody.Builder()
* .add("username", "admin")
* .add("password", "123456")
* .builder();
* 然后在Request.Builder构造器调用post()方法将RequestBody对象传入
* Request request = new Request.Builder()
* .url("http://www.baidu.com")
* .post(requestBody)
* .build();后面的都一样了*/
sendRequestWithOkHttp();
}
}
private void sendRequestWithOkHttp() {
//开启线程来发起网络请求
new Thread(new Runnable () {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
//指定访问的服务器地址是电脑本机
.url("http://10.0.2.2/get_data.xml") //通过url()方法设定目标的网络地址
.build();
Response response = client.newCall(request).execute();
String responseData = response.body().string();
Log.d("MainActivity", responseData);
//showResponse(responseData);
parseXMLWithPull(responseData);
//parseXMLWithSAX(responseData);
} catch(Exception e)
{
e.printStackTrace();
}
}
}).start();
}
private void parseXMLWithSAX(String xmlData) {
/*parseXMLWithSAX()方法中先是创建了一个SAXParserFactory对象,然后
* 再获取到XMLReader对象,接着将我们编写的ContentHandler的实例设置到XMLReader中
* ,最后调用parse()方法执行解析就好了*/
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
ContentHandler handler = new ContentHandler();
//将ContentHandler实例设置到xmlReader中
xmlReader.setContentHandler(handler);
//开始执行解析
xmlReader.parse(new InputSource(new StringReader(xmlData)));
} catch(Exception e) {
e.printStackTrace();
}
}
private void parseXMLWithPull(String xmlData) {
try {
//获得一个XmlPullParserFactory实例
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
//得到XmlPullParser对象
XmlPullParser xmlPullParser = factory.newPullParser();
//调用xmlPullParser的setInput方法将服务器返回的XML数据传入开始解析
xmlPullParser.setInput(new StringReader(xmlData));
//获得当前的解析事件
int eventType = xmlPullParser.getEventType();
String id = "";
String name = "";
String version = "";
while (eventType != XmlPullParser.END_DOCUMENT) {
//getName()方法获得当前节点的名字
String nodeName = xmlPullParser.getName();
//如果发现节点名等于id,name或version,就调用nextText()方法来获取节点内具体的内容
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;
}
//完成解析某个节点就将id,name,version全都打印出来
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();
}
}
private void sendRequestWithURLConnection() {
//开启线程来发起网络请求
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try{
URL url = new URL("https://www.baidu.com");
//获取HttpURLConnection实例
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
//设置连接超时
connection.setConnectTimeout(8000);
//设置读取超时的毫秒数
connection.setReadTimeout(8000);
//获取到服务器返回的输入流,字节输入流InputStream对象
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) {
connection.disconnect(); //将Http连接关闭掉
}
}
}
}).start();
}
private void showResponse(final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//在这里进行UI操作,将结果显示到界面上
responseText.setText(response);
}
});
}
}
新建类ContentHandler
package com.gougoucompany.clarence.networktest;
import android.util.Log;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Created by Clarence on 2018/4/13.
* Sax解析是一种特别常用的xml解析方式,虽然用法比Pull解析要复杂一些,但在
* 语义方面会更加清楚
* 通常情况下我们都会新建一个类继承自DefaultHandler,并重写父类的5个方法
*/
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
//开始xml解析的时候调用
@Override
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();
version = new StringBuilder();
}
//开始解析某个节点的时候调用
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
//记录当前的节点名
nodeName = localName;
}
//characters()方法会在获取节点中的内容的时候调用
//StringBuilder的append(char[], int offset, int len)方法将数组从下标offset开始的len个字符依次添加到当前字符串的末尾
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
//根据当前节点名判断将内容添加到哪一个StringBuilder对象中
if("id".equals(nodeName)) {
id.append(ch, start,length);
} else if("name".equals(nodeName)) {
name.append(ch, start, length);
} else if("version".equals(nodeName)) {
version.append(ch, start, length);
}
}
//会在完成解析某个节点的时候调用
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if("app".equals(localName)){
Log.d("ContentHandler", "id is " + id.toString().trim());
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
//最后要将StringBuilder清空掉 java.lang.StringBuilder.setLength(int newLength)来改变字符序列的长度
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
//会在完成整个xml解析的时候调用
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}
下面是解析服务器发送的xml文件后得到的信息,我们将它显示到日志Debug中
需要注意的是:模拟机访问127.0.0.1都是访问模拟器本身,你想在模拟器上访问安装模拟器的电脑,
那么就使用Android内置的ip:10.0.2.2 另外记住要打开模拟器网络开关
解析JSON格式数据
我们在Apache\htdocs目录中新建一个get_data.json的文件,然后编辑这个文件,并加入如下JSON格式的内容
[{"id" : "5", "version" : "5.5", "name" : "Clash of Clans"},
{"id" : "6", "version" : "7.0", "name" : "Boom Beach"},
{"id" : "7", "version" : "3.5", "name" : "Clash Royale"}]
在浏览器中访问http://127.0.0.1/get_data.json
解析JSON数据也有很多方法,可以使用官方提供的JSONObject,也可以使用谷歌的开源库GSON。另外,一些第三方的
开源库如Jackson、FastJSON等也非常不错。这里我们介绍前两种
JSONObject解析 首先将服务器返回的数据传入到了一个JSONArray对象中,然后循环遍历这个JSONArray,从中取出的每一个元素都是一个JSONObject对象,每个JSONObject对象中又会包含id,name和version这些数据。接下来只需要调用getString()
方法将这些数据取出,并打印出来即可。
要使用GJSON,必须在项目中添加GSON库的依赖。编辑app/build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
testCompile 'junit:junit:4.12'
compile 'com.squareup.okhttp3:okhttp:3.10.0'
compile 'com.google.code.gson:gson:2.7'
}
GSON库可以将一段JSON格式的字符串自动映射成一个对象。
eg: 比如一段JSON格式的数据 {"name" : "Tom", "age" : 20}
我们可以定义一个Person类,并加入name和age这两个字段
Gson gson= new Gson();
Person person = gson.fromJson(jsonData, Person.class);
如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TypeToken将期望解析的数据类型
传入到fromJson()方法中
List>(){}.getType());
fromJson中提供两个参数,分别是json字符串以及需要转换成对象的类型
new TypeToken>(){}.geType(),这个位置的参数是一个Type,表示是xx类型,但是Type是个
接口如下:
public interface Type {
/**
* Returns a string describing this type, including information
* about any type parameters.
*
* @implSpec The default implementation calls {@code toString}.
*
* @return a string describing this type
* @since 1.8
*/
default String getTypeName() {
return toString();
}
}
new XXX();这样是一个构造函数,但是接口是不能直接new的,所以这时用到了匿名内部类,实现接口称为一种具体的类型
TypeToken,它是gson提供的数据类型转换器,可以支持各种数据类型转换,先调用TypeToken
再由该匿名内部类对象调用getType()方法得到想要转换成的type,这里type就是List
我们先增加一个App类,并加入id、name和version这三个字段,并自动生成getter和setter方法
package com.gougoucompany.clarence.networktest;
/**
* Created by Clarence on 2018/4/14.
*/
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;
}
}
然后修改MainActivity中的代码
我们只要添加一个私有的方法,然后在这个方法完成解析任务
//使用GSON开源库解析json格式的数据
private void parseJSONWithGSON(String jsonData) {
Gson gson = new Gson();
List appList = gson.fromJson(jsonData, new TypeToken>(){}.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());
}
}
这样点击按钮之后就会打印出数据
优化程序:
一个应用程序很可能会在许多地方都是用到网络功能,而发送HTTP请求的代码基本都是相同的,我们应该将这些
通用的网络操作提取到一个公共的类里,并提供一个静态方法,当想要发起网络请求的时候,只需简单调用一下这个
方法即可 新建一个HttpUtil工具类
先是使用HttpURLConnection来处理网络请求
package com.gougoucompany.clarence.networktest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by Clarence on 2018/4/14.
* 将通用的网络操作提取到一个公共的类里
*/
public class HttpUtil {
public static String sendHttpRequest(String address) {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
/*httpUrlConnection.setDoOutput(true);以后就可以使用conn.getOutputStream().write()
httpUrlConnection.setDoInput(true);以后就可以使用conn.getInputStream().read();
get请求用不到conn.getOutputStream(),因为参数直接追加在地址后面,因此默认是false。
post请求(比如:文件上传)需要往服务区传输大量的数据,这些数据是放在http的body里面的,
因此需要在建立连接以后,往服务端写数据. 因为总是使用conn.getInputStream()获取服务端
的响应,因此默认值是true。 */
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
//InputStreamReader是字节流通向字符流的桥梁:它使用指定的charset读取字节并将其解码为字符
//为了达到效率,可以在BufferedReader内包装InputStreamReader
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} catch(Exception e) {
e.printStackTrace();
//返回异常的名称
return e.getMessage();
} finally {
if(connection != null) {
connection.disconnect();
}
}
}
}
注意:
网络请求通常都是属于耗时操作,而sendHttpRequest()方法内部并没有开启线程,这样就可能导致在
调用sendHttpRequest()方法的时候使得主线程被阻塞住
但是在sendHttpRequest()方法中开启一个线程来发起HTTP请求,那么服务器相应的数据是无法进行返回的,所有的
耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就
无法返回响应的数据了。那么我们可以使用java的回调机制来解决这个问题
首先需要定义一个接口
* Created by Clarence on 2018/4/14. * 我们在接口中定义了两个方法,onFinish()方法表示当服务成功相应我们的请求的时候调用 * onError()表示当进行网络操作出现错误的时候调用.onFinish()方法中的参数代表着服务器返回的 * 参数,而onError()方法中的参数记录着错误的详细信息
package com.gougoucompany.clarence.networktest;
/**
* Created by Clarence on 2018/4/14.
*/
public interface HttpCallbackListener {
void onFinish(String response);
void onError(Exception e);
}
接着修改HttpUtil中的代码
package com.gougoucompany.clarence.networktest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by Clarence on 2018/4/14.
* 将通用的网络操作提取到一个公共的类里
*/
public class HttpUtil {
public static void sendHttpRequest(final String address, final HttpCallbackListener listener) {
new Thread(new Runnable(){
@Override
public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
/*httpUrlConnection.setDoOutput(true);以后就可以使用conn.getOutputStream().write()
httpUrlConnection.setDoInput(true);以后就可以使用conn.getInputStream().read();
get请求用不到conn.getOutputStream(),因为参数直接追加在地址后面,因此默认是false。
post请求(比如:文件上传)需要往服务区传输大量的数据,这些数据是放在http的body里面的,
因此需要在建立连接以后,往服务端写数据. 因为总是使用conn.getInputStream()获取服务端
的响应,因此默认值是true。 */
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
//InputStreamReader是字节流通向字符流的桥梁:它使用指定的charset读取字节并将其解码为字符
//为了达到效率,可以在BufferedReader内包装InputStreamReader
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
if(listener != null) {
//回调onFinish()方法
listener.onFinish(response.toString());
}
} catch(Exception e) {
if(listener != null) {
//回调onError()方法
listener.onError(e);
}
} finally {
if(connection != null) {
connection.disconnect();
}
}
}
}).start();
}
}
我们首先给sendHttpRequest()方法添加一个HttpCallbackListener参数,并在方法的内部开启了一个子线程,然后在子线程中执行具体的网络操作。(子线程中是无法通过return返回数据的)这里我们将服务器响应的数据传入了HttpCallbackListener的onFinish()方法中,如果出现了异常就将异常原因传入到onError()方法中。
现在sendHttpRequest()方法接受两个参数,我们还需将HttpCallbackListener实例传入
HttpUtil.sendHttpRequest(address,, new HttpCallbackListener() {
@Override
public void onFinish(String response) {
//在这里根据返回内容执行具体的逻辑
}
@Override
public void onError(Exception e){
//在这里对异常情况进行处理
}
}
使用OkHttp来处理网络请求就非常的简单了
我们在HttpUtil类中添加一个静态方法如下
public static void sendOkHttpRequest(String address, okhttp3.Callback callback) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(address)
.build();
client.newCall(request).enqueue(callback);
}
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import java.io.IOException;
public interface Callback {
/**
* Called when the request could not be executed due to cancellation, a connectivity problem or
* timeout. Because networks can fail during an exchange, it is possible that the remote server
* accepted the request before the failure.
*/
void onFailure(Call call, IOException e);
/**
* Called when the HTTP response was successfully returned by the remote server. The callback may
* proceed to read the response body with {@link Response#body}. The response is still live until
* its response body is {@linkplain ResponseBody closed}. The recipient of the callback may
* consume the response body on another thread.
*
* Note that transport-layer success (receiving a HTTP response code, headers and body) does
* not necessarily indicate application-layer success: {@code response} may still indicate an
* unhappy HTTP response code like 404 or 500.
*/
void onResponse(Call call, Response response) throws IOException;
}
可以看到okhttp3.Callback是OkHttp库中自带的一个回调接口,类似于我们刚才自己编写的HttpCallbackListener
OkHttp在equeue()方法的内部帮我们开启好子线程,然后会在子线程中去执行HTTP请求,并将最终的结果回调到okhttp3.Callback中
我们在调用sendOkHttpRequest()方法的时候可以这样写。
HttpUtil.sendOkHttpRequest(address, new okhttp3.Callback(){
@Override
public void onResponse(Call call, Response response) throws IOException{
//得到服务器返回的具体内容
String responseData = resposne.body().toString(0;)
}
@Override
public void onFailure(Call call, IOException e) {
//在这里对异常情况进行处理
}
})
要注意的是不论是HttpURLConnection还是OkHttp,最终的回调接口都是在子线程中执行,因此我们不可以在这里执行
任何的UI操作,除非借助RunOnUiThread()方法切换到主线程中执行。