1. 程式人生 > >Android程序間通訊 - Socket使用(TCP、UDP)

Android程序間通訊 - Socket使用(TCP、UDP)

在使用Socket實現程序間通訊前,先對網路協議相關知識進行簡單回顧。

網路分層

一般情況會將網路分為5層:

  • 應用層     常見協議:HTTP、FTP、POP3等
  • 傳輸層     常見協議:TCP、UDP
  • 網路層     常見協議:IP
  • 資料鏈路層
  • 物理層

這裡寫圖片描述
這裡寫圖片描述

TCP、UDP

  • TCP:面向連線的、可靠的流協議,提供可靠的通訊傳輸。
    • 所謂流,就是指不間斷的資料結構,你可以把它想象成排水管道中的水流。當應用程式採用TCP傳送訊息時,雖然可以保證傳送的順序,但還是猶如沒有任何間隔的資料流傳送給接收端。
    • 有順序控制、丟包重發機制
  • UDP:面向無連線的,具有不可靠性的資料報協議。(讓廣播和細節控制交給應用的通訊傳輸)
    • 無順序控制、丟包重發機制

TCP用於在傳輸層有必要實現可靠傳輸的情況,由於它是面向連線並具有“順序控制”、重發控制等機制;而UDP則主要用於那些對高速傳輸和實時性有較高要求的通訊或廣播通訊。
因此TCP和UDP應該根據應用的目的按需使用,沒有絕對優缺點。

TCP三次握手與四次揮手

使用TCP協議的連線建立與斷開,正常過程下至少需要傳送7個包才能完成,就是我們常說的三次握手,四次揮手。
這裡寫圖片描述

標誌位Flags、序號

  • 序列號 Sequeuece number(seq): 資料包本身的序列號,初始序列號是隨機的。
  • 確認號 Acknowledgment number(ack): 在接收端,用來通知傳送端資料成功接收
  • 標誌位,標誌位只有為 1 的時候才有效
    • SYN(synchronize):表示在連線建立時用來同步序號。
    • ACK:TCP協議規定,只有ACK=1時有效,也規定連線建立後所有傳送的報文的ACK必須為1.
    • FIN(finish):用來釋放一個連線。當FIN=1時,表明此報文段的傳送方的資料已經發送完畢,並要求釋放連線。
  • -

三次握手

三次握手:是指建立一個TCP連線時需要客戶端和服務端總共傳送3個包確認連線的建立。在Socket程式設計中,這一個過程由客戶端執行connect來觸發。

這裡寫圖片描述

  1. 第一次握手:客戶端向服務端傳送請求報文;即SYN=1ACK=0,seq=x。
  2. 第二次握手:服務端收到客戶端的請求報文,服務端會確認應答,告訴客戶端已經收到請求了;即SYN=1,ACK=1,seq=y,ack=x+1;
  3. 第三次握手:客戶端收到服務端的確認應答後,再次向服務端進行確認應答,建立完整的連線;即ACK=1,seq=x+1,ack=y+1

為什麼要進行三次握手呢,或兩次確認??
這裡寫圖片描述
下面使用Wireshark抓包工具體驗下三次握手的過程
這裡寫圖片描述
紅色框內就是一個TCP建立連線的過程

  1. 53324 —>80:嘿,哥們,我想訪問你的web資源,能不能把你的80埠開啟
  2. 80 —> 53324:可以啊,我已經把80埠打開了,為了保證我們的資料能可靠傳輸,你那邊也需要把53324埠開啟;
  3. 53324 —> 80:沒問題,我已經把53324埠打開了,儘管的傳送資料過來吧。

下面看看在三次握手的標誌位的變化

1、53324 —>80
這裡寫圖片描述
2、80 —> 53324
這裡寫圖片描述
3、53324 —> 80
這裡寫圖片描述

四次揮手

四次揮手:即終止TCP連線,就是斷開一個TCP連線時,需要客戶端和服務端總共傳送4個包以確認連線的斷開。在Socket程式設計中,這一工程由客戶端或服務端任意一方執行close來觸發。

