Android网络应用

13.1基于TCP/IP协议的网络通讯

TCP/IP通讯协议是一种可靠的网络协议,它在通信的两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。一旦建立了虚拟的网路链路,两端的程序就可以通过虚拟链路进行通信。Java对基于TCP协议的网络通信提供了良好的封装,Java使用Socket对象来代表两端的通信接口,并通过Socket产生IO流来进行网络通信。

13.1.1 TCP/IP协议基础

TCP/IP协议是两个协议,一个TCP协议,一个是IP协议

  • IP协议:IP协议是Internet上使用的一个关键协议,它的全称是Internet Protocol,即Internet协议,通常简称IP协议。通过使用IP协议,使Internet成为一个允许连接不同类型的计算机和不同操作系统的网络。IP协议负责将消息从一个主机传送到另一个主机,消息在传送的过程中被分割成一个个小包。IP协议只保证计算机能发送和接收分组数据,但不能解决数据分组在传输过程中可能出现的问题。

  • TCP协议:被称为一种端对端协议,用于提供可靠且无差错的通信服务,它使用重发机制,保证了数据包在传送中准确无误。

虽然IP和TCP这两个协议的功能不尽相同,也可以分开单独使用,但他们是在同一时期作为一个协议来设计额,并且在功能上也是互补的。通常把这两个协议统称为TCP/IP协议,实际上它们也总是结合在一起使用

13.1.2 使用ServerSocket创建TCP服务器端

两台机器通信,必须有一台服务器做出“主动姿态”,主动接受其他通信实体的连接请求,Java中能够接受其它通信实体连接请求的类是ServerSocket,该ServerSocket对象用于监听来自客户端的连接,如果没有连接,它会一直等待,直到有客户端连接,连接成功后两台机器就可以通信了,这个时候就两个通信实体之间已经就没有服务器端合客户端之分了

  1. ServerSocket包含一个监听客户端连接请求的方法:

    Socket accept(): 如果接收到一个客户端Socket的连接请求,该方法将返回一个与连接客户端的Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞。
  2. ServerSocket类提供了以下一个构造函数:

    • ServerSocket(int port): 用指定的端口port(该端口应该是一个有效的端口整数值:0~65535)创建一个ServerSocket
    • ServerSocket(int port, int backlog): 增加一个用来改变连接队列长度的参数backlog。
    • ServerSocket(int port, int backlog, InetAddress localAddr): 在机器存在多个IP地址的情况下,允许通过localAddr这个参数来指定将ServerSocket绑定到指定的IP地址。
  3. 当ServerSocket使用完毕后,应使用close()方法来关闭该ServerSocket。基本代码如下所示:

    ServerSocket ss = new ServerSocket(30000);
    while(true){
     Socket s = ss.accept();
     ...
     这里进行socket通信
     ...
    }
    ss.close();

13.1.3 使用Socket进行通信

  1. 客户端通常可使用Socket的构造器来连接到指定服务器,Socket通常可使用如下两个构造器:

    • Socket(InetAddress/String remoteAddress, int port): 创建连接到指定远程主机、远程端口的Socket,该构造器没有指定本地地址,本地端口,默认使用本地主机的默认IP地址,默认使用系统动态指定的端口。
    • Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort): 创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口号,适用于本地主机有多个IP地址的情形。
  2. 当客户端、服务器端产生了对应的Socket之后,此时就无须再区分服务器端、客户端,而是通过各自对应的Socket进行通信,Socket提供如下两个方法来获取输入流和输出流:

    • InputStream getInputSream(): 返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。
    • OutputStream getOutputStream(): 返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据。
  3. Socket对象提供了一个setSoTimeout(int timeout)来设置超时时长,如下面的代码片段所示:

    Socket s = new Socket("127.0.0.1", 30000);
    s.setSoTimeou(10000);//设置10秒之后即认为超时

    为Socket对象指定了超时时长之后,如果使用Socket进行读写操作完成之前已经超出了该时间限制,那么这些方法就会抛出SocketTimeoutException异常。
    假设程序需要为Socket连接服务器时指定时长(设置连接时长,而非读写操作时长),因为Socket的所有构造器都没有提供指定超时时长的参数,所以程序应该先创建一个无连接的Socket,再调用Socket的connect()方法来连接远程服务器,而connect()方法就可以接受一个超时时长参数,代码如下:

    Socket s = new Socket();
    s.connect(new InetAddress(host, port), 10000);//设置超时时长为10秒

下面给出一个最简单的网络通信Demo,包括服务端和客户端

服务端 SimpleServer.java

