一、前言:为了公司研究推送和保活方案
尝试了以Android作为Socket长连接的服务端server,以后台作为Socket的客户端client,后台进行推送,这样的目的是保证,后台不需要长时间连接多个设备,后台每次有新的消息,创建多个Client通过Socket推送给Android终端设备,数据传输完成后,所有的client终端关闭,释放资源。而Android作为SocketServer一直监听端口,处于挂起状态,所以保证了推送数据的实时性。当然这种做法缺点很明显:1.Android长期挂起耗电,2.一旦Android终端程序被杀,无法推送到手机,3.Android应用层没有真正的保活,4.也是最大的问题Android终端能通过3G、4G上网,WIFI上网,VPN上网,作为SocketServer需要提供公网的IP,因此Android端的IP是多少?怎么获取?
二、开发过程:
2.1、带着以上问题,进行Demo的编写和测试
2.2 开发环境:Android 5.1设备,后台Springboot集成的Tomcat服务
2.3 具体代码:
后台代码
@RequestMapping("/efss/test")
@RestController
public class TestController extends BaseController {
@RequestMapping("/uploadTerminalIp")
public ApiResponse
模拟后台接口中直接写了一个定时,每5秒通过Socket向Android发送一个时间。
贴下SocketUtil 只负责写(推送消息),不读,当然如果需要自己添加实现
/**
* * Created by [email protected]
* * on 2019/5/31
* *
*/
public class SocketUtil {
private static final Logger logger = LoggerFactory.getLogger(SocketUtil.class);
/**
* 发送socket请求
*
* @param clientIp
* @param clientPort
* @param msg
* @return
*/
public static synchronized String tcpPost(String clientIp, String clientPort, String msg) {
String rs = "";
if (clientIp == null || "".equals(clientIp) || clientPort == null || "".equals(clientPort)) {
logger.error("Ip或端口不存在...");
return null;
}
int clientPortInt = Integer.parseInt(clientPort);
logger.info("clientIp:" + clientIp + " clientPort:" + clientPort);
Socket s = null;
OutputStream out = null;
//InputStream in = null;
try {
s = new Socket(clientIp, clientPortInt);
s.setSendBufferSize(4096);
s.setTcpNoDelay(true);
s.setSoTimeout(60 * 1000);
s.setKeepAlive(true);
out = s.getOutputStream();
//in = s.getInputStream();
//准备报文msg
logger.info("准备发送报文:" + msg);
out.write(msg.getBytes("GBK"));
out.flush();
} catch (Exception e) {
logger.error("tcpPost发送请求异常:" + e.getMessage());
} finally {
logger.info("tcpPost(rs):" + rs);
try {
if (out != null) {
out.close();
out = null;
}
if (s != null) {
s.close();
s = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return rs;
}
}
前端
package com.yxytech.parkingcloud.efsspda.service;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import com.yxytech.parkingcloud.baselibrary.utils.LogUtil;
import com.yxytech.parkingcloud.efsspda.R;
import com.yxytech.parkingcloud.efsspda.utils.NetWorkUtil;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* 文件描述:
*
作者:jambestwick
*
创建时间:2019/5/30
*
更新时间:2019/5/30
*
版本号:${VERSION}
*/
public class SingASongService extends Service {
private static final String TAG = SingASongService.class.getName();
private MediaPlayer mMediaPlayer;
private Thread thread;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
MyThread myThread = new MyThread();
thread = new Thread(myThread);
mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.no_notice);
mMediaPlayer.setLooping(true);//循环播放
LogUtil.d(TAG, "onCreate() 创建播放对象:" + mMediaPlayer.hashCode());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
thread.start();
LogUtil.d(TAG, "播放时 线程名称:" + thread.getName());
return START_STICKY;
}
private void startPlaySong() {
if (mMediaPlayer == null) {
mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.no_notice);
LogUtil.d(TAG, "音乐启动播放,播放对象为: " + mMediaPlayer.hashCode());
mMediaPlayer.start();
} else {
mMediaPlayer.start();
LogUtil.d(TAG, "音乐启动播放,播放对象为: " + mMediaPlayer.hashCode());
}
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
// if (mMediaPlayer != null) {
// mMediaPlayer.pause();
// LogUtil.d(TAG, "音乐启动播放,播放对象为: " + mMediaPlayer.hashCode());
// int progress = mMediaPlayer.getCurrentPosition();
// LogUtil.d(TAG, "音乐暂停,播放进度:" + progress);
// }
}
private void stopPlaySong() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
LogUtil.d(TAG, "音乐停止播放,播放对象为:" + mMediaPlayer.hashCode());
LogUtil.d(TAG, "音乐播放器是否在循环:" + mMediaPlayer.isLooping());
LogUtil.d(TAG, "音乐播放器是否还在播放:" + mMediaPlayer.isPlaying());
mMediaPlayer.release();
LogUtil.d(TAG, "播放对象销毁,播放对象为:" + mMediaPlayer.hashCode());
mMediaPlayer = null;
}
}
private void startSocketConn() {
//服务端在8899端口监听客户端请求的TCP连接
try {
ServerSocket server = new ServerSocket(8899);
Socket client = null;
//通过调用Executors类的静态方法,创建一个ExecutorService实例
//ExecutorService接口是Executor接口的子接口
Executor service = Executors.newCachedThreadPool();
while (true) {
//等待客户端的连接
System.out.println("等待客户端连接!"+new Date());
LogUtil.e(NetWorkUtil.class, "等待客户端连接" );
client = server.accept();
System.out.println("与客户端连接成功!" + new Date());
LogUtil.e(NetWorkUtil.class, "与客户端连接成功" );
//调用execute()方法时,如果必要,会创建一个新的线程来处理任务,但它首先会尝试使用已有的线程,
//如果一个线程空闲60秒以上,则将其移除线程池;
//另外,任务是在Executor的内部排队,而不是在网络中排队
service.execute(new NetWorkUtil.ServerThread(client));
}
} catch (IOException e) {
e.printStackTrace();
}
//server.close();
}
@Override
public void onDestroy() {
super.onDestroy();
mMediaPlayer.pause();
LogUtil.d(TAG, "恢复播放 时当前播放器对象:" + mMediaPlayer.hashCode());
stopPlaySong();
LogUtil.d(TAG, "应用播放服务被杀死,正在重启");
LogUtil.d(TAG, "目标播放工作线程是否存活:" + thread.isAlive());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(new Intent(getApplicationContext(), SingASongService.class));
} else {
startService(new Intent(getApplicationContext(), SingASongService.class));
}
}
class MyThread implements Runnable {
@Override
public void run() {
startPlaySong();
startSocketConn();
}
}
}
package com.yxytech.parkingcloud.efsspda.utils;
import android.content.Context;
import android.util.Log;
import com.yxytech.parkingcloud.baselibrary.utils.LogUtil;
import com.yxytech.parkingcloud.baselibrary.utils.ToastUtil;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.util.Enumeration;
/**
* 文件描述:
*
作者:jambestwick
*
创建时间:2019/5/31
*
更新时间:2019/5/31
*
版本号:${VERSION}
*
*/
public class NetWorkUtil {
public static final int SOCKET_PORT = 8899;
/**
* 获取内网IP地址
*
* @return
* @throws SocketException
*/
public static String getLocalIPAddress() throws SocketException {
for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface intf = en.nextElement();
for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && (inetAddress instanceof Inet4Address)) {
return inetAddress.getHostAddress().toString();
}
}
}
return "null";
}
/****
* 获取外网的IP地址
* @return
*
*
* ***/
public static String getNetIp() {
String IP = "";
try {
String address = "http://pv.sohu.com/cityjson?ie=utf-8";
URL url = new URL(address);
//URLConnection htpurl=url.openConnection();
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setUseCaches(false);
connection.setRequestMethod("GET");
connection.setRequestProperty("user-agent",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.7 Safari/537.36"); //设置浏览器ua 保证不出现503
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream in = connection.getInputStream();
// 将流转化为字符串
BufferedReader reader = new BufferedReader(
new InputStreamReader(in));
String tmpString = "";
StringBuilder retJSON = new StringBuilder();
while ((tmpString = reader.readLine()) != null) {
retJSON.append(tmpString + "\n");
}
JSONObject jsonObject = new JSONObject(retJSON.toString());
String code = jsonObject.getString("code");
if (code.equals("0")) {
JSONObject data = jsonObject.getJSONObject("data");
IP = data.getString("ip") + "(" + data.getString("country")
+ data.getString("area") + "区"
+ data.getString("region") + data.getString("city")
+ data.getString("isp") + ")";
Log.e("提示", "您的IP地址是:" + IP);
} else {
IP = "";
Log.e("提示", "IP接口异常,无法获取IP地址!");
}
} else {
IP = "";
Log.e("提示", "网络连接异常,无法获取IP地址!");
}
} catch (
Exception e)
{
IP = "";
Log.e("提示", "获取IP地址时出现异常,异常信息是:" + e.toString());
}
return IP;
}
public static class serverPool {
public static void conn(int port) throws IOException {
//服务端在20006端口监听客户端请求的TCP连接
final ServerSocket server = new ServerSocket(port);
//在线程池中一共只有THREADPOOLSIZE个线程,
//最多有THREADPOOLSIZE个线程在accept()方法上阻塞等待连接请求
//匿名内部类,当前线程为匿名线程,还没有为任何客户端连接提供服务
Thread thread = new Thread() {
public void run() {
//线程为某连接提供完服务后,循环等待其他的连接请求
while (true) {
try {
//等待客户端的连接
Socket client = server.accept();
System.out.println("与客户端连接成功!");
//一旦连接成功,则在该线程中与客户端通信
ServerThread.execute(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
//先将所有的线程开启
thread.start();
}
}
/**
* 该类为多线程类,用于服务端
*/
public static class ServerThread implements Runnable {
private Socket client = null;
public ServerThread(Socket client) {
this.client = client;
}
//处理通信细节的静态方法,这里主要是方便线程池服务器的调用
public static void execute(Socket client) {
String str = null;
try {
//获取Socket的输出流,用来向客户端发送数据
PrintStream out = new PrintStream(client.getOutputStream());
//获取Socket的输入流,用来接收从客户端发送过来的数据
BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));
boolean flag = true;
while (flag) {
//接收从客户端发送过来的数据
str = buf.readLine();
if (str == null || "".equals(str)) {
flag = false;
} else {
if ("bye".equals(str)) {
flag = false;
} else {
//将接收到的字符串前面加上echo,发送到对应的客户端
out.println("echo:" + str);
LogUtil.e(NetWorkUtil.class, "接受到的推送信息:" + str);
}
}
}
out.close();
buf.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
execute(client);
}
}
}
这张图片是访问上面后台的接口需要的参数,根据自己的业务需求做,
逻辑是这样,Android端用户登录APP,通过NetWorkUtil.getNetIp()方法获取到手机的IP,定义一个端口(8899),发送给后台,后台通过发来的IP端口,连接到Socket并进行推送。
代码讲完了现在开始说问题
3.1 Android长期作为Socket的Server长期挂起耗电,由于我们是定制设备,一线人员都有充电设备,而且一个最多班次12小时,在锁屏的情况下是足够的。
3.2 Android进程被杀,这个需要大量真机适配,而且我们可以命令用户加入白名单,因此存活率大大提高。
还有大部分一线人员是在道路上,用的是4G网络,而运营商卡的IP地址会动态变化的,如下:
1.49.128.0,中国,贵州,安顺,电信 1.49.129.0,中国,贵州,安顺,电信 1.49.131.0,中国,贵州,安顺,电信 1.49.132.0,中国,贵州,安顺,电信 1.49.136.0,中国,贵州,贵阳,电信 1.49.192.0,中国,贵州,黔西南,电信 1.86.37.95,中国,陕西,西安,电信
总结:
或者你们可能有数据的安全考虑,那就自己开启服务,手机Pull拉取消息,其实也就短连接的接口访问一样了。