由於 TCP 連線是全雙工的,因此每個方向都必須單獨進行關閉。這一原則是當一方資料傳送完成,傳送一個標誌位為 FIN 的報文來終止這一方向的連線,收到標誌位為 FIN 的報文意味著這一方向上不會再收到資料了。但是在 TCP 連線上仍然能夠傳送資料,直到這一方向也傳送了 FIN 。傳送 FIN 的一方執行主動關閉,另一方則執行被動關閉。

這裡寫圖片描述

  1. 第一次揮手:客戶端傳送一個FIN=1,用來關閉客戶端到伺服器端的資料傳送,客戶端進入FIN_WAIT_1狀態。意思是說”我客戶端沒有資料要發給你了”,但是如果你伺服器端還有資料沒有傳送完成,則不必急著關閉連線,可以繼續傳送資料。
  2. 第二次揮手:伺服器端收到FIN後,先發送ack=u+1,告訴客戶端,你的請求我收到了,但是我還沒準備好,請繼續你等我的訊息。這個時候客戶端就進入FIN_WAIT_2 狀態,繼續等待伺服器端的FIN報文。
  3. 第三次揮手:當伺服器端確定資料已傳送完成,則向客戶端傳送FIN=1報文,告訴客戶端,好了,我這邊資料發完了,準備好關閉連線了。伺服器端進入LAST_ACK狀態。
  4. 第四次揮手:客戶端收到FIN=1報文後,就知道可以關閉連線了,但是他還是不相信網路,怕伺服器端不知道要關閉,所以傳送ack=w+1後進入TIME_WAIT狀態,如果Server端沒有收到ACK則可以重傳。伺服器端收到ACK後,就知道可以斷開連線了。客戶端等待了2MSL後依然沒有收到回覆,則證明伺服器端已正常關閉,那好,我客戶端也可以關閉連線了。最終完成了四次握手。

Socket

socket:

  • 位於傳輸層,是網路上的兩個程式通過一個雙向的通訊連線 實現資料的交換的一種程序通訊方式之一。
  • 成對出現,一對套接字
 Socket socket = new Socket("localhost", 8888);
 localhost:IP地址
 8888:埠號
即
IP地址 -- 埠號成對出現

socket本質是程式設計介面(API),對TCP/IP的封裝,TCP/IP也要提供了網路開發所用的介面;HTTP是轎車,提供了封裝或顯示資料的具體形式;socket是發動機,提供了網路通訊的能力。

典型的應用就是C/S結構:
這裡寫圖片描述
從圖可知,socket的使用是基於TCP或UDP協議。

Socket java簡單實現

我們知道socket是基於TCPUDP協議實現的,下面以TCP協議為例實現,因為TCP更加常用些。

使用步驟

  1. 客戶端
    1. 建立socket物件,指定成對的服務端IP地址和埠號
    2. 通過socket獲取輸出流,寫入資料發給服務端
    3. 通過socket獲取輸入流,接受服務端的傳送的資料
    4. 關閉資源close
  2. 服務端(與服務端類似)
    1. 建立ServerSocket物件,並指定埠號,其埠號必須與客服端一致
    2. 通過ServerSocket物件,獲取客戶端的socket例項(ServerSocket.accept方法
    3. 通過socket獲取輸入流,接受客戶端發來的訊息
    4. 通過socket獲取輸出流,寫入資料向客戶端傳送資料作為迴應
    5. 關閉資源close

具體例項

客戶端Client

public class Client {

    private static final String TAG = "Client";

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                //IO操作不能放在主執行緒執行
                connectServer();
            }
        }).start();

    }

    private static void connectServer() {
        try {
            //1、建立客戶端socket,指定服務端地址和埠
            Socket socket = new Socket("localhost", 8888);
            boolean connected = socket.isConnected(); //檢查客戶端與服務端是否連線成功
            System.out.println(connected?"連線成功":"連線失敗,請重試!");

            //2、獲取輸出流,向伺服器傳送訊息
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream),true);
            writer.write("第一次來到廣州\n");
            writer.flush();

            //3、獲取輸入流,並讀取服務端的響應資訊
            InputStream inputStream = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String info=reader.readLine();
            System.out.println("客戶端收到服務端迴應:"+info);


            //4、關閉資源
            outputStream.close();
            writer.close();
            inputStream.close();
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服務端Server

public class Server {

