第9章 看看精彩的世界——使用网络技术

无人机上下通信信号解析程序和代码 总的来说,无人机上下行信号解析是无人机系统中非常重要的一部分,它关系到无人机的飞行控制和图像传输能力,对于保障无人机的飞行安全和数据传输质量具有重要作用。3. 读取和解析地面控制中心发送的命令和数据:程序还需要能够读取地面控制中心发送给无人机的命令和数据。1. 读取和解析无人机发送的命令和数据:程序需要能够读取无人机发送的信号,并解析其中的命令和数据。4. 将解析结果传递给无人机:解析地面控制中心发送的命令和数据后,程序需要将解析结果传递给无人机。具体的实现方式可以根据无人机的通信协议和硬件设备来确定。 阅读详情

第9章 看看精彩的世界——使用网络技术

现在早已不是玩单机的时代了,无论是PC、手机、平板,还是电视,几乎都会具备上网的功能,在可预见的未来,手表、眼镜、汽车等设备也会逐个加入到这个行列,21世纪的确是互联网的时代。

当然,Android手机肯定也是可以上网的,所以作为开发者,我们就需要考虑如何利用网络来编写出更加出色的应用程序,像QQ、微博、微信等常见的应用都会大量使用网络技术。

本章主要会讲述如何在手机端使用HTTP协议和服务器端进行网络交互,并对服务器返回的数据进行解析,这也是Android中最常使用到的网络技术,下面就让我们一起来学习一下吧。

9.1 WebView的用法

比如说,要求在应用程序里展示一些网页。加载和显示网页通常都是浏览器的任务,但是需求里又明确指出,不允许打开系统浏览器,而我们当然也不可能自己去编写一个浏览器出来,这时应该怎么办呢?

Android早就已经考虑到了,并提供了一个WebView控件,借助它就可以在自己的应用程序里嵌入一个浏览器,从而非常轻松地展示各种各样的网页。WebView的用法也是相当简单,下面我们就通过一个例子来学习一下吧。新建一个WebViewTest项目,然后修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <WebView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/web_view"/>
</LinearLayout>

在布局文件中使用到了一个新的控件:WebView。这个控件用来显示网页的,给它设置了一个id,并让它充满整个屏幕。然后修改MainActivity中的代码,如下所示:

package com.zhouzhou.webviewtest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WebView webView =(WebView) findViewById(R.id.web_view);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("https://www.cnblogs.com/1693977889zz/");
    }
}

MainActivity中,首先使用findViewById()方法获取到了WebView的实例,然后调用WebView的getSettings()方法可以去设置一些浏览器的属性,这里只是调用了setJavaScriptEnabled()方法来让WebView支持JavaScript脚本。

接下来,调用了WebView的setWebViewClient()方法,并传入了一个WebViewClient的实例。这段代码的作用是,当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器。

最后一步,调用WebView的loadUrl()方法,并将网址传入,即可展示相应网页的内容。

另外还需要注意,由于本程序使用到了网络功能,而访问网络是需要声明权限的,因此我们还得修改AndroidManifest.xml文件,并加入权限声明,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.webviewtest">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>

在开始运行之前,首先需要保证你的手机或模拟器是联网的,如果你使用的是模拟器,只需保证电脑能正常上网即可。然后就可以运行一下程序了,效果如图:

image

可以看到,WebViewTest这个程序现在已经具备了一个简易浏览器的功能,不仅成功将我的博客展示了出来,还可以通过点击链接浏览更多的网页。

当然,WebView还有很多更加高级的使用技巧,就不再继续进行探讨了。这里先介绍了一下WebView的用法,只是希望你能对HTTP协议的使用有一个最基本的认识,接下来就要利用这个协议来做一些真正的网络开发工作。

9.2 使用HTTP协议访问网络

如果说真的要去深入分析HTTP协议,可能需要花费整整一本书的篇幅。这里你只需要稍微了解一些就足够了,它的工作原理特别简单,就是客户端向服务器发出一条HTTP请求,服务器收到请求之后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理就可以了。

比如,上一节中使用到的WebView控件,其实也就是我们向服务器发起了一条HTTP请求,接着服务器分析出我们想要访问的页面,于是会把该网页的HTML代码进行返回,然后WebView再调用手机浏览器的内核对返回的HTML代码进行解析,最终将页面展示出来。

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

9.2.1 使用HttpURLConnection

在过去,Android上发送HTTP请求一般有两种方式:HttpURLConnection和HttpClient。不过由于HttpClient存在API数量过多、扩展困难等缺点,Android团队越来越不建议我们使用这种方式。

终于在Android 6.0系统中,HttpClient的功能被完全移除了,标志着此功能被正式弃用,因此本小节我们就学习一下现在官方建议使用的HttpURLConnection的用法。

首先需要获取到HttpURLConnection的实例,一般只需new出一个URL对象,并传入目标的网络地址,然后调用一下openConnection()方法即可,如下所示:

URL url = new URL("http://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的用法。新建一个NetworkTest项目,首先修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/send_request"
        android:text="Send Request"/>
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/response_text"/>
    </ScrollView>
</LinearLayout>

使用了一个新的控件:ScrollView,它是用来做什么的呢?

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

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

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 view) {
        if (view.getId() == R.id.send_request) {
            sendRequestWithHttpURLConnection();
        }
    }

    private void sendRequestWithHttpURLConnection() {
        //开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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();
                    }
                }
            }
        }).start();
    }
    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在这里进行Ui操作,将结果显示到界面
                responseText.setText(response);
            }
        });
    }
}

可以看到,在Send Request按钮的点击事件里调用了sendRequestWithHttpURLConnection()方法,在这个方法中先是开启了一个子线程,然后在子线程里使用HttpURLConnection发出一条HTTP请求,请求的目标地址就是百度的首页。

