Android網絡編程之Socket

前言

  • Socket的使用在 Android網絡編程中很是重要
  • 今天我將帶你們全面瞭解 Socket 及 其使用方法

目錄

1.網絡基礎

1.1 計算機網絡分層

計算機網絡分爲五層:物理層、數據鏈路層、網絡層、運輸層、應用層java

其中:android

  • 網絡層:負責根據IP找到目的地址的主機
  • 運輸層:經過端口把數據傳到目的主機的目的進程,來實現進程與進程之間的通訊

1.2 端口號(PORT)

端口號規定爲16位,即容許一個IP主機有2的16次方65535個不一樣的端口。其中:git

  • 0~1023:分配給系統的端口號github

    > 咱們不能夠亂用
  • 1024~49151:登記端口號,主要是讓第三方應用使用apache

    > 可是必須在IANA(互聯網數字分配機構)按照規定手續登記,
  • 49152~65535:短暫端口號,是留給客戶進程選擇暫時使用,一個進程使用完就能夠供其餘進程使用。

在Socket使用時,能夠用1024~65535的端口號編程

1.3 C/S結構

  • 定義:即客戶端/服務器結構,是軟件系統體系結構
  • 做用:充分利用兩端硬件環境的優點,將任務合理分配到Client端和Server端來實現,下降了系統的通信開銷。服務器

    > Socket正是使用這種結構創建鏈接的,一個套接字接客戶端,一個套接字接服務器。

如圖:網絡

能夠看出,Socket的使用能夠基於TCP或者UDP協議。session

1.4 TCP協議

  • 定義:Transmission Control Protocol,即傳輸控制協議,是一種傳輸層通訊協議app

    基於TCP的應用層協議有FTP、Telnet、SMTP、HTTP、POP3與DNS。

  • 特色:面向鏈接、面向字節流、全雙工通訊、可靠

    • 面向鏈接:指的是要使用TCP傳輸數據,必須先創建TCP鏈接,傳輸完成後釋放鏈接,就像打電話同樣必須先撥號創建一條鏈接,打完後掛機釋放鏈接。
    • 全雙工通訊:即一旦創建了TCP鏈接,通訊雙方能夠在任什麼時候候都能發送數據。
    • 可靠的:指的是經過TCP鏈接傳送的數據,無差錯,不丟失,不重複,而且按序到達。
    • 面向字節流:流,指的是流入到進程或從進程流出的字符序列。簡單來講,雖然有時候要傳輸的數據流太大,TCP報文長度有限制,不能一次傳輸完,要把它分爲好幾個數據塊,可是因爲可靠性保證,接收方能夠按順序接收數據塊而後從新組成分塊以前的數據流,因此TCP看起來就像直接互相傳輸字節流同樣,面向字節流。
  • TCP創建鏈接
    必須進行三次握手:若A要與B進行鏈接,則必須

    • 第一次握手:創建鏈接。客戶端發送鏈接請求報文段,將SYN位置爲1,Sequence Number爲x;而後,客戶端進入SYN_SEND狀態,等待服務器的確認。即A發送信息給B
    • 第二次握手:服務器收到客戶端的SYN報文段,須要對這個SYN報文段進行確認。即B收到鏈接信息後向A返回確認信息
    • 第三次握手:客戶端收到服務器的(SYN+ACK)報文段,並向服務器發送ACK報文段。即A收到確認信息後再次向B返回確認鏈接信息

      > 此時,A告訴本身上層鏈接創建;B收到鏈接信息後告訴上層鏈接創建。

這樣就完成TCP三次握手 = 一條TCP鏈接創建完成 = 能夠開始發送數據

  1. 三次握手期間任何一次未收到對面回覆都要重發。
  2. 最後一個確認報文段發送完畢之後,客戶端和服務器端都進入ESTABLISHED狀態。

爲何TCP創建鏈接須要三次握手?

