进程间通讯篇系列文章目录:
Android 查缺补漏(IPC 篇)-- 进程间通讯基础知识热身
Android 查缺补漏(IPC 篇)-- Bundle 文件共享 ContentProviderMessenger 四种进程间通讯介绍
Android 查缺补漏(IPC 篇)-- 进程间通讯之 AIDL 详解
Android 查缺补漏(IPC 篇)-- 进程间通讯之 Socket 简介及示例
学过计算机网络的人多多少少对 Socket 都会有所了解, 在 Android 中, 我们也可以借助 Socket 来实现进程间通讯, 即使对 Socket 不熟悉也没关系, 本篇文章将会用一个非常简单的例子, 来说明通过 Socket 实现进程间通讯的步骤, 为了打消大家对 Socket 的陌生感, 我们先来看看 Socket 的基本概念和用法
一 Socket 是什么
Socket 又称套接字, 是网络通信中的概念, 应用程序通常通过套接字向网络发出请求或者应答网络请求网络上的两个程序通过一个双向的通讯链接实现数据交换, 这个链接的一端称为一个 Socket, 它本身可以支持传输任意的字节流
Socket 分为流式套接字和用户数据报套接字两种, 分别对应于网络的传输控制层的 TCP 和 UDP 协议
TCP 是面向链接的协议, 提供稳定的双向通信功能, 需要通过三次握手才能完成建立链接, 为了保证稳定性, 它内部提供了超时重连机制
UDP 是无连接的, 提供不稳定的单向通信功能(当然我们也可以通过它实现双向通信), 其在性能上的效率更高, 但无法保证数据一定能够正确传输
接着再说下 TCP 的三次握手, 它是指 TCP 建立链接的如下三个步骤:
服务器监听: 服务端 Socket 是不知道具体的客户端 Socket 的, 而是一直处于等待链接的状态, 实时监控网络状态
客户端请求: 客户端 Socket 首先描述好它要链接的服务端 Socket, 指出服务端 Socket 的地址和端口, 然后提出链接请求
连接确认: 服务端 Socket 接收到客户端 Socket 的链接请求后, 就会响应它的请求并建立一个新的线程把服务端 Socket 的描述发给客户端, 客户端确认后连接就此建立成功而此时, 服务端 Socket 继续保持监听状态, 等待其他的客户端请求
在 java 中通过 Socket 和 ServerSocket 两个类可以很方便的实现 Socket 通讯, ServerSocket 用于服务器端, Socket 是建立网络连接时使用的在连接成功时, 两端都会产生一个 Socket 实例, 操作这个实例, 完成所需的会话接下来创建一个 Socket 连接的示例, 这个示例同时也说明了 Socket 可以实现进程间通讯
二 Socket 实现进程间通讯的基本步骤
1 在 AndroidManifest 文件中声明权限, Socket 是属于网络通信, 自然离不开以下权限:
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
2 服务端设计
首先新建一个 Service, 用于承载 ServerSocket
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- public class TCPSocketService extends Service {
- private static final String TAG = TCPSocketService.class.getSimpleName();
- private boolean mIsServiceDestroyed = false;
- @Override
- public void onCreate() {
- super.onCreate();
- new Thread(new TcpServer()).start();
- }
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
- @Override
- public void onDestroy() {
- mIsServiceDestroyed = true;
- super.onDestroy();
- }
- }
在上面的 Service 中新建一个内部类来创建 ServerSocket 连接, 监听本地 8088 端口要注意的时 Socket 属于耗时的网络操作, 一定要在线程中执行, 否则会在 Android 4.0 以上抛出异常, 同时如果放在主线程中对用户体验也非常不好
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- private class TcpServer implements Runnable {
- @Override
- public void run() {
- ServerSocket serverSocket = null;
- try {
- // 监听本地端口
- serverSocket = new ServerSocket(8088);
- } catch (IOException e) {
- Log.i(TAG, "run: 8088 failed");
- e.printStackTrace();
- return;
- }
- // 接收客户端请求
- while (!mIsServiceDestroyed) {
- try {
- final Socket client = serverSocket.accept();
- Log.i(TAG, "run: 接收客户端请求: client =" + client);
- // 回应客户端
- new Thread(new Runnable() {
- @Override
- public void run() {
- responeClient(client);
- }
- }).start();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
接收到客户端的请求后, 回应客户端:
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- private void responeClient(Socket client) {
- BufferedReader in = null;
- PrintWriter out = null;
- try {
- // 用于接收客户端消息
- in = new BufferedReader(new InputStreamReader(client.getInputStream()));
- // 用于想客户端发送消息
- out = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true);
- // 向客户端发送消息
- out.println("欢迎交流!");
- while (!mIsServiceDestroyed) {
- // 读取客户端发来的消息
- String msgFromClient = in.readLine();
- Log.i(TAG, "responeClient: msg from client:" + msgFromClient);
- if (msgFromClient == null) {
- // 当客户端断开连接时 realLine()就会返回 null, 在此时跳出循环
- break;;
- }
- // 向客户端回应消息
- out.println("已经收到你发来的消息:" + msgFromClient + ", 请放心!");
- }
- Log.i(TAG, "responeClient: client quit!");
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (in != null) {
- in.close();
- }
- if (out != null) {
- out.close();
- }
- if (client != null) {
- client.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
3 客户端实现
为了更加直观的让我们感受到 Socket 确实是可以夸进程通信, 我们将客户端的 Socket 请求放在另外一个 APP 中实现(当然, 要知道即使是在同一个 APP, 只要将上面的 TCPSocketService 在 AndroidManifest 中设置上 process 属性也就会变成两个进程效果和两个 APP 是一样的)
不要忘记在客户端 Socket 所在的 APP 中声明权限
新建一个 TCPClientActivity, 在其 onCreate 方法中指定服务端 Socket 的地址和端口号发起连接请求为了保证连接成功率, 我们让客户端的 Socket 每个 1s 就去循环发起超时重连
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- // 循环连接服务端 Socket
- Socket socket = null;
- while (socket == null) {
- try {
- // 指定服务端 Socket 地址和端口号, 初始化 Socket
- socket = new Socket("localhost", 8088);
- mClientSocket = socket;
- mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
- Log.i(TAG, "onCreate: 连接服务端 Socket 成功!");
- } catch (IOException e) {
- e.printStackTrace();
- SystemClock.sleep(1000); // 如果连接失败了, 就等 1s 重试
- Log.i(TAG, "onCreate: 连接服务端 Socket 失败, 正在重试...");
- }
- }
上面 Socket 连接建立成功后, 我们可以通过 mPrintWriter 向服务端发送一条测试消息:
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- // 连接成功后向服务端发送一条测试消息
- mPrintWriter.println("你好, 服务端, 我是客户端");
还要建立个循环去不断的读取服务端发送过来的消息 (这里我们要知道, 并不是傻傻的空循环, 而是如果没有新消息发来, 在 in.readLine() 就会被自动阻塞, 所以不用担心啦)
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- // 成功后就去循环读取服务端发送过来的消息
- try {
- BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- while (!TCPClientActivity.this.isFinishing()) {
- String msgFromServer = in.readLine();
- Log.i(TAG, "onCreate: msg from server:" + msgFromServer);
- }
- // 循环结束, 关闭相关流, 关闭 socket
- Log.i(TAG, "onCreate: 客户端退出!");
- in.close();
- mPrintWriter.close();
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
最后在 onDestroy 方法中将 Socket 连接关闭
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- @Override
- protected void onDestroy() {
- super.onDestroy();
- // 退出时关闭 socket 连接
- if (mClientSocket != null) {
- try {
- mClientSocket.shutdownInput();
- mClientSocket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
上面由于需要将代码分段解说, 客户端的实现代码有些零碎, 下面贴出 TCPClientActivity 的完整代码以方便参考:
- /**
- * @author CodingBlock
- * @博客地址 http://www.cnblogs.com/codingblock/
- */
- public class TCPClientActivity extends AppCompatActivity {
- private final static String TAG = TCPClientActivity.class.getSimpleName();
- private Socket mClientSocket;
- private PrintWriter mPrintWriter;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_tcp_client);
- new Thread(new Runnable() {
- @Override
- public void run() {
- // 循环连接服务端 Socket
- Socket socket = null;
- while (socket == null) {
- try {
- // 指定服务端 Socket 地址和端口号, 初始化 Socket
- socket = new Socket("localhost", 8088);
- mClientSocket = socket;
- mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
- Log.i(TAG, "onCreate: 连接服务端 Socket 成功!");
- } catch (IOException e) {
- e.printStackTrace();
- SystemClock.sleep(1000); // 如果连接失败了, 就等 1s 重试
- Log.i(TAG, "onCreate: 连接服务端 Socket 失败, 正在重试...");
- }
- }
- // 连接成功后向服务端发送一条测试消息
- mPrintWriter.println("你好, 服务端, 我是客户端");
- // 成功后就去循环读取服务端发送过来的消息
- try {
- BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- while (!TCPClientActivity.this.isFinishing()) {
- Log.i(TAG, "run: in.readLine()");
- String msgFromServer = in.readLine();
- Log.i(TAG, "onCreate: msg from server:" + msgFromServer);
- }
- // 循环结束, 关闭相关流, 关闭 socket
- Log.i(TAG, "onCreate: 客户端退出!");
- in.close();
- mPrintWriter.close();
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }).start();
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- // 退出时关闭 socket 连接
- if (mClientSocket != null) {
- try {
- mClientSocket.shutdownInput();
- mClientSocket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
到此为止, 一个完整的 Socket 通讯代码已经写完了, 测试一下:
三运行测试
首先启动服务端, 本次示例中服务端在 ipc 工程中, 启动后, log 如下:
.../cn.codingblock.ipc I/TCPSocketService: onCreate: 正在启动 ServerSocket...
.../cn.codingblock.ipc I/TCPSocketService: run: 8088 started
可以看到, 8088 端口已经启动了
再启动客户端, 客户端的代码在 ipcclient 工程中, log 如下:
.../cn.codingblock.ipcclient I/TCPClientActivity: onCreate: 连接服务端 Socket 成功!
.../cn.codingblock.ipcclient I/TCPClientActivity: run: in.readLine()
.../cn.codingblock.ipcclient I/TCPClientActivity: onCreate: msg from server: 欢迎交流!
.../cn.codingblock.ipcclient I/TCPClientActivity: run: in.readLine()
.../cn.codingblock.ipcclient I/TCPClientActivity: onCreate: msg from server: 已经收到你发来的消息: 你好, 服务端, 我是客户端, 请放心!
- .../cn.codingblock.ipcclient I/TCPClientActivity: run: in.readLine()
- .../cn.codingblock.ipcclient D/EGL_emulation: eglMakeCurrent: 0x9b4850c0: ver 2 0 (tinfo 0x9b4831d0)
可以看到, 客户端的 Socket 和服务端的 Socket 已经可以成功交流了, 在代码中, Socket 链接成功后我们向服务端发送了一条你好, 服务端, 我是客户端的消息也收到了服务端的回应
同时通过最后两行 log 我们也可以看到, 当没有收到新消息时程序并没有陷入死循环, 而是在 readLine()时阻塞了
回头再看服务端的 log:
.../cn.codingblock.ipc I/TCPSocketService: run: 接收客户端请求: client = Socket[address=/127.0.0.1,port=57073,localPort=8088]
.../cn.codingblock.ipc I/TCPSocketService: responeClient: msg from client: 你好, 服务端, 我是客户端
最后, 我们将客户端退出, 观察服务端的 log:
- .../cn.codingblock.ipc I/TCPSocketService: responeClient: msg from client:null
- .../cn.codingblock.ipc I/TCPSocketService: responeClient: client quit!
通过测试 log 可知, Socket 可以很好进行进程间通讯, 我们也可以将上面的示例做的更复杂一下, 例如可以为服务端 APP 和客户端 APP 都加上聊天窗口, 这样就变成了一个简单的聊天软件, 是不是很酷, 感兴趣的童鞋可以试着实现一下
四小结
通过上面的文章我们可以发现 Socket 功能确实很强大, 支持在网络间 (同时也包括进程间) 传输任意字节流, 并且也支持一对多并发实时通信但同时我们也发现, Socket 在使用起来相对来说比较繁琐, 而且不支持 RPC 也就是说我们无法通过获取某个对象就可以在本地方便的远程调用服务端的方法 Socket 的使用场景一般是用于网络数据交换
最后想说的是, 本系列文章为博主对 Android 知识进行再次梳理, 查缺补漏的学习过程, 一方面是对自己遗忘的东西加以复习重新掌握, 另一方面相信在重新学习的过程中定会有巨大的新收获, 如果你也有跟我同样的想法, 不妨关注我一起学习, 互相探讨, 共同进步!
参考文献:
Android 开发艺术探索
socket_百度百科
源码地址: http://www.cnblogs.com/codingblock/p/8425736.html
来源: https://www.cnblogs.com/codingblock/p/8425736.html