接着利用BufferedReader对服务器返回的流进行读取,并将结果传入到了showResponse()方法中。而在showResponse()方法里则是调用了一个runOnUiThread()方法,然后在这个方法的匿名类参数中进行操作,将返回的数据显示到界面上。

那么这里为什么要用这个runOnUiThread()方法呢?这是因为Android是不允许在子线程中进行UI操作的,我们需要通过这个方法将线程切换到主线程,然后再更新UI元素。

关于这部分内容,我们将会在下一章中进行详细讲解,现在你只需要记得必须这么写就可以了。完整的一套流程就是这样,不过在开始运行之前,仍然别忘了要声明网络权限。修改AndroidManifest.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.networktest">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>

好了,现在运行一下程序,并点击Send Request按钮,结果如图:

image

服务器返回给我们的就是这种HTML代码,只是通常情况下浏览器都会将这些代码解析成漂亮的网页后再展示出来。

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

connection.setRequestMethod("POST");
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()) ;
outputStream.writeBytes("username=admin&&password=123456");
9.2.2 使用OkHttp

当然,我们并不是只能使用HttpURLConnection,完全没有任何其他选择,事实上在开源盛行的今天,有许多出色的网络通信库都可以替代原生的HttpURLConnection,而其中OkHttp无疑是做得最出色的一个。

OkHttp是由鼎鼎大名的Square公司开发的,这个公司在开源事业上面贡献良多,除了OkHttp之外,还开发了像Picasso、Retrofit等著名的开源项目。

OkHttp不仅在接口封装上面做得简单易用,就连在底层实现上也是自成一派,比起原生的HttpURLConnection,可以说是有过之而无不及,现在已经成了广大Android开发者首选的网络通信库。

那么本小节我们就来学习一下OkHttp的用法,OkHttp的项目主页地址是:https://github.com/square/okhttp。

See the project website for documentation and APIs.

HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth.

OkHttp is an HTTP client that’s efficient by default:

  • HTTP/2 support allows all requests to the same host to share a socket.
  • Connection pooling reduces request latency (if HTTP/2 isn’t available).
  • Transparent GZIP shrinks download sizes.
  • Response caching avoids the network completely for repeat requests.

OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses, OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and services hosted in redundant data centers. OkHttp supports modern TLS features (TLS 1.3, ALPN, certificate pinning). It can be configured to fall back for broad connectivity.

Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks.

在使用OkHttp之前,我们需要先在项目中添加OkHttp库的依赖。编辑app/build.gradle文件,在dependencies闭包中添加如下内容:

    dependencies {
       implementation("com.squareup.okhttp3:okhttp:4.9.3")
    }

添加上述依赖会自动下载两个库,一个是OkHttp库,一个是Okio库,后者是前者的通信基础。

image

下面我们来看一下OkHttp的具体用法,首先需要创建一个OkHttpClient的实例,如下所示:

OkHttpClient client = new OkHttpClient();

接下来,如果想要发起一条HTTP请求,就需要创建一个Request对象:

Request request = new Request.Builder().build();

当然,上述代码只是创建了一个空的Request对象,并没有什么实际作用,我们可以在最终的build()方法之前连缀很多其他方法来丰富这个Request对象。比如可以通过url()方法来设置目标的网络地址,如下所示:

Request request = new Request.Builder()
      .url("https://www.baidu.com")
      .build();

之后,调用OkHttpClient的newCall()方法来创建一个Call对象,并调用它的execute()方法来发送请求并获取服务器返回的数据,写法如下:

Response response = client.newCall(request).execute();

其中,Response对象就是服务器返回的数据了,我们可以使用如下写法来得到返回的具体内容:

String responseData = response.body().string();

如果是发起一条POST请求会比GET请求稍微复杂一点,我们需要先构建出一个RequestBody对象来存放待提交的参数,如下所示:

RequestBody requestBody = new FormBody.Builder()
      .add("username","admin")
      .add("password","123456")
      .build();

然后,在Request.Builder中调用一下post()方法,并将RequestBody对象传入:

Request request = new Request.Builder()
      .url("https://www.baidu.com")
      .post(requestBody)
      .build();

接下来的操作就和GET请求一样了,调用execute()方法来发送请求并获取服务器返回的数据即可。

书中后面所有网络相关的功能我们都将会使用OkHttp来实现,到时候再进行进一步的学习。那么现在我们先把NetworkTest这个项目改用OkHttp的方式再实现一遍吧。由于布局部分完全不用改动,所以现在直接修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ...
    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.send_request) {
            //sendRequestWithHttpURLConnection();
            sendRequestWithOkHttp();
        }
    }

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("https://www.baidu.com")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    showResponse(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
	...
}

这里并没有做太多的改动,只是添加了一个sendRequestWithOkHttp()方法,并在Send Request按钮的点击事件里去调用这个方法。在这个方法中同样还是先开启了一个子线程,然后在子线程里使用OkHttp发出一条HTTP请求,请求的目标地址还是百度的首页,OkHttp的用法也正如前面所介绍的一样。

最后仍然还是调用了showResponse()方法来将服务器返回的数据显示到界面上。重新运行一下程序了。点击SendRequest按钮后,测试结果OK,由此证明,使用OkHttp来发送HTTP请求的功能也已经成功实现了。

9.3 解析XML格式数据

通常情况下,每个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交数据,也可以从服务器上获取数据。

不过这个时候就出现了一个问题,这些数据到底要以什么样的格式在网络上传输呢?随便传递一段文本肯定是不行的,因为另一方根本就不会知道这段文本的用途是什么。因此,一般我们都会在网络上传输一些格式化后的数据,这种数据会有一定的结构规格和语义,当另一方收到数据消息之后就可以按照相同的结构规格进行解析,从而取出他想要的那部分内容。在网络上传输数据时最常用的格式有两种:XML和JSON,下面我们就来一个一个地进行学习,本节首先学习一下如何解析XML格式的数据。