答:防止服務器端由於接收了早已失效的鏈接請求報文從而一直等待客戶端請求,從而浪費資源

  • 「已失效的鏈接請求報文段」的產生在這樣一種狀況下:Client發出的第一個鏈接請求報文段並無丟失,而是在某個網絡結點長時間的滯留了,以至延誤到鏈接釋放之後的某個時間纔到達server。
  • 這是一個早已失效的報文段。但Server收到此失效的鏈接請求報文段後,就誤認爲是Client再次發出的一個新的鏈接請求。
  • 因而就向Client發出確認報文段,贊成創建鏈接。
  • 假設不採用「三次握手」:只要Server發出確認,新的鏈接就創建了。
  • 因爲如今Client並無發出創建鏈接的請求,所以不會向Server發送數據。
  • 但Server卻覺得新的運輸鏈接已經創建,並一直等待Client發來數據。>- 這樣,Server的資源就白白浪費掉了。

採用「三次握手」的辦法能夠防止上述現象發生:

  • Client不會向Server的確認發出確認
  • Server因爲收不到確認,就知道Client並無要求創建鏈接
  • 因此Server不會等待Client發送數據,資源就沒有被浪費
  • TCP釋放鏈接
    TCP釋放鏈接須要四次揮手過程,如今假設A主動釋放鏈接:(數據傳輸結束後,通訊的雙方均可釋放鏈接)

    • 第一次揮手:A發送釋放信息到B;(發出去以後,A->B發送數據這條路徑就斷了)
    • 第二次揮手:B收到A的釋放信息以後,回覆確認釋放的信息:我贊成你的釋放鏈接請求
    • 第三次揮手:B發送「請求釋放鏈接「信息給A
    • 第四次揮手:A收到B發送的信息後向B發送確認釋放信息:我贊成你的釋放鏈接請求

      > B收到確認信息後就會正式關閉鏈接;
      > A等待2MSL後依然沒有收到回覆,則證實B端已正常關閉,因而A關閉鏈接

爲何TCP釋放鏈接須要四次揮手?

爲了保證雙方都能通知對方「須要釋放鏈接」,即在釋放鏈接後都沒法接收或發送消息給對方

  • 須要明確的是:TCP是全雙工模式,這意味着是雙向均可以發送、接收的
  • 釋放鏈接的定義是:雙方都沒法接收或發送消息給對方,是雙向的
  • 當主機1發出「釋放鏈接請求」(FIN報文段)時,只是表示主機1已經沒有數據要發送 / 數據已經所有發送完畢;

    > 可是,這個時候主機1仍是能夠接受來自主機2的數據。
  • 當主機2返回「確認釋放鏈接」信息(ACK報文段)時,表示它已經知道主機1沒有數據發送了

    > 但此時主機2仍是能夠發送數據給主機1
  • 當主機2也發送了FIN報文段時,即告訴主機1我也沒有數據要發送了

    > 此時,主機1和2已經沒法進行通訊:主機1沒法發送數據給主機2,主機2也沒法發送數據給主機1,此時,TCP的鏈接纔算釋放
1.5 UDP協議
  • 定義:User Datagram Protocol,即用戶數據報協議,是一種傳輸層通訊協議。

    基於UDP的應用層協議有TFTP、SNMP與DNS。

  • 特色:無鏈接的、不可靠的、面向報文、沒有擁塞控制

    • 無鏈接的:和TCP要創建鏈接不一樣,UDP傳輸數據不須要創建鏈接,就像寫信,在信封寫上收信人名稱、地址就能夠交給郵局發送了,至於能不能送到,就要看郵局的送信能力和送信過程的困難程度了。
    • 不可靠的:由於UDP發出去的數據包發出去就無論了,無論它會不會到達,因此極可能會出現丟包現象,使傳輸的數據出錯。
    • 面向報文:數據報文,就至關於一個數據包,應用層交給UDP多大的數據包,UDP就照樣發送,不會像TCP那樣拆分。
    • 沒有擁塞控制:擁塞,是指到達通訊子網中某一部分的分組數量過多,使得該部分網絡來不及處理,以至引發這部分乃至整個網絡性能降低的現象,嚴重時甚至會致使網絡通訊業務陷入停頓,即出現死鎖現象,就像交通堵塞同樣。TCP創建鏈接後若是發送的數據由於信道質量的緣由不能到達目的地,它會不斷重發,有可能致使愈來愈塞,因此須要一個複雜的原理來控制擁塞。而UDP就沒有這個煩惱,發出去就無論了。
  • 應用場景
    不少的實時應用(如IP電話、實時視頻會議、某些多人同時在線遊戲等)要求源主機以很定的速率發送數據,而且容許在網絡發生擁塞時候丟失一些數據,可是要求不能有太大的延時,UDP就恰好適合這種要求。因此說,只有不適合的技術,沒有真正沒用的技術。
