服务器端打包
把服务器打成jar包对于后台开发的朋友来说小菜一碟,但对于前端开发可能有些细节要注意一下,尤其是有依赖其他第三方库的情况下,这里梳理了一下流程:
File – Project Structure – Artifacts – add – JAR – From modules and dependencies
选中module和主类,其他保持默认即可 – OK
Add copy of – Module Output – OK
右侧全选引用到的类库,右键鼠标,Exact into Output Root – OK
Build – Build Artifacts – Rebuild
前台运行(日志即时打印,但关闭窗口程序会退出):
java -jar rt.jar
后台运行:
nohup java -jar rt.jar &
客户端保活
在目前国内手机厂商的严格管制下,绝对的保活是不存在的,只能说,我们自己使用的APP,尽可能自己在手机上把APP加入白名单,使用前台服务,忽略电池使用,允许后台联网,心跳保活等等,多管齐下,才可能让APP尽可能长时间存活。
①忽略电池优化
/**
* 忽略电池优化
*/
fun Activity.ignoreBatteryOptimizations(): Boolean {
// 这个方法返回true的时候表明已经关闭,false则是已开启优化
val powerManager = getSystemService(Service.POWER_SERVICE) as PowerManager
val hasBeanIgnore = powerManager.isIgnoringBatteryOptimizations(packageName)
if (!hasBeanIgnore) {
ToastTools.showLong("请忽略电池优化后继续")
try {
val i = Intent().apply {
action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
}
startActivity(i)
} catch (e: Exception) {
L.e(e.message)
}
}
return hasBeanIgnore
}
②使用前台服务
fun Service.startForegroundNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
var mChannel: NotificationChannel? = null
mChannel = NotificationChannel(ControlService.CHANNEL_ID_STRING, getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH)
mChannel.importance = NotificationManager.IMPORTANCE_LOW
notificationManager.createNotificationChannel(mChannel)
val notification: Notification = Notification.Builder(applicationContext,
ControlService.CHANNEL_ID_STRING
)
.setSmallIcon(R.mipmap.ic_notification)
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
.build()
notification.flags = Notification.FLAG_ONGOING_EVENT
startForeground(ControlService.ID, notification)
}
}
③解决Socket在后台运行断开连接的问题
fun ZMQ.Socket.enableCommonAuth() {
curvePublicKey = clientCert.publicKey
curveSecretKey = clientCert.secretKey
curveServerKey = FollowManager.currentFollowBean.k
// 重连
setTCPKeepAlive(1)
setTCPKeepAliveIdle(20)
setTCPKeepAliveInterval(10)
// 心跳周期
setHeartbeatIvl(2*1000)
// 心跳超时
setHeartbeatTimeout(5*1000)
// 超过则会让连接超时
setHeartbeatTtl(10*1000)
setReconnectIVL(500)
setReconnectIVLMax(1000)
}
④定时唤醒
/**
* 定时检查是否已经没有网络了,没有网络则亮屏唤醒一下
*/
object AliveManager {
private const val ACTION_STOP_SCAN = "keep_alive_rest"
/**
* 5分钟检查一下
*/
private const val CHECK_INTERVAL_MILLIS = 5 * 60_000L
private val checkNetExecutor = Executors.newSingleThreadExecutor()
private val mAlarmManager = GlobalContext.getContext().getSystemService(ALARM_SERVICE) as AlarmManager
private val taskReceiver = object : WakefulBroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
checkConnectServerStatus()
}
}
private fun checkConnectServerStatus() {
pinging = true
val serverIp = FollowManager.currentFollowBean.p
if (!TextUtils.isEmpty(serverIp)) {
checkNetExecutor.submit(Runnable {
val networkAvailable = NetworkUtils.isAvailableByPing(serverIp)
// 到服务器的网络不可达,那么就亮屏唤起CPU
if (!networkAvailable && !stopped) {
AppUtils.wakeupScreen()
}
GlobalContext.runOnUiThread {
// 继续下一次扫描
pinging = false
if (!stopped) {
start()
}
}
})
} else {
// 继续下一次扫描
pinging = false
if (!stopped) {
start()
}
}
}
private val restIntent = Intent(ACTION_STOP_SCAN)
private val restPendingIntent = PendingIntent.getBroadcast(
GlobalContext.getContext(),
0,
restIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
private val intentFilter = IntentFilter().apply {
addAction(ACTION_STOP_SCAN)
}
@Volatile
private var stopped = true
@Volatile
private var pinging = false
/**
* 开始定时检查
*/
fun start() {
if (pinging) {
return
}
stopped = false
mAlarmManager.cancel(restPendingIntent)
GlobalContext.getContext().registerReceiver(taskReceiver, intentFilter, null, null)
val alarmClockInfo = AlarmClockInfo(System.currentTimeMillis() + CHECK_INTERVAL_MILLIS, null)
mAlarmManager.setAlarmClock(alarmClockInfo, restPendingIntent)
}
fun stop() {
stopped = true
mAlarmManager.cancel(restPendingIntent)
try {
GlobalContext.getContext().unregisterReceiver(taskReceiver)
} catch (e: Exception) {
L.e(e.message)
}
}
}
防止串流
防串流的方法也很简单,就是主控端在调用的时候,把当前正在操控的受控端ID也通过命令传过去,受控端解析命令,如果发现命令不是发给自己的,就停止推流。前提是只有一个主控端,如果多个主控端同时看,还是会串流的,但对于我们个人使用而言,这个是可以控制的,如果要严格要求只有一个主控端,目前看来应用单点登陆策略是一个办法。
/**
* 受控端连接服务器
*/
fun startFollowService() {
followCmdUrl = "tcp://${FollowManager.currentFollowBean.p}:${KeyValue.publishPortCmd}"
followExecutorService.submit(Runnable {
if (followSocket != null) {
return@Runnable
}
try {
followContext = ZContext()
followSocket = followContext?.createSocket(SocketType.SUB)
followSocket?.enableCommonAuth()
followSocket?.enableCommonChat()
followSocket?.connect(followCmdUrl)
followSocket?.subscribe("".toByteArray())
} catch (e: Exception) {
AppUtils.relaunchAppOnChildThread("受控端命令服务失败")
}
while (followSocket != null) {
val receivedData = followSocket?.recv(0)
// 当前不是受控端的话,不用处理指令
if (receivedData == null || receivedData.isEmpty()) {
continue
}
val cmdString = String(receivedData, ZMQ.CHARSET)
val cmdArray = cmdString.split(RtConstants.MESSAGE_SPLIT)
// 非法指令,不处理
if (cmdArray.size != RtConstants.CMD_SIZE) {
continue
}
// 指令不是发送给当前设备的
if (!TextUtils.equals(cmdArray[0], KeyValue.deviceId)) {
lastCmdMillis = 0L
continue
}
// 解析命令
parseCmd(cmdArray)
}
})
}
其他要点
剩下就是一些辅助的知识点了,例如服务器启动时发邮件,发件人的账号密码加密隐藏等等,都是可选的,这些在实现的时候,可以选择性实现。