public class SimpleServer
{
    public static void main(String[] args) throws IOException
    {
        // 创建一个ServerSocket,用于监听客户端Socket的连接请求
        ServerSocket ss = new ServerSocket(30000);  //①
        // 采用循环不断接受来自客户端的请求
        while (true)
        {
            // 每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
            Socket s = ss.accept();
            OutputStream os = s.getOutputStream();
            os.write("您好,您收到了服务器的新年祝福!\n".getBytes("utf-8"));
            // 关闭输出流,关闭Socket
            os.close();
            s.close();
        }
    }
}

客户端 SimpleClient.java

public class SimpleClient extends Activity
{
    EditText show;
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        show = (EditText) findViewById(R.id.show);
        new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    // 建立连接到远程服务器的Socket
                    Socket socket = new Socket("192.168.1.88" , 30000);  //①
                    // 将Socket对应的输入流包装成BufferedReader
                    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    // 进行普通IO操作
                    String line = br.readLine();
                    show.setText("来自服务器的数据:" + line);
                    // 关闭输入流、socket
                    br.close();
                    socket.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

该客户端Android应用需要访问互联网,因此还需要为该应用赋予访问互联网的权限。
<!-- 授权访问互联网-->
<uses-permission android:name="android.permission.INTERNET"/>

13.1.4 加入多线程

像上面例子使用传统的BufferReaer的readLine()方法读取数据时,当该方法成功返回之前,线程被阻塞,程序无法继续执行。所以服务器应该为每个Socket单独启动一条线程,每条线程负责与一个客户端进行通信。

下面考虑实现一个简单的C/S聊天室应用,服务器端则应该包含多条线程,每个Socket对应一条线程,该线程负责读取Socket对应输入流的数据(从客户端发过来的数据),并将读取到的数据向每个Socket输出流发送一遍,因此需要在服务器端使用List来保存所有的Socket

下面是服务器端的实现代码,程序为服务器提供了两个类,一个是创建ServerSocket监听的主类,另一个是负责处理每个Socket通信的线程类。

MyServer.java:创建ServerSocket监听的主类

public class MyServer
{
    // 定义保存所有Socket的ArrayList
    public static ArrayList<Socket> socketList = new ArrayList<Socket>();
    public static void main(String[] args) throws IOException
    {
        ServerSocket ss = new ServerSocket(30000);
        while(true)
        {
            // 此行代码会阻塞,将一直等待别人的连接
            Socket s = ss.accept();
            socketList.add(s);
            // 每当客户端连接后启动一条ServerThread线程为该客户端服务
            new Thread(new ServerThread(s)).start();
        }
    }
}

ServerThread.java:负责处理每个线程通信的线程类

// 负责处理每个线程通信的线程类
public class ServerThread implements Runnable
{
    // 定义当前线程所处理的Socket
    Socket s = null;
    // 该线程所处理的Socket所对应的输入流
    BufferedReader br = null;
    public ServerThread(Socket s) throws IOException
    {
        this.s = s;
        // 初始化该Socket对应的输入流
        br = new BufferedReader(new InputStreamReader(s.getInputStream() , "utf-8"));   //②
    }
    public void run()
    {
        try
        {
            String content = null;
            // 采用循环不断从Socket中读取客户端发送过来的数据
            while ((content = readFromClient()) != null)
            {
                // 遍历socketList中的每个Socket,
                // 将读到的内容向每个Socket发送一次
                for (Socket s : MyServer.socketList)
                {
                    OutputStream os = s.getOutputStream();
                    os.write((content + "\n").getBytes("utf-8"));
                }
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    // 定义读取客户端数据的方法
    private String readFromClient()
    {
        try
        {
            return br.readLine();
        }
        // 如果捕捉到异常,表明该Socket对应的客户端已经关闭
        catch (IOException e)
        {
            // 删除该Socket。
            MyServer.socketList.remove(s);    //①
        }
        return null;
    }
}

下面是客户端的代码,每个客户端同样包含两条线程:一条负责生成主界面,并响应用户动作,并将用户输入的数据写入Socket对应的输出流中;另一条负责读取Socket对应输入流中的数据,并负责将这些数据在界面上显示出来

MultiThreadClient.java:负责界面显示和绑定事件监听器

public class MultiThreadClient extends Activity
{
    // 定义界面上的两个文本框
    EditText input;
    TextView show;
    // 定义界面上的一个按钮
    Button send;
    Handler handler;
    // 定义与服务器通信的子线程
    ClientThread clientThread;
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        input = (EditText) findViewById(R.id.input);
        send = (Button) findViewById(R.id.send);
        show = (TextView) findViewById(R.id.show);
        handler = new Handler() //①
        {
            @Override
            public void handleMessage(Message msg)
            {
                // 如果消息来自于子线程
                if (msg.what == 0x123)
                {
                    // 将读取的内容追加显示在文本框中
                    show.append("\n" + msg.obj.toString());
                }
            }
        };
        clientThread = new ClientThread(handler);
        // 客户端启动ClientThread线程创建网络连接、读取来自服务器的数据
        new Thread(clientThread).start(); //①
        send.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                try
                {
                    // 当用户按下发送按钮后,将用户输入的数据封装成Message,
                    // 然后发送给子线程的Handler
                    Message msg = new Message();
                    msg.what = 0x345;
                    msg.obj = input.getText().toString();
                    clientThread.revHandler.sendMessage(msg);
                    // 清空input文本框
                    input.setText("");
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
        });
    }
}

ClientThread.java:负责建立于服务器端的连接和通信

public class ClientThread implements Runnable
{
    private Socket s;
    // 定义向UI线程发送消息的Handler对象
    private Handler handler;
    // 定义接收UI线程的消息的Handler对象
    public Handler revHandler;
    // 该线程所处理的Socket所对应的输入流
    BufferedReader br = null;
    OutputStream os = null;

