一、引言
最近的语音交互项目中,需要用到 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 即可