美文网首页
课程 2: HTTP 网络

课程 2: HTTP 网络

作者: HsuJin | 来源:发表于2018-01-18 17:12 被阅读0次

    这节课是 Android 开发(入门)课程 的第三部分《访问网络》的第二节课,导师是 Chris Lei 和 Joe Lewis。由于上节课的 JSON 是硬编码的占位符,并不是真正从网络获取的数据,所以按照计划的开发步骤,要实现从网络获取数据,这节课先通过一个叫作 Soonami 的示例应用 (Sample App) 来验证网络 (Networking) 相关的代码。因为网络命题的内容很庞大,所以课程中会从实用性出发,仅对用到的部分提供相应的资讯,不作深入讨论。

    Soonami App 同样使用 USGS API 显示是否有地震引起的海啸预警,分四个步骤完成:

    1. Form HTTP Request
    2. Send the Request
    3. Receive the Response and make sense of it
    4. Update the UI

    关键词:Android Permissions、Android System Architecture、Exception、try/catch/finally block、HTTP Request、URL Class、HttpURLConnection、HTTP Verb、HTTP Status Code、StringBuilder、InputStream、InputStreamReader、BufferedReader、Method Chaining

    Android Permissions

    在进行 Android 中的网络操作前,先了解一下 Android 权限的相关知识。默认情况下 Android 应用不具备任何权限,当应用需要使用设备的蓝牙、网络连接、指纹识别,或者访问用户的日历、地址、联系人等操作时,应用就需要请求权限,完整的 Android 权限列表可以到 Android Developers 网站 查看。

    Android 权限按保护等级分为几种类型,其中最重要的两种是正常权限 (Normal Permissions) 和危险权限 (Dangerous Permissions)。

    1. 正常权限:允许的操作对用户信息和其它应用的数据无影响,例如使用设备的蓝牙、网络连接、指纹识别等,完整列表可以到 Android Developers 网站 查看。当应用请求正常权限时,Android 会自动授予应用该权限,无需用户介入。

    2. 危险权限:允许访问用户的个人信息,可能会对其它应用的数据产生影响,例如访问用户的日历、地址、联系人等。当应用请求危险权限时,需要由用户手动处理该请求。危险权限是通过 权限组 (Permission Groups) 来管理的(正常权限也可能包含在权限组内,不过权限组对其无影响,所以无需考虑权限组内的正常权限)。
      (1)当设备运行在 Android 6.0 (API Level 23) 以及应用的 targetSdkVersion 为 23 或以上时,Android 会在应用运行时 (Runtime),弹出对话框,显示应用请求的危险权限所在的权限组。如果用户拒绝权限请求,应用未能获得该权限,那么它就无法提供对应的功能,但仍能正常运行;如果用户同意该请求,就相当于授予应用整个权限组的权限。例如应用请求 READ_CONTACTS 权限时,这个权限属于 CONTACTS 权限组,系统就会在应用运行时弹出对话框,显示应用请求 CONTACTS 权限组,如果用户同意该请求,此时应用只获得 READ_CONTACTS 权限,但是在这个基础上,如果应用再请求同一权限组的 WRITE_CONTACTS 权限,Android 会自动授予应用该权限。
      (2)当设备运行在 Android 5.1 (API Level 22) 或应用的 targetSdkVersion 为 22 或以下时,Android 会在应用安装时 (Install Time),弹出对话框,显示应用请求的所有权限组列表,用户必须同意所有的权限请求,否则无法安装应用。

    为应用请求 Android 权限的方法是在 AndroidManifest 中添加 <uses-permission> 标签以及对应的属性,注意标签名不是 <user-permission>,例如在 Soonami App 中请求网络访问的权限:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.android.soonami">
        <uses-permission android:name="android.permission.INTERNET"/>
        ...
    </manifest>
    

    正如上面描述的,网络访问属于 Android 的正常权限,系统会自动授予应用该权限,无需用户介入。

    Tips:
    1. 虽然应用获得一个危险权限就意味着获得了整个权限组的权限,但是在将来的 Android SDK 中一些权限可能会从一个权限组移动到另一个权限组,所以不应该根据权限组来假定应用是否获得某些权限,最佳做法 (Best Practice) 是在 AndroidManifest 中明确请求每个权限。
    2. 当应用要用到拍照、地图等功能时,可以通过 Intent 调用相应的应用来实现,从而避免请求过多的权限。过多的权限请求会引起用户的怀疑,所以应用应该尽可能少地请求权限,同时确保具有充分的理由向用户解释需要请求权限的原因。

    Android System Architecture

    之所以 Android 有系统权限的概念,是因为 Android 是一种权限分离 (privilege-separated) 的操作系统,应用以唯一的身份标识运行。也就是说,每个 Android 应用都运行在一个进程沙盒 (Process Sandbox) 中,应用需要明确请求沙盒外的资源和数据。这种模式是由 Android 系统框架决定的,应用与设备之间的交互通过一系列的层抽象 (Layer Abstraction) 实现,每一层实现一部分功能,越底层实现的功能越小。

    上图是简化的 Android 系统框架,完整的图表可以到 Android Developers 网站 查看。

    1. 顶层是应用层,开发者写的所有应用程序都在这一层。App 通过调用下一层的 Android Framework class,如 TextView、Activity,使应用仅用几行代码就完成很多复杂的工作,如显示文本、打开一个新页面。
    2. 次层是框架层,这一层提供了许多 Android Framework class,它们通过调用下一层的代码来避免很多重复的复杂工作,最终达到控制设备硬件的效果。框架层是连接应用和设备的桥梁。
    3. 次底层是系统层,这一层有一套复杂的控制设备硬件的代码,用来规范应用和系统进程如何访问硬件资源,从而实现设备上的多个应用共享同一套硬件。
    4. 底层是物理层,指的是设备硬件,如 Wi-Fi、蓝牙,以及 CPU、GPU、内存等电子器件。

    在 Soonami App 中,应用通过 Android Framework 的 HttpURLCOnnection 类使用设备上的蜂窝或 Wi-Fi 硬件设备,以从网络上获取数据,而不是直接操作 Android 系统,更不是直接控制设备硬件。

    Exception

    如果 Soonami App 在没有获得网络访问权限的情况下进行相关的操作,应用会产生 SecurityException 导致应用崩溃。事实上,一些应用崩溃的原因往往是没有正确处理 Exception(例外/异常)。Exception 是 Throwable class 的一个扩展类(另一个是 Error),当代码运行失败或遇到意外状态时会触发异常(Throw an Exception),称为异常事件 (Exception Event)。Exception class 的子类定义了许多异常事件的类型,例如 IllegalStateException 表示有 method 被非法状态下调用;NullPointerException 表示对空对象进行非法操作。所以异常可以理解为错误 (error),但它可以被捕获 (catch) 处理或包含到 Exception 类的实例中;与其它类一样,开发者也可以创建自定义的 Exception class,例如下面的 InvalidPurchaseException。

    public void completePurchase() throws InvalidPurchaseException {
        ...
        ...
        throw new InvalidPurchaseException();
        ...
    }
    
    1. 在任何地方触发异常时都要用到 Java 关键字 throw
    2. 异常触发后下面的代码不会执行。

    异常可分为检查异常和非检查异常 (Checked and Unchecked Exception)。

    • 所有非 RuntimeException(Exception 的子类)的异常都是检查异常,都必须在方法签名 (Method Signature) 中声明,表示调用该 method 时必须处理异常,例如调用上面的 completePurchase() method 时必须处理 InvalidPurchaseException 异常。这种做法在一个类内的辅助方法 (Helper Method) 很常用,可以将异常处理转移到调用 method 的地方。
    • 所有 RuntimeException 都是非检查异常,编译器不会强制 (check) 代码处理异常。虽然 Java 有使用异常的标准框架,但这并不意味着一定要在发生错误时触发异常。理论上,在出现错误或意外情况时,应该在代码中提供一些合理的默认行为,尽可能地使代码继续运行,这种做法叫静默失败 (Failing Silently);但是如果错误会对接下来的代码造成影响,那么就要触发异常以通知此错误。开发者需要根据实际情况进行权衡。

    处理 Exception 的方法通常是将可能触发异常的 method 放到 try/catch/finally 区块中,例如下面的 openFile method:

    public void openFile() {
        FileReader reader = null;
        try {
            // constructor may throw FileNotFoundException
            reader = new FileReader("someFile");
            int i = 0;
            while (i != -1) {
                //reader.read() may throw IOException
                i = reader.read();
                System.out.println((char) i);
            }
        } catch (FileNotFoundException e) {
            Log.e(LOG_TAG, "Problem reading the file.", e);
        } catch (IOException e) {
            Log.e(LOG_TAG, "Problem opening the file.", e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    Log.e(LOG_TAG, "Problem closing the file.", e);
                }
            }
            System.out.println("--- File End ---");
        }
    }
    
    1. FileReader()reader.read() 两个可能触发异常的 method 放进 try 区块,并用两个 catch 区块分别处理不同的异常,在这里是通过 Log 日志记录错误信息。
    2. 如果 FileReader() 触发异常,那么就不再执行 try 区块内的代码,而是跳到 catch (FileNotFoundException e) 处理异常,然后跳到 finally 执行该区块内的代码,最后跳出 try/catch 区块,从上至下继续执行下面的代码。如果 reader.read() 触发异常,则会跳到 catch (IOException e) 处理异常,接下来的步骤与上面相同。因此,try 区块内的代码无法保证总是会执行,代码不会同时进入两个 catch 区块。
    3. 无论是否触发异常,finally 区块内的代码都会执行。
    4. 注意变量的作用域,例如这里的 reader 变量是在 try/catch 区块外声明的,如果在 try 区块内声明变量,那么变量的作用域仅在 try/catch 区块内。

    Tips:
    1. 在 Android Studio 中打开 Java 文件,选中左侧的 "7:Structure" 标签,可以按照嵌套结构清晰地选择浏览文件中的 Java 变量、类、对象。
    2. 在 Android Studio 中选中被识别出错误的代码(有波浪下划线)使用快捷键 opt(alt)+enter 可以选择 Android Studio 提供的解决方案。例如选择 "Surround with try/catch" 可以快速添加 try/catch 区块,在 catch 区块内还会自动添加 e.printStackTrace(); 表示打印错误堆栈。

    Networking

    网络是计算机(包括手机、笔记本电脑、服务器等)之间交换信息的概念,它的基本原理是一台计算机向另一台计算机发送 HTTP 请求,发送端通常称为客户端,接收端为 Web 服务器;服务器作出响应后,客户端能够获取响应并从中提取信息。例如使用浏览器打开 Google 搜索主页时,浏览器作为客户端向 Google 服务器发送 HTTP 请求,浏览器接收到 Google 服务器的响应后解析数据,最后刷新页面显示一个完整的网页。利用 Chrome 浏览器的开发者工具(在空白处右键选择 Inspect),在 Network 界面可以看到浏览器已加载的资源 (HTML, CSS, JavaScript),选中其中一项,在 Headers 标签页下可以看到浏览器向 Google 服务器发送的请求的相关信息,如 URL、method、响应代码等。

    HTTP 请求 (HTTP Request) 是网络交换信息的基础部分,HTTP(超文本传输协议,Hypertext Transfer Protocol)是其中的核心技术。类似在餐厅点披萨,顾客需要明确告诉服务员披萨的尺寸和配料等信息,客户端发送 HTTP 请求也需要明确指出请求内容以及提供方式,其中一项重要指标是 URL(统一资源定位器,Uniform Resource Locator),它决定了数据源的地址或位置,在 API 中称为端点 (Endpoints)。一个 URL 示例如下,它包含了五个基本元素:

    https://example.com/animal/mammal/primate?diet=omnivore&active=night#tarsier
    
    1. 传送协议 (Protocol/Scheme):通常为 http 或 https,后接 // 标记符。
    2. 服务器 (Host/Domain/Authority):Web 资源的主体,通常是域名,如 google.com,有时是 IP 地址,如 192.168.0.1。后面可以接网络端口号(数字,若为 HTTP 的默认值 ":80" 可省略)。
    3. 资源路径 (Resource Path):类似目录结构,表示资源在服务器中的位置。
    4. 查询 (Query):可选,以 ? 为开始,每个参数用 & 分隔。
    5. 片段 (Fragment):可选,以 # 为开始,指页面中的某些资源 ID,表示页面会从该资源开始显示。

    在 Android 中利用 URL class 来生成访问 API 端点的 URL,例如在 Soonami App 中新建一个名为 createUrl 的 URL 对象,在 try/catch 区块内通过字符串构造 URL 对象,同时可捕获 MalformedURLException 并通过 Log 日志记录错误信息。

    /**
     * Returns new URL object from the given string URL.
     */
    private URL createUrl(String stringUrl) {
        URL url = null;
        try {
            url = new URL(stringUrl);
        } catch (MalformedURLException exception) {
            Log.e(LOG_TAG, "Error making the HTTP request.", exception);
            return null;
        }
        return url;
    }
    

    创建 URL 对象后,通过调用 url.openConnection() 创建一个 HttpURLConnection 对象,通过调用其中的 method 就可以在 Android 中生成 HTTP 请求了。这种模式是有 Android 系统框架决定的,通过层抽象使 App 仅用几行代码就能够完成复杂的工作。例如在这里就通过 Android Framework 的 HttpURLCOnnection 类使用设备上的蜂窝或 Wi-Fi 硬件设备,以从网络上获取数据,而不是直接操作 Android 系统,更不是直接控制设备硬件。

    Tip: OkHttp 是一个开源的 HTTP 客户端第三方库,它也可以实现 Android 的网络操作。

    /**
     * Make an HTTP request to the given URL and return a String as the response.
     */
    private String makeHttpRequest(URL url) throws IOException {
        String jsonResponse = "";
    
        // If the URL is null, then return early.
        if (url == null) {
            return jsonResponse;
        }
        HttpURLConnection urlConnection = null;
        InputStream inputStream = null;
        try {
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setRequestMethod("GET");
            urlConnection.setReadTimeout(10000 /* milliseconds */);
            urlConnection.setConnectTimeout(15000 /* milliseconds */);
            urlConnection.connect();
    
            // If the request is successful (response code 200),
            // then read the input stream and parse the response.
            if (urlConnection.getResponseCode() == 200) {
                inputStream = urlConnection.getInputStream();
                jsonResponse = readFromStream(inputStream);
            } else {
                Log.e(LOG_TAG, "Error response code: " + urlConnection.getResponseCode());
            }
        } catch (IOException e) {
            Log.e(LOG_TAG, "Problem retrieving the earthquake JSON results.", e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (inputStream != null) {
                // function must handle java.io.IOException here
                inputStream.close();
            }
        }
        return jsonResponse;
    }
    
    1. urlConnection = (HttpURLConnection) url.openConnection();
      通过 url.openConnection() 创建一个 HttpURLConnection 对象,默认返回的数据类型为 URLConnection,不过 HttpURLConnection 是 URLConnection 的子类,所以这里可以转换数据类型。
    2. urlConnection.setRequestMethod("GET");
      调用 HttpURLConnection 的 setRequestMethod method 来设置 HTTP 动词。

    HTTP 方法或动词 (Method/Verb) 是客户端发送 HTTP 请求的另一项重要指标,通过它来完成客户端与服务器之间的交互,通常是四种操作,创建 (Create)、读取 (Read)、更新 (Update)、删除 (Delete),简写 CRUD)。常用的 HTTP 动词有:

    1. GET: 客户端从服务器获取或检索数据。
    2. POST: 客户端向服务器发送一些数据。
    3. PUT: 客户端更新服务器上的数据。
    4. DELETE: 客户端删除服务器上的数据。

    在 Soonami App 中,客户端要从服务器中获取地震信息,属于读取操作,所以这里设置 HTTP 动词为 GET。HTTP 动词的详细信息可以到这个网站查看,里面详细叙述了每个 HTTP 动词的用法,以及对应的服务器响应。尽管 HTTP 动词的用法遵循一定的规则,但是对于不同 API 而言会有差异,最终应用要以 API 文档为准。

    1. urlConnection.setReadTimeout(10000 /* milliseconds */);
      调用 HttpURLConnection 的 setReadTimeout method 来设置读取数据的延时为 10000 毫秒。

    2. urlConnection.setConnectTimeout(15000 /* milliseconds */);
      调用 HttpURLConnection 的 setConnectTimeout method 来设置连接延时为 15000 毫秒。

    3. urlConnection.connect();
      打包 HTTP 请求并将其发送到服务器。这行代码是客户端与服务器建立 HTTP 连接的位置,在此之前的内容属于设置 HTTP 请求,在此之后的属于接收响应并解析数据的内容。

    4. urlConnection.getResponseCode() == 200
      调用 HttpURLConnection 的 getResponseCode method 来获取 HTTP 响应代码。

    服务器对 HTTP 请求的响应通过 HTTP 响应代码 (HTTP Status Code) 表示,响应代码为三位数字,按首位数字分为五类响应,完整的 HTTP 响应代码列表可以到 Wikipedia 查看。

    1. 1xx Informational Responses
      信息状态码,表示请求已被服务器接收,但仍需继续处理。
    2. 2xx Success
      成功状态码,表示请求已成功被服务器接收、理解、并接受。常见 "200 OK" 表示请求已成功,请求的数据返回客户端。
    3. 3xx Redirection
      重定向状态码,表示客户端需要采取进一步的操作才能完成请求。常见
      "301 Moved Permanently" 表示请求的资源已永久移动到新位置。
    4. 4xx Client Errors
      客户端错误状态码,表示客户端可能发生了错误,妨碍服务器的处理。常见 "400 Bad Request" 表示由于明显的客户端错误(请求语法错误,欺骗性路由请求等),服务器不会处理该请求;"403 Forbidden" 表示服务器已经理解请求,但是拒绝执行它;"404 Not Found" 表示请求的资源未在服务器上找到。
    5. 5xx Server Errors
      服务器错误状态码,表示服务器在处理请求的过程中有错误或者异常状态发生,无法完成有效的请求。常见 "502 Bad Gateway" 表示作为网关或代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。

    根据 HTTP 响应代码,针对服务器的不同响应作出对应的处理方案,使代码能够正常运行,同时增加代码的鲁棒性。例如在 Soonami App 中,USGS 服务器对 GET 动词的响应可能是 200 表示 OK,也可能是 404 表示未找到资源,应用通过 if-else 语句实现仅在 USGS 服务器响应代码为 "200 OK" 时接收响应并解析数据,收到其它响应代码时通过 Log 日志记录错误信息。

    1. inputStream = urlConnection.getInputStream();
      将服务器返回的数据存放在 InputStream 中。对于计算机而言,每一段数据,无论是文本还是图片,都是存放在字节大小的块中,应用在接收数据时数据以数据流 (InputStream) 的形式输入。数据流是抽象的,以二进制 (0/1) 保存。

    2. jsonResponse = readFromStream(inputStream);
      通过 readFromStream 辅助方法来解析 InputStream 数据流,最终传给
      jsonResponse 字符串。由于数据流是二进制 (0/1) 保存的原始数据,所以应用在使用前需要解析成有意义的内容。例如这里需要将服务器返回的 GeoJSON 原始二进制数据转换成字符串,readFromStream 辅助方法的代码如下:

    /**
     * Convert the {@link InputStream} into a String which contains the
     * whole JSON response from the server.
     */
    private String readFromStream(InputStream inputStream) throws IOException {
        StringBuilder output = new StringBuilder();
        if (inputStream != null) {
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("UTF-8"));
            BufferedReader reader = new BufferedReader(inputStreamReader);
            String line = reader.readLine();
            while (line != null) {
                output.append(line);
                line = reader.readLine();
            }
        }
        return output.toString();
    }
    
    1. 通过 InputStreamReader 将 InputStream 的二进制数据转换为字符。其中 Charset 指定了如何将原始数据的逐个字节转换成字符,UTF-8 是一种广泛应用的 Unicode 字符编码。
    2. 由于 InputStreamReader 一次只能转换一个字符,根据 InputStream 实际提供数据的不同方式,这可能会导致严重的性能问题,因此将 InputStreamReader 包装到 BufferedReader 可以避免这个问题。例如上面的 reader.readLine(); 使 BufferedReader 在收到对某个字符的请求后会读取并保存该字符前后的一整行字符,当请求另一个字符时就能利用 BufferedReader 提前读取的字符来实现请求,无需再调用 InputStreamReader。
    3. 由于数据解析过程中会不断生成新的字符,如果将解析的字符存入字符串,那么就要不断地对字符串重新赋值,实际上是对 String 对象反复进行删除和创建操作,因此这里引入一个新的 StringBuilder class。相对 String 而言,StringBuilder 是可变的 (Mutable),在改变字符时能够节省很多系统资源。
      (1)StringBuilder output = new StringBuilder();
      与其它类一样,通过构造函数创建一个 StringBuilder 对象。
      (2)output.append(line);
      通过 append method 添加字符序列。append method 可以在一行内多次调用,如 output.append(line1).append(line2);,这种方法叫做方法链 (Method Chaining)。
      (3)StringBuilder 的其它一些常用 method 有 output.deleteCharAt(3) 表示删除索引号 3 的字符;output.toString() 表示将 StringBuilder 保存到一个不可变 (Immutable) 的字符串中。

    相关文章

      网友评论

          本文标题:课程 2: HTTP 网络

          本文链接:https://www.haomeiwen.com/subject/wfjsoxtx.html