    public ClientThread(Handler handler)
    {
        this.handler = handler;
    }

    public void run()
    {
        try
        {
            s = new Socket("192.168.1.88", 30000);
            br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            os = s.getOutputStream();
            // 启动一条子线程来读取服务器响应的数据
            new Thread()
            {
                @Override
                public void run()
                {
                    String content = null;
                    // 不断读取Socket输入流中的内容。
                    try
                    {
                        while ((content = br.readLine()) != null)
                        {
                            // 每当读到来自服务器的数据之后,发送消息通知程序界面显示该数据
                            Message msg = new Message();
                            msg.what = 0x123;
                            msg.obj = content;
                            handler.sendMessage(msg);
                        }
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                    }
                }
            }.start();
            // 为当前线程初始化Looper
            Looper.prepare();
            // 创建revHandler对象
            revHandler = new Handler()
            {
                @Override
                public void handleMessage(Message msg)
                {
                    // 接收到UI线程中用户输入的数据
                    if (msg.what == 0x345)
                    {
                        // 将用户在文本框内输入的内容写入网络
                        try
                        {
                            os.write((msg.obj.toString() + "\r\n")
                                .getBytes("utf-8"));
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
            };
            // 启动Looper
            Looper.loop();
        }
        catch (SocketTimeoutException e1)
        {
            System.out.println("网络连接超时!!");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

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"
    >
<LinearLayout 
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
<!-- 定义一个文本框,它用于接受用户的输入 -->
<EditText
    android:id="@+id/input"  
    android:layout_width="240dp" 
    android:layout_height="wrap_content" 
    />
<Button
    android:id="@+id/send"  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:paddingLeft="8px"
    android:text="@string/send"
    />
</LinearLayout>
<!-- 定义一个文本框,它用于显示来自服务器的信息 -->
<TextView
    android:id="@+id/show" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:gravity="top"
    android:background="#ffff"
    android:textSize="14dp"
    android:textColor="#f000"
    />
</LinearLayout>

13.2 使用URL访问网络资源

URL(Uniform Resource Locator)对象代表统一资源定位器,它是指向互联网“资源”的指针。通常情况下,URL可以由协议名、主机、端口和资源组成。即可满足如下格式:

protocol://host:port/resourceName

例如如下URL地址:http://www.crazyit.org/index.php

URL类提供了多个构造器用于创建URL对象,一旦获得了URL对象之后,可以调用如下常用方法来访问该URL对应的资源:

  • String getFile():获取该URL的资源名
  • String getHost():主机名
  • String getPath():路径
  • int getPort():端口号
  • String getProtocol(): 获取URL的协议名称
  • String getQuery():获取该URL的查询字符串部分
  • URLConnection OpenConnection():返回一个URLConnection对象,它表示到URL所引用的远程对象的连接。
  • InputStream openStream():打开与此URL的连接,并返回一个用于读取该URL资源的InputStream。

13.2.1 使用URL读取网络数据

通过URL对象提供的openStream()可以读取该URL资源的InputStream,因此通过该方法可以非常方便地读取远程资源。

public class URLTest extends Activity
{
    ImageView show;
    // 代表从网络下载得到的图片
    Bitmap bitmap;
    Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            if(msg.what == 0x123)
            {
                // 使用ImageView显示该图片
                show.setImageBitmap(bitmap);
            }
        }
    };
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        show = (ImageView) findViewById(R.id.show);
        new Thread()
        {
            public void run()
            {
                try
                {
                    // 定义一个URL对象
                    URL url = new URL("http://www.crazyit.org/"
                        + "attachments/month_1008/20100812_7763e970f"
                        + "822325bfb019ELQVym8tW3A.png");
                    // 打开该URL对应的资源的输入流
                    InputStream is = url.openStream();
                    // 从InputStream中解析出图片
                    bitmap = BitmapFactory.decodeStream(is);
                    // 发送消息、通知UI组件显示该图片
                    handler.sendEmptyMessage(0x123);
                    is.close();
                    // 再次打开URL对应的资源的输入流
                    is = url.openStream();
                    // 打开手机文件对应的输出流
                    OutputStream os = openFileOutput("crazyit.png"
                        , MODE_WORLD_READABLE);
                    byte[] buff = new byte[1024];
                    int hasRead = 0;
                    // 将URL对应的资源下载到本地
                    while((hasRead = is.read(buff)) > 0)
                    {
                        os.write(buff, 0 , hasRead);
                    }
                    is.close();
                    os.close();
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

上面程序读取网络资源中图片,然后下载到本地,同样需要增加授权代码

    <!-- 授权访问网络 -->
    <uses-permission android:name="android.permission.INTERNET"/>

13.2.2 使用URLConnect提交请求

URL的openConnection()方法将返回一个URLConnection对象,该对象表示应用程序和URL之间的通信连接,程序可以通过URLConnection实例向该URL发送请求,读取URL引用的资源

通常创建一个和URL的连接,并发送请求、读取此URL引用的资源需要如下几个步骤:

  1. 通过调用URL对象openConnection()方法来创建URLConnection对象。
  2. 设置URLConnection的参数和普通请求属性。
  3. 如果只是发送GET方式请求,使用connect方法建立和远程资源之间的实际连接即可;如果需要发送POST方式的请求,需要获取URLConnection实例对应的输出流来发送请求参数。
  4. 远程资源变为可用,程序可以访问远程资源的头字段,或通过输入流读取远程资源的数据。

在建立和远程资源的实际连接请求之前,程序可以通过如下方法来设置请求头字段:

  1. setAllowUserInteraction
  2. setDoInput
  3. setDoOutput
  4. setIfMoifiedSince
  5. setUseCaches
  6. setRequestProperty(String key, String value)
  7. addReuestProperty(String key, String value)

当远程资源可用之后,程序可以使用以下方法用于访问头字段和内容:

  1. Object getContext():获取该URLConnection的内容。
  2. String getHeaderField(String name):获取指定响应头字段的值。因为某些头字段由于经常需要访问,所有Java提供了以下方法来访问特定响应头字段的值:
    • getContentEncoding
    • getContentLength()
    • getContentType
    • getDate()
    • getExpiration
    • getLastModified()
  3. getInputStream():返回该URLConnection对应的输入流,用于获取URLConnection响应的内容。
  4. getOutputStream():返回该URLConnection对应的输出流,用于向URLConnection发送请求参数

下面的程序示范了如何向Web站点发送get请求和post请求,并从web站点取得响应

GetPostUtil.java 发送Get和POST请求的工具类

public class GetPostUtil
{
    /**
     * 向指定URL发送GET方法的请求
     * @param url 发送请求的URL
     * @param params 请求参数,请求参数应该是name1=value1&name2=value2的形式。
     * @return URL所代表远程资源的响应
     */
    public static String sendGet(String url, String params)
    {
        String result = "";
        BufferedReader in = null;
        try
        {
            String urlName = url + "?" + params;
            URL realUrl = new URL(urlName);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent",
                "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
            // 建立实际的连接
            conn.connect();  //①
            // 获取所有响应头字段
            Map<String, List<String>> map = conn.getHeaderFields();
            // 遍历所有的响应头字段
            for (String key : map.keySet())
            {
                System.out.println(key + "--->" + map.get(key));
            }
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(
                new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null)
            {
                result += "\n" + line;
            }
        }
        catch (Exception e)
        {
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally
        {
            try
            {
                if (in != null)
                {
                    in.close();
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 向指定URL发送POST方法的请求
     * @param url 发送请求的URL
     * @param params 请求参数,请求参数应该是name1=value1&name2=value2的形式。
     * @return URL所代表远程资源的响应
     */
    public static String sendPost(String url, String params)
    {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try
        {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent",
                "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(params);  //②
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(
                new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null)
            {
                result += "\n" + line;
            }
        }
        catch (Exception e)
        {
            System.out.println("发送POST请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输出流、输入流
        finally
        {
            try
            {
                if (out != null)
                {
                    out.close();
                }
                if (in != null)
                {
                    in.close();
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
        }
        return result;
    }
}

GetPostMain.java:用于发送get和post请求的activity

public class GetPostMain extends Activity
{
    Button get , post;
    TextView show;
    // 代表服务器响应的字符串
    String response;
    Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            if(msg.what == 0x123)
            {
                // 设置show组件显示服务器响应
                show.setText(response);
            }
        }
    };
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        get = (Button) findViewById(R.id.get);
        post = (Button) findViewById(R.id.post);
        show = (TextView)findViewById(R.id.show);
        get.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                new Thread()
                {
                    @Override
                    public void run()
                    {
                        response = GetPostUtil.sendGet(
                            "http://192.168.1.88:8888/abc/a.jsp"
                            , null);
                        // 发送消息通知UI线程更新UI组件
                        handler.sendEmptyMessage(0x123);
                    }
                }.start();
            }
        });
        post.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                new Thread()
                {
                    @Override
                    public void run()
                    {
                        response = GetPostUtil.sendPost(
                            "http://192.168.1.88:8888/abc/login.jsp"
                            , "name=crazyit.org&pass=leegang");
                    }
                }.start();
                // 发送消息通知UI线程更新UI组件
                handler.sendEmptyMessage(0x123);
            }
        });
    }
}

abc的Web应用`

13.3 使用Http访问网络

URLConnection还有一个子类:HttpURLConnection,HttpURLConnection在URLConnection的基础上做了进一步改进,增加了一些用于操作HTTP资源的便捷方法。

13.3.1 使用HTTPURLConnection

HTTPURLConnection继承了URLConnection,它在URLConnection的基础上提供了如下便捷的方法:

  • int getResponseCode():获取服务器的响应代码
  • String getResponseMessage():获取服务器的响应消息
  • String getRequestMethod():获取发送请求的方法
  • void setRequestMethod(String method):设置发送请求的方法

下面通过一个使用的示例来示范使用HttpURLConnection实现多线程下载:

为了实现多线程,程序可以按如下步骤进行:

  1. 创建URL对象
  2. 获取指定URL对象所指向资源的大小(由getContentLength()方法实现),此处用到了HttpURLConnection类
  3. 在本地磁盘上创建一个与网络资源相同大小的空文件
  4. 计算每条线程应该下载网络资源的哪个部分(从哪个字节开始,到哪个字节结束)
  5. 依次创建、启动多条线程来下载网络资源的指定部分

为实现上面5个功能,我们专门写一个工具类,代码如下:

DownUtil.java:多线程下载文件

public class DownUtil
{
    // 定义下载资源的路径
    private String path;
    // 指定所下载的文件的保存位置
    private String targetFile;
    // 定义需要使用多少线程下载资源
    private int threadNum;
    // 定义下载的线程对象
    private DownThread[] threads;
    // 定义下载的文件的总大小
    private int fileSize;

    public DownUtil(String path, String targetFile, int threadNum)
    {
        this.path = path;
        this.threadNum = threadNum;
        // 初始化threads数组
        threads = new DownThread[threadNum];
        this.targetFile = targetFile;
    }

    public void download() throws Exception
    {
        URL url = new URL(path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(5 * 1000);
        conn.setRequestMethod("GET");
        conn.setRequestProperty(
            "Accept",
            "image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
            + "application/x-shockwave-flash, application/xaml+xml, "
            + "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
            + "application/x-ms-application, application/vnd.ms-excel, "
            + "application/vnd.ms-powerpoint, application/msword, */*");
        conn.setRequestProperty("Accept-Language", "zh-CN");
        conn.setRequestProperty("Charset", "UTF-8");
        conn.setRequestProperty("Connection", "Keep-Alive");
        // 得到文件大小
        fileSize = conn.getContentLength();
        conn.disconnect();
        int currentPartSize = fileSize / threadNum + 1;
        RandomAccessFile file = new RandomAccessFile(targetFile, "rw");
        // 设置本地文件的大小
        file.setLength(fileSize);
        file.close();
        for (int i = 0; i < threadNum; i++)
        {
            // 计算每条线程的下载的开始位置
            int startPos = i * currentPartSize;
            // 每个线程使用一个RandomAccessFile进行下载
            RandomAccessFile currentPart = new RandomAccessFile(targetFile,
                "rw");
            // 定位该线程的下载位置
            currentPart.seek(startPos);
            // 创建下载线程
            threads[i] = new DownThread(startPos, currentPartSize,
                currentPart);
            // 启动下载线程
            threads[i].start();
        }
    }

    // 获取下载的完成百分比
    public double getCompleteRate()
    {
        // 统计多条线程已经下载的总大小
        int sumSize = 0;
        for (int i = 0; i < threadNum; i++)
        {
            sumSize += threads[i].length;
        }
        // 返回已经完成的百分比
        return sumSize * 1.0 / fileSize;
    }

    private class DownThread extends Thread
    {
        // 当前线程的下载位置
        private int startPos;
        // 定义当前线程负责下载的文件大小
        private int currentPartSize;
        // 当前线程需要下载的文件块
        private RandomAccessFile currentPart;
        // 定义已经该线程已下载的字节数
        public int length;

        public DownThread(int startPos, int currentPartSize,
            RandomAccessFile currentPart)
        {
            this.startPos = startPos;
            this.currentPartSize = currentPartSize;
            this.currentPart = currentPart;
        }

        @Override
        public void run()
        {
            try
            {
                URL url = new URL(path);
                HttpURLConnection conn = (HttpURLConnection)url
                    .openConnection();
                conn.setConnectTimeout(5 * 1000);
                conn.setRequestMethod("GET");
                conn.setRequestProperty(
                    "Accept",
                    "image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
                    + "application/x-shockwave-flash, application/xaml+xml, "
                    + "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
                    + "application/x-ms-application, application/vnd.ms-excel, "
                    + "application/vnd.ms-powerpoint, application/msword, */*");
                conn.setRequestProperty("Accept-Language", "zh-CN");
                conn.setRequestProperty("Charset", "UTF-8");
                InputStream inStream = conn.getInputStream();
                // 跳过startPos个字节,表明该线程只下载自己负责哪部分文件。
                inStream.skip(this.startPos);
                byte[] buffer = new byte[1024];
                int hasRead = 0;
                // 读取网络数据,并写入本地文件
                while (length < currentPartSize
                    && (hasRead = inStream.read(buffer)) > 0)
                {
                    currentPart.write(buffer, 0, hasRead);
                    // 累计该线程下载的总大小
                    length += hasRead;
                }
                currentPart.close();
                inStream.close();
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}

提供了上面的DownUtil工具类之后,接下来就可以在Activity中调用该DownUtil类来执行下载任务了

MultiThreadDown.java:下载界面

public class MultiThreadDown extends Activity
{
    EditText url;
    EditText target;
    Button downBn;
    ProgressBar bar;
    DownUtil downUtil;
    private int mDownStatus;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        // 获取程序界面中的三个界面控件
        url = (EditText) findViewById(R.id.url);
        target = (EditText) findViewById(R.id.target);
        downBn = (Button) findViewById(R.id.down);
        bar = (ProgressBar) findViewById(R.id.bar);
        // 创建一个Handler对象
        final Handler handler = new Handler()
        {
            @Override
            public void handleMessage(Message msg)
            {
                if (msg.what == 0x123)
                {
                    bar.setProgress(mDownStatus);
                }
            }
        };
        downBn.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                // 初始化DownUtil对象(最后一个参数指定线程数)
                downUtil = new DownUtil(url.getText().toString(),
                    target.getText().toString(), 6);
                new Thread()
                {
                    @Override
                    public void run()
                    {
                        try
                        {
                            // 开始下载
                            downUtil.download();
                        }
                        catch (Exception e)
                        {
                            e.printStackTrace();
                        }
                        // 定义每秒调度获取一次系统的完成进度
                        final Timer timer = new Timer();
                        timer.schedule(new TimerTask()
                        {
                            @Override
                            public void run()
                            {
                                // 获取下载任务的完成比率
                                double completeRate = downUtil.getCompleteRate();
                                mDownStatus = (int) (completeRate * 100);
                                // 发送消息通知界面更新进度条
                                handler.sendEmptyMessage(0x123);
                                // 下载完全后取消任务调度
                                if (mDownStatus >= 100)
                                {
                                    timer.cancel();
                                }
                            }
                        }, 0, 100);
                    }
                }.start();
            }
        });
    }
}

上面的代码需要访问SD卡和网络,因此需要授权

    <!-- 在SD卡中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <!-- 向SD卡写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!-- 授权访问网络 -->
    <uses-permission android:name="android.permission.INTERNET"/>

13.3.2 使用Apache HttpClient

HttpClient是一个增强版的HttpURLConnection,具有处理Session、Cookie等细节问题的功能,但不会缓存服务器的响应,不能执行HTML页面中嵌入的JavaScript代码;也不会对页面内容进行任何解析、处理。

使用HttpClient发送请求、接收响应,只需要以下几个步骤:

  1. 创建HttpClient对象
  2. 创建HttpGet对象(发送Get请求)或者创建HttpPost对象(发送POST请求)
  3. 如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HttpParams params)方法来添加请求参数;对应HttpPost对象而言,也可调用setEntity(HttpEntity entity)方法来设置请求参数
  4. 调用HttpClient对象的execute(HttpUriRequest request)发送请求,执行该方法返回一个HttpResponse
  5. 调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。

下面通过一个实例:访问被保护资源来讲解HttpClient的使用方法,该实例需要向指定页面发送请求,但该页面并不是一个简单的嗯,只有当用户已经登录,而且登录的用户名是crazyit.org时才可以访问该页面,当然也可以使用HttpURLConnection也可以实现,但需要处理的细节太复杂,而使用HttpClient则简单很多,因为HttpClient会自动维护与服务器之间的Session状态。

public class HttpClientTest extends Activity
{
    TextView response;
    HttpClient httpClient;
    Handler handler = new Handler()
    {
        public void handleMessage(Message msg)
        {
            if(msg.what == 0x123)
            {
                // 使用response文本框显示服务器响应
                response.append(msg.obj.toString() + "\n");
            }
        }
    };
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        // 创建DefaultHttpClient对象
        httpClient = new DefaultHttpClient();
        response = (TextView) findViewById(R.id.response);
    }
    public void accessSecret(View v)
    {
        response.setText("");
        new Thread()
        {
            @Override
            public void run()
            {
                // 创建一个HttpGet对象
                HttpGet get = new HttpGet(
                    "http://192.168.1.88:8888/foo/secret.jsp");  //①
                try
                {
                    // 发送GET请求
                    HttpResponse httpResponse = httpClient.execute(get);//②
                    HttpEntity entity = httpResponse.getEntity();
                    if (entity != null)
                    {
                        // 读取服务器响应
                        BufferedReader br = new BufferedReader(
                            new InputStreamReader(entity.getContent()));
                        String line = null;

                        while ((line = br.readLine()) != null)
                        {
                            Message msg = new Message();
                            msg.what = 0x123;
                            msg.obj = line;
                            handler.sendMessage(msg);
                        }
                    }
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
        }.start();
    }
    public void showLogin(View v)
    {
        // 加载登录界面
        final View loginDialog = getLayoutInflater().inflate(
            R.layout.login, null);
        // 使用对话框供用户登录系统
        new AlertDialog.Builder(HttpClientTest.this)
            .setTitle("登录系统")
            .setView(loginDialog)
            .setPositiveButton("登录",
            new DialogInterface.OnClickListener()
            {
                @Override
                public void onClick(DialogInterface dialog,
                    int which)
                {
                    // 获取用户输入的用户名、密码
                    final String name = ((EditText) loginDialog
                        .findViewById(R.id.name)).getText()
                        .toString();
                    final String pass = ((EditText) loginDialog
                        .findViewById(R.id.pass)).getText()
                        .toString();
                    new Thread()
                    {
                        @Override
                        public void run()
                        {
                            try
                            {
                                HttpPost post = new HttpPost("http://192.168"
                                    + ".1.88:8888/foo/login.jsp");//③
                                // 如果传递参数个数比较多的话可以对传递的参数进行封装
                                List<NameValuePair> params = new
                                    ArrayList<NameValuePair>();
                                params.add(new BasicNameValuePair
                                    ("name", name));
                                params.add(new BasicNameValuePair
                                    ("pass", pass));                                
                                // 设置请求参数
                                post.setEntity(new UrlEncodedFormEntity(
                                    params, HTTP.UTF_8));
                                // 发送POST请求
                                HttpResponse response = httpClient
                                    .execute(post);  //④
                                // 如果服务器成功地返回响应
                                if (response.getStatusLine()
                                    .getStatusCode() == 200)
                                {
                                    String msg = EntityUtils
                                        .toString(response.getEntity());
                                    Looper.prepare();
                                    // 提示登录成功
                                    Toast.makeText(HttpClientTest.this,
                                        msg, Toast.LENGTH_SHORT).show();
                                    Looper.loop();
                                }
                            }
                            catch (Exception e)
                            {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                }
            }).setNegativeButton("取消", null).show();
    }
}

foo的web应用`

13.4 使用WebView视图显示网页

WebView组件本身就是一个浏览器实现,它的内核基于开源WebKit引擎

13.4.1 使用WebView浏览网页

WebView的用法于普通的ImageView组件的用法基本类似:

  • void goBack: 后退
  • void goForward():前进
  • void loadUrl(String url):加载指定URL对应的网页
  • boolean zoomIn():放大网页
  • boolean zoomOut():缩小网页
WebView show = (WebView) findViewByID(R.id.show);
show.loadUrl("http://www.baidu.com");

13.4.2 使用WebView加载HTML代码

WebView提供了以下两个方法,用于加载并显示HTML代码:

  1. loadData(String data, String mimeType, String encoding):在实际使用过程中,发现该方法在包含中文的HTML内容时,WebView会显示乱码

  2. loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding, String historyUrl):该方法是loadData(String data, String mimeType, String encoding)方法的增强版,它不会产生乱码,参数说明:

    • data:指定需要加载的HTML代码
    • mimeType:指定HTML代码的MIME类型,对于HTML代码可指定为text/html。
    • encoding:指定HTML代码编码所用的字符集。比如指定为GBK
public class ViewHtml extends Activity
{
    WebView show;
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        // 获取程序中的WebView组件
        show = (WebView) findViewById(R.id.show);
        StringBuilder sb = new StringBuilder();
        // 拼接一段HTML代码
        sb.append("<html>");
        sb.append("<head>");
        sb.append("<title> 欢迎您 </title>");
        sb.append("</head>");
        sb.append("<body>");
        sb.append("<h2> 欢迎您访问<a href=\"http://www.crazyit.org\">"
            + "疯狂Java联盟</a></h2>");
        sb.append("</body>");
        sb.append("</html>");
        // 使用简单的loadData方法会导致乱码,可能是Android API的Bug
        // show.loadData(sb.toString() , "text/html" , "utf-8");
        // 加载、并显示HTML代码
        show.loadDataWithBaseURL(null, sb.toString()
            , "text/html" , "utf-8", null);
    }
}

13.4.3 使用WebView中的JavaScript调用Android方法

为了让WebView中的JavaScript脚本调用Android方法,WebView提供了一个配套的WebSettings工具类,该工具类提供了大量方法来管理WebView的选项设置,其中它的setJavaScriptEnabled(true)即可让WebView中的JavaScript脚本来调用Android方法。另外,为了把Android对象暴露给WebView中的JavaScript代码,WebView提供了addJavascriptInterface(Object object, String name)方法,该方法负责把object对象暴露成JavaScript中的name对象。

在WebView的JavaScript中调用Android方法只要如下三个步骤:

  1. 调用WebView关联的WebSettings的setJavaScriptEnabled(true)启用JavaScript调用功能
  2. 调用WebView的addJavascriptInterface(Object object, String name)方法将Object对象暴露给JavaScript
  3. 在JavaScript脚本中通过刚才暴露的name对象调用Android方法

MyObject.java

public class MyObject
{
    Context mContext;

    MyObject(Context c)
    {
        mContext = c;
    }
    // 该方法将会暴露给JavaScript脚本调用
    public void showToast(String name)
    {
        Toast.makeText(mContext, name + ",您好!"
            , Toast.LENGTH_LONG).show();
    }
    // 该方法将会暴露给JavaScript脚本调用
    public void showList()
    {
        // 显示一个普通的列表对话框
        new AlertDialog.Builder(mContext)
            .setTitle("图书列表")
            .setIcon(R.drawable.ic_launcher)
            .setItems(new String[]{"疯狂Java讲义"
            , "疯狂Android讲义" , "轻量级Java EE企业应用实战"} , null)
            .setPositiveButton("确定", null)
            .create()
            .show();
    }
}

JsCallAndroid.java

public class JsCallAndroid extends Activity 
{
    WebView myWebView;
    @Override
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        myWebView = (WebView) findViewById(R.id.webview);
        // 此处为了简化编程,使用file协议加载本地assets目录下的HTML页面
        // 如果有需要,也可使用http协议加载远程网站的HTML页面。
        myWebView.loadUrl("file:///android_asset/test.html");
        // 获取WebView的设置对象
        WebSettings webSettings = myWebView.getSettings();
        // 开启JavaScript调用
        webSettings.setJavaScriptEnabled(true);
        // 将MyObject对象暴露给JavaScript脚本
        // 这样test.html页面中的JavaScript可以通过myObj来调用MyObject的方法
        myWebView.addJavascriptInterface(new MyObject(this), "myObj");
    }
}

..\assets\text.html

<!DOCTYPE html>
<html>
<head>
    <meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title> Js调用Android </title>
</head>
<body>
<!-- 注意此处的myObj是Android暴露出来的对象 -->
<input type="button" value="打招呼" 
    onclick="myObj.showToast('孙悟空');" />
<input type="button" value="图书列表" 
    onclick="myObj.showList();" />  
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值