进程间通信
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);
}
}