    private static final String TAG = "Server";

    public static void main(String[] args) {

        try {
            //1、建立ServerSocket物件,指定與客戶端一樣的埠號
            ServerSocket serverSocket = new ServerSocket(8888);
            //2、獲取Socket例項
            final Socket socket = serverSocket.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //3、獲取輸入流,接受客戶端發來的訊息
                        InputStream inputStream = socket.getInputStream();
                        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                        BufferedReader reader = new BufferedReader(inputStreamReader);
                        String info=reader.readLine();
                        System.out.println("服務端收到客戶端的資訊: " +info);

                        //4、獲取輸出流,向客戶端傳送訊息迴應
                        OutputStream outputStream = socket.getOutputStream();
                        PrintWriter writer = new PrintWriter(outputStream);
                        writer.write("羊城歡迎你!"+"\n");
                        writer.flush();

                        //4、關閉IO資源
                        inputStream.close();
                        reader.close();
                        outputStream.close();
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

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


}

注意:
使用write方法寫入資料,字串必須在中間或者末尾新增轉義符\r或者\n,否則在使用readLine方法讀取資料時,會一直阻塞從而讀取失敗。如下:
writer.write("第一次來到廣州\n");

執行結果

首先執行服務端,再接著執行客戶端
服務端Server:
這裡寫圖片描述
客戶端Client
這裡寫圖片描述

Socket Android使用

既然scoket能夠實現兩個程式間的資訊傳輸,很明顯在Android下是一種IPC方式。
下面以一個簡單跨程序聊天程式為例,功能點能夠自動回覆。

實現流程

  1. 建立一個遠端Service服務,在其建立TCP服務(服務端
  2. 在介面上(Activity、Fragment等)連線TCP服務(客戶端
  3. 在Mainfest中宣告網路許可權及註冊Service

具體實現

服務端:

public class TCPServerService extends IntentService {

    private static final String[] defaultMessages = {
            "你好啊,嘻嘻",
            "看了你相片,你好帥哦,很喜歡你這樣的",
            "我是江西人,你呢?",
            "你在哪裡工作?"};

    private int index = 0;

    private boolean isServiceDestroy = false;
     //需注意,必須傳入引數
    public TCPServerService() {
        super("TCP");
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        try {
            //1、監聽本地埠號
            ServerSocket serverSocket = new ServerSocket(8954);
            Socket socket = serverSocket.accept();


            //2、獲取輸入流,接受使用者發來的訊息(Activity)
            InputStream inputStream = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

            //3、獲取輸出流,向客戶端(Activity)回覆訊息
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));

            //4、通過迴圈不斷讀取客戶端發來的訊息 ,併發送
            while (!isServiceDestroy) {
                String readLine = reader.readLine();

                if (!TextUtils.isEmpty(readLine)) {
                    String sendMag = index < defaultMessages.length ? defaultMessages[index] : "已離線";
                    SystemClock.sleep(500); //延遲傳送
                    writer.println(sendMag+"\r"); // `\r或\n`必須要有,否則會影響客戶端接受訊息
                    writer.flush();  //重新整理流
                    index++;
                }
            }


            //關閉流
            inputStream.close();
            reader.close();
            outputStream.close();
            writer.close();
            socket.close();
            //需關閉,否則再次連線時,會報埠號已被使用
            serverSocket.close();

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

    @Override
    public void onDestroy() {
        super.onDestroy();
        isServiceDestroy = true;
        Log.d(TAG, "onDestroy: ");
    }

    private static final String TAG = "TCPServerService";
}

客戶端:

public class SocketActivity extends AppCompatActivity {

    private TextView mTvChatContent;
    private EditText mEtSendContent;
    private Intent mIntent;

    private static final int CONNECT_SERVER_SUCCESS = 0; //與服務端連線成功
    private static final int MESSAGE_RECEIVE_SUCCESS = 1; //接受到服務端的訊息
    private static final int MESSAGE_SEND_SUCCESS=2; //訊息傳送
    @SuppressLint("all")
    private Handler mHandler = new Handler(new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case CONNECT_SERVER_SUCCESS:
                    //與服務端連線成功
                    mTvChatContent.setText("與聊天室連線成功\n");
                    break;
                case MESSAGE_RECEIVE_SUCCESS:
                    String msgContent = mTvChatContent.getText().toString();
                    mTvChatContent.setText(msgContent+msg.obj.toString()+"\n");
                    break;
                case MESSAGE_SEND_SUCCESS:
                    mEtSendContent.setText("");
                    mTvChatContent.setText(mTvChatContent.getText().toString()+msg.obj.toString()+"\n");
                    break;
            }
            return false;
        }
    });
    private PrintWriter mPrintWriter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_socket);
        mTvChatContent = findViewById(R.id.tv_chat_content);
        mEtSendContent = findViewById(R.id.et_send_content);