1.6 HTTP協議

詳情請看我寫的另一篇文章你須要瞭解的HTTP知識都在這裏了!

    • *

2. Socket定義

  • 即套接字,是一個對 TCP / IP協議進行封裝 的編程調用接口(API)

    > 1.  即經過`Socket`,咱們才能在Andorid平臺上經過 `TCP/IP`協議進行開發
    > 2.  `Socket`不是一種協議,而是一個編程調用接口(`API`),屬於傳輸層(主要解決數據如何在網絡中傳輸)
  • 成對出現,一對套接字:
Socket ={(IP地址1:PORT端口號),(IP地址2:PORT端口號)}

3. 原理

Socket的使用類型主要有兩種:

  • 流套接字(streamsocket) :基於 TCP協議,採用 流的方式 提供可靠的字節流服務
  • 數據報套接字(datagramsocket):基於 UDP協議,採用 數據報文 提供數據打包發送的服務

具體原理圖以下:

4. Socket 與 Http 對比

  • Socket屬於傳輸層,由於 TCP / IP協議屬於傳輸層,解決的是數據如何在網絡中傳輸的問題
  • HTTP協議 屬於 應用層,解決的是如何包裝數據

因爲兩者不屬於同一層面,因此原本是沒有可比性的。但隨着發展,默認的Http裏封裝了下面幾層的使用,因此纔會出現Socket & HTTP協議的對比:(主要是工做方式的不一樣):

  • Http:採用 請求—響應 方式。

    > 1.  即創建網絡鏈接後,當 客戶端 向 服務器 發送請求後,服務器端才能向客戶端返回數據。
    > 2.  可理解爲:**是客戶端有須要才進行通訊**
  • Socket:採用 服務器主動發送數據 的方式

    > 1.  即創建網絡鏈接後,服務器可主動發送消息給客戶端,而不須要由客戶端向服務器發送請求
    > 2.  可理解爲:**是服務器端有須要才進行通訊**

5. 使用步驟

  • Socket可基於TCP或者UDP協議,但TCP更加經常使用
  • 因此下面的使用步驟 & 實例的Socket將基於TCP協議
// 步驟1:建立客戶端 & 服務器的鏈接

    // 建立Socket對象 & 指定服務端的IP及端口號 
    Socket socket = new Socket("192.168.1.32", 1989);  

    // 判斷客戶端和服務器是否鏈接成功  
    socket.isConnected());

// 步驟2:客戶端 & 服務器 通訊
// 通訊包括:客戶端 接收服務器的數據 & 發送數據 到 服務器

    <-- 操做1:接收服務器的數據 -->

            // 步驟1:建立輸入流對象InputStream
            InputStream is = socket.getInputStream() 

            // 步驟2:建立輸入流讀取器對象 並傳入輸入流對象
            // 該對象做用:獲取服務器返回的數據
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            // 步驟3:經過輸入流讀取器對象 接收服務器發送過來的數據
            br.readLine();

    <-- 操做2:發送數據 到 服務器 -->                  

            // 步驟1:從Socket 得到輸出流對象OutputStream
            // 該對象做用:發送數據
            OutputStream outputStream = socket.getOutputStream(); 

            // 步驟2:寫入須要發送的數據到輸出流對象中
            outputStream.write(("Carson_Ho"+"\n").getBytes("utf-8"));
            // 特別注意:數據的結尾加上換行符纔可以讓服務器端的readline()中止阻塞

            // 步驟3:發送數據到服務端 
            outputStream.flush();  