在开始之前我们还需要先解决一个问题,就是从哪儿才能获取一段XML格式的数据呢?

这里我准备教你搭建一个最简单的Web服务器,在这个服务器上提供一段XML文本,然后我们在程序里去访问这个服务器,再对得到的XML文本进行解析。

搭建Web服务器其实非常简单,有很多的服务器类型可供选择,这里我准备使用Apache服务器。首先你需要去下载一个Apache服务器的安装包,官方下载地址是:http://httpd.apache.org/download.cgi。如果你在这个网址中找不到Windows版的安装包,也可以直接在百度上搜索“Apache服务器下载”,将会找到很多下载链接。

下面简介,官方下载步骤:

  1. 点击链接 a number of third party vendors.:

image

  1. 找到Downloading Apache for Windows,点击ApacheHaus链接:

image

  1. 点击红框中的图标即可开始下载,x86是32位的,x64是64位的,根据自己的操作系统选择下载:

image

  1. 点击之后跳转到下载页面,Your download will start shortly...

image

  1. 经常会超时哦~所以,我在网上下载的【Apache HTTP Server 官方版 v2.4.17】,保存到想要保存的位置(我保存到了:F:\ApacheServer),解压压缩包,打开httpd.conf文件(我在F:\ApacheServer\conf),修改Apache安装目录,(其中“${SRVROOT}”指定义的SRVROOT路径变量):

image

  1. 若80端口被占用(可在cmd下用命令netstat -an -o | findstr 80),则将80端口改为别的保存:

image

  1. 以管理员身份运行cmd,复制输入"F:\ApacheServer\bin\httpd.exe" -k install -n apache,该命令意思是安装这个目录(F:\ApacheServer\bin\)下的Apache服务,并将该服务命名为“apache”。

    也可以,以管理员身份运行cmd,进入到F:\ApacheServer\bin目录下输入 httpd.exe -k install -n apache安装apache服务

image

  1. 双击运行bin目录下的ApacheMonitor.exe,也可通过服务打开:

image

  1. 双击打开窗口界面,会看到如图所示:

image


10. 点击 start,测试Apache服务器是否可用,在浏览器中输入http://localhost:8099,(80端口被占用,修改成了8099)出现下面页面,表示安装配置成功!

image

接下来进入到F:\ApacheServer\htdocs目录下,在这里新建一个名为get_data.xml的文件,然后编辑这个文件,并加入如下XML格式的内容:

<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>

这时在浏览器中访问http://127.0.0.1:8099/get_data.xml这个网址,就应该出现如图:

image

准备工作到此结束,接下来就让我们在Android程序里去获取并解析这段XML数据吧。

9.3.1 Pull解析方式

解析XML格式的数据其实也有挺多种方式的,本节中我们学习比较常用的两种,Pull解析和SAX解析。

那么简单起见,这里仍然是在NetworkTest项目的基础上继续开发,这样我们就可以重用之前网络通信部分的代码,从而把工作的重心放在XML数据解析上。

既然XML格式的数据已经提供好了,现在要做的就是从中解析出我们想要得到的那部分内容。修改MainActivity中的代码,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

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 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 view) {
        if (view.getId() == R.id.send_request) {
            //sendRequestWithHttpURLConnection();
            sendRequestWithOkHttp();
        }
    }

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            .url("http://127.0.0.1:8099/get_data.xml")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    //showResponse(responseData);
                    parseXMLWithPull(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void parseXMLWithPull(String xmlData) {
        try {
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlData));
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String version = "";
            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();
        }
    }

    private void sendRequestWithHttpURLConnection() {
        //开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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();
                    }
                }
            }
        }).start();
    }
    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在这里进行Ui操作,将结果显示到界面
                responseText.setText(response);
            }
        });
    }
}

可以看到,这里首先是将HTTP请求的地址改成了http://10.0.2.2:8099/get_data.xml。

10.0.2.2对于模拟器来说就是电脑本机的IP地址。如果用:http://127.0.0.1:8099/get_data.xml或者http://localhost:8099/get_data.xml 的话,报出的错误信息是:java.net.ConnectException: failed to connect to /127.0.0.1 (port 8099) after 2500ms: isConnected failed: ECONNREFUSED (Connection refused)

image

另外,可能报错:W/System.err: java.net.UnknownServiceException:
CLEARTEXT communication 你的域名 not permitted by network security policy.

image

此时的报错原因是,网络安全策略不允许:

为保证用户数据和设备的安全,Google针对下一代 Android 系统(Android P) 的应用程序,将要求默认使用加密连接,这意味着 Android P 将禁止 App 使用所有未加密的连接,因此运行 Android P 系统的安卓设备无论是接收或者发送流量,未来都不能明码传输,需要使用下一代(Transport Layer Security)传输层安全协议,而 Android Nougat 和 Oreo 则不受影响。

在Android P系统的设备上,如果应用使用的是非加密的明文流量的http网络请求,则会导致该应用无法进行网络请求,https则不会受影响,同样地,如果应用嵌套了webview,webview也只能使用https请求。

解决办法:

(1)APP改用https请求

(2)targetSdkVersion 降到27以下(此问题发生在 API>=27 的新项目工程)

(3)在 application 元素中添加:

android:usesCleartextTraffic="true"

更规范一点是:

在res目录下新建xml目录,在xml目录下新增 network_security_config.xml 文件,内容:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config cleartextTrafficPermitted="true" />
    </network-security-config>

再在 AndroidManifest.xml 的 Application 标签中新增属性:

android:networkSecurityConfig="@xml/network_security_config" 

报错全部解决,程序运行OK。继续,

