26
2017
09

3.1 使用 HTTP 访问网络的两种方式详解

点此进入:从零快速构建APP系列目录导图
点此进入:UI编程系列目录导图
点此进入:四大组件系列目录导图
点此进入:数据网络和线程系列目录导图

对于 HTTP 协议,它的工作原理特别的简单,就是客户端向服务器发出一条HTTP 请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。一个浏览器的基本工作原理也就是如此了,比如说之前文章中使用到的 WebView 控件,其实也就是我们向百度的服务器发起了一条 HTTP 请求,接着服务器分析出我们想要访问的是百度的首页,于是会把该网页的 HTML 代码进行返回,然后 WebView 再调用手机浏览器的内核对返回的 HTML 代码进行解析,最终将页面展示出来。

简单来说, WebView 已经在后台帮我们处理好了发送 HTTP 请求、接收服务响应、解析返回数据,以及最终的页面展示这几步工作,不过由于它封装得实在是太好了,反而使得我们不能那么直观地看出 HTTP 协议到底是如何工作的。因此,接下来就让我们通过手动发送HTTP 请求的方式,来更加深入地理解一下这个过程。

本节例程下载地址:WillFlowHTTP

一、使用 HttpURLConnection

在 Android 上发送 HTTP 请求的方式一般有两种, HttpURLConnection 和 HttpClient,接下来我们先来学习一下 HttpURLConnection 的用法。首先需要获取到 HttpURLConnection 的实例,一般只需 new 出一个 URL 对象,并传入目标的网络地址,然后调用一下 openConnection() 方法即可,如下所示:

URL url = new URL("http://www.baidu.com");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

得到了 HttpURLConnection 的实例之后,我们可以设置一下 HTTP 请求所使用的方法。常用的方法主要有两个:GET 和 POST。GET 表示希望从服务器那里获取数据,而 POST 则表示希望提交数据给服务器。写法如下:

connection.setRequestMethod("GET");

接下来就可以进行一些自由的定制了,比如设置连接超时、读取超时的毫秒数,以及服务器希望得到的一些消息头等。这部分内容根据自己的实际情况进行编写,示例写法如下:

connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);

之后再调用 getInputStream() 方法就可以获取到服务器返回的输入流了,剩下的任务就是对输入流进行读取,如下所示:

InputStream in = connection.getInputStream();

最后可以调用 disconnect()方法将这个 HTTP 连接关闭掉,如下所示:

connection.disconnect();

下面就让我们通过一个具体的例子来真正体验一下 HttpURLConnection 的用法。

首先修改 activity_main.xml 中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.wgh.willflowhttp.MainActivity">

    <Button  android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="发送请求" android:textColor="#fe4629" android:textSize="25dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.034" />


    <ScrollView  android:layout_width="match_parent" android:layout_height="495dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_marginTop="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/button" tools:ignore="MissingConstraints" app:layout_constraintHorizontal_bias="0.502">

        <TextView  android:id="@+id/response" android:layout_width="match_parent" android:layout_height="match_parent" android:textColor="#0894f9" tools:layout_editor_absoluteX="192dp" tools:layout_editor_absoluteY="226dp" />

    </ScrollView>

</android.support.constraint.ConstraintLayout>

这里我们使用了一个新的控件 ScrollView,由于手机屏幕的空间一般都比较小,有些时候过多的内容一屏是显示不下的,借助 ScrollView 控件的话就可以允许我们以滚动的形式查看屏幕外的那部分内容。另外,布局中还放置了一个 Button 和一个 TextView, Button 用于发送 HTTP 请求,TextView 用于将服务器返回的数据显示出来。

接着修改 MainActivity 中的代码,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private static final int SHOW_RESPONSE = 0;
    private Button mButton;
    private TextView mTextView;

    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW_RESPONSE:
                    String response = (String) msg.obj;
                    // 在这里进行UI操作,将结果显示到界面上
                    mTextView.setText(response);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }

    private void initView() {
        mButton = (Button) findViewById(R.id.button);
        mTextView = (TextView) findViewById(R.id.response);

        mButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                sendRequestWithHttpURLConnection();
                break;
        }
    }

    private void sendRequestWithHttpURLConnection() {
        // 开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection httpURLConnection = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    httpURLConnection = (HttpURLConnection) url.openConnection();
                    httpURLConnection.setRequestMethod("GET");
                    httpURLConnection.setConnectTimeout(6666);
                    httpURLConnection.setReadTimeout(6666);
                    InputStream inputStream = httpURLConnection.getInputStream();
                    // 下面对获取到的输入流进行读取
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                    StringBuilder response = new StringBuilder();
                    String readLine;
                    while ((readLine = bufferedReader.readLine()) != null) {
                        response.append(readLine);
                    }
                    Message message = new Message();
                    message.what = SHOW_RESPONSE;
                    // 将服务器返回的结果存放到Message中
                    message.obj = response.toString();
                    mHandler.sendMessage(message);
                    Log.i("MainActivity","response : " + response.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (httpURLConnection != null) {
                        httpURLConnection.disconnect();
                    }
                }
            }
        }).start();
    }
}