// 步驟3:斷開客戶端 & 服務器 鏈接

             os.close();
            // 斷開 客戶端發送到服務器 的鏈接,即關閉輸出流對象OutputStream

            br.close();
            // 斷開 服務器發送到客戶端 的鏈接,即關閉輸入流讀取器對象BufferedReader

            socket.close();
            // 最終關閉整個Socket鏈接

6. 具體實例

  • 實例 Demo 代碼包括:客戶端 & 服務器
  • 本文着重講解客戶端,服務器僅採用最簡單的寫法進行展現

6.1 客戶端 實現

步驟1:加入網絡權限

<uses-permission android:name="android.permission.INTERNET" />

步驟2:主佈局界面設置

包括建立Socket鏈接、客戶端 & 服務器通訊的按鈕

<Button
        android:id="@+id/connect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="connect" />

    <Button
        android:id="@+id/disconnect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="disconnect" />

    <TextView
        android:id="@+id/receive_message"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/Receive"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Receive from message" />

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="send"/>

步驟3:建立Socket鏈接、客戶端 & 服務器通訊

具體請看註釋

MainActivity.java

package scut.carson_ho.socket_carson;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    /**
     * 主 變量
     */

    // 主線程Handler
    // 用於將從服務器獲取的消息顯示出來
    private Handler mMainHandler;

    // Socket變量
    private Socket socket;

    // 線程池
    // 爲了方便展現,此處直接採用線程池進行線程管理,而沒有一個個開線程
    private ExecutorService mThreadPool;

    /**
     * 接收服務器消息 變量
     */
    // 輸入流對象
    InputStream is;

    // 輸入流讀取器對象
    InputStreamReader isr ;
    BufferedReader br ;

    // 接收服務器發送過來的消息
    String response;

    /**
     * 發送消息到服務器 變量
     */
    // 輸出流對象
    OutputStream outputStream;

    /**
     * 按鈕 變量
     */

    // 鏈接 斷開鏈接 發送數據到服務器 的按鈕變量
    private Button btnConnect, btnDisconnect, btnSend;

    // 顯示接收服務器消息 按鈕
    private TextView Receive,receive_message;

    // 輸入須要發送的消息 輸入框
    private EditText mEdit;

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

        /**
         * 初始化操做
         */

        // 初始化全部按鈕
        btnConnect = (Button) findViewById(R.id.connect);
        btnDisconnect = (Button) findViewById(R.id.disconnect);
        btnSend = (Button) findViewById(R.id.send);
        mEdit = (EditText) findViewById(R.id.edit);
        receive_message = (TextView) findViewById(R.id.receive_message);
        Receive = (Button) findViewById(R.id.Receive);

        // 初始化線程池
        mThreadPool = Executors.newCachedThreadPool();

        // 實例化主線程,用於更新接收過來的消息
        mMainHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 0:
                        receive_message.setText(response);
                        break;
                }
            }
        };

        /**
         * 建立客戶端 & 服務器的鏈接
         */
        btnConnect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 利用線程池直接開啓一個線程 & 執行該線程
                mThreadPool.execute(new Runnable() {
                    @Override
                    public void run() {

                        try {

                            // 建立Socket對象 & 指定服務端的IP 及 端口號
                            socket = new Socket("192.168.1.172", 8989);

                            // 判斷客戶端和服務器是否鏈接成功
                            System.out.println(socket.isConnected());

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

                    }
                });

            }
        });

        /**
         * 接收 服務器消息
         */
        Receive.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 利用線程池直接開啓一個線程 & 執行該線程
                mThreadPool.execute(new Runnable() {
                    @Override
                    public void run() {

                          try {
                            // 步驟1:建立輸入流對象InputStream
                            is = socket.getInputStream();

                              // 步驟2:建立輸入流讀取器對象 並傳入輸入流對象
                              // 該對象做用:獲取服務器返回的數據
                              isr = new InputStreamReader(is);
                              br = new BufferedReader(isr);

                              // 步驟3:經過輸入流讀取器對象 接收服務器發送過來的數據
                              response = br.readLine();

                              // 步驟4:通知主線程,將接收的消息顯示到界面
                              Message msg = Message.obtain();
                              msg.what = 0;
                              mMainHandler.sendMessage(msg);

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

                    }
                });

            }
        });

        /**
         * 發送消息 給 服務器
         */
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 利用線程池直接開啓一個線程 & 執行該線程
                mThreadPool.execute(new Runnable() {
                    @Override
                    public void run() {

                        try {
                            // 步驟1:從Socket 得到輸出流對象OutputStream
                            // 該對象做用:發送數據
                            outputStream = socket.getOutputStream();

                            // 步驟2:寫入須要發送的數據到輸出流對象中
                            outputStream.write((mEdit.getText().toString()+"\n").getBytes("utf-8"));
                            // 特別注意:數據的結尾加上換行符纔可以讓服務器端的readline()中止阻塞

                            // 步驟3:發送數據到服務端
                            outputStream.flush();

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

                    }
                });

            }
        });

        /**
         * 斷開客戶端 & 服務器的鏈接
         */
        btnDisconnect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                try {
                    // 斷開 客戶端發送到服務器 的鏈接,即關閉輸出流對象OutputStream
                    outputStream.close();

                    // 斷開 服務器發送到客戶端 的鏈接,即關閉輸入流讀取器對象BufferedReader
                    br.close();

                    // 最終關閉整個Socket鏈接
                    socket.close();

                    // 判斷客戶端和服務器是否已經斷開鏈接
                    System.out.println(socket.isConnected());

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

            }
        });

    }
}

