1. 程式人生 > >Android網路程式設計之--Socket

Android網路程式設計之--Socket

引言

這篇文章你能學到什麼?

  • 瞭解網路通訊的基本原理
  • 學會最基礎的Socket通訊原理(萬丈高樓平地起)
  • 明白TCP協議與UDP協議的區別與適用場景

網路程式設計基礎

TCP/IP協議

我們先看看從巨集觀上來看兩臺機器是如何通訊的。
我們通過QQ和伺服器進行通訊,都需要哪些東西呢?
兩臺主機進行通訊,需要知道雙方電腦的的地址(也就是IP地址);知道兩個電腦的地址之後,我們還需要知道我傳送到目的電腦的目的軟體(使用埠標記)。這樣兩臺電腦連線成功之後就可以進行通訊了。
那麼這些東西例如:目的地如何規定,傳送的資料如何包裝,放到哪裡?這中間就需要有各種協議。大家都使用這個協議,統一成一個規範,這樣符合這個規範的各種裝置之間能夠進行相容性的通訊。
最為廣泛的的協議就是OSI協議和TCP/IP協議了,但是OSI協議較為繁瑣,未推廣(想了解的自己Google)。反而TCP/IP(transfer control protocol/internet protocol,傳輸控制協議/網際協議)協議簡單明瞭,得到現今的廣泛使用。
TCP/IP準確的說是一組協議,是很多協議的集合,是一組協議集合的簡稱。來看看:

名稱 協議 功能
應用層 HTTP、Telnet、FTP、TFTP 提供應用程式網路介面
傳輸層 TCP、UDP 建立端到端的連線
網路層 IP 定址和路由
資料鏈路層 Ethernet、802.3、PPP 物理介質訪問
物理層 介面和電纜 二進位制資料流傳輸

下面以QQ的資料傳輸為例子:

QQ的資料傳輸

IP地址、埠

在上節中我們知道端到端的連線提到了幾個關鍵的字眼:IP地址、埠;
IP地址用來標記唯一的計算機位置,埠號用來標記一臺電腦中的不同應用程式。
其中IP地址是32為二進位制,例如:192.168.0.0.1等等,這個組合方式是一種協議拼起來的,詳情Google。
埠號範圍是065536,其中0

1023是系統專用,例如:

協議名稱 協議功能 預設埠號
HTTP(HypertextTransfer Protocol)超文字傳輸協議 瀏覽網頁 80
FTP(File TransferProtocol) 檔案傳輸協議 用於網路上傳輸檔案 21
TELNET 遠端終端訪問 23
POP3(Post OfficeProtocol) 郵局協議版本 110

IP地址和埠號組成了我們的Socket,也就是“套接字”,Socket只是一個API。
Socket原理機制:
通訊的兩端都有Socket
網路通訊其實就是Socket間的通訊
資料在兩個Socket間通過IO傳輸

單獨的Socke是沒用任何作用的,基於一定的協議(比如:TCP、UDP協議)下的socket程式設計才能使得資料暢通傳輸,下面我們就開始吧。

基於TCP(傳輸控制協議)協議的Socket程式設計

以下將“基於TCP(傳輸控制協議)協議的Socket程式設計”簡稱為TCP程式設計

既然基於TCP,那麼就有著它的一套程式碼邏輯體系。我們只需要在Socket API的幫助下,使用TCP協議,就可以進行一個完整的TCP程式設計了。

主要API:
Socket,客戶端相關

  • 構造方法
    public Socket(String host, int port) throws UnknownHostException, IOException
    釋義:建立一個流套接字並將其連線到指定主機上的指定埠號(就是用來連線到host主機的port埠的)
  • 方法

|方法名稱 | 方法功能|
| ————- :|————-:|
|getInputStream()) | 拿到此套接字的輸入流,收到的資料就在這裡 |
|getOutputStream()| 返回此套接字的輸出流。 要傳送的資料放到這裡|

ServerSocket,伺服器相關

  • 構造方法
    ServerSocket(int port)
    釋義:建立服務端的監聽port埠的套接字
  • 方法
    Socket accept() throws IOException偵聽並接受到此套接字的連線。此方法在連線傳入之前一直阻塞。服務端通過這個方法拿到與客戶端建立端到端的連線的socket。

總體流程圖示:

Socket通訊流程
  • TCP程式設計的服務端流程:
    1.建立ServerSocket類物件-serverSocket
    2.使用serverSocket開始一直阻塞監聽,等待客戶端傳送來資料並且得到socket
    3.根據socket的輸入流讀取客戶端的資料,根據socket的輸出流返回給客戶端資料
    4.關閉socket以及IO流
  • TCP程式設計的客戶端物件
    1.建立客戶端的socket物件
    2.使用客戶端的socket物件的輸出流傳送給伺服器資料,使用客戶端的socket物件的輸入流得到服務端的資料

