Android的IPC機制(七)—— Socket的原理簡析與使用

yangxi_001發表於2017-12-19

綜述

  在前面的幾篇文章中,我們介紹了許多在Android中有關程式間通訊的方式,但都是在一個裝置上進行的程式間通訊,而這時候我們兩個應用在不同的裝置上的時候,在這個時候我們就不能通過前方介紹的那些方法來解決了。但是我們通過網路進行通訊來處理這個問題。今天就來介紹一下Android中網路通訊的其中一種方式——Socket。Socket翻譯為中文為套接字,而現在套接字也成為了作業系統中的一部分。下面我們就來看一下如何使用這個套接字的。

TCP/IP介紹

  這裡有一點需要說明一下,在Internet所使用的各種協議中,最重要的和最著名的就是TCP和IP兩個協議。而我們現在經常提到的TCP/IP並不一定單指TCP和IP這兩個具體的協議,而往往表示Internet所使用的整個TCP/IP協議族。

TCP/IP的體系結構

  TCP/IP協議可以為各式各樣的應用提供服務,同時TCP/IP協議也允許IP協議在各式各樣的網路構成的網際網路上執行。TCP/IP協議是一個四層的體系結構,它包含應用層、傳輸層、網路層、網路介面層。在傳輸層中主要使用的有兩種協議:TCP(Transmission Control Protocol,傳輸控制協議)和UDP(User Datagram Protocol,使用者資料包協議)。下面是TCP/IP體系結構示意圖。 
這裡寫圖片描述

傳輸控制協議TCP

  TCP是TCP/IP體系中非常複雜的一個協議,在這裡我們簡單看一下TCP協議的特點,對於TCP協議的詳細內容,可以檢視一些計算機網路的相關書籍。 
1. TCP是面向連線傳輸協議,也就是說在我們的應用程式在使用TCP協議之前,必須先建立起TCP連線。在傳送資料完畢後,必須釋放已經建立的TCP連線。就像我們打電話一樣,打電話之前首先需要撥號進行建立連線,等通話結束後再結束通話釋放連線。 
2. 每一條TCP連線只能有兩個端點,每一條TCP連線只能是點對點的。 
3. TCP提供了一個可靠交付的服務,也就是說通過TCP連線傳送的資料,無差錯,不丟失,不重複,並且按序到達。 
4. TCP提供全雙工通訊,它允許通訊雙方的應用程式在任何時候都能夠傳送資料。 
5. TCP通訊中是面向位元組流的,其中的“流”指的是流入到程式或從程式流出的位元組序列。

使用者資料包協議UDP

  使用者資料包協議UDP只是在IP的資料服務上增加了很少的一點功能(複用、分用的功能以及差錯檢測的功能)。在這裡簡單說一下UDP的特點。 
1. UDP是無連線,也就是在傳送資料之前是不需要建立連線的,也就減少了開銷和傳送資料之間的延時。 
2. UDP它只能是盡最大努力地交付,也就是不能夠保證可靠交付。 
3. UDP它是面向報文的。傳送方的UDP對應用程式交下來的報文,再新增首部後就向下交付給IP層。 
4. UDP它沒有擁塞控制,也就是說在網路出現擁塞的情況下不會使源主機的傳送速率降低。 
5. UDP支援一對一,一對多,多對一和多對多的互動通訊。

Socket在TCP/IP中的作用

  Socket是工作於TCP/IP協議中應用層和傳輸層之間的一種抽象(不屬於應用層也不屬於傳輸層)。在Android系統中,它可以分為流套接字(streamsocket)和資料包套接字(datagramsocket)。而Socket中的流套接字將TCP協議作為其端對端協議,提供了一個可信賴的位元組流服務;資料包套接字使用UDP協議,提供資料打包傳送服務。 
  在網路程式設計的時候,我們經常把Socket作為應用程式和傳輸層協議之間的介面。在下面圖中表示了這樣一個概念。在圖中我們假定了運輸層使用的是TCP協議(如果使用的是UDP協議,情況也是類似的,只是UDP是無連線的通訊的兩端依然可以用兩個套接字來標誌)。並且現在套接字已經成為作業系統核心的一部分。 
