Android中WebSocket通信初步分析及使用



一、引言

最近的语音交互项目中,需要用到 WebSocket,所以花了不少时间去了解 WebSocket。感觉初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?每种协议有他的长处和短处,HTTP适合接口通信、单次通信,但有一个缺陷:通信只能由客户端发起,例如,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果,HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。因此,工程师们一直在思考,有没有更好的方法,而 WebSocket 就是这样诞生的。

WebSocket,服务器和客户端建立连接之后就可以自由的通信,双方都可以发送消息,非常方方便。值得一提的是,WebSocket 也是需要客户端和服务器建立连接,连接的这部分使用的是HTTP的,但是后面的通信部分就和HTTP无关了。

二、WebSocket 简介

WebSocket 最大特点就是服务器端可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是双向通信。其特点包括:

  • 建立在 TCP 协议之上,服务器端的实现比较容易
  • 与 HTTP 协议有着良好的兼容性,默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器
  • 数据格式比较轻量,性能开销小,通信高效
  • 可以发送文本、二进制数据
  • 没有同源限制,客户端可以与任意服务器通信
  • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL

三、经典框架

Android 上经典的 WebSocket 框架有:

  • AndroidAsyn
  • Autobahn
  • java-webSocket
  • Netty
  • Okhttp

3.1 AndroidAsyn

GitHub:AndroidAsyn

在项目Gradle中,添加依赖

    compile 'com.koushikdutta.async:androidasync:2.+'

数据交互

// url is the URL to download.
AsyncHttpClient.getDefaultInstance().getJSONArray(url, new AsyncHttpClient.JSONArrayCallback() {
    // Callback is invoked with any exceptions/errors, and the result, if available.
    @Override
    public void onCompleted(Exception e, AsyncHttpResponse response, JSONArray result) {
        if (e != null) {
            e.printStackTrace();
            return;
        }
        System.out.println("I got a JSONArray: " + result);
    }
});

AsyncHttpClient.getDefaultInstance().websocket(get, "my-protocol", new WebSocketConnectCallback() {
    @Override
    public void onCompleted(Exception ex, WebSocket webSocket) {
        if (ex != null) {
            ex.printStackTrace();
            return;
        }
        webSocket.send("a string");
        webSocket.send(new byte[10]);
        webSocket.setStringCallback(new StringCallback() {
            public void onStringAvailable(String s) {
                System.out.println("I got a string: " + s);
            }
        });
        webSocket.setDataCallback(new DataCallback() {
            public void onDataAvailable(DataEmitter emitter, ByteBufferList byteBufferList) {
                System.out.println("I got some bytes!");
                // note that this data has been read
                byteBufferList.recycle();
            }
        });
    }
});

3.2 Autobahn

GitHub: Autobahn

在项目Gradle中,添加依赖

    implementation 'io.crossbar.autobahn:autobahn-android:18.5.1'
    private WebSocketConnection mConnect = new WebSocketConnection();
    String url = "ws://192.168.1.40:8181/";
    public void init() {
        try {
            mConnect.connect(url, new WebSocketHandler() {
                @Override
                    public void onOpen() {
                    Log.i(TAG, "onOpen: ");
                }
                @Override
                    public void onTextMessage(String payload) {
                    Log.i(TAG, "onTextMessage: "+payload);
                }
                @Override
                    public void onClose(int code, String reason) {
                    Log.i(TAG, "onClose: " + code + "|" + reason);
                }
            });
        } catch (WebSocketException e) {
            e.printStackTrace();
        }
    }

3.3 java-webSocket

GitHub: java-webSocket

添加依赖

    compile "org.java-websocket:Java-WebSocket:1.3.9"
    private String address = "ws://192.168.1.40:8181/";
    private URI uri;
    private static final String TAG = "WebSocket";

    public void initSockect() {
        try {
            uri = new URI(address);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        if (null == mWebSocketClient) {
            mWebSocketClient = new WebSocketClient(uri) {
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                    Log.i(TAG, "onOpen: ");
                }
                @Override
                public void onMessage(String s) {
                    Log.i(TAG, "onMessage: " + s);
                }
                @Override
                public void onClose(int i, String s, boolean b) {
                    Log.i(TAG, "onClose: ");
                }
                @Override
                public void onError(Exception e) {
                    Log.i(TAG, "onError: ");
                }
            };
            mWebSocketClient.connect();
        }
    }

4.4 Netty