在得到了服务器返回的数据后,我们并不再直接将其展示,而是调用了parseXMLWithPull()方法来解析服务器返回的数据。parseXMLWithPull()方法中的代码,首先要获取到一个XmlPullParserFactory的实例,并借助这个实例得到XmlPullParser对象,然后调用XmlPullParser的setInput()方法将服务器返回的XML数据设置进去就可以开始解析了。解析的过程也非常简单,通过getEventType()可以得到当前的解析事件,然后在一个while循环中不断地进行解析,如果当前的解析事件不等于XmlPullParser.END_DOCUMENT,说明解析工作还没完成,调用next()方法后可以获取下一个解析事件。

在while循环中,我们通过getName()方法得到当前节点的名字,如果发现节点名等于id、name或version,就调用nextText()方法来获取节点内具体的内容,每当解析完一个app节点后就将获取到的内容打印出来。好了

整体的过程就是这么简单,下面就让我们来测试一下吧。运行NetworkTest项目,然后点击Send Request按钮,观察logcat中的打印日志,如图:

image

9.3.2 SAX解析方式

Pull解析方式虽然非常好用,但它并不是我们唯一的选择。SAX解析也是一种特别常用的XML解析方式,虽然它的用法比Pull解析要复杂一些,但在语义方面会更加清楚。通常情况下我们都会新建一个类继承自DefaultHandler,并重写父类的5个方法,如下所示:

public class MyHandler extends DefaultHandler {

    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        super.startElement(uri, localName, qName, attributes);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
}

startDocument()方法会在开始XML解析的时候调用;

startElement()方法会在开始解析某个节点的时候调用;

characters()方法会在获取节点中内容的时候调用;

endElement()方法会在完成解析某个节点的时候调用;

endDocument()方法会在完成整个XML解析的时候调用。

其中,startElement()、characters()和endElement()这3个方法是有参数的,从XML中解析出的数据就会以参数的形式传入到这些方法中。

需要注意的是,在获取节点中的内容时,characters()方法可能会被调用多次,一些换行符也被当作内容解析出来,我们需要针对这种情况在代码中做好控制。

那么下面就让我们尝试用SAX解析的方式来实现和上一小节中同样的功能吧。新建一个ContentHandler类继承自DefaultHandler,并重写父类的5个方法,如下所示:

package com.zhouzhou.networktest;

import android.util.Log;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class ContentHandler extends DefaultHandler {
    private String nodeName;
    private StringBuilder id;
    private StringBuilder name;
    private StringBuilder version;
    
    // startDocument()方法会在开始XML解析的时候调用;
    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
        id = new StringBuilder();
        name = new StringBuilder();
        version = new StringBuilder();
    }
    
    // startElement()方法会在开始解析某个节点的时候调用;
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        super.startElement(uri, localName, qName, attributes);
        //记录当前节点名
        nodeName = localName;
    }
    
    // characters()方法会在获取节点中内容的时候调用;
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
        // 根据当前的节点名判断将内容添加到哪一个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);
        }
    }
    
    // endElement()方法会在完成解析某个节点的时候调用;
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
        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清空掉
            id.setLength(0);
            name.setLength(0);
            version.setLength(0);
        }
    }
    
    // endDocument()方法会在完成整个XML解析的时候调用。
    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
}

可以看到,我们首先给id、name和version节点分别定义了一个StringBuilder对象,并在startDocument()方法里对它们进行了初始化。

每当开始解析某个节点的时候,startElement()方法就会得到调用,其中localName参数记录着当前节点的名字,这里我们把它记录下来。

接着在解析节点中具体内容的时候就会调用characters()方法,我们会根据当前的节点名进行判断,将解析出的内容添加到哪一个StringBuilder对象中。

最后在endElement()方法中进行判断,如果app节点已经解析完成,就打印出id、name和version的内容。

需要注意的是,目前id、name和version中都可能是包括回车或换行符的,因此在打印之前我们还需要调用一下trim()方法,并且打印完成后还要将StringBuilder的内容清空掉,不然的话会影响下一次内容的读取。接下来的工作就非常简单了,修改MainActivity中的代码,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.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 view) {
        if (view.getId() == R.id.send_request) {
            //sendRequestWithHttpURLConnection();
            sendRequestWithOkHttp();
        }
    }

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            //指定访问的服务器地址是本机电脑
                            // 模拟器默认把127.0.0.1和localhost当做本身了,在模拟器上可以用10.0.2.2代替127.0.0.1和localhost
                            .url("http://10.0.2.2:8099/get_data.xml")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    //showResponse(responseData);
                    //parseXMLWithPull(responseData);
                    parseXMLWithSAX(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void parseXMLWithSAX(String xmlData) {
        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 factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlData));
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String version = "";
            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();
        }
    }

    private void sendRequestWithHttpURLConnection() {
        //开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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();
                    }
                }
            }
        }).start();
    }
    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在这里进行Ui操作,将结果显示到界面
                responseText.setText(response);
            }
        });
    }
}

在得到了服务器返回的数据后,我们这次去调用parseXMLWithSAX()方法来解析XML数据。parseXMLWithSAX()方法中先是创建了一个SAXParserFactory的对象,然后再获取到XMLReader对象,接着将我们编写的ContentHandler的实例设置到XMLReader中,最后调用parse()方法开始执行解析就好了。

现在重新运行一下程序,点击Send Request按钮后观察logcat中的打印日志,你会看到和上面一样的结果。

image

除了Pull解析和SAX解析之外,其实还有一种DOM解析方式也算挺常用的,不过这里我们就不再展开进行讲解了,感兴趣的话你可以自己去查阅一下相关资料。

9.4 解析JSON格式数据

如何解析JSON格式的数据了。比起XML, JSON的主要优势在于它的体积更小,在网络上传输的时候可以更省流量。但缺点在于,它的语义性较差,看起来不如XML直观。