我们在发送请求按钮的点击事件里调用了 sendRequestWithHttpURLConnection() 方法,在这个方法中先是开启了一个子线程,然后在子线程里使用 HttpURLConnection 发出一条 HTTP 请求,请求的目标地址就是百度的首页。接着利用 BufferedReader 对服务器返回的流进行读取,并将结果存放到了一个 Message 对象中。这里为什么要使用 Message 对象呢?当然是因为子线程中无法对 UI 进行操作了。我们希望可以将服务器返回的内容显示到界面上,所以就创建了一个 Message 对象,并使用 Handler 将它发送出去。之后又在 Handler 的 handleMessage() 方法中对这条 Message 进行处理,最终取出结果并设置到 TextView 上。

最后别忘了要声明一下网络权限:
<uses-permission android:name="android.permission.INTERNET" />
编译运行看效果:

看着可能有点乱,但服务器返回给我们的就是这种 HTML 代码,只是通常情况下浏览器都会将这些代码解析成漂亮的网页后再展示出来,我们这里只做了最原始的展示。

那么如果是想要提交数据给服务器应该怎么办呢?只需要将 HTTP 请求的方法改成 POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据都要以键值对的形式存在,数据与数据之间用&符号隔开,比如说我们想要向服务器提交用户名和密码,就可以这样写:

connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=wgh&password=123456");

下面我们来学习一下HttpClient 的用法。

二、使用 HttpClient

HttpClient 是 Apache 提供的 HTTP 网络访问接口,从一开始的时候就被引入到了 Android API 中。它可以完成和 HttpURLConnection 几乎一模一样的效果,但两者之间的用法却有较大的差别,那么我们接下来看一下 HttpClient 是如何使用的。

首先你需要知道, HttpClient 是一个接口,因此无法创建它的实例,通常情况下都会创建一个 DefaultHttpClient 的实例,如下所示:

HttpClient httpClient = new DefaultHttpClient();

接下来如果想要发起一条 GET 请求,就可以创建一个 HttpGet 对象,并传入目标的网络地址,然后调用 HttpClient 的 execute()方法即可:

HttpGet httpGet = new HttpGet("https://www.baidu.com");
httpClient.execute(httpGet);

如果是发起一条 POST 请求会比 GET 稍微复杂一点,我们需要创建一个 HttpPost 对象,并传入目标的网络地址,如下所示:

HttpPost httpPost = new HttpPost("https://www.baidu.com");

然后通过一个 NameValuePair 集合来存放待提交的参数,并将这个参数集合传入到一个 UrlEncodedFormEntity 中,然后调用 HttpPost 的 setEntity()方法将构建好的 UrlEncodedFormEntity 传入,如下所示:

List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("username", "wgh"));
params.add(new BasicNameValuePair("password", "123456"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "utf-8");
httpPost.setEntity(entity);

接下来的操作就和 HttpGet 一样了,调用 HttpClient 的 execute() 方法,并将 HttpPost 对象传入即可:

httpClient.execute(httpPost);

执行 execute() 方法之后会返回一个 HttpResponse 对象,服务器所返回的所有信息就会包含在这里面。通常情况下我们都会先取出服务器返回的状态码,如果等于 200 就说明请求和响应都成功了,如下所示:

if (httpResponse.getStatusLine().getStatusCode() == 200) {
      // 请求和响应都成功了
}

接下来在这个 if 判断的内部取出服务返回的具体内容,可以调用 getEntity() 方法获取到一个 HttpEntity 实例,然后再用 EntityUtils.toString() 这个静态方法将 HttpEntity 转换成字符串即可,如下所示:

HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity);

注意如果服务器返回的数据是带有中文的,直接调用 EntityUtils.toString() 方法进行转换会有乱码的情况出现,这个时候只需要在转换的时候将字符集指定成 utf-8 就可以了,如下所示:

String response = EntityUtils.toString(entity, "utf-8");

基本的用法就是如此,接下来就让我们把刚才的项目改用 HttpClient 的方式再实现一遍。

直接修改 MainActivity 中的代码,如下所示:
    private void sendRequestWithHttpClient() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    HttpClient httpClient = new DefaultHttpClient();
                    HttpGet httpGet = new HttpGet("https://www.baidu.com");
                    HttpResponse httpResponse = httpClient.execute(httpGet);
                    if (httpResponse.getStatusLine().getStatusCode() == 200) {
                        // 请求和响应都成功了
                        HttpEntity entity = httpResponse.getEntity();
                        String response = EntityUtils.toString(entity, "utf-8");
                        Message message = new Message();
                        message.what = SHOW_RESPONSE;
                        // 将服务器返回的结果存放到Message中
                        message.obj = response.toString();
                        mHandler.sendMessage(message);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

这里我们并没有做太多的改动,只是添加了一个 sendRequestWithHttpClient() 方法,并在发送请求按钮的点击事件里去调用这个方法。在这个方法中同样还是先开启了一个子线程,然后在子线程里使用 HttpClient 发出一条 HTTP 请求,请求的目标地址还是百度的首页,HttpClient 的用法也正如前面所介绍的一样。然后为了能让结果在界面上显示出来,这里仍然是将服务器返回的数据存放到了 Message 对象中,并用 Handler 将 Message 发送出去。

我们可以重新运行一下程序了,然后点击发送请求按钮后,然后我们会看到和上面同样的运行结果,由此证明,使用 HttpClient 来发送 HTTP 请求的功能也已经成功实现了。

点此进入:GitHub开源项目“爱阅”,下面是“爱阅”的效果图:


联系方式:

简书:WillFlow
CSDN:WillFlow
微信公众号:WillFlow

微信公众号:WillFlow

上一篇:1.2 SharedPreference 的使用技巧 下一篇:3.3 基于 Socket 协议的网络通信详解