TCP程式設計

下面我們使用上面的TCP程式設計的流程來實現:手機發送資訊到伺服器,伺服器返回給我們資料。

服務端的話,這裡使用eclipse。使用Eclipse新建一個Server.java來處理伺服器端的邏輯。客戶端的話使用AS來新建一個Client.java檔案。然後執行伺服器,在執行手機上的程式,從手機上傳送一段內容到伺服器端接收。大概就是這裡流程。

手機發送資訊到伺服器,伺服器返回給我們資料

伺服器端:

伺服器端新建TcpSocketDemo工程

Code:


package com.hui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) {

        try {
            // 為了看流程,我就把所有的程式碼都放在main函式裡了,也沒有捕捉異常,直接丟擲去了。實際開發中不可取。
            // 1.新建ServerSocket物件,建立指定埠的連線
            ServerSocket serverSocket = new ServerSocket(12306);
            System.out.println("服務端監聽開始了~~~~");
            // 2.進行監聽
            Socket socket = serverSocket.accept();// 開始監聽9999埠,並接收到此套接字的連線。
            // 3.拿到輸入流(客戶端傳送的資訊就在這裡)
            InputStream is = socket.getInputStream();
            // 4.解析資料
            InputStreamReader reader = new InputStreamReader(is);
            BufferedReader bufReader = new BufferedReader(reader);
            String s = null;
            StringBuffer sb = new StringBuffer();
            while ((s = bufReader.readLine()) != null) {
                sb.append(s);
            }
            System.out.println("伺服器:" + sb.toString());
            // 關閉輸入流
            socket.shutdownInput();

            OutputStream os = socket.getOutputStream();
            os.write(("我是服務端,客戶端發給我的資料就是:"+sb.toString()).getBytes());
            os.flush();
            // 關閉輸出流
            socket.shutdownOutput();
            os.close();

            // 關閉IO資源
            bufReader.close();
            reader.close();
            is.close();

            socket.close();// 關閉socket
            serverSocket.close();// 關閉ServerSocket

        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


注意:
在使用TCP程式設計的時候,最後需要釋放資源,關閉socket(socket.close());關閉socket輸入輸出流(socket.shutdownInput()以及socket.shutdownOutput());關閉IO流(is.close() os.close())。需要注意的是:關閉socket的輸入輸出流需要放在關閉io流之前。因為, <u>**關閉IO流會同時關閉socket,一旦關閉了socket的,就不能再進行socket的相關操作了。而,只關閉socket輸入輸出流(socket.shutdownInput()以及socket.shutdownOutput())不會完全關閉socket,此時任然可以進行socket方面的操作。 **</u>所以要先呼叫socket.shutdownXXX,然後再呼叫io.close();

客戶端:

頁面檔案沒什麼好看的。然後就是點選button的時候傳送資料,收到資料展示出來。我們這裡主要看點選按鈕時做的事情。

public void onClick(View view){
        new Thread(){
            @Override
            public void run() {
                super.run();
                try {
                    //1.建立監聽指定伺服器地址以及指定伺服器監聽的埠號
                    Socket socket = new Socket("111.111.11.11", 12306);//111.111.11.11為我這個本機的IP地址,埠號為12306.
                    //2.拿到客戶端的socket物件的輸出流傳送給伺服器資料
                    OutputStream os = socket.getOutputStream();
                    //寫入要傳送給伺服器的資料
                    os.write(et.getText().toString().getBytes());
                    os.flush();
                    socket.shutdownOutput();
                    //拿到socket的輸入流,這裡儲存的是伺服器返回的資料
                    InputStream is = socket.getInputStream();
                    //解析伺服器返回的資料
                    InputStreamReader reader = new InputStreamReader(is);
                    BufferedReader bufReader = new BufferedReader(reader);
                    String s = null;
                    final StringBuffer sb = new StringBuffer();
                    while((s = bufReader.readLine()) != null){
                        sb.append(s);
                    }
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tv.setText(sb.toString());
                        }
                    });
                    //3、關閉IO資源(注:實際開發中需要放到finally中)
                    bufReader.close();
                    reader.close();
                    is.close();
                    os.close();
                    socket.close();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();

    }

注意!
實際開發中的關閉IO資源需要放到finally中。這裡主要是為了先理解TCP程式設計的socket通訊。還有,上面講過的io.close()需要放到socket.showdownXX()後面。
關於new Socket("111.111.11.11", 12306),如何檢視本機地址,自己百度哦~~~

整體執行結果如下:

TCP的單執行緒程式設計

在上圖中,我們手機端傳送完一個請求後,服務端(Server)拿到資料,解析資料,返回給客戶端資料,關閉所有資源,也就是伺服器關閉了。這時,如果另一個客戶端再想跟伺服器進行通訊時,發現伺服器已經關閉了,無法與伺服器再次進行通訊。換句話說,只能跟伺服器通訊一次,服務端 只能支援單執行緒資料處理。也就是說,上面的伺服器的程式碼無法實現多執行緒程式設計,只能進行一次通訊。
那麼如果我們想實現server的多執行緒資料處理,使得server處理完我這個請求後不會關閉,任然可以處理其他客戶端的請求,怎麼辦呢?

TCP的多執行緒程式設計

思路:
在上面例子中,我們執行serversocket.accept()等待客戶端去連線,與客戶建立完連線後,拿到對應的socket,然後進行相應的處理。那麼多個客戶端的請求,我們就一直不關閉ServerSocket,一直等待客戶端連線,一旦建立連線拿到socket,就可以吧這個socket放到單獨的執行緒中,從而實現這個建立連線的端到端通訊的socket在自己單獨的執行緒中處理。這樣就能實現Socket的多執行緒處理。

  • step1:
    建立ServerThread,單獨處理拿到的socket,使得客戶端到伺服器端的這個socket會話在一個單獨的執行緒中。
package com.hui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class ServerThread extends Thread{

    private Socket socket;

    //在構造中得到要單獨會話的socket
    public ServerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        super.run();
        InputStreamReader reader = null;
        BufferedReader bufReader = null;
        OutputStream os = null; 
        try {
            reader = new InputStreamReader(socket.getInputStream());
            bufReader = new BufferedReader(reader);
            String s = null;
            StringBuffer sb = new StringBuffer();
            while((s = bufReader.readLine()) != null){
                sb.append(s);
            }
            System.out.println("伺服器:"+sb.toString());
            //關閉輸入流
            socket.shutdownInput();

            //返回給客戶端資料
            os = socket.getOutputStream();
            os.write(("我是服務端,客戶端發給我的資料就是:"+sb.toString()).getBytes());
            os.flush();
            socket.shutdownOutput();
        } catch (IOException e2) {
            e2.printStackTrace();
        } finally{//關閉IO資源
            if(reader != null){
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if(bufReader != null){
                try {
                    bufReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


    }

}
  • step2:
    建立MultiThreadServer
package com.hui;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiThreadServer {

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(12306);
            //死迴圈
            while(true){
                System.out.println("MultiThreadServer~~~監聽~~~");
                //accept方法會阻塞,直到有客戶端與之建立連線
                Socket socket = serverSocket.accept();
                ServerThread serverThread = new ServerThread(socket);
                serverThread.start();
            }


        } catch (IOException e) {
            e.printStackTrace();
        } catch(Exception e){
            e.printStackTrace();
        }
    }

}

下面我使用兩個手機,多次進行與伺服器的連線,演示如下:
總體結果:

TCP 的多執行緒通訊 單獨看兩個手機

重要的事情說三遍!萬丈高樓平地起!萬丈高樓平地起!!萬丈高樓平地起!!!只有當我們明白了最底層的,知識才是最牢固的。上面的講解的是基於TCP協議的socket程式設計。而我們後來將要講的HTTP相關的大都是基於TCP/IP協議的。一個TCP/IP協議我們又不能直接使用,Socket可以說是TCP/IP協議的抽象與包裝,然後我們就可以做相對於TCP/IP的網路通訊與資訊傳輸了。

UDP程式設計

上面我們講解了基於TCP協議的Socket程式設計,現在開始我們就開始講解基於UDP協議的Socket程式設計了。
UDP,是User Datagram Protocol,也就是使用者資料包協議。關鍵點在於“資料包”。主要就是把資料進行打包然後丟給目標,而不管目標是否接收到資料。主要的流程就是:<u>傳送者打包資料(DatagramPacket)然後通過DatagramSocket傳送,接收者收到資料包解開資料。</u>

主要API:
DatagramPacket,用來包裝傳送的資料
構造方法

  • 傳送資料的構造
    DatagramPacket(byte[] buf, int length,SocketAddress address)
    DatagramPacket(byte[] buf, int length, InetAddress address, int port)
    用來將長度為 length 的包傳送到指定主機上的指定埠號。length 引數必須小於等於 buf.length。

  • 接收資料的構造:
    public DatagramPacket(byte[] buf, int length)
    用來接收長度為 length 的資料包。

DatagramSocket:

構造方法
DatagramSocket()
構造資料報套接字並將其繫結到 <u>本地主機上任何可用的埠 </u>。套接字將被繫結到萬用字元地址,IP 地址由核心來選擇。

DatagramSocket(int port)
建立資料報套接字並將其繫結到<u>本地主機上的指定埠</u>。套接字將被繫結到萬用字元地址,IP 地址由核心來選擇。

傳送資料
send(DatagramPacket p)
從此套接字傳送資料報包。DatagramPacket 包含的資訊指示:將要傳送的資料、其長度、遠端主機的 IP 地址和遠端主機的埠號。
接收資料
receive(DatagramPacket p)
從此套接字接收資料報包。當此方法返回時,DatagramPacket 的緩衝區填充了接收的資料。資料報包也包含傳送方的 IP 地址和傳送方機器上的埠號。

下面開始程式碼了

客戶端

主要頁面與上面的tcp一致,只不過是通訊時的方法改了。如下:

private void udp() {
        byte[] bytes = et.getText().toString().getBytes();
        try {
            /*******************傳送資料***********************/
            InetAddress address = InetAddress.getByName("192.168.232.2");
            //1.構造資料包
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, 12306);
            //2.建立資料報套接字並將其繫結到本地主機上的指定埠。
            DatagramSocket socket = new DatagramSocket();
            //3.從此套接字傳送資料報包。
            socket.send(packet);
            /*******************接收資料***********************/
        //1.構造 DatagramPacket,用來接收長度為 length 的資料包。
            final byte[] bytes1 = new byte[1024];
            DatagramPacket receiverPacket = new DatagramPacket(bytes1, bytes1.length);
            socket.receive(receiverPacket);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tv.setText(new String(bytes1, 0, bytes1.length));
                }
            });

//            socket.close();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

服務端

UDPServer

package com.hui;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

public class UDPServer {
    public static void main(String[] args) throws IOException {

        byte[] buf = new byte[1024];
        // 一:接受資料
        // 1.建立接受資料的資料包
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        // 2.建立UPD 的 socket
        DatagramSocket socket = new DatagramSocket(12306);
        // 3.接收資料
        System.out.println("服務端開始監聽!~~~~");
        socket.receive(packet);
        // 4.處理資料
        System.out.println("服務端:" + new String(buf, 0, buf.length));

        // 二:返回資料
        DatagramPacket p = new DatagramPacket(buf, buf.length, packet.getAddress(), packet.getPort());
        socket.send(p);
        socket.close();
    }
}
UDP通訊

TCP與UDP區別與使用場景

至此,基於TCP、UDP協議的Socket通訊已經講完了基礎部分。那麼這兩個協議在實際中有什麼區別,分別適用於什麼場景呢?

TCP

對於TCP的資料傳輸而言,傳輸資料之前需要進行三次握手建立穩定的連線。建立連線通道後,資料包會在這個通道中以位元組流的形式進行資料的傳輸。由於建立穩定連線後才開始傳輸資料,而同時還是以位元組流的形式傳送資料,所以傳送資料速度較慢,但是不會造成資料包丟失。即使資料包丟失了,會進行資料重發。同時,如果收到的資料包順序錯亂,會進行排序糾正。

三次握手??
這個網路上的解釋太多了,想詳細瞭解的自行去百度上Google一下。<u>簡單理解</u>的就是這樣的:我家是農村的,記得小時後爺爺在田裡種地。到了晌午時間,奶奶快燒好飯後我都要去喊爺爺吃飯,因為幹農活的地離家裡不遠不近的,我就跑到隔壁家裡的平頂房上喊爺爺吃飯。我先大喊一聲“爺爺,回家吃飯啦”。爺爺如果聽到我說的話就會給我一個應答“好的!知道了,馬上就回去,你們先吃吧!”我只有聽到了這句話,才知道爺爺這個時候能聽到我說的話,我然後就再次回答爺爺:“好的!那你快點!”這三句話說完,就確定了我能聽到爺爺的應答,爺爺也能聽到我的回覆。這樣我就確定我跟爺爺之間的喊話通道是正常的,如果還想對爺爺說什麼話,直接說就好了。最後,爺爺聽到了我說的話,就不再回復我的話了,然後,拿起鋤頭回來了。

總結下來,就是面向連線、資料可靠,速度慢,有序的
<u>適用於需要安全穩定傳輸資料的場景。例如後面要講解的HTTP、HTTPS網路協議,FTP檔案傳輸協議以及POP、SMTP郵件傳輸協議。或者開發交易類、支付類等軟體時,都需要基於TCP協議的Socket連線進行安全可靠的資料傳輸等等</u>

UDP

對於UDP的資料傳輸而言,UDP不會去建立連線。它不管目的地是否存在,直接將資料傳送給目的地,同時不會過問傳送的資料是否丟失,到達的資料是否順序錯亂。如果你想處理這些問題的話,需要自己在應用層自行處理。
總結下來,不面向連線、資料不可靠、速度快、無序的
<u>適用於需要實時性較高不較為關注資料結果的場景,例如:打電話、視訊會議、廣播電臺,等。</u>