在开始之前,还需要在F:\ApacheServer\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:8099/get_data.json这个网址,就应该出现如图:

image

把JSON格式的数据也准备好了,下面就开始学习如何在Android程序中解析这些数据吧。

9.4.1 使用JSONObject

类似地,解析JSON数据也有很多种方法,可以使用官方提供的JSONObject,也可以使用谷歌的开源库GSON。另外,一些第三方的开源库如Jackson、FastJSON等也非常不错。本节中我们就来学习一下前两种解析方式的用法。修改MainActivity中的代码,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONObject;
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 view) {
        if (view.getId() == R.id.send_request) {
            //sendRequestWithHttpURLConnection();
            sendRequestWithOkHttp();
        }
    }

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            //指定访问的服务器地址是本机电脑
                            // 模拟器默认把127.0.0.1和localhost当做本身了,在模拟器上可以用10.0.2.2代替127.0.0.1和localhost
                            .url("http://10.0.2.2:8099/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    //showResponse(responseData);
                    //parseXMLWithPull(responseData);
                    //parseXMLWithSAX(responseData);
                    parseJSONWithJSONObject(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    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 name = jsonObject.getString("name");
                String version = jsonObject.getString("version");
                Log.d("MainActivity","id is " + id);
                Log.d("MainActivity","name is " + name);
                Log.d("MainActivity","version is " + version);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void parseXMLWithSAX(String xmlData) {
        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 factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlData));
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String version = "";
            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();
        }
    }

    private void sendRequestWithHttpURLConnection() {
        //开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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();
                    }
                }
            }
        }).start();
    }
    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在这里进行Ui操作,将结果显示到界面
                responseText.setText(response);
            }
        });
    }
}

现在重新运行一下程序,并点击Send Request按钮,结果如图:

image

9.4.2 使用GSON

如果你认为使用JSONObject来解析JSON数据已经非常简单了,那你就太容易满足了。谷歌提供的GSON开源库(https://github.com/google/gson/)可以让解析JSON数据的工作简单到让你不敢想象的地步,那我们肯定是不能错过这个学习机会的。

不过GSON并没有被添加到Android官方的API中,因此如果想要使用这个功能的话,就必须要在项目中添加GSON库的依赖。编辑app/build.gradle文件,在dependencies闭包中添加如下内容:

dependencies {
  implementation 'com.google.code.gson:gson:2.9.0'
}

那么,GSON库究竟是神奇在哪里呢?其实它主要就是可以将一段JSON格式的字符串自动映射成一个对象,从而不需要我们再手动去编写代码进行解析了。比如说一段JSON格式的数据如下所示:

{"name":"Tom","age":20}

那我们就可以定义一个Person类,并加入name和age这两个字段,然后只需简单地调用如下代码就可以将JSON数据自动解析成一个Person对象了:

Gson gson = new Gson();
Person person = gson.fromJson(jsonData,Person.class);

如果需要解析的是一段JSON数组会稍微麻烦一点,我们需要借助TypeToken将期望解析成的数据类型传入到fromJson()方法中,如下所示:

List<Person> appList = gson.fromJson(gsonData,new TypeToken<List<Person>>(){}.getType());

基本的用法就是这样,下面就让我们来真正地尝试一下吧。首先新增一个App类,并加入id、name和version这3个字段,如下所示:

package com.zhouzhou.networktest;

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中的代码,如下所示:

package com.zhouzhou.networktest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import org.json.JSONArray;
import org.json.JSONObject;
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 java.util.List;

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 view) {
        if (view.getId() == R.id.send_request) {
            //sendRequestWithHttpURLConnection();
            sendRequestWithOkHttp();
        }
    }

    private void sendRequestWithOkHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    OkHttpClient client = new OkHttpClient();
                    Request request = new Request.Builder()
                            //指定访问的服务器地址是本机电脑
                            // 模拟器默认把127.0.0.1和localhost当做本身了,在模拟器上可以用10.0.2.2代替127.0.0.1和localhost
                            .url("http://10.0.2.2:8099/get_data.json")
                            .build();
                    Response response = client.newCall(request).execute();
                    String responseData = response.body().string();
                    //showResponse(responseData);
                    //parseXMLWithPull(responseData);
                    //parseXMLWithSAX(responseData);
                    //parseJSONWithJSONObject(responseData);
                    parseJSONWithGSON(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void parseJSONWithGSON(String gsonData) {
        Gson gson = new Gson();
        List<App> appList = gson.fromJson(gsonData,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());
        }
    }

    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 name = jsonObject.getString("name");
                String version = jsonObject.getString("version");
                Log.d("MainActivity","id is " + id);
                Log.d("MainActivity","name is " + name);
                Log.d("MainActivity","version is " + version);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void parseXMLWithSAX(String xmlData) {
        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 factory = XmlPullParserFactory.newInstance();
            XmlPullParser xmlPullParser = factory.newPullParser();
            xmlPullParser.setInput(new StringReader(xmlData));
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String version = "";
            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();
        }
    }

    private void sendRequestWithHttpURLConnection() {
        //开启线程来发起网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try {
                    URL url = new URL("https://www.baidu.com");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET");
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    InputStream inputStream = connection.getInputStream();
                    //下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(inputStream));
                    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();
                    }
                }
            }
        }).start();
    }
    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //在这里进行Ui操作,将结果显示到界面
                responseText.setText(response);
            }
        });
    }
}

现在重新运行程序,点击Send Request按钮后观察logcat中的打印日志,测试OK!

9.5 网络编程的最佳实践

也许你还没有发现,之前我们的写法其实是很有问题的。因为一个应用程序很可能会在许多地方都使用到网络功能,而发送HTTP请求的代码基本都是相同的,如果我们每次都去编写一遍发送HTTP请求的代码,这显然是非常差劲的做法。