6.2 服務器 實現

  • 因本文主要講解客戶端,因此服務器僅僅是爲了配合客戶端展現;
  • 爲了簡化服務器使用,此處採用Mina框架

    > 1.  服務器代碼請在`eclipse`平臺運行
    > 2.  按照個人步驟一步步實現就能夠無腦運行了

步驟1:導入Mina

請直接移步到百度網盤:下載連接(密碼: q73e)

步驟2:建立服務器線程
TestHandler.java

package mina;
// 導入包

public class TestHandler extends IoHandlerAdapter {

    @Override
    public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
        System.out.println("exceptionCaught: " + cause);
    }

    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        System.out.println("recieve : " + (String) message);
        session.write("hello I am server");
    }

    @Override
    public void messageSent(IoSession session, Object message) throws Exception {

    }

    @Override
    public void sessionClosed(IoSession session) throws Exception {
        System.out.println("sessionClosed");
    }

    @Override
    public void sessionOpened(IoSession session) throws Exception {
        System.out.println("sessionOpen");
    }

    @Override
    public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
    }

}

步驟3:建立服務器主代碼
TestHandler.java

package mina;

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

import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class TestServer {
    public static void main(String[] args) {
        NioSocketAcceptor acceptor = null;
        try {
            acceptor = new NioSocketAcceptor();
            acceptor.setHandler(new TestHandler());
            acceptor.getFilterChain().addLast("mFilter", new ProtocolCodecFilter(new TextLineCodecFactory()));
            acceptor.setReuseAddress(true);
            acceptor.bind(new InetSocketAddress(8989));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

至此,客戶端 & 服務器的代碼均實現完畢。

6.3 測試結果

  • 點擊 Connect按鈕: 鏈接成功

  • 輸入發送的消息,點擊 Send 按鈕發送

  • 服務器接收到客戶端發送的消息
  • 點擊 Receive From Message按鈕,客戶端 讀取 服務器返回的消息

  • 點擊 DisConnect按鈕,斷開 客戶端 & 服務器的鏈接

客戶端示意圖

服務器示意圖

6.4 源碼地址

Socket具體實例

相關文章
相關標籤/搜索