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
作者:CodingBlock
出处:http://www.cnblogs.com/codingblock/
本文版权归作者和共博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果文中有什么错误,欢迎指出。以免更多的人被误导。