没错,通常情况下我们都应该将这些通用的网络操作提取到一个公共的类里,并提供一个静态方法,当想要发起网络请求的时候,只需简单地调用一下这个方法即可。比如使用如下的写法:

package com.zhouzhou.networktest;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpUtil {
    public static String sendHttpRequest(String address) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL(address);
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(8000);
            connection.setReadTimeout(8000);
            connection.setDoInput(true);
            connection.setDoOutput(true);
            InputStream inputStream = connection.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            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();
            }
        }
    }
}

以后每当需要发起一条HTTP请求的时候就可以这样写:

String address = "http://www.baidu.com"
String response = HttpUtil.sendHttpRequest(address);

在获取到服务器响应的数据后,我们就可以对它进行解析和处理了。但是需要注意,网络请求通常都是属于耗时操作,而sendHttpRequest()方法的内部并没有开启线程,这样就有可能导致在调用sendHttpRequest()方法的时候使得主线程被阻塞住。

你可能会说,很简单嘛,在sendHttpRequest()方法内部开启一个线程不就解决这个问题了吗?其实没有你想象中的那么容易,因为如果我们在sendHttpRequest()方法中开启了一个线程来发起HTTP请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,当然也就无法返回响应的数据了。

那么遇到这种情况时应该怎么办呢?其实解决方法并不难,只需要使用Java的回调机制就可以了,下面就让我们来学习一下回调机制到底是如何使用的。首先需要定义一个接口,比如将它命名成HttpCallbackListener,代码如下所示:

package com.zhouzhou.networktest;

public interface HttpCallbackListener {
    void onFinish(String response);
    void onError(Exception e);
}

可以看到,我们在接口中定义了两个方法,onFinish()方法表示当服务器成功响应我们请求的时候调用,onError()表示当进行网络操作出现错误的时候调用。这两个方法都带有参数,onFinish()方法中的参数代表着服务器返回的数据,而onError()方法中的参数记录着错误的详细信息。接着修改HttpUtil中的代码,如下所示:

package com.zhouzhou.networktest;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

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);
                    connection.setDoInput(true);
                    connection.setDoOutput(true);
                    InputStream inputStream = connection.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                    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) {
                    e.printStackTrace();
                    // 回调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) {
        //在这里对异常情况进行处理
    }
});

这样的话,当服务器成功响应的时候,我们就可以在onFinish()方法里对响应数据进行处理了。类似地,如果出现了异常,就可以在onError()方法里对异常情况进行处理。

如此一来,我们就巧妙地利用回调机制将响应数据成功返回给调用方了。不过你会发现,上述使用HttpURLConnection的写法总体来说还是比较复杂的,那么使用OkHttp会变得简单吗?答案是肯定的,而且要简单得多,下面我们来具体看一下。在HttpUtil中加入一个sendOkHttpRequest()方法,如下所示:

public class 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);
    }
}

可以看到,sendOkHttpRequest()方法中有一个okhttp3.Callback参数,这个是OkHttp库中自带的一个回调接口,类似于我们刚才自己编写的HttpCallbackListener。

然后在client. newCall()之后没有像之前那样一直调用execute()方法,而是调用了一个enqueue()方法,并把okhttp3.Callback参数传入。OkHttp在enqueue()方法的内部已经帮我们开好子线程了,然后会在子线程中去执行HTTP请求,并将最终的请求结果回调到okhttp3.Callback当中。那么我们在调用sendOkHttpRequest()方法的时候就可以这样写:

HttpUtil.sendOkHttpRequest("http://www.baidu.com",new okhttp3.Callback(){
    @Override
    public void onResponse(Call call,Response response) throws IOException {
        //得到服务器返回的具体内容
        String responseData = response.body().string();
    }
    @Override
    public void onFailure(Call call,IOException e) {
        //在这里对异常情况进行处理
    }
});

由此可以看出,OkHttp的接口设计得确实非常人性化,它将一些常用的功能进行了很好的封装,使得我们只需编写少量的代码就能完成较为复杂的网络操作。当然这并不是OkHttp的全部,后面我们还会继续学习它的其他相关知识。

另外需要注意的是,不管是使用HttpURLConnection还是OkHttp,最终的回调接口都还是在子线程中运行的,因此我们不可以在这里执行任何的UI操作,除非借助runOnUiThread()方法来进行线程转换。至于具体的原因,我们很快就会在下一章中学习到了。

9.6 小结与点评

本章中主要学习了在Android中使用HTTP协议来进行网络交互的知识,虽然Android中支持的网络通信协议有很多种,但HTTP协议无疑是最常用的一种。通常我们有两种方式来发送HTTP请求,分别是HttpURLConnection和OkHttp。

接着又学习了XML和JSON格式数据的解析方式,无论是XML还是JSON,它们各自又拥有多种解析方式,如果以后工作中还需要用到其他的解析方式,可以自行去学习。

最后,主要学习了如何利用Java的回调机制来将服务器响应的数据进行返回。其实除此之外,还有很多地方都可以使用到Java的回调机制,希望能举一反三。

科研绘图系列:R语言绘制极坐标饼图(pie plot) 科研绘图系列:R语言极坐标饼图(pie plot) 阅读详情

相关推荐

学术版GPT安装教程——保姆版(附带原生GPT注册教程)

学术版GPT图文安装教程

DBenD的博客 2万+

8月9日 第 10 章 看看精彩的世界,使用网络技术

如果我们在 sendHttpRequest()方法中开启了一个线程来发起 HTTP 请求,那么服务器响应的数据是无法进行返回的,所有的耗时逻辑都是在子线程里进行的,sendHttpRequest()方法会在服务器还没来得及响应的时候就执行结束了,也就无法返回响应的数据了。因此,一般会在网络上传输一些格式化后的数据,这种数据会有一定的结构规则和语义,当另一方收到数据消息之后,就可以按照相同的结构规则进行解析,从而取出想要的那部分内容。在获取到服务器响应的数据后,我们就可以对它进行解析和处理了。

