進程間通信
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並不能實現跨進程操作,我們可以使用
- 1、AIDL Service
- 2、LocalSocket
來實現跨進程通信。
AIDL Service
Android 接口定義語言(AIDL),我們可以利用AIDL定義多個應用都認可的編程接口,方便二者使用進程間通信(IPC)。
在我們定義 AIDL 接口之前,我們需要明確一些事情
1、AIDL 接口的調用是直接的函數調用,如果涉及線程的切換,需要在接口調用方進行處理
2、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 接口的實例。
補充說明
當我們在接口方法中使用這些類型的時候,需要爲各自的類型加入一條 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機制)。
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獲取展示內容
項目代碼結構
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
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交互效果展示
- 客戶端向服務端發送文本消息時,服務端將正在展示的照片及相關信息發送給客戶端
- 服務端切換照片時,將正在展示的照片及相關信息發送給客戶端
使用Socket
LocalSocket 在某些設備上出現 權限拒絕等錯誤,將上述demo中的 LocalSocket 替換爲 Socket
SocketClientImpl
替換後的代碼
if (null == client) {
// client = new LocalSocket();
client = new Socket("localhost", 8080);
// client.connect(new LocalSocketAddress(localSocketAddress));
client.setSoTimeout(timeout);
Log.i(TAG, "Server Connected");
}
SocketServerImpl
替換後的代碼
if (null == server) {
// server = new LocalServerSocket(localSocketAddress);
server = new ServerSocket(8080);
}
流裏面取每一幀的策略
//25 Kb 的緩衝區
int bufferSizeOutput = 1024 * 25;
os = new BufferedOutputStream(client.getOutputStream(), bufferSizeOutput);
is = new BufferedInputStream(client.getInputStream(), bufferSizeOutput);
//yuv data的長度 = 視頻幀width*height*1.5
int srcWidth = 480, srcHeight = 320;
int totalSize = srcWidth * srcHeight * 3 / 2;
int tmpSize = 0;
byte[] buffer = new byte[bufferSizeOutput];
while (client.isConnected()) {
if (is.read() == 0xA0) {
ByteArrayOutputStream tempStream = new ByteArrayOutputStream();
while (tmpSize < totalSize) {
int len = is.read(buffer);
tmpSize += len;
tempStream.write(buffer, 0, len);
}
Frame frame = new Frame(tempStream.toByteArray(), srcWidth, srcHeight);
LiveStreamRepository.getInstance().addFrame(frame);
tmpSize = 0;
tempStream.close();
Log.e(TAG, "receive " + frame.toString());
}
}
使用 DatagramSocket
使用數據報套接字實現進程間通信,客戶端和服務端應用各自監聽自己的端口。實現類SocketTextImpl
客戶端和服務端使用同一個類,區別在於監聽和發送數據包的端口不同
/**
* 採用數據包的方式發送文本類型的數據
* 本實例區別於 SocketClientImpl ,僅用作於文本信息的傳遞,採用 UDP 協議
*
* @author zuo
* @date 2020/5/14 15:08
*/
public class SocketTextImpl implements Runnable {
private static final String TAG = "SocketClientTextImpl";
public static final int bufferSize = 1024 * 1024;
private DatagramSocket socket;
private final int SERVER_PORT = 8090;
private final int CLIENT_PORT = 8091;
public SocketTextImpl() {
}
@Override
public void run() {
Log.i(TAG, "Client isOpen");
try {
if (null == socket) {
//監聽對應端口
socket = new DatagramSocket(SERVER_PORT, InetAddress.getLocalHost());
}
} catch (IOException e1) {
Log.i(TAG, e1.getMessage());
e1.printStackTrace();
}
//接收信息
while (true) {
byte[] buffer = new byte[bufferSize];
DatagramPacket recDp = new DatagramPacket(buffer, buffer.length);
try {
//定義1M的文本消息緩存,如果消息大於1M,會被截斷
socket.receive(recDp);
String recMsg = new String(buffer, 0, recDp.getLength());
LiveStreamRepository.getInstance().addData(recMsg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 發送給客戶端的數據,使用客戶端監聽的端口
*
* @param data
*/
public void send(String data) throws Exception {
if (null != socket) {
byte[] bytes = data.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), CLIENT_PORT);
socket.send(packet);
}
}
/**
* 關閉監聽
*/
public void close() {
try {
if (null != socket) {
socket.close();
socket = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
其他
可用端口範圍
一個有效的端口整數值:0 --65535
- 0~1023:分配給系統的端口號
- 1024~49151:登記端口號,主要是讓第三方應用使用
- 49152~65535:短暫端口號,是留給客戶進程選擇暫時使用,一個進程使用完就可以供其他進程使用。
在Socket使用時,可以用1024~65535的端口號
輔助類,數據存儲隊列
/**
* 無人機互聯,數據存儲隊列
*
* @author zuo
* @date 2020/5/19 14:13
*/
public class LiveStreamRepository {
//隊列,可存儲20幀數據
private int mQueueSize = 10;
private int mBufferSize = 5;
private ArrayBlockingQueue<String> mQueue = new ArrayBlockingQueue<>(mQueueSize);
private LiveStreamRepository() {
}
private final static class UavVideoInfoInstanceHolder {
private static final LiveStreamRepository ins = new LiveStreamRepository();
}
public static LiveStreamRepository getInstance() {
return UavVideoInfoInstanceHolder.ins;
}
public String getData() {
return mQueue.poll();
}
public boolean addData(String data) {
//如果插入失敗(),移除前5幀
if (mQueue.size() == mQueueSize) {
for (int i = 0; i < mBufferSize; i++) {
mQueue.remove(i);
}
}
return mQueue.offer(data);
}
}