官网:Netty
GitHub: Netty
Demo: Netty

    // 连接到Socket服务端
    private void connected() {
        new Thread() {
            @Override
                public void run() {
                group = new NioEventLoopGroup();
                try {
                    // Client服务启动器 3.x的ClientBootstrap
                    // 改为Bootstrap,且构造函数变化很大,这里用无参构造。
                    Bootstrap bootstrap = new Bootstrap();
                    // 指定EventLoopGroup
                    bootstrap.group(group);
                    // 指定channel类型
                    bootstrap.channel(NioSocketChannel.class);
                    // 指定Handler
                    bootstrap
                        .handler(new MyClientInitializer(MainActivity.this));
                    //如果没有数据,这个可以注释看看
                    bootstrap.option(ChannelOption.SO_KEEPALIVE, true);  
                    bootstrap.option(ChannelOption.TCP_NODELAY, true);
                    // 连接到本地的7878端口的服务端
                    cf = bootstrap.connect(new InetSocketAddress(HOST, PORT));
                    mChannel = cf.sync().channel();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    // 发送数据
    private void sendMessage() {
        mHandler.post(new Runnable() {
            @Override
                public void run() {
                try {
                    Log.i(TAG, "mChannel.write sth & " + mChannel.isOpen());
                    mChannel.writeAndFlush("我是android客户端");
                    mChannel.read();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

据传,netty是又快又准,也很有名。但个人感觉就是用的不习惯。

4.5 Okhttp

GitHub: Okhttp

添加依赖,在项目的Gradle:

    implementation("com.squareup.okhttp3:okhttp:3.12.0")
    implementation("com.squareup.okhttp3:mockwebserver:3.12.0")

代码示例:

      private long sendTime = 0L;
        private static final long HEART_BEAT_RATE = 2 * 1000; // 每隔2秒发送一次心跳包,检测连接没有断开

        private Handler mHandler = new Handler();

        // 发送心跳包
        private Runnable heartBeatRunnable = new Runnable() {
            @Override
            public void run() {
                if (System.currentTimeMillis() - sendTime >= HEART_BEAT_RATE) {
                    String message = sendData();
                    mSocket.send(message);
                    sendTime = System.currentTimeMillis();
                }
                mHandler.postDelayed(this, HEART_BEAT_RATE); //每隔一定的时间,对长连接进行一次心跳检测
            }
        };

        private WebSocket mSocket;
        private void setListener() {
            OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                    .readTimeout(3, TimeUnit.SECONDS)//设置读取超时时间
                    .writeTimeout(3, TimeUnit.SECONDS)//设置写的超时时间
                    .connectTimeout(3, TimeUnit.SECONDS)//设置连接超时时间
                    .build();

            Request request = new Request.Builder().url("ws://lost:8081/websocket/8").build();
            EchoWebSocketListener socketListener = new EchoWebSocketListener();

            // 刚进入界面,就开启心跳检测
            mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);

            mOkHttpClient.newWebSocket(request, socketListener);
            mOkHttpClient.dispatcher().executorService().shutdown();
        }

        private final class EchoWebSocketListener extends WebSocketListener {

            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                super.onOpen(webSocket, response);
                mSocket = webSocket;
                output("连接成功!");    //连接成功后,发送登录信息
            }

            @Override
            public void onMessage(WebSocket webSocket, ByteString bytes) {
                super.onMessage(webSocket, bytes);
                output("receive bytes:" + bytes.hex());
            }

            @Override
            public void onMessage(WebSocket webSocket, String text) {
                super.onMessage(webSocket, text);
                output("服务器端发送来的信息:" + text);
                //具体可以根据自己实际情况断开连接,比如点击返回键页面关闭时,执行下边逻辑
                if (!TextUtils.isEmpty(text)){
                    if (mSocket  != null) {
                        mSocket .close(1000, null);
                    }
                    if (mHandler != null){
                        mHandler.removeCallbacksAndMessages(null);
                        mHandler = null ;
                    }
                }
            }

            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {
                super.onClosed(webSocket, code, reason);
                output("closed:" + reason);
            }

            @Override
            public void onClosing(WebSocket webSocket, int code, String reason) {
                super.onClosing(webSocket, code, reason);
                output("closing:" + reason);
            }

            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                super.onFailure(webSocket, t, response);
                output("failure:" + t.getMessage());
            }
        }

        private void output(final String text) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Log.e("TAG" , "text: " + text) ;
                }
            });
        }

        private String sendData() {
            String jsonHead="";
            Map<String,Object> mapHead=new HashMap<>();
            mapHead.put("qrCode", "123456") ;
            jsonHead=buildRequestParams(mapHead);
            Log.e("TAG" , "sendData: " + jsonHead) ;
            return jsonHead ;
        }


        public  static String buildRequestParams(Object params){
            Gson gson=new Gson();
            String jsonStr=gson.toJson(params);
            return jsonStr;
        }

        private String sendHeart() {
            String jsonHead="";
            Map<String,Object> mapHead=new HashMap<>();
            mapHead.put("heart", "heart") ;
            jsonHead=buildRequestParams(mapHead);
            Log.e("TAG" , "sendHeart:" + jsonHead) ;
            return jsonHead ;
        }
    }

注意点:

  • 上边通过 Handler 每隔5秒时间,给服务器发送一次消息,让服务器端知道自己还活着就可以
  • 在自己执行完自己操作逻辑之后、或者在点击返回键时、在 onDestroy() 方法中,对 websocket 判断,如果不为空,就关闭连接,然后将其置为 null 就可以
  • 给服务器发送数据的地方,就在 心跳包 heartBeatRunnable 中的 run() 方法发送数据即可,数据格式由服务器端与客户端自己商定就可以,一般就是 json 居多就 OK
  • 这里是通过 handler 每隔 5 秒发送一次消息,时间无上限,只要每隔 5 秒就会发送消息,如果需要的场景是:定义一个 3 分钟定时器,每隔5秒发送消息,可以使用 CountDownTimer 即可