Android完整Socket解决方案
作者:laozhang 发布时间:2023-12-22 14:42:40
整体步骤流程
先来说一下整体的步骤思路吧:
发送 UDP 广播,大家都知道 UDP 广播的特性是整个网段的设备都可以收到这个消息。
接收方收到了 UDP 的广播,将自己的 ip 地址,和双方约定的端口号,回复给 UDP 的发送方。
发送方拿到了对方的 ip 地址以及端口号,就可以发起 TCP 请求了,建立 TCP 连接。
保持一个 TCP 心跳,如果发现对方不在了,超时重复 1 步骤,重新建立联系。
整体的步骤就和上述的一样,下面用代码展开:
搭建 UDP 模块
public UDPSocket(Context context) {
this.mContext = context;
int cpuNumbers = Runtime.getRuntime().availableProcessors();
// 根据CPU数目初始化线程池
mThreadPool = Executors.newFixedThreadPool(cpuNumbers * Config.POOL_SIZE);
// 记录创建对象时的时间
lastReceiveTime = System.currentTimeMillis();
messageReceiveList = new ArrayList<>();
Log.d(TAG, "创建 UDP 对象");
// createUser();
}
首先进行一些初始化操作,准备线程池,记录对象初始的时间等等。
public void startUDPSocket() {
if (client != null) return;
try {
// 表明这个 Socket 在设置的端口上监听数据。
client = new DatagramSocket(CLIENT_PORT);
client.setReuseAddress(true);
if (receivePacket == null) {
// 创建接受数据的 packet
receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH);
}
startSocketThread();
} catch (SocketException e) {
e.printStackTrace();
}
}
紧接着就创建了真正的一个 UDP Socket 端,DatagramSocket,注意这里传入的端口号 CLIENT_PORT 的意思是这个 DatagramSocket 在此端口号接收消息。
/**
* 开启发送数据的线程
*/
private void startSocketThread() {
clientThread = new Thread(new Runnable() {
@Override
public void run() {
receiveMessage();
}
});
isThreadRunning = true;
clientThread.start();
Log.d(TAG, "开启 UDP 数据接收线程");
startHeartbeatTimer();
}
我们都知道 Socket 中要处理数据的发送和接收,并且发送和接收都是阻塞的,应该放在子线程中,这里就开启了一个线程,来处理接收到的 UDP 消息(UDP 模块上一篇文章讲得比较详细了,所以这里就不详细展开了)
/**
* 处理接受到的消息
*/
private void receiveMessage() {
while (isThreadRunning) {
try {
if (client != null) {
client.receive(receivePacket);
}
lastReceiveTime = System.currentTimeMillis();
Log.d(TAG, "receive packet success...");
} catch (IOException e) {
Log.e(TAG, "UDP数据包接收失败!线程停止");
stopUDPSocket();
e.printStackTrace();
return;
}
if (receivePacket == null || receivePacket.getLength() == 0) {
Log.e(TAG, "无法接收UDP数据或者接收到的UDP数据为空");
continue;
}
String strReceive = new String(receivePacket.getData(), receivePacket.getOffset(), receivePacket.getLength());
Log.d(TAG, strReceive + " from " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort());
//解析接收到的 json 信息
notifyMessageReceive(strReceive);
// 每次接收完UDP数据后,重置长度。否则可能会导致下次收到数据包被截断。
if (receivePacket != null) {
receivePacket.setLength(BUFFER_LENGTH);
}
}
}
在子线程接收 UDP 数据,并且 notifyMessageReceive 方法通过接口来向外通知消息。
/**
* 发送心跳包
*
* @param message
*/
public void sendMessage(final String message) {
mThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
BROADCAST_IP = WifiUtil.getBroadcastAddress();
Log.d(TAG, "BROADCAST_IP:" + BROADCAST_IP);
InetAddress targetAddress = InetAddress.getByName(BROADCAST_IP);
DatagramPacket packet = new DatagramPacket(message.getBytes(), message.length(), targetAddress, CLIENT_PORT);
client.send(packet);
// 数据发送事件
Log.d(TAG, "数据发送成功");
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
接着 startHeartbeatTimer 开启一个心跳线程,每间隔五秒,就去广播一个 UDP 消息。注意这里 getBroadcastAddress 是获取的网段 ip,发送这个 UDP 消息的时候,整个网段的所有设备都可以接收到。
到此为止,我们发送端的 UDP 算是搭建完成了。
搭建 TCP 模块
接下来 TCP 模块该出场了,UDP 发送心跳广播的目的就是找到对应设备的 ip 地址和约定好的端口,所以在 UDP 数据的接收方法里:
/**
* 处理 udp 收到的消息
*
* @param message
*/
private void handleUdpMessage(String message) {
try {
JSONObject jsonObject = new JSONObject(message);
String ip = jsonObject.optString(Config.TCP_IP);
String port = jsonObject.optString(Config.TCP_PORT);
if (!TextUtils.isEmpty(ip) && !TextUtils.isEmpty(port)) {
startTcpConnection(ip, port);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
这个方法的目的就是取到对方 UDPServer 端,发给我的 UDP 消息,将它的 ip 地址告诉了我,以及我们提前约定好的端口号。
怎么获得一个设备的 ip 呢?
public String getLocalIPAddress() {
WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
return intToIp(wifiInfo.getIpAddress());
}
private static String intToIp(int i) {
return (i & 0xFF) + "." + ((i >> 8) & 0xFF) + "." + ((i >> 16) & 0xFF) + "."
+ ((i >> 24) & 0xFF);
}
现在拿到了对方的 ip,以及约定好的端口号,终于可以开启一个 TCP 客户端了。
private boolean startTcpConnection(final String ip, final int port) {
try {
if (mSocket == null) {
mSocket = new Socket(ip, port);
mSocket.setKeepAlive(true);
mSocket.setTcpNoDelay(true);
mSocket.setReuseAddress(true);
}
InputStream is = mSocket.getInputStream();
br = new BufferedReader(new InputStreamReader(is));
OutputStream os = mSocket.getOutputStream();
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os)), true);
Log.d(TAG, "tcp 创建成功...");
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
当 TCP 客户端成功建立的时候,我们就可以通过 TCP Socket 来发送和接收消息了。
细节处理
接下来就是一些细节处理了,比如我们的 UDP 心跳,当 TCP 建立成功之时,我们要停止 UDP 的心跳:
if (startTcpConnection(ip, Integer.valueOf(port))) {// 尝试建立 TCP 连接
if (mListener != null) {
mListener.onSuccess();
}
startReceiveTcpThread();
startHeartbeatTimer();
} else {
if (mListener != null) {
mListener.onFailed(Config.ErrorCode.CREATE_TCP_ERROR);
}
}
// TCP已经成功建立连接,停止 UDP 的心跳包。
public void stopHeartbeatTimer() {
if (timer != null) {
timer.exit();
timer = null;
}
}
对 TCP 连接进行心跳保护:
/**
* 启动心跳
*/
private void startHeartbeatTimer() {
if (timer == null) {
timer = new HeartbeatTimer();
}
timer.setOnScheduleListener(new HeartbeatTimer.OnScheduleListener() {
@Override
public void onSchedule() {
Log.d(TAG, "timer is onSchedule...");
long duration = System.currentTimeMillis() - lastReceiveTime;
Log.d(TAG, "duration:" + duration);
if (duration > TIME_OUT) {//若超过十五秒都没收到我的心跳包,则认为对方不在线。
Log.d(TAG, "tcp ping 超时,对方已经下线");
stopTcpConnection();
if (mListener != null) {
mListener.onFailed(Config.ErrorCode.PING_TCP_TIMEOUT);
}
} else if (duration > HEARTBEAT_MESSAGE_DURATION) {//若超过两秒他没收到我的心跳包,则重新发一个。
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put(Config.MSG, Config.PING);
} catch (JSONException e) {
e.printStackTrace();
}
sendTcpMessage(jsonObject.toString());
}
}
});
timer.startTimer(0, 1000 * 2);
}
首先会每隔两秒,就给对方发送一个 ping 包,看看对面在不在,如果超过 15 秒还没有回复我,那就说明对方掉线了,关闭我这边的 TCP 端。进入 onFailed 方法。
@Override
public void onFailed(int errorCode) {// tcp 异常处理
switch (errorCode) {
case Config.ErrorCode.CREATE_TCP_ERROR:
break;
case Config.ErrorCode.PING_TCP_TIMEOUT:
udpSocket.startHeartbeatTimer();
tcpSocket = null;
break;
}
}
当 TCP 连接超时,我就会重新启动 UDP 的广播心跳,寻找等待连接的设备。进入下一个步骤循环。
对于数据传输的格式啊等等细节,这个和业务相关。自己来定就好。
还可以根据自己业务的模式,是 CPU 密集型啊,还是 IO 密集型啊,来开启不同的线程通道。这个就涉及线程的知识了。
源码分享:https://github.com/itsMelo/AndroidSocket


猜你喜欢
- 前言 这其实是一道面试题,是我在面试百度的时候被问到的,当时没有答出来(因为自己真的很菜),后来在网上寻找答案,看到也是一头雾水,
- 目录前言导入okhttp3方法简介两种版本超时设置用法总结前言okhttp是一个处理网络请求的开源项目,是安卓端最火热的轻量级框架,由移动支
- 1. 参数中直接加入%%param.setUsername("%CD%"); param.set
- 本文讲述如何用Java打印一个菱形,以及打印直角和等腰三角形的方法, 本文教程比较详细,如果想要直接学习菱形可以直接翻到本文最下方!!!左下
- 如何获取二维数组中的元素个数呢?int[,] array = new int[,] {{1,2,3},{4,5,6},{7,8,9}};//
- <script>//验证身份证号方法var test=function(idcard){var Errors=new Array
- 有了前几篇的基础,相信大家对于Struts2已经有了一个很不错的认识,本篇我将为大家介绍一些关于Action接收参数的三种形式,以及简单的表
- 前言在上一篇普通的加载千篇一律,有趣的 loading 万里挑一 中,我们介绍了使用Path类的PathMetrics属性来控制绘制点在路径
- 前言上一篇 文章我们完成了在 thymeleaf 模板引擎中使用 shiro 标签,也就是根据不同的用户身份信息,前端页面来显示不同的页面内
- 前言基于SpingBoot框架中, 我们随处可以见的便是各种各样的功能注解, 注解的实现原理AOP之前有说过(翻看本系列的前面几章即可),
- logback filter过滤某个类 屏蔽某个类使用logback配置日志文件,有的时候需要我们过滤或者屏蔽掉某个类的日志,便可以通过以下
- 前言:有时候我们在用Spring Aop面向切面编程,需要获取连接点(JoinPoint)方法参数名、参数值。环境:Mac OSXIntel
- 依赖倒置原则(DIP)定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。问题由来:类A直接依赖类
- java转换字符串编码格式 (解码错误,重新解码)字符集概念:规定了某个文字对应的二进制数字存放方式(编码)和某串二进制数值代表了哪个文字(
- Spring Boot 异常处理异常处理是一种识别并响应错误的一致性机制,异常机制可以把程序中的异常处理代码和正常的业务逻辑代码分离,包装程
- 目录引言SqlSessionFactory不使用 XML 构建 SqlSessionFactorySqlSessionFactoryBuil
- 一、概要介绍 本文要介绍的是Java中的transient关键字,transient是短暂的意思。对于transie
- SpringBoot 整合 Redis 数据库实现数据缓存的本质是整合 Redis 数据库,通过对需要“缓存&r
- List接口是Collection接口的子接口,List有一个重要的实现类--ArrayList类,List中的元素是有序排列的而且可重复,
- 一. Base64编码由来为什么会有Base64编码呢?因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASC