進程間通信,數據流傳遞(AIDL、LocalSocket)

進程間通信

Android 四大組件

Android 進程間通信可以通過Android 四大組件實現。

Activity

使用 Intent

Intent callIntent = new  Intent(Intent.ACTION_CALL, Uri.parse("tel:12345678" );  
startActivity(callIntent);

Content Provider

Content Provider可以跨進程訪問其他應用程序中的數據(以Cursor對象形式返回),當然,也可以對其他應用程序的數據進行增、刪、改操 作;

Content Provider返回的數據是二維表的形式

Broadcast

廣播是一種被動跨進程通訊的方式。當某個程序向系統發送廣播時,其他的應用程序只能被動地接收廣播數據。

Service

普通的Service並不能實現跨進程操作,我們可以使用


AIDL Service

Android 接口定義語言(AIDL),我們可以利用AIDL定義多個應用都認可的編程接口,方便二者使用進程間通信(IPC)。

在我們定義 AIDL 接口之前,我們需要明確一些事情

1、AIDL 接口的調用是直接的函數調用,如果涉及線程的切換,需要在接口調用方進行處理

2、AIDL 接口的實現必須基於完全的線程安全,調用方要對併發的情況做好處理

谷歌文檔

AIDL的具體實現

正式進行開發

1、服務端APP創建.aidl文件

在 src 目錄下右鍵創建 AIDL 文件

// IMyAIDLService.aidl
package com.zuo.aidlservice;


interface IMyAIDLService {
   //獲取展示的數據
   String getShowStr();
}

創建完成後build一下,會生成以 .aidl 文件命名的 .java 接口文件。
在 項目 build/generated/aidl_source_output_dir/[debug/release]/compile*Aidl/out/包名/ 下。

生成的接口包含一個名爲 Stub 的子類(例如,YourInterface.Stub),該子類是其父接口的抽象實現,並且會聲明 .aidl 文件中的所有方法。

  • 重要提醒
    Stub 的子類中還會定義幾個輔助方法,其中最值得注意的是 asInterface() ,該方法會接收 IBinder (**通常是傳遞給客戶端 OnServiceConnected() 回調方法的參數 **),並返回 Stub 接口的實例。

補充說明

AIDL 支持的數據類型

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5fLk1tIi-1589773353400)(_v_images/20200512120919440_1925.png)]

當我們在接口方法中使用這些類型的時候,需要爲各自的類型加入一條 import 語句,才能使用。


2、服務端APP實現aidl文件定義的接口

我們定義一個 Binder 類用來繼承 aidl 接口文件的 Stub 子類,或者用匿名內部類的方式實現

    class MyBinder extends IMyAIDLService.Stub {

        @Override
        public String getShowStr() throws RemoteException {
            //todo 實現服務端的邏輯
            return "來自服務端的問好";
        }
    }

現在,binder 是 Stub 類的一個實例(一個 Binder),其定義了服務的遠程過程調用 (RPC) 接口。
在下一步中,我們會向客戶端公開此實例,以便客戶端能與服務進行交互(Binder機制)。

簡單理解Binder機制的原理


3、服務端APP向客戶端APP公開接口

我們定義一個服務類,實現 onBind() 方法來公開我們的服務,onBind() 方法中返回 IBinder 接口的實現類(繼承自 aidl 接口文件的 Stub 子類)

服務類路徑爲java代碼路徑 ,而非 aidl 文件路徑

/**
 * 向客戶端公開 IMyAIDLService 接口
 *
 * @author zuo
 * @date 2020/5/12 14:55
 */
public class MyAIDLService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    class MyBinder extends IMyAIDLService.Stub {

        @Override
        public String getShowStr() throws RemoteException {
            //todo 實現服務端的邏輯
            return "來自服務端的問好";
        }
    }
}

現在當客戶端APP中的組件(如 Activity)調用 bindService() 以連接此服務的時候,客戶端APP的 onServiceConnected() 回調方法就會接收到服務端 onBind() 方法所返回的 binder 實例。

  • 注意事項

1、服務類需要在清單文件中註冊

<service
	android:name=".MyAIDLService"
	android:enabled="true"
	android:exported="true" />

2、客戶端必須擁有 IMyAIDLService 接口類的訪問權限,才能調用上述服務
因此當客戶端和服務不在同一個應用內時,客戶端應用也必須包含.aidl 文件的副本。
(該文件會生成 android.os.Binder 接口,進而爲客戶端提供 AIDL 方法的訪問權限)

3、**當客戶端在 onServiceConnected() 回調中收到 IBinder 時,必須調用接口服務的asInterface方法,用來把返回的參數轉換成 IMyAIDLService 類型,如

 iMyAIDLService= IMyAIDLService.Stub.asInterface(IBinder)

4、進程間傳遞對象

我們可以通過上述的 IPC 接口,在進程間傳遞實體對象,該實體對象需要支持 Parcelable 接口。

備註
如果需要創建 Parcelable 類的 .aidl 文件,請參考Rect.aidl 文件所示步驟

  • 如果我們需要傳遞 Bundle 參數
    當客戶端傳遞過來一個 Bundle 數據時,我們在讀取之前必須調用Bundle.setClassLoader(ClassLoader) 設置軟件包的類加載器
    否則,即使您在應用中正確定義 Parcelable 類型,也會遇到 ClassNotFoundException。參考如下代碼:
private final IRectInsideBundle.Stub binder = new IRectInsideBundle.Stub() {
    public void saveRect(Bundle bundle){
        bundle.setClassLoader(getClass().getClassLoader());
        Rect rect = bundle.getParcelable("rect");
        process(rect); // Do more with the parcelable.
    }
};

5、客戶端調用IPC方法和服務端通信

客戶端APP調用 aidl 接口實現和服務端APP的進程間通信。

  • 1、在項目的 src/目錄中加入 .aidl 文件

我這裏是直接將服務端APP的aidl 文件拷貝過來使用的

// IMyAIDLService.aidl
package com.zuo.aidlservice;


interface IMyAIDLService {
   //獲取展示的數據
   String getShowStr();
}
  • 2、聲明一個 IBinder 接口實例(基於 AIDL 生成)

同樣是將服務端APP的文件拷貝過來使用,區別在於客戶端只拷貝了 Binder 類,沒有拷貝 Service 類。
** Binder 類必須要,沒有則無法訪問到服務端APP的 getShowStr() 方法。**

/**
 * IMyAIDLService 接口
 *
 * @author zuo
 * @date 2020/5/12 14:55
 */
public class MyBinder extends IMyAIDLService.Stub {

    @Override
    public String getShowStr() throws RemoteException {
        return "來自客戶端的問好";
    }
}
  • 3、實現 ServiceConnection

在需要調用的地方,如 Activitty 實現ServiceConnection

    /**
     * 實現 ServiceConnection。
     */
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
  • 4、調用 Context.bindService(),傳入 ServiceConnection 實現

這裏需要注意,Android5.0以後綁定啓動Service考慮到安全原因,不允許隱式意圖的方式啓動,也就是說要給出一個明確的組件Service。
intent.setPackage(String packageName)或者intent.setComponent(ComponentName componentName)都可以顯示設置組件處理意圖。

    /**
     * 綁定服務,設置綁定後自動開啓服務
     *
     * @return
     */
    private void bindService() {
        Intent intent = new Intent();
        intent.setAction("com.zuo.aidlservice.MyAIDLService");
        //待使用遠程Service所屬應用的包名
        intent.setPackage("com.zuo.aidlservice");
        try {
            bindService(intent, conn, BIND_AUTO_CREATE);
            isBound = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 5、在 onServiceConnected() 實現中,您將收到一個 IBinder 實例(名爲 service)。調用 MyAIDLService.Stub.asInterface((IBinder)service),以將返回的參數轉換爲 MyAIDLService 類型。
    /**
     * 實現 ServiceConnection。
     */
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMyAIDLService iMyAIDLService = IMyAIDLService.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
  • 6、調用您在接口上定義的方法。

我們需要在調用方法的時候捕獲 DeadObjectException 異常,該異常是系統在連接中斷時拋出的。
我們還需要捕獲 SecurityException 異常,這個異常是 IPC 方法調用中兩個進程的 AIDL 定義發生衝突時,系統拋出的異常。

    /**
     * 實現 ServiceConnection。
     */
    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMyAIDLService iMyAIDLService = IMyAIDLService.Stub.asInterface(service);
            try {
                String showStr = iMyAIDLService.getShowStr();
                binding.text.setText(TextUtils.isEmpty(showStr) ? "返回錯誤!" : showStr);
            } catch (Exception e) {
                Log.i(TAG, "onServiceConnected: " + e.getMessage());
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
  • 7、如要斷開連接,請使用您的接口實例調用 Context.unbindService()
    @Override
    protected void onPause() {
        super.onPause();
        //解綁服務
        if (isBound) {
            try {
                unbindService(conn);
                isBound = false;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

效果
先啓動 AIDL_SERVICE APP ,然後啓動 AIDL_Client APP,
點擊AIDL_Client界面展示的 “Hello Worlf!”從AIDL_SERVICE獲取展示內容
在這裏插入圖片描述

項目代碼結構
12


LocalSocket & LocalServerSocket

LocalSocket

本地 socket 是在unix 域名空間創建一個套接字(非服務器)。

構造函數

  • LocalSocket() , 無參構造函數,創建一個 SOCKET_STREAM 類型的本地套接字
  • LocalSocket(int sockType),有參構造函數,創建對應類型的本地套接字

可以創建的類型
SOCKET_DGRAM – 數據報,數據報是通過網絡傳輸的數據的基本單元,包含一個報頭(header)和數據本身,類似於 UDP
SOCKET_STREAM – 流,類似於 TCP
SOCKET_SEQPACKET – 順序數據包


公共方法

1、bind(LocalSocketAddress bindpoint)

綁定套接字到本地地址上,該方法只能調用一次,如果已綁定的套接字實例繼續調用該方法會報IOException("already bound")異常。

我們可以通過 isBound()方法來判斷當前實例是否已經綁定。

2、close()

關閉當前的套接字

3、connect()

連接套接字到本地地址上,該方法有兩個重載方法 connect(LocalSocketAddress endpoint)connect(LocalSocketAddress endpoint, int timeout)
區別在於一個可以設置連接超時時間。

同樣的,如果已經綁定的套接字實例繼續調用該方法會報IOException("already connected")異常

我們可以通過 isConnected()方法來判斷當前實例是否已經綁定。

另外,如果套接字處於無效狀態或者連接的地址不存在。也會報IOException異常

4、getAncillaryFileDescriptors() 、setFileDescriptorsForSend(FileDescriptor[] fds)

set 方法,發送一組文件描述,將在普通數據下一次寫入時發送,並以單個輔助信息的方式到達。
get方法,獲取一組文件描述,通過輔助信息返回的一組文件描述,FileDescriptor[] 。

文件描述只能和常規數據一起傳遞,因此此方法只能在讀取操作後返回非null。

5、getFileDescriptor()

返回文件描述符;如果尚未打開/已經關閉,則返回null

6、getInputStream()

返回套接字實例的輸入流,InputStream

7、getOutputStream()

返回套接字實例的輸出流,OutputStream

8、getLocalSocketAddress()

返回套接字綁定的地址,可能爲 null 。LocalSocketAddress

9、getPeerCredentials()

返回套接字的證書,包含 pid 、uid 、gid 。已 root 的設備可能被篡改。

10、其他方法

  • getReceiveBufferSize() ,接收緩存的size
  • setReceiveBufferSize(int size) ,設置緩存的size
  • getSendBufferSize() ,發送緩存的size
  • setSendBufferSize(int n) ,設置發送緩存的size
  • getRemoteSocketAddress() ,獲取遠端socket 的地址
  • getSoTimeout() , 獲取讀取超時的時間
  • setSoTimeout(int n) , 設置讀取超時的時間
  • isBound() ,socket 是否已經綁定
  • isClosed() ,socket 是否已經關閉
  • isConnected() ,socket 是否已經連接
  • isInputShutdown() ,是否已經終止輸入
  • shutdownInput(),終止socket的輸入
  • isOutputShutdown() , 是否已經終止輸出
  • shutdownOutput(),終止socket的輸出

相關概念

1、LocalSocketAddress

兩個構造函數,LocalSocketAddress(String name)LocalSocketAddress(String name, Namespace namespace)

區別在於是否指定命名空間,不指定時默認爲:ABSTRACT

可選擇的命名空間類型
ABSTRACT – Linux 中抽象的命名空間
RESERVED – Android保留命名空間,位於/ dev / socket中。 只有init進程可以在此處創建套接字。
FILESYSTEM – 以普通文件系統路徑命名的套接字。

2、pid 、uid 、gid

Android中UID、GID和PID的講解

Linux中的概念

  • UID
    在Linux中用戶的概念分爲:普通用戶、根用戶和系統用戶。
    普通用戶:表示平時使用的用戶概念,在使用Linux時,需要通過用戶名和密碼登錄,獲取該用戶相應的權限,其權限具體表現在對系統中文件的增刪改查和命令執行的限制,不同用戶具有不同的權限設置,其UID通常大於500。
    根用戶:該用戶就是ROOT用戶,其UID爲0,可以對系統中任何文件進行增刪改查處理,執行任何命令,因此ROOT用戶極其危險,如操作不當,會導致系統徹底崩掉。
    系統用戶:該用戶是系統虛擬出的用戶概念,不對使用者開發的用戶,其UID範圍爲1-499,例如運行MySQL數據庫服務時,需要使用系統用戶mysql來運行mysqld進程。

  • GID
    GID顧名思義就是對於UID的封裝處理,就是包含多個UID的意思,實際上在Linux下每個UID都對應着一個GID。設計GID是爲了便於對系統的統一管理,例如增加某個文件的用戶權限時,只對admin組的用戶開放,那麼在分配權限時,只需對該組分配,其組下的所有用戶均獲取權限。同樣在刪除時,也便於統一操作。

除了UID和GID外,還包括其擴展的有效的用戶、組(euid、egid)、文件系統的用戶、組(fsuid、fsgid)和保存的設置用戶、組(suid、sgid)等。

  • PID
    系統在程序運行時,會爲每個可執行程序分配一個唯一的進程ID(PID),PID的直接作用是爲了表明該程序所擁有的文件操作權限,不同的可執行程序運行時互不影響,相互之間的數據訪問具有權限限制。

Android 中的概念

在Android中一個UID的對應的就是一個可執行的程序,對於普通的程序其UID就是對應與GID,程序在Android系統留存期間,其UID不變。
PID 同樣是進程的 ID。

3、FileDescriptor

文件描述符,用來表示打開的文件、打開的套接字或者其他流。
主要用途是創建一個輸入流或者輸出流,FileInputStream or FileOutputStream。


LocalServerSocket

在Linux抽象命名空間中創建一個 在 UNIX域名 邊界內的套接字

構造函數

  • LocalServerSocket(String name),創建一個監聽指定地址的新服務器套接字,該地址 是 Linux 抽象命名空間中的,不是手機的文件管理系統
    public LocalServerSocket(String name) throws IOException
    {
        impl = new LocalSocketImpl();

        impl.create(LocalSocket.SOCKET_STREAM);

        localAddress = new LocalSocketAddress(name);
        impl.bind(localAddress);

        impl.listen(LISTEN_BACKLOG);
    }
  • LocalServerSocket(FileDescriptor fd),從一個已經創建並綁定了的文件描述符中創建服務器套接字,創建後 listen 將被立即調用
    public LocalServerSocket(FileDescriptor fd) throws IOException
    {
        impl = new LocalSocketImpl(fd);
        impl.listen(LISTEN_BACKLOG);
        localAddress = impl.getSockAddress();
    }

公共方法

1、accept()

接收一個新的socket連接,阻塞直到這個新的連接到達。

返回一個 新連接的套接字,LocalSocket。

2、close()

關閉服務器套接字

3、getFileDescriptor()

返回文件描述符;如果尚未打開/已經關閉,則返回null

4、getLocalSocketAddress()

獲取套接字的本地地址


LocalSocket 使用示例

服務端APP

LocalServerSocket實現類

  • 和客戶端進行數據的收發
  • 實現Runnable接口,在工作線程中持續進行消息接收的監聽,並將接收到的消息通過handler發送給外部
  • 實現發送方法
  • 實現close方法
/**
 * 和客戶端進行數據收發
 * <p>
 * 傳遞的數據爲 二進制數組 byte[]
 *
 * @author zuo
 * @date 2020/5/14 15:08
 */
public class SocketServerImpl implements Runnable {
    private static final String TAG = "SocketServerImpl";
    private String localSocketAddress = "com.zuo.service";
    private BufferedOutputStream os;
    private BufferedInputStream is;
    public static final int bufferSizeOutput = 1024 * 1024;
    LocalServerSocket server;
    LocalSocket client;
    Handler handler;

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

    @Override
    public void run() {
        Log.i(TAG, "Server isOpen");
        try {
            if (null == server) {
                server = new LocalServerSocket(localSocketAddress);
            }
            if (null == client) {
                client = server.accept();
                Log.i(TAG, "Client Connected");
            }
            Credentials cre = client.getPeerCredentials();
            Log.i(TAG, "ClientID:" + cre.getUid());
            os = new BufferedOutputStream(client.getOutputStream(), bufferSizeOutput);
            is = new BufferedInputStream(client.getInputStream(), bufferSizeOutput);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        while (null != is) {
            try {
                if (is.available() <= 0) continue;
                Message msg = handler.obtainMessage();
                msg.obj = is;
                msg.arg1 = 1;
                handler.sendMessage(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 發送數據
     *
     * @param data
     */
    public void send(byte[] data) throws Exception {
        if (null != os) {
            os.write(data);
            os.flush();
        }
    }

    /**
     * 關閉監聽
     */
    public void close() {
        try {
            if (null != os) {
                os.close();
                os = null;
            }
            if (null != is) {
                is.close();
                is = null;
            }
            if (null != client) {
                client.close();
                client = null;
            }
            if (null != server) {
                server.close();
                server = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

服務端活動界面

  • 啓動 SocketServer
  • 處理接收到的客戶端信息,
  • 展示活動界面,並將數據發送給客戶端
/**
 * @author zuo
 * @date 2020/5/18 11:01
 */
public class MainActivity extends AppCompatActivity {
    
    private SocketServerImpl socketServer;
    private ActivityMainBinding binding;
    private List<Integer> data;
    @IntRange(from = 0, to = 3)
    private int index = 0;

    //持續接收客戶端反饋信息
    private StringBuilder buffer = new StringBuilder();
    Handler handler = new Handler(new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.arg1 == 1) {
                SocketParseBean bean = null;
                try {
                    bean = SendDataUtils.parseSendData((BufferedInputStream) msg.obj);
                    if (null == bean || TextUtils.isEmpty(bean.getInfo())) return false;
                    showImg();
                } catch (Exception e) {
                    return false;
                }
                buffer.append(bean.getInfo());
                buffer.append("\r\n");
                showSocketMsg();
            }
            return false;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setPresenter(new Presenter());
        initData();
        startSocketServer();
    }

    private void showSocketMsg() {
        if (null != binding) {
            binding.backMsgShow.setText("客戶端消息:" + buffer.toString());
        }
    }

    private void startSocketServer() {
        socketServer = new SocketServerImpl(handler);
        new Thread(socketServer).start();
    }

    private void initData() {
        data = new ArrayList<>();
        data.add(R.drawable.kb890);
        data.add(R.drawable.kb618);
        data.add(R.drawable.kb224);
    }

    private void showImg() {
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), data.get(index));
        binding.imgShow.setImageBitmap(bmp);
        binding.indexShow.setText((index + 1) + "/" + data.size());
        String hint = "服務端正在展示第 " + (index + 1) + " 張照片";
        sendData(hint, bmp);
    }

    public void sendData(final String hint, final Bitmap bmp) {
        if (null != socketServer) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] array = null;
            try {
                if (null != bmp) {
                    bmp.compress(Bitmap.CompressFormat.PNG, 100, baos);
                    array = baos.toByteArray();
                }
                byte[] bytes = SendDataUtils.makeSendData(hint, array);
                socketServer.send(bytes);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != socketServer) {
            socketServer.close();
        }
    }

    public class Presenter {

        public void last(View view) {
            if (index <= 0) {
                Toast.makeText(MainActivity.this, "沒有上一張了!", Toast.LENGTH_SHORT).show();
                return;
            }
            index--;
            showImg();
        }

        public void next(View view) {
            if (index >= 2) {
                Toast.makeText(MainActivity.this, "沒有下一張了!", Toast.LENGTH_SHORT).show();
                return;
            }
            index++;
            showImg();
        }
    }

}

客戶端APP

LocalSocket實現類

  • 和服務端進行數據的收發
  • 實現Runnable接口,在工作線程中持續進行消息接收的監聽,並將接收到的消息通過handler發送給外部
  • 實現發送方法
  • 實現close方法
/**
 * 和服務端進行數據收發
 *
 * @author zuo
 * @date 2020/5/14 15:08
 */
public class SocketClientImpl implements Runnable {
    private static final String TAG = "SocketClientImpl";
    private String localSocketAddress = "com.zuo.service";
    private BufferedOutputStream os;
    private BufferedInputStream is;
    private int timeout = 30000;
    public static final int bufferSizeOutput = 1024 * 1024;
    private LocalSocket client;
    private Handler handler;

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

    @Override
    public void run() {
        Log.i(TAG, "Client isOpen");
        try {
            if (null == client) {
                client = new LocalSocket();
                client.connect(new LocalSocketAddress(localSocketAddress));
                client.setSoTimeout(timeout);
                Log.i(TAG, "Server Connected");
            }
            os = new BufferedOutputStream(client.getOutputStream(), bufferSizeOutput);
            is = new BufferedInputStream(client.getInputStream(), bufferSizeOutput);
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        //將接收到的數據發送出去
        while (null != is) {
            try {
                if (is.available() <= 0) continue;
                Message msg = handler.obtainMessage();
                msg.obj = is;
                msg.arg1 = 1;
                handler.sendMessage(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 發送數據
     *
     * @param data
     */
    public void send(byte[] data) throws Exception {
        if (null != os) {
            os.write(data);
            os.flush();
        }
    }

    /**
     * 關閉監聽
     */
    public void close() {
        try {
            if (null != os) {
                os.close();
                os = null;
            }
            if (null != is) {
                is.close();
                is = null;
            }
            if (null != client) {
                client.close();
                client = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

客戶端活動界面

  • 啓動 SocketClient
  • 處理接收到的服務端信息,
  • 展示活動界面,並將數據發送給服務端
/**
 * @author zuo
 * @date 2020/5/18 11:29
 */
public class MainActivity extends AppCompatActivity {

    private SocketClientImpl socketClient;
    private ActivityMainBinding binding;

    //持續接收服務端反饋信息
    private StringBuilder buffer = new StringBuilder();
    Handler handler = new Handler(new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.arg1 == 1) {
                SocketParseBean bean = null;
                try {
                    bean = SendDataUtils.parseSendData((BufferedInputStream) msg.obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (null == bean || TextUtils.isEmpty(bean.getInfo())) return false;
                buffer.append(bean.getInfo());
                buffer.append("\r\n");
                showSocketMsg(bean.getData());
            }
            return true;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setPresenter(new Presenter());
        startSocketClient();
    }

    private void showSocketMsg(final byte[] data) {
        if (null != binding) {
            binding.backMsgShow.setText(buffer.toString());
        }
        showImg(data);
    }

    private void startSocketClient() {
        socketClient = new SocketClientImpl(handler);
        new Thread(socketClient).start();
    }

    private void showImg(byte[] data) {
        if (null == data) return;
        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
        binding.imgShow.setImageBitmap(bitmap);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != socketClient) {
            socketClient.close();
        }
    }

    public void sendData2Server(final String hint, final Bitmap bmp) throws Exception {
        if (null != socketClient) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] array = null;
            if (null != bmp) {
                bmp.compress(Bitmap.CompressFormat.PNG, 100, baos);
                array = baos.toByteArray();
            }
            byte[] bytes = SendDataUtils.makeSendData(hint, array);
            socketClient.send(bytes);
        }
    }

    public class Presenter {

        public void sendData(View view) {
            String text = binding.clientInput.getText().toString().trim();
            if (TextUtils.isEmpty(text)) {
                Toast.makeText(MainActivity.this, "消息內容不能爲空!", Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                sendData2Server(text, null);
            } catch (Exception e) {
                Toast.makeText(MainActivity.this, "消息發送失敗!", Toast.LENGTH_SHORT).show();
            }
        }

    }
}

共用工具類

封裝、解析流數據

  • 字符串信息統一使用 utf-8 編碼格式,防止出現亂碼
  • 提供信息流數據封裝方法及數據流解析方法
/**
 * LocalSocket 傳輸數據(封裝、解析)工具類
 * <p>
 * 數據傳輸規則:
 * [0,7)  -- infoSize
 * [7,14) -- dataSize
 * [14,14+infoSize) -- info
 * [14+infoSize,14+infoSize+dataSize)  -- data
 *
 * @author zuo
 * @date 2020/5/14 19:20
 */
public class SendDataUtils {
    /**
     * 對應數據的 size ,7 位 (9.5M)
     */
    private static final int infoSize = 7;
    private static final int dataSize = 7;

    /**
     * 封裝 LocalSocket 發送的數據
     *
     * @param info -- 需要發送的字符串數據
     * @param data -- 需要發送的字節流數據
     * @return 封裝後的字節流數據
     */
    public static byte[] makeSendData(@NonNull String info, byte[] data) throws Exception {
        //文本信息
        Charset charset_utf8 = Charset.forName("utf-8");
        ByteBuffer buff = charset_utf8.encode(info);
        byte[] infoBytes = buff.array();
        int infoLength = infoBytes.length;
        byte[] headSizeBytes = String.valueOf(infoLength).getBytes();
        int dataLength = data == null ? 0 : data.length;
        byte[] dataSizeBytes = String.valueOf(dataLength).getBytes();
        int totalSize = infoSize + dataSize + infoLength + dataLength;
        byte[] output = new byte[totalSize];
        //1、頭部信息(info size)
        System.arraycopy(headSizeBytes, 0, output, 0, headSizeBytes.length);
        //2、頭部信息(data size)
        System.arraycopy(dataSizeBytes, 0, output, infoSize, dataSizeBytes.length);
        //2、info 信息
        System.arraycopy(infoBytes, 0, output, infoSize + dataSize, infoLength);
        if (dataLength > 0) {
            //拷貝 data 信息
            System.arraycopy(data, 0, output, infoSize + dataSize + infoLength, dataLength);
        }
        return output;
    }

    /**
     * 解析 LocalSocket 接收到的數據
     *
     * @param is -- 待解析的輸入流
     * @return 解析後的數據
     * @throws Exception
     */
    public static SocketParseBean parseSendData(BufferedInputStream is) throws Exception {
        if (null == is || is.available() <= 0) return null;
        //拿到info信息的size
        byte[] infoSizeByte = new byte[infoSize];
        is.read(infoSizeByte);
        String infoLength = new String(infoSizeByte);
        String infoSizeStr = infoLength.trim();
        Integer infoSize = Integer.valueOf(infoSizeStr);
        //拿到data的size
        byte[] dataSizeByte = new byte[dataSize];
        is.read(dataSizeByte);
        String dataLength = new String(dataSizeByte);
        String dataSizeStr = dataLength.trim();
        Integer dataSize = Integer.valueOf(dataSizeStr);
        //數據讀取
        SocketParseBean parseBean = new SocketParseBean();
        if (infoSize <= 0 && dataSize <= 0) {
            return parseBean;
        }
        //讀取info
        byte[] infoByte = new byte[infoSize];
        is.read(infoByte, 0, infoSize);
        String s = new String(infoByte, "utf-8");
        parseBean.setInfo(s.trim());
        //讀取data
        if (dataSize > 0) {
            byte[] buffer = new byte[dataSize];
            is.read(buffer, 0, dataSize);
            parseBean.setData(buffer);
        }
        return parseBean;
    }

}

解析數據封裝實體

/**
 * 解析socket服務傳遞的數據
 *
 * @author zuo
 * @date 2020/5/15 17:03
 */
public class SocketParseBean {

    private String info;
    private byte[] data;

    public SocketParseBean() {
    }

    public SocketParseBean(String info, byte[] data) {
        this.info = info;
        this.data = data;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }
}

項目結構
在這裏插入圖片描述


LocalSocket交互效果展示

  • 客戶端向服務端發送文本消息時,服務端將正在展示的照片及相關信息發送給客戶端
  • 服務端切換照片時,將正在展示的照片及相關信息發送給客戶端

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章