本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8425736.html

进程间通讯篇系列文章目录:

学过计算机网络的人多多少少对Socket都会有所了解,在Android中,我们也可以借助Socket来实现进程间通讯,即使对Socket不熟悉也没关系,本篇文章将会用一个非常简单的例子,来说明通过Socket实现进程间通讯的步骤,为了打消大家对Socket的陌生感,我们先来看看Socket的基本概念和用法。

一、Socket是什么

Socket又称“套接字”,是网络通信中的概念,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。网络上的两个程序通过一个双向的通讯链接实现数据交换,这个链接的一端称为一个Socket,它本身可以支持传输任意的字节流。

Socket分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层的TCP和UDP协议。

  • TCP是面向链接的协议,提供稳定的双向通信功能,需要通过“三次握手”才能完成建立链接,为了保证稳定性,它内部提供了超时重连机制。
  • UDP是无连接的,提供不稳定的单向通信功能(当然我们也可以通过它实现双向通信),其在性能上的效率更高,但无法保证数据一定能够正确传输。

接着再说下TCP的“三次握手”,它是指TCP建立链接的如下三个步骤:

  1. 服务器监听:服务端Socket是不知道具体的客户端Socket的,而是一直处于等待链接的状态,实时监控网络状态。
  2. 客户端请求:客户端Socket首先描述好它要链接的服务端Socket,指出服务端Socket的地址和端口,然后提出链接请求。
  3. 连接确认:服务端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_百度百科》

源码地址:本系列文章所对应的全部源码已同步至github,感兴趣的同学可以下载查看,结合代码看文章会更好。源码传送门

本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8425736.html

Android查缺补漏(IPC篇)-- 进程间通讯之Socket简介及示例的更多相关文章

  1. 知识点查缺补漏贴01-进程间通讯之mmap文件共享

    引文: 个人名言:“同一条河里淹死两次的人,是傻子,淹死三次及三次以上的人是超人”.经历过上次悲催的面试,决定沉下心来,好好的补充一下基础知识点.本文是这一系列第一篇:进程间通讯之mmap. 一.概念 ...

  2. Android查缺补漏(IPC篇)-- 进程间通讯基础知识热身

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8479282.html 在Android中进程间通信是比较难的一部分,同时又非常 ...

  3. Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  4. Android查缺补漏(IPC篇)-- 进程间通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  5. Android查缺补漏(IPC篇)-- 款进程通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  6. Android查缺补漏(线程篇)-- IntentService的源码浅析

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8975114.html 在Android中有两个比较容易弄混的概念,Servic ...

  7. Android查缺补漏(View篇)--自定义 View 的基本流程

    View是Android很重要的一部分,常用的View有Button.TextView.EditView.ListView.GridView.各种layout等等,开发者通过对这些View的各种组合以 ...

  8. Android查缺补漏(View篇)--事件分发机制源码分析

    在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 <Android查缺补漏(View篇)-- ...

  9. Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解

    上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定 ...

随机推荐

  1. oracle02

    SQL语句完整结构: select from where group by having order by 今天分享的知识点:(1)分组查询 select 中非组函数的列需要在group by 进行参 ...

  2. 编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串 'welcome to masm!'

    body, table{font-family: 微软雅黑; font-size: 13.5pt} table{border-collapse: collapse; border: solid gra ...

  3. MYSQL问题解决方案:Access denied for user 'root'@'localhost' (using password:YES)

    这两天在MyEclipse中开发Web项目时,连接MYSQL数据库,出现问题:Access denied for user 'root'@'localhost' (using password:YES ...

  4. linux_运维职责

    运维准则: 不要删文件,移动文件,可以复原,一个月后什么事没有,删除 运维人员主要关注哪些方面? CPU:对计算机工作速度和效率起决定性作用(intel amd) 内存: 临时存放数据:容量和处理速度 ...

  5. yum 安装zabbix2.4 /3.2.4

    yum 安装zabbix2.4 首先zabbix需要的环境是web环境,默认的是lamp或者lnmp,讲道理,要是使用tomcat也是可以的,不过实验没有成功 系统:centos6.8_x64 附yu ...

  6. 【转】高斯-克吕格投影与UTM投影异同

    高斯-克吕格(Gauss-Kruger)投影与UTM投影(Universal Transverse Mercator,通用横轴墨卡托投影)都是横轴墨卡托投影的变种,目前一些国外的软件或国外进口仪器的配 ...

  7. 简单的.editconfig文件

    root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_n ...

  8. ABP官方文档翻译 6.1.3 异常处理

    处理异常 介绍 启用错误处理 Non-Ajax请求 显示异常 UserFriendlyException Error模型 AJAX请求 异常事件 介绍 此文档是与ASP.NET MVC和Web API ...

  9. java导入项目有红色叹号

    原因:缺少jar包 解决:         选中项目  ->  右键  -> Build Path  -> Configer Builder Path  ->  删除掉有错的J ...

  10. Java程序占用的内存可能会大于Xmx

    很多人认为Xmx和-Xms参数指定的就是Java程序将会占用的内存,但是这实际上只是Java堆对象将会占用的内存.堆只是影响Java程序占用内存数量的一个因素. 除了堆,影响Java程序所占用内存的因 ...