這裡寫圖片描述
  不過有一點我們要注意,在套接字以上的程式是受應用程式控制的,而在套接字以下的的傳輸層協議軟體則是由計算機作業系統控制。因此,只要我們的應用程式使用TCP/IP協議進行通訊,它就必須通過Socket與作業系統互動並請求服務。從這裡可以看出來,我們對Socket以上的應用程式具有完全的控制,但對Socket以下的傳輸層卻只有很少的控制。例如,我們可以選擇傳輸層協議(TCP或UDP)和一些傳輸層的引數(如最大快取空間和最大報文長度)。

Socket使用案例

  在這裡我們選擇傳輸層協議為TCP協議,也就是我們將使用流套接字作為例子進行舉例說明。現在我們現在做一個聊天室功能。在這裡我們建立兩個應用程式,分別執行在兩個不同的裝置上。首先看一下效果圖。

演示

     

客戶端程式碼

package com.example.ljd.socketclient;

import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.Date;
import java.text.SimpleDateFormat;

import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity{

    private static final int RECEIVE_NEW_MESSAGE = 1;
    private static final int SOCKET_CONNECT_SUCCESS = 2;
    private static final int SOCKET_CONNECT_FAIL = 3;

    @Bind(R.id.msg_edit_text)
    EditText mMessageEditText;

    @Bind(R.id.show_linear)
    LinearLayout mShowLinear;

    private PrintWriter mPrintWriter;
    private Socket mClientSocket;
    private boolean mIsConnectServer = false;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {

                case RECEIVE_NEW_MESSAGE:
                    TextView textView = new TextView(MainActivity.this);
                    textView.setText((String)msg.obj);
                    mShowLinear.addView(textView);
                    break;

                case SOCKET_CONNECT_SUCCESS:
                    Toast.makeText(MainActivity.this,"連線服務端成功",Toast.LENGTH_SHORT).show();
                    break;

                case SOCKET_CONNECT_FAIL:
                    Toast.makeText(MainActivity.this,"連線服務端失敗,請重新嘗試",Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;

            }
        }
    };

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

    }

    @Override
    protected void onDestroy() {
        ButterKnife.unbind(this);
        disConnectServer();
        super.onDestroy();
    }

    @OnClick({R.id.send_btn,R.id.connect_btn,R.id.disconnect_btn})
    public void onClickButton(View v) {

        switch (v.getId()){
            case R.id.send_btn:
                sendMessageToServer();
                break;
            case R.id.connect_btn:
                new Thread() {
                    @Override
                    public void run() {
                        connectServer();
                    }
                }.start();
                break;
            case R.id.disconnect_btn:
                Toast.makeText(MainActivity.this,"已經斷開連線",Toast.LENGTH_SHORT).show();
                disConnectServer();
                break;
        }

    }

    private String getTime(long time) {
        return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time));
    }

    private void connectServer() {

        if (mIsConnectServer)
            return;

        int count = 0;
        while (mClientSocket == null) {
            try {
                mClientSocket = new Socket("10.10.14.160", 8088);
                mPrintWriter = new PrintWriter(new BufferedWriter(
                        new OutputStreamWriter(mClientSocket.getOutputStream())), true);
                mIsConnectServer = true;
                mHandler.obtainMessage(SOCKET_CONNECT_SUCCESS).sendToTarget();
            } catch (IOException e) {
                SystemClock.sleep(1000);
                count++;
                if (count == 5){
                    mHandler.obtainMessage(SOCKET_CONNECT_FAIL).sendToTarget();
                    return;
                }
            }
        }

        try {
            // 接收伺服器端的訊息
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
                    mClientSocket.getInputStream()));
            while (!MainActivity.this.isFinishing()) {
                String msg = bufferedReader.readLine();
                if (msg != null) {
                    String time = getTime(System.currentTimeMillis());
                    final String showedMsg = "server " + time + ":" + msg;
                    mHandler.obtainMessage(RECEIVE_NEW_MESSAGE, showedMsg)
                            .sendToTarget();
                }
            }
            mPrintWriter.close();
            bufferedReader.close();
            mClientSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void disConnectServer(){
        mIsConnectServer = false;
        if (mClientSocket != null) {
            try {
                mClientSocket.shutdownInput();
                mClientSocket.close();
                mClientSocket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void sendMessageToServer(){
        if (!mIsConnectServer){
            Toast.makeText(this,"沒有連線上服務端,請重新連線",Toast.LENGTH_SHORT).show();
            return;
        }
        final String msg = mMessageEditText.getText().toString();
        if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
            mPrintWriter.println(msg);
            mMessageEditText.setText("");
            String time = getTime(System.currentTimeMillis());
            final String showedMsg = "client " + time + ":" + msg;
            TextView textView = new TextView(this);
            textView.setText(showedMsg);
            mShowLinear.addView(textView);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187

客戶端佈局程式碼

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp"
    >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" >

        <LinearLayout
            android:id="@+id/show_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >
        </LinearLayout>
    </ScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/msg_edit_text"
            android:layout_width="0dp"
            android:layout_gravity="bottom"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            />

        <Button
            android:id="@+id/send_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="傳送" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/connect_btn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="連線"/>

        <Button
            android:id="@+id/disconnect_btn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="斷開連線"/>
    </LinearLayout>
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

服務端程式碼 

package com.example.ljd.socketserver;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.Date;
import java.text.SimpleDateFormat;


import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity{

    @Bind(R.id.show_linear)
    LinearLayout mShowLinear;

    @Bind(R.id.msg_edit_text)
    EditText mMessageEditText;

    private ServerSocket mServerSocket;
    private PrintWriter mPrintWriter;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 0){
                TextView textView = new TextView(MainActivity.this);
                textView.setText((String)msg.obj);
                mShowLinear.addView(textView);
            }
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        try {
            mServerSocket = new ServerSocket(8088);
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(new AcceptClient()).start();
    }


    @Override
    public void onDestroy() {
        ButterKnife.unbind(this);
        if (mServerSocket != null){
            try {
                mServerSocket.close();
                mServerSocket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }

    @OnClick(R.id.send_btn)
    public void onClickButton() {
        final String msg = mMessageEditText.getText().toString();
        if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
            //將訊息寫入到流中
            mPrintWriter.println(msg);
            mMessageEditText.setText("");
            String time = getTime(System.currentTimeMillis());
            final String showedMsg = "server " + time + ":" + msg;
            TextView textView = new TextView(this);
            textView.setText(showedMsg);
            mShowLinear.addView(textView);
        }
    }

    private String getTime(long time) {
        return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time));
    }

    class AcceptClient implements Runnable{

        @Override
        public void run() {
            try {
                Socket clientSocket = null;
                while (clientSocket == null){
                    clientSocket = mServerSocket.accept();
                    mPrintWriter = new PrintWriter(new BufferedWriter(
                            new OutputStreamWriter(clientSocket.getOutputStream())), true);
                }
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
                        clientSocket.getInputStream()));
                while (!MainActivity.this.isFinishing()) {
                    //讀取客戶端發來的訊息
                    String msg = bufferedReader.readLine();
                    if (msg != null) {
                        String time = getTime(System.currentTimeMillis());
                        final String showedMsg = "client " + time + ":" + msg;
                        mHandler.obtainMessage(0, showedMsg)
                                .sendToTarget();
                    }
                }
                bufferedReader.close();
                clientSocket.close();
                mPrintWriter.close();

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129

服務端佈局程式碼

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp"
    >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" >

        <LinearLayout
            android:id="@+id/show_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >
        </LinearLayout>
    </ScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/msg_edit_text"
            android:layout_width="0dp"
            android:layout_gravity="bottom"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            />

        <Button
            android:id="@+id/send_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="傳送" />
    </LinearLayout>

</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

新增許可權

  在客戶端與服務端應用中我們還需要新增一些許可權

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

總結

  在網路通訊中我們除了socket我們最常用的還有一種方式那就是http通訊,而http連線採用的是”請求—響應方式“,也就是說只有當客戶端發出請求時,服務端才能夠向客戶端返回資料。在這裡我們就不在詳細介紹。對於Android的IPC機制就說到這裡了,當然還有其他方式可以進行跨程式通訊,我們可以自行研究了。

原始碼下載

相關文章