Android查缺补漏(IPC篇)-- 进程间通讯之Socket简介及示例
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8425736.html
进程间通讯篇系列文章目录:
- Android查缺补漏(IPC篇)-- 进程间通讯基础知识热身
- Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍
- 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_百度百科》
源码地址:本系列文章所对应的全部源码已同步至github,感兴趣的同学可以下载查看,结合代码看文章会更好。源码传送门
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8425736.html
Android查缺补漏(IPC篇)-- 进程间通讯之Socket简介及示例的更多相关文章
- 知识点查缺补漏贴01-进程间通讯之mmap文件共享
引文: 个人名言:“同一条河里淹死两次的人,是傻子,淹死三次及三次以上的人是超人”.经历过上次悲催的面试,决定沉下心来,好好的补充一下基础知识点.本文是这一系列第一篇:进程间通讯之mmap. 一.概念 ...
- Android查缺补漏(IPC篇)-- 进程间通讯基础知识热身
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8479282.html 在Android中进程间通信是比较难的一部分,同时又非常 ...
- Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...
- Android查缺补漏(IPC篇)-- 进程间通讯之AIDL详解
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...
- Android查缺补漏(IPC篇)-- 款进程通讯之AIDL详解
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...
- Android查缺补漏(线程篇)-- IntentService的源码浅析
本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8975114.html 在Android中有两个比较容易弄混的概念,Servic ...
- Android查缺补漏(View篇)--自定义 View 的基本流程
View是Android很重要的一部分,常用的View有Button.TextView.EditView.ListView.GridView.各种layout等等,开发者通过对这些View的各种组合以 ...
- Android查缺补漏(View篇)--事件分发机制源码分析
在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 <Android查缺补漏(View篇)-- ...
- Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解
上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定 ...
随机推荐
- 深入浅出docker
笔者在海外工作多年,所以文中多用英文单词,有些时候是为了更精准的描述,请见谅.希望这篇随笔能帮大家入门docker.由于在海外连博客园有些慢,所以我图片用的比较少,以后再考虑一下如何更好的解决图片上传 ...
- python判断两个list包含关系
a = [1,2] b = [1,2,3] c = [0, 1] set(b) > set(a) set(b) > set(c)
- vuejs axios安装配置与使用
1.安装服务 npm install --save axios vue-axios 2.在main.js import axios from 'axios' import VueAxios from ...
- python操作mysql,增,删,改,查
import MySQLdb conn = MySQLdb.connect(host='192.168.1.21',user='yangqw',passwd='1',db='free')cur = c ...
- KVM虚拟机绑定物理CPU进行性能调优
PS:前提是虚拟cpu总个数不多于物理cpu总个数. 绑定方法: 虚拟机的虚拟cpu有1个,实体机物理cpu有8个,可以通过以下方式绑定cpu,绑定的动作不一定要在虚机启动时,可以在任何时候: 绑 ...
- eclipse修改默认workspace
1.进入 Window > Preferences > General > Startup and Shutdown 选中 Prompt for workspace on start ...
- MySQL密码重置(root用户)
分别在Windows下和Linux下重置了MYSQL的root的密码: 在windows下: 1:进入cmd,停止mysql服务:Net stop mysql 到mysql的安装路径启动mysql,在 ...
- python 列表操作方法详解
列表是Python中最基本的数据结构,列表是最常用的Python数据类型,列表是一个数据的集合,集合内可以放任何数据类型,可对集合方便的增删改查操作.Python已经内置确定序列的长度以及确定最大和最 ...
- Java中常见的排序方法
本博主要介绍Java中几种常见的排序算法: /* 排序方法的演示1)插入排序(直接插入排序.希尔排序)2)交换排序(冒泡排序.快速排序)3)选择排序(直接选择排序.堆排序)4)归并排序5)分配排序(基 ...
- ABP官方文档翻译 5.4 SwaggerUI集成
SwaggerUI集成 介绍 ASP.NET Core 安装Nuget包 配置 测试 ASP.NET 5.x 安装Nuget包 配置 测试 介绍 在它的网站上:“...使用Swagger可用的API, ...