告别WebView使用WebSocket采坑记

对WebSocket的认识

    WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范。WebSocket API也被W3C定为标准。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

来自维基百科

需求(为什么要使用WebSocket)

    我们公司做的一个WEB项目,而这个WEB项目中有POS机支付的功能。当WEB上点击使用POS支付时,在POS机端要调起支付功能,完成支付。起初第一个版本是这样的,当WEB 页面点击POS支付会弹出一个二维码进行扫码支付。而这个版本在现场发现使用起来过于繁琐,就决定使用去掉扫码操作,当WEB页面点击使用POS支付时直接调起POS机的支付功能进行支付,这种让我想到了推送,而第三方推送考虑到 安全就放弃了。最终结合我们后台服务为tomcat8.0,决定使用WebSocket。

开发

    好吧,明白了需求,那就开始开发吧,然后我就开始疯狂似的编码。首先我为了满足需求,让这个应用保活的状态,我写了一个前台服务(只是为了保护进程不被杀死和在没有打开程序也能接收到推送)。写一个类继承Service然后在onCreate中调用:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT < 18) {
startForeground(SOCKET_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标
} else {
Intent innerIntent = new Intent(this, WebSocketService.class);
startService(innerIntent);
startForeground(SOCKET_SERVICE_ID, new Notification());
}
}

注意:  在写前台服务的时候不要onStartCommand()在这个方法里做任何事情,因为这个方法在Service中多次调用很容易导致内存溢出(当然我看过一本书上是这么说的,如果抛出OutOfMemoryException这个异常时,我们可以在AndroidMainfest.xml中的application largelHeap属性设置为true, 这样就可以增加系统为当前App分配内存,甚至到100M。然而在这个方法中有执行操作的话并不会减少崩溃),如果是使用绑定服务的话,就更不要在这个方法写你的逻辑代码,因为使用绑定服务这个方法是不会被调用的。然后要在onDestrou要停止掉前台服务

1
2
3
4
5
6
@Override
public void onDestroy() {
   Log.d(TAG, "onDestroy()");
   stopForeground(true);// 停止前台服务
   super.onDestroy();
}

    前面的都是开胃菜,真正的该上主菜了,就是如何去使用WebSocket。网上有使用Okhttp进行封装,在封装的,还有使用了java-websocket这个包的。而我选择的是后者。因为这个包有maven包,好像还是麻省理工实验室出的,使用起来也算比较简单。

第一步:去gitHub上导包gitHub地址Java-WebSocket。

第二步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
try {
WebSocketClient client = new WebSocketClient(new URI(webSocketUrl), new Draft_17()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
LogUtil.e(TAG + "lal-open", "已经连接到服务器【" + getURI() + "】");
}
@Override
public void onMessage(String message) {
LogUtil.e(TAG + "lal-message", "获取到服务器信息【" + message + "】");
}
@Override
public void onClose(int code, String reason, boolean remote) {
LogUtil.e(TAG + "lal-close", "断开服务器连接【" + getURI() + ",状态码: " + code + ",断开原因:" + reason + "】times:" + closeNum);
}
@Override
public void onError(Exception ex) {
LogUtil.e(TAG + "lal-error", "连接发生了异常【异常原因:" + ex + "】Times:" + errNum);
}
};
} catch (URISyntaxException e) {
LogUtil.e("--TAG--", e.toString());
e.printStackTrace();
}

webSocketUrl是后台的URL包括参数。

第三步:

1
client.connect();

    好了,经过这三步成功的使用了WebSocket连上后台,在onDestroy()方法中记得调用client.close()这个方法进行断开链接。是不是很简单。然而并不是这么简单,如果由于网络的原因或者后台发生故障了,断开了怎么办呢?你肯定会想,这还不好办,这个包肯定有断开重连的方法吧!你写个广播监听网络状态。如果网络异常了就去重连。呵呵,这个包是没有重连的方法的,在github上还有人提过这个Bug,但是博主并没有解决,目前还是open状态bug号#392。这个问题让后思索了很久,后来我我就想,既然断开了,我就不如重新开给连接,重新走上面的方法。然而,在测试的时候,我自己将网络断开,发现无限的走上面的方法,因为我在onClose和onError是有重新走上面的方法,最后抛异常了,异常是OutOfMemoryException没错就是我上面所指的内存溢出。这很明显吧,断开或者异常断开就走上面的方法,就会去重新创建一个WebSocketClient对象,而原来的也没有回收掉,还无限的去创建新对象,不抛异常才怪。既然问题找到了,那就找解决办法呗。我的解决方案是这样的,在调用这个方法之前,我判断这个对象是否为空,如果不为空,我会想调用client.close()这个方法。为什么要调用这个方法呢?其实很简单,因为这个方法会将里面的一些变量变为空。然后将client变为空。经过改良后是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class WebSocketUtil {
private static final String TAG = WebSocketUtil.class.getSimpleName();
private WebSocketClient client;// 连接客户端
private IWebSocketCallBack callBack;
private WebSocketUtil(){
}
private static class SingletonHolder{
public static WebSocketUtil instance = new WebSocketUtil();
}
public static WebSocketUtil newInstance(){
return SingletonHolder.instance;
}
public void setWebSocketCallBack(IWebSocketCallBack callBack){
this.callBack = callBack;
}
public void requestNetWork(String mSn){
String webSocketUrl = ServiceGenerator.WEBSOCKET_BASE_URL+mSn;
if(client != null){
client.close();
client = null;
}
if(client == null) {
try {
client = new WebSocketClient(new URI(webSocketUrl), new Draft_17()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
LogUtil.e(TAG + "lal-open", "已经连接到服务器【" + getURI() + "】");
callBack.onOpen(handshakedata);
}
@Override
public void onMessage(String message) {
LogUtil.e(TAG + "lal-message", "获取到服务器信息【" + message + "】");
callBack.onMessage(message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
LogUtil.e(TAG + "lal-close", "断开服务器连接【" + getURI() + ",状态码: " + code + ",断开原因:" + reason + "】");
}
@Override
public void onError(Exception ex) {
LogUtil.e(TAG + "lal-error", "连接发生了异常【异常原因:" + ex + "】");
}
};
} catch (URISyntaxException e) {
callBack.failure();
LogUtil.e("--TAG--", e.toString());
e.printStackTrace();
}
}
client.connect();
}
public void close(){
if(null != client){
client.close();
}
}
public void sendMessage(String msg) throws WebsocketNotConnectedException {
if(null != client){
client.send(msg);
}
}
}

    这样Android移动端出现问题,自己可以检测到,而且可以重连的问题解决了,但是如果是后台发生问题呢怎么办呢。最好的办法就是进行心跳检测。那什么是心跳检测呢?知道的可以跳过。心跳检测是这样的,客户端:”你还活着吗?”,后台服务:”嗯,还活着”,然后过一段时间客户端又问一句,后台回复一句,如果超过一定时间没有回答,就视为客户端和后台断开了,然后进行重连。这样包括客户端自己出问题了重连,和后台出问题了重连,可以保证客户端一直连着的状态了。

    最后完整代码放在github上。代码上还有点问题,比如重连的次数,我的重连的次数是在onCreate方法中进行请求,其实大可以通过心跳进行获取,而后台可以将这个值放到缓存中,比如放到redis中,不需要每次去数据库中获取。而Android客户端不需要重启应用进行获取。这点不足是我的好朋友看了代码给的提醒。还有写不足的地方还希望大家提醒,点拨一下 ^^ ^^!!

-------------本文结束感谢您的阅读-------------