最近参与公司的餐厅接单项目,遇到了一个坑
android手机连接IP打印机,一段时间没有接到打印任务,Socket会被android系统给断开掉
网上有说使用PowerManager 设置wakeLock
try {
if (wl!=null)
PowerManager pm = (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE);
if (pm!=null) {
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "app:ScreenOff");
wl.acquire( /*100 minutes*/);
}
} catch (Exception e) {
e.printStackTrace();
}
然而对于我的项目并不起作用,大概5分钟以上不打印,Socket依然会被断开
既然无法保证Socket一定不会断开,就只能每次在打印之前,获取到socket的连接状态,如果已经断开,重新连接,保证连接成功后,再次进行打印
但是socket类的方法isClosed()、isConnected()、isInputStreamShutdown()、isOutputStreamShutdown()等,这些方法都是本地端的状态,无法判断远端是否已经断开连接。
后来看到这篇文章
https://coral0212.iteye.com/blog/1810905
如果想要知道Socket的实时是否已断开, socket.sendUrgentData(0xFF);采用此方法,发心跳包,如果已断开,则会抛出IOException,否则就表示已连接!
public void sendDataImmediately(final Vector<Byte> data) {
if (this.mPort == null) {
return;
}
if (mPort instanceof EthernetPort) {//如果是网络连接
try {
EthernetPort port = (EthernetPort) mPort;
Class clz = port.getClass();
Field mSocket = clz.getDeclaredField("mSocket");
mSocket.setAccessible(true);
Socket socket = (Socket) mSocket.get(port);
socket.sendUrgentData(0xFF);//心跳包
Log.e(TAG, "sendDataImmediately: socket is connected");
SharedPreferencesUtil.putString(BaseApplication.getInstance(), "connected", "time is :" + DateUtil.getFormatString(Calendar.getInstance(), "yyyy-MM-dd hh:mm:ss:SSS"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
Log.e(TAG, "sendDataImmediately:", e);
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.e(TAG, "sendDataImmediately:", e);
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "sendDataImmediately: socket is disconnected!", e);
openWifiPort();
SharedPreferencesUtil.putString(BaseApplication.getInstance(), "disconnected", "time is :" + DateUtil.getFormatString(Calendar.getInstance(), "yyyy-MM-dd hh:mm:ss:SSS"));
} catch (NullPointerException e) {
e.printStackTrace();
Log.e(TAG, "sendDataImmediately: socket is disconnected!", e);
openWifiPort();
SharedPreferencesUtil.putString(BaseApplication.getInstance(), "disconnected", "time is :" + DateUtil.getFormatString(Calendar.getInstance(), "yyyy-MM-dd hh:mm:ss:SSS"));
}
writeDataByWifi(mPort, data);
} else {
try {
Log.e(TAG, "data -> " + new String(com.gprinter.utils.Utils.convertVectorByteTobytes(data), "gb2312"));
this.mPort.writeDataImmediately(data, 0, data.size());
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "sendDataImmediately: writeDataImmediately error", e);
}
}
}
来来来接着写,测试反馈还是不行,在socket被断开后,即使使用 socket.sendUrgentData(0xFF);发送新心跳包方式查询到已断开,但是尝试重新连接会经常出现连接失败,必须得把打印机开关关闭后再打开后,才能重新连接上...莫名其妙的一个问题!
既然通过查询连接状态然后重连走不通了,只能再想其他方式了
于是想到可不可以开启个线程隔几秒给打印机发送查询指令就相当于给打印机发送心跳包了,这样长连接就不会被断开了吧
于是我在socket打开端口成功之后,开启线程,定时给打印机发送查询指令,从而充当心跳的作用
打开端口方法
/**
* 打开端口
*
* @return
*/
public void openPort() {
isOpenPort = false;
sendStateBroadcast(CONN_STATE_CONNECTING);
switch (connMethod) {
case BLUETOOTH:
System.out.println("id -> " + id);
mPort = new BluetoothPort(macAddress);
isOpenPort = mPort.openPort();
break;
case USB:
mPort = new UsbPort(mContext, mUsbDevice);
isOpenPort = mPort.openPort();
if (isOpenPort) {
IntentFilter filter = new IntentFilter(ACTION_USB_DEVICE_DETACHED);
mContext.registerReceiver(usbStateReceiver, filter);
}
break;
case WIFI:
mPort = new EthernetPort(ip, port);
isOpenPort = mPort.openPort();
break;
case SERIAL_PORT:
mPort = new SerialPort(serialPortPath, baudrate, 0);
isOpenPort = mPort.openPort();
break;
default:
break;
}
//端口打开成功后,检查连接打印机所使用的打印机指令ESC、TSC
if (isOpenPort) {
queryCommand();
} else {
sendStateBroadcast(CONN_STATE_FAILED);
}
}
开启线程发送查询指令,充当心跳
/**
* 查询当前连接打印机所使用打印机指令
*/
private void queryCommand() {
/**
* 查询当前连接打印机所使用打印机指令(ESC(EscCommand.java)、TSC(LabelCommand.java))
*/
private void queryCommand() {
//开启读取打印机返回数据线程
readPrinterState();
//查询打印机实时状态
queryPrinterState();
}
}
/**
* ESC查询打印机实时状态指令
*/
private static byte[] esc = {0x10, 0x04, 0x02};
/**
* 查询打印机实时状态
*/
private void queryPrinterState() {
Timer timer = new Timer(true);
//每5秒查询一次打印机的实时状态
timer.schedule(new TimerTask() {
@Override
public void run() {
//发送ESC查询打印机状态指令
sendCommand = esc;
Vector<Byte> data = new Vector<>(esc.length);
for (byte anEsc : esc) {
data.add(anEsc);
}
sendDataImmediately(data);
}
}, 0, 5000);
}
读取打印机的实时状态
private void readPrinterState() {
new Thread(new PrinterReader())
.start();
}
class PrinterReader implements Runnable {
private boolean isRun = false;
private byte[] buffer = new byte[100];
public PrinterReader() {
isRun = true;
}
@Override
public void run() {
try {
while (isRun) {
//读取打印机返回信息
int len = readDataImmediately(buffer);
if (len > 0) {
Message message = Message.obtain();
message.what = READ_DATA;
Bundle bundle = new Bundle();
bundle.putInt(READ_DATA_CNT, len);
bundle.putByteArray(READ_BUFFER_ARRAY, buffer);
message.setData(bundle);
mHandler.sendMessage(message);
}
}
} catch (Exception e) {
if (deviceConnFactoryManagers.get(id) != null) {
closePort();
}
}
}
public void cancel() {
isRun = false;
}
}
static class MyHandler extends Handler {
WeakReference<DeviceConnFactoryManager> reference;
MyHandler(DeviceConnFactoryManager manager, Looper looper) {
super(looper);
reference = new WeakReference<>(manager);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (!ObjectUtils.checkNonNull(reference)) {
return;
}
switch (msg.what) {
case READ_DATA:
int cnt = msg.getData().getInt(READ_DATA_CNT);
byte[] buffer = msg.getData().getByteArray(READ_BUFFER_ARRAY);
//这里只对查询状态返回值做处理,其它返回值可参考编程手册来解析
if (buffer == null) {
return;
}
int result = reference.get().judgeResponseType(buffer[0]);
String status = BaseApplication.getInstance().getString(R.string.str_printer_conn_normal);
if (sendCommand == esc) {
//设置当前打印机模式为ESC模式
if (reference.get().currentPrinterCommand == null) {
reference.get().currentPrinterCommand = PrinterCommand.ESC;
reference.get().sendStateBroadcast(CONN_STATE_CONNECTED);
} else {//查询打印机状态
if (result == 0) {//打印机状态查询
Intent intent = new Intent(ACTION_QUERY_PRINTER_STATE);
intent.putExtra(DEVICE_ID, reference.get().id);
BaseApplication.getInstance().sendBroadcast(intent);
} else if (result == 1) {//查询打印机实时状态
if ((buffer[0] & ESC_STATE_PAPER_ERR) > 0) {
status += " " + BaseApplication.getInstance().getString(R.string.str_printer_out_of_paper);
}
if ((buffer[0] & ESC_STATE_COVER_OPEN) > 0) {
status += " " + BaseApplication.getInstance().getString(R.string.str_printer_open_cover);
}
if ((buffer[0] & ESC_STATE_ERR_OCCURS) > 0) {
status += " " + BaseApplication.getInstance().getString(R.string.str_printer_error);
}
System.out.println(BaseApplication.getInstance().getString(R.string.str_state) + status);
ToastUtils.showShortToast(BaseApplication.getInstance(), status);
}
}
} else if (sendCommand == tsc) {
//设置当前打印机模式为TSC模式
if (reference.get().currentPrinterCommand == null) {
reference.get().currentPrinterCommand = PrinterCommand.TSC;
reference.get().sendStateBroadcast(CONN_STATE_CONNECTED);
} else {
if (cnt == 1) {//查询打印机实时状态
if ((buffer[0] & TSC_STATE_PAPER_ERR) > 0) {//缺纸
status += " " + BaseApplication.getInstance().getString(R.string.str_printer_out_of_paper);
}
if ((buffer[0] & TSC_STATE_COVER_OPEN) > 0) {//开盖
status += " " + BaseApplication.getInstance().getString(R.string.str_printer_open_cover);
}
if ((buffer[0] & TSC_STATE_ERR_OCCURS) > 0) {//打印机报错
status += " " + BaseApplication.getInstance().getString(R.string.str_printer_error);
}
System.out.println(BaseApplication.getInstance().getString(R.string.str_state) + status);
ToastUtils.showShortToast(BaseApplication.getInstance(), status);
} else {//打印机状态查询
Intent intent = new Intent(ACTION_QUERY_PRINTER_STATE);
intent.putExtra(DEVICE_ID, reference.get().id);
BaseApplication.getInstance().sendBroadcast(intent);
}
}
}
break;
default:
break;
}
}
嗯,采用这种方式开始自测是没问题,but,后来又发现问题了,app在前台的情况下,socket确实是一直没有断开,但是当点击home键返回桌面之后,一段时间之后,PrinterReader会抛异常,线程会终止。项目紧急,也懒得追根溯源。总之长连接在android 6.0之后是越来越难以确保不断开了!
private void readPrinterState() {
new Thread(new PrinterReader())
.start();
}
阿西吧,唉呀妈呀脑瓜疼...继续思考...
叮~ 有了,既然长连接如此不可控,采用短连接如何,于是乎,代码改为了打印完了直接closePort,竟然就这么ok了,之后打印再没遇到问题~~~原来这么简单...
总结:长连接改为短连接,网络打印机功能恢复正常!