lin_li17的博客 1111

机器人视觉实战:用OpenCV搞定手眼标定(附完整C++代码)

本文详细介绍了使用OpenCV实现机器人视觉中的手眼标定技术,包括核心算法、工业场景优化及完整C++代码示例。通过解析坐标系转换问题,提供从数据采集到标定验证的全流程解决方案,帮助开发者在复杂工业环境中实现高精度机械臂与视觉系统的协同作业。

weixin_29306571的博客 481

Android第一行代码——第九章使用网络技术

WebView用法:在应用程序里镶嵌浏览器使用HTTP协议访问网络使用HttpURLConntection WebView用法:在应用程序里镶嵌浏览器 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WebView web.

m0_50639018的博客 267

Upwork入门指南:如何创建吸引人的个人资料

本文详细介绍了如何在Upwork上创建并优化个人资料,帮助自由职业者吸引更多客户。内容包括: 注册与验证:从注册账号到邮箱验证的完整步骤。 个人资料填写:如何撰写吸引人的标题、简介,选择技能标签,以及设置专业头像和费率。 作品集与优化:上传高质量作品集,提升搜索排名和客户信任度。 常见问题解答:解决新手常见问题,如无工作经验或审核不通过。 通过本文的指导,你将学会打造一个专业且高效的个人资料,为在Upwork上获得更多机会奠定基础。

qq_61813593的博客 3519

日前日内多阶段多时间尺度源荷储协调调度(matlab代码)

多阶段多时间尺度的协调调度的优势是考虑新能源出力的波 动性与随机性,需求响应负荷的不确定性会对电网制定的日前调度计划准确性造成影响,也就是能够更加精准的进行调度和分析,优化结果的可用性更强。在这方面论文里面,金力的《考虑特性分布的储能电站接入的电网多时间尺度源储荷协调调度策略》就考虑了日前日内实时三个阶段调度,每个阶段考虑了不同负荷需求响应策略,同时考虑了负荷波动性的影响,具有很强的参考意义。 目录 一、模型 二、程序运行 三、程序链接 一、模型 负荷需求响应考虑电价型和激励型两种,电.

zhangxd212489的博客 7915

第九章 看看精彩的世界-使用网络技术

9.1 WebView的用法 在res文件夹下新建xml文件夹,再在下面创建一个network_security_config.xml文件,内容如下 <?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTra...

julicliy的博客 198

09、Flutter FFI Dart Native API

本文介绍了 Dart Native API 的基本用法,解决了 Dart 与 C 异步通讯的问题

eieihihi的专栏 4400

第二行代码学习笔记——第九章:看看精彩的世界——使用网络技术

本章要点21世纪的确是互联网时代。 Android手机必须可以上网的,作为开发者的我们,需要考虑如何利用网络来编写更加出色的应用程序(大量使用网络技术)。接下来我们学习在手机端使用HTTP协议和服务器进行交互,并对服务器返回的数据进行解析(Android最常用的网络技术)。9.1 WebView的使用WebView控件指在自己的应用程序里嵌入一个浏览器,来展示各种各样的网页。WebView的用法:

JiangWeiHu的博客 1367

redhawk:使用rh_setup.pl产生gsr模板

必须加的option工具会在rh_setup.pl给出提示,按照要求填写即可,另外工具会把所有填写的输入文件和信息放到rh_setup.init文件中,拷贝这个文件可在任何平级运行目录下,直接rh_setup.pl产生gsr以及run sta/dyn/em的脚本。rhsetup.pl -top top_cell_name -vdd_nets VDD 1.2 VDD2 1.2 -vss_nets VSS -frequency 100e6 (100m是默认值)更多学习内容请关注「拾陆楼」知识星球。

m0_61544122的博客 1055

【实战】CANOE实现UDS安全访问过程的CAPL脚本

首先我已经有了包含安全访问算法的dll文件,并通过Diagnostics/ISO TP Configuration中加载了此dll文件,如下。在on start部分,要将“ECU qualifier“作为参数,执行diagSetTarget函数。这个函数是用于设置tester从现在开始与之通信的目标ECU,并配置/建立与该ECU的通信通道。我最近在做密钥学习上位机时,遇到一些问题:如何用CAPL脚本实现安全访问过程?ECU Qualifier可以通过加载cdd文件后,在如下位置找到。CAPL脚本如何编写。

weixin_46481662的博客 4939

C++ 游戏开发进阶:打造更精彩的游戏世界

本博客为C++游戏开发的进阶教学,希望可以为对游戏开发有兴趣的各位有一点点帮助,谢谢大家的阅读与喜欢!

安年的小小博客有大大的梦想 1307

【项目实战】功能安全目标有四个ASIL等级:A、B、C、D,ASIL D是最高的等级

功能安全是一种关注产品在异常情况下如何避免造成伤害的概念功能安全在汽车电子行业中非常重要。功能安全涉及到系统、电子和软件三个部分。(1)系统部分主要涉及概念设计(2)而具体的实施则是在电子和软件部分完成。

奶爸的编程之路,也就一周冷个三天 2829

第一行代码——第九章:看看精彩的世界——使用网络技术

目录: 9.1 WebView的用法 9.2 使用HTTP协议访问网络 9.2.1 使用HttpURLConnection 9.2.2 使用OkHttp 9.3 解析XML格式数据 9.3.1 Pull解析方式 9.3.2 SAX解析方式 9.4 解析JSON格式数据 9.4.1 使用JSONObject 9.4.2 使用GSON 9.5 网络编程的最佳实践 9.6 小结与...

李某人_ 5955

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

里用gam使用_“买打野到JDG”火了,GAM用三局盖伦闯进了S9世界赛!

前言:最新英雄联盟S9世界赛快讯,又一支战队成功晋级到了S9世界赛,它就是来自于VCS赛区的GAM战队。对于GAM这支战队我们并不陌生,在S7世界赛上GAM外号“越南黑马”“创造力第一战队”等等,GAM的上单厄加特给我们留下了深刻的印象。今年GAM卷土重来,实力不容小觑,首先我们先回顾一下GAM战队击败FL晋级到S9世界赛的三场比赛,可以简单归纳为:GAM用三局盖伦闯进了S9世界赛! ...

weixin_39962125的博客 172

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

第一行代码 学习笔记 看看精彩的世界--使用网络技术

本章主要会讲述如何在手机端使用HTTP协议和服务器端进行网络交互,并对服务器返回的数据进行解析,这也是Android中最常用到的网络技术。9.1 WebView的用法Android提供了一个WebView控件,借助它我们就可以在自己的应用里嵌入一个浏览器,从而非常轻松的展示各种各样的网页WebView还有很多更高级的用法9.2 使用HTTP协议访问网络9.2.1 使用HttpURLConnecti...

qq_37635604的博客 455

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

《构建Debian的精彩世界》

2007-10-06  星期六  12:04                                darkblue    --------------------------------------------------------------------------------------------- 这段时间在公司一直使用Ubuntu系统,其实刚来的时候用的是Debian,也是我

Dark Blue 的 Blog 3348

正版验证我的世界服务器有哪些,我的世界电脑版1.9版本服务器汇总 1.9.0服务器大全...

我的世界电脑版1.9版本服务器汇总,下面百度攻略&搞趣网小编为大家整合了我的世界1.9版本所有新服务器,希望这篇我的世界1.9.0服务器大全对大家有所帮助。游戏简介:Minecraft是一款沙盒游戏,所呈现的世界并不是华丽的画面与特效,而是注重在游戏性上面。整个游戏没有剧情,玩家在游戏中自由建设和破坏,透过像乐高一样的积木来组合与拼凑,轻而易举的就能制作出小木屋、城堡甚至城市,但是若再加上...

weixin_39912163的博客 810

4月高分游戏佳作速览,带你玩转精彩世界!

ROG魔霸9搭载AMD锐龙9 9955HX3D处理器,基于新一代"Zen5"架构打造,拥有16核心32线程,并配备了64MB的3D V-Cache高速缓存,让L2+L3总缓存容量达到144MB,在处理复杂任务和高帧数游戏场景时表现尤为出色。游戏采用虚幻5引擎,画面焕然一新,角色模型细腻,口型同步真实,自然环境优美,氛围特效浓厚。在这个热闹而又奇妙的宇宙中,玩家要依靠机智的头脑,自信的表情,风骚的走位,努力地生存下去...游戏中,玩家可体验22个战役任务,感受基地建造、资源采集与单位生产的经典玩法。

???? 430

2019世界计算机大会举办在即:12名院士精彩亮相

2019世界计算机大会将于今年9月9日至11日在湖南长沙梅溪湖国际文化艺术中心和长沙梅溪湖金茂酒店举办。随着大会的日益临近,大会论坛相关细节正陆续公布,敬请期待!本届世界...

中国计算机报 338

Linux 2.6内核的精彩世界

Linux 2.6内核的精彩世界内容:到目前为止的故事...多种平台支持深入Linux统一设备模型核心硬件支持块设备支持输入输出设备软件方面的改进其他特性写在最后参考资料关于译

Sah的Blog搬家了!新地址为:sah-lee.51.net 1325

Linux 2.6内核的精彩世界(zz)

From:  http://www-128.ibm.com/developerworks/cn/linux/kernel/l-kernel26/index.html内容:到目前为止的故事...多种平台支持深入Linux统一设备模型核心硬件支持块设备支持输入输出设备软件方面的改进其他特性写在最后参考资料 关于作者对本文的评价订阅:developerWorks 时事通讯Joseph Praneich2

unizero的专栏 1154

九月份精彩网络讲座让您深入了解微软最新开发技术

九月份精彩网络讲座让您深入了解微软最新开发技术发布日期: 8/19/2004 | 更新日期: 8/19/2004ASP.NET系列讲座让您深入接触Web应用开发SharePoint Portal 开发讲座给您揭秘门户开发秘籍深入浅出的讲解如何构建Windows Server 2003应用请按以下步骤参加讲座:第一步:获得账号。用此账号您可以参加微软在线的任何讲座。(

1405

Linux 2.6内核的精彩世界(转)

Linux 2.6内核的精彩世界(转)翻译: 《Linux 2.6内核的精彩世界》翻译小组2003 年 9 月Linux内核2.6版的开发已经接近尾声,2.6测试版发布已经二个月了,正式版预计明年年初发布。作为一个主要发布版本,...

cizn2013的博客 339

手把手教你利用Python网络爬虫获取旅游景点信息

点击上方“Python爬虫与数据挖掘”,进行关注回复“书籍”即可获赠Python从入门到进阶共10本电子书今日鸡汤桃之夭夭,灼灼其华。/1 前言/当我们出去旅游时,会看这个地方有...

pdcfighting的博客 6566

智慧的物联网—感知中国和世界的技术

智慧的物联网—感知中国和世界的技术 基本信息 作者: 吴功宜   出版社:机械工业出版社 ISBN:9787111307105 上架时间:2010-7-5 出版日期:2010 年6月 开本:16开 页码:266 更多详细信息查看:http://www.china-pub.com/196848更有精彩样张抢先免费试读:http://www.china-pub.com/computers/co...

weixin_34289744的博客 232
上一篇: 第8章 丰富你的程序——运用手机多媒体
下一篇: 第10章 后台默默的劳动者——探究服务
Roderick888888
博客等级 码龄6年 295粉丝 23原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值