        //啟動服務
        mIntent = new Intent(this, TCPServerService.class);
        startService(mIntent);

        new Thread(new Runnable() {
            @Override
            public void run() {
                //連線服務端,實現通訊互動
                //IO操作必須放在子執行緒執行
                connectTCPServer();
            }
        }).start();

    }

    private  Socket mSocket=null;
    private void connectTCPServer() {
        //通過迴圈來判斷Socket是否有被建立,若沒有則會每隔1s嘗試建立,目的是保證客戶端與服務端能夠連線
        while (mSocket == null) {
            try {
                //建立Socket物件,指定IP地址和埠號
                mSocket = new Socket("localhost", 8954);
                mPrintWriter = new PrintWriter(new OutputStreamWriter(mSocket.getOutputStream()),true);
                if (mSocket.isConnected()) //判斷是否連線成功
                    mHandler.sendEmptyMessage(CONNECT_SERVER_SUCCESS);
            } catch (IOException e) {
                e.printStackTrace();
                //設計休眠機制,每次重試的間隔時間為1s
                SystemClock.sleep(1000);
            }

        }

        //通過迴圈來,不斷的接受服務端發來的訊息
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
            while (!SocketActivity.this.isFinishing()){ //當Activity銷燬後將不接受
                String msg = reader.readLine();
                if (!TextUtils.isEmpty(msg)){
                    //發訊息通知更新UI
                    mHandler.obtainMessage(MESSAGE_RECEIVE_SUCCESS,msg).sendToTarget();
                }

            }
            //關閉流
            mPrintWriter.close();
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }



    }


    @SuppressLint("SetTextI18n")
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.but_send:
                //必須開啟子執行緒,不能在UI執行緒操作網路
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String msg = mEtSendContent.getText().toString();
                        if (mPrintWriter!=null && !TextUtils.isEmpty(msg)){
                            mPrintWriter.println(msg+"\n");
                            //此處可以不用重新整理流的方法,因為在建立mPrintWriter物件時,在其構造方法中設定了自動重新整理快取
//                            mPrintWriter.flush(); 
                            //通知更新UI
                            mHandler.obtainMessage(MESSAGE_SEND_SUCCESS,msg).sendToTarget();
                        }
                    }
                }).start();


                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //關閉輸入流和連線
        if (mSocket!=null){
            try {
                mSocket.shutdownInput();
                mSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //停止後臺服務
        stopService(mIntent);
    }

    private static final String TAG = "TCPServerService";
}

Mainfest:

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

<service android:name=".socket.TCPServerService"
            android:exported="true"
            android:process=":socket"/>

執行結果:
這裡寫圖片描述

注意事項:
1、在使用IntentService時,必須在重寫其構造方法並指定執行緒名,否則報錯has no zero argument constructor如下:

  public TCPServerService( ) {
        super("TCP");
    }

2、連線服務端和傳送訊息必須放在子執行緒中執行,否則會報NetworkOnMainThreadException
3、在writer.println()writer.write()方法中傳入的字串必須要有\r\n轉義符,同時需重新整理流flush(),否則會影響訊息及時性
4、服務端在停止之前必須關閉ServerSocket,呼叫close()即可,否則再次連線時,會報埠號已被使用錯誤,java.net.BindException: bind failed: EADDRINUSE (Address already in use)
5、客戶端Socket與服務端ServerSocket的埠號port必須一致
6、在Mainfest中必須宣告網路許可權,否則連線失敗,提示沒有許可權socket failed: EACCES (Permission denied)

以上幾點是在開發中容易入坑的地方。

參考