一、為什么需要 WebSocket
初次接觸 WebSocket 的人,都會問同樣的問題:我們已經(jīng)有了 HTTP 協(xié)議,為什么還需要另一個(gè)協(xié)議?它能帶來什么好處?
答案很簡單,因?yàn)?HTTP 協(xié)議有一個(gè)缺陷:通信只能由客戶端發(fā)起。
舉例來說,我們想了解今天的天氣,只能是客戶端向服務(wù)器發(fā)出請求,服務(wù)器返回查詢結(jié)果。HTTP 協(xié)議做不到服務(wù)器主動向客戶端推送信息。
這種單向請求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。我們只能使用["輪詢"]:每隔一段時(shí)候,就發(fā)出一個(gè)詢問,了解服務(wù)器有沒有新的信息,最典型的場景就是聊天室。
輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開)。因此,開發(fā)工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發(fā)明的。
二、 WebSocket的簡介
WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)。所有瀏覽器都已經(jīng)支持了。
它的最大特點(diǎn)就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話,屬于[“服務(wù)器推送技術(shù)”]的一種。
WebSocket的特點(diǎn)包括:
- 建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。
2.與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過各種 HTTP 代理服務(wù)器。
3.支持雙向通信,實(shí)時(shí)性更強(qiáng)
4.數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。
5.可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。
6.沒有同源限制,客戶端可以與任意服務(wù)器通信。
7.協(xié)議標(biāo)識符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。
ws://echo.websocket.org
三、WebSocket 實(shí)現(xiàn)Android客戶端與服務(wù)器的長連接
Android客戶端選擇已經(jīng)成熟的框架,Java-WebSocket,GitHub地址:https://github.com/TooTallNate/Java-WebSocket
(一)引入Java-WebSocket
1、build.gradle中加入
implementation "org.java-websocket:Java-WebSocket:1.5.1"
2、加入網(wǎng)絡(luò)請求權(quán)限
<uses-permission android:name="android.permission.INTERNET" />
3、新建客戶端類
新建一個(gè)客戶端類并繼承WebSocketClient,需要實(shí)現(xiàn)它的四個(gè)抽象方法、構(gòu)造函數(shù)和onSetSSLParameters方法
public class JWebSocketClient extends WebSocketClient {
@Override
protected void onSetSSLParameters(SSLParameters sslParameters) {
// super.onSetSSLParameters(sslParameters);
}
public JWebSocketClient(URI serverUri) {
super(serverUri, new Draft_6455());
}
@Override
public void onOpen(ServerHandshake handShakeData) {//在webSocket連接開啟時(shí)調(diào)用
}
@Override
public void onMessage(String message) {//接收到消息時(shí)調(diào)用
}
@Override
public void onClose(int code, String reason, boolean remote) {//在連接斷開時(shí)調(diào)用
}
@Override
public void onError(Exception ex) {//在連接出錯(cuò)時(shí)調(diào)用
}
}
其中onOpen()方法在websocket連接開啟時(shí)調(diào)用,onMessage()方法在接收到消息時(shí)調(diào)用,onClose()方法在連接斷開時(shí)調(diào)用,onError()方法在連接出錯(cuò)時(shí)調(diào)用。構(gòu)造方法中的new Draft_6455()代表使用的協(xié)議版本,這里可以不寫或者寫成這樣即可。
4、建立websocket連接
建立連接只需要初始化此客戶端再調(diào)用連接方法,需要注意的是WebSocketClient對象是不能重復(fù)使用的,所以不能重復(fù)初始化,其他地方只能調(diào)用當(dāng)前這個(gè)Client。
URI uri = URI.create("ws://*******");
JWebSocketClient client = new JWebSocketClient(uri) {
@Override
public void onMessage(String message) {
//message就是接收到的消息
Log.e("JWebSClientService", message);
}
};
為了方便對接收到的消息進(jìn)行處理,可以在這重寫onMessage()方法。初始化客戶端時(shí)需要傳入websocket地址(測試地址:ws://echo.websocket.org),websocket協(xié)議地址大致是這樣的,協(xié)議標(biāo)識符是ws(如果加密,則為wss)
ws:// ip地址 : 端口號
連接時(shí)可以使用connect()方法或connectBlocking()方法,建議使用connectBlocking()方法,connectBlocking多出一個(gè)等待操作,會先連接再發(fā)送。
try {
client.connectBlocking();
} catch (InterruptedException e) {
e.printStackTrace();
}
5、發(fā)送消息
發(fā)送消息只需要調(diào)用send()方法,如下
if (client != null && client.isOpen()) {
client.send("你好");
}
6、關(guān)閉socket連接
關(guān)閉連接調(diào)用close()方法,最后為了避免重復(fù)實(shí)例化WebSocketClient對象,關(guān)閉時(shí)一定要將對象置空。
/**
* 斷開連接
*/
private void closeConnect() {
try {
if (null != client) {
client.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
client = null;
}
}
(二)WebSocket的封裝
1、新建一個(gè)客戶端類并繼承WebSocketClient
public class JWebSocketClient extends WebSocketClient {
@Override
protected void onSetSSLParameters(SSLParameters sslParameters) {
// super.onSetSSLParameters(sslParameters);
}
public JWebSocketClient(URI serverUri) {
super(serverUri, new Draft_6455());
}
@Override
public void onOpen(ServerHandshake handShakeData) {//在webSocket連接開啟時(shí)調(diào)用
}
@Override
public void onMessage(String message) {//接收到消息時(shí)調(diào)用
}
@Override
public void onClose(int code, String reason, boolean remote) {//在連接斷開時(shí)調(diào)用
}
@Override
public void onError(Exception ex) {//在連接出錯(cuò)時(shí)調(diào)用
}
}
2、新建WebSocketEvent,用于傳遞消息事件
public class WebSocketEvent {
private String message;
public WebSocketEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
3、新建WebSocketService服務(wù),用于消息管理和保持長連接狀態(tài)
public class WebSocketService extends Service {
private final static String TAG = WebSocketService.class.getSimpleName();
public JWebSocketClient client;
private JWebSocketClientBinder mBinder = new JWebSocketClientBinder();
private final static int GRAY_SERVICE_ID = 1001;
private static final long CLOSE_RECON_TIME = 100;//連接斷開或者連接錯(cuò)誤立即重連
//用于Activity和service通訊
public class JWebSocketClientBinder extends Binder {
public WebSocketService getService() {
return WebSocketService.this;
}
}
//灰色保活
public static class GrayInnerService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(GRAY_SERVICE_ID, new Notification());
stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
@Override
public IBinder onBind(Intent intent) {
LogUtil.i(TAG, "WebSocketService onBind");
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//初始化WebSocket
initSocketClient();
mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);//開啟心跳檢測
//設(shè)置service為前臺服務(wù),提高優(yōu)先級
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
//Android4.3以下 ,隱藏Notification上的圖標(biāo)
startForeground(GRAY_SERVICE_ID, new Notification());
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
//Android4.3 - Android8.0,隱藏Notification上的圖標(biāo)
Intent innerIntent = new Intent(this, GrayInnerService.class);
startService(innerIntent);
startForeground(GRAY_SERVICE_ID, new Notification());
} else {
//Android8.0以上app啟動后通知欄會出現(xiàn)一條"正在運(yùn)行"的通知
NotificationChannel channel = new NotificationChannel(NotificationUtil.channel_id, NotificationUtil.channel_name,
NotificationManager.IMPORTANCE_HIGH);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (manager != null) {
manager.createNotificationChannel(channel);
Notification notification = new Notification.Builder(getApplicationContext(), NotificationUtil.channel_id_tai_bang).build();
startForeground(GRAY_SERVICE_ID, notification);
}
}
return START_STICKY;
}
private void initSocketClient() {
String url = BuildConfig.WS_PERFIX;
URI uri = URI.create(url);
client = new JWebSocketClient(uri) {
@Override
public void onMessage(String message) {
//message就是接收到的消息
LogUtil.i(TAG, "WebSocketService收到的消息:" + message);
EventBus.getDefault().post(new WebSocketEvent(message));
}
@Override
public void onOpen(ServerHandshake handShakeData) {//在webSocket連接開啟時(shí)調(diào)用
LogUtil.i(TAG, "WebSocket 連接成功");
}
@Override
public void onClose(int code, String reason, boolean remote) {//在連接斷開時(shí)調(diào)用
LogUtil.e(TAG, "onClose() 連接斷開_reason:" + reason);
mHandler.removeCallbacks(heartBeatRunnable);
mHandler.postDelayed(heartBeatRunnable, CLOSE_RECON_TIME);//開啟心跳檢測
}
@Override
public void onError(Exception ex) {//在連接出錯(cuò)時(shí)調(diào)用
LogUtil.e(TAG, "onError() 連接出錯(cuò):" + ex.getMessage());
mHandler.removeCallbacks(heartBeatRunnable);
mHandler.postDelayed(heartBeatRunnable, CLOSE_RECON_TIME);//開啟心跳檢測
}
};
connect();
}
/**
* 連接WebSocket
*/
private void connect() {
new Thread() {
@Override
public void run() {
try {
//connectBlocking多出一個(gè)等待操作,會先連接再發(fā)送,否則未連接發(fā)送會報(bào)錯(cuò)
client.connectBlocking();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
/**
* 發(fā)送消息
*/
public void sendMsg(String msg) {
if (null != client) {
LogUtil.i(TAG, "發(fā)送的消息:" + msg);
try {
client.send(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public boolean onUnbind(Intent intent) {
LogUtil.e(TAG, "Service onUnbind");
return super.onUnbind(intent);
}
@Override
public void onDestroy() {
closeConnect();
super.onDestroy();
}
/**
* 斷開連接
*/
public void closeConnect() {
mHandler.removeCallbacks(heartBeatRunnable);
try {
if (null != client) {
client.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
client = null;
}
}
// -------------------------------------WebSocket心跳檢測------------------------------------------------
private static final long HEART_BEAT_RATE = 10 * 1000;//每隔10秒進(jìn)行一次對長連接的心跳檢測
private Handler mHandler = new Handler();
private Runnable heartBeatRunnable = new Runnable() {
@Override
public void run() {
if (client != null) {
if (client.isClosed()) {
reconnectWs();
LogUtil.e(TAG, "心跳包檢測WebSocket連接狀態(tài):已關(guān)閉");
} else if (client.isOpen()) {
LogUtil.d(TAG, "心跳包檢測WebSocket連接狀態(tài):已連接");
} else {
LogUtil.e(TAG, "心跳包檢測WebSocket連接狀態(tài):已斷開");
}
} else {
//如果client已為空,重新初始化連接
initSocketClient();
LogUtil.e(TAG, "心跳包檢測WebSocket連接狀態(tài):client已為空,重新初始化連接");
}
//每隔一定的時(shí)間,對長連接進(jìn)行一次心跳檢測
mHandler.postDelayed(this, HEART_BEAT_RATE);
}
};
/**
* 開啟重連
*/
private void reconnectWs() {
mHandler.removeCallbacks(heartBeatRunnable);
new Thread() {
@Override
public void run() {
try {
LogUtil.e(TAG, "開啟重連");
client.reconnectBlocking();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
4、在Application中開啟WebSocketService服務(wù)
/**
* 開啟并綁定WebSocket服務(wù)
*/
public void startWebSocketService() {
Intent bindIntent = new Intent(this, WebSocketService.class);
startService(bindIntent);
bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
}
5、通過WebSocketService向服務(wù)器發(fā)送消息
if (NsApplication.getInstance().getWebSocketService() != null &&
NsApplication.getInstance().getWebSocketService().client != null && NsApplication.getInstance().getWebSocketService().client.isOpen()) {
JSONObject jsonObject = new JSONObject();
NsApplication.getInstance().getWebSocketService().sendMsg(jsonObject );
} else {
LogUtil.e(TAG, "WebSocket連接已斷開");
}
}
6、通過WebSocketService接收服務(wù)器發(fā)來的消息
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(WebSocketEvent event) {
if (event != null) {
LogUtil.e(TAG, "接收消息內(nèi)容:" + event.getMessage());
}
}
7、Application
public class NsApplication extends Application {
private final static String TAG = NsApplication.class.getSimpleName();
private static NsApplication instance;
private static final String DEVICE_TOKEN = "device_token";//設(shè)備token
public WebSocketService mWebSocketService;
// -------------------------------------WebSocket發(fā)送空消息心跳檢測------------------------------------------------
private static final long HEART_BEAT_RATE = 60 * 1000;//每隔1分鐘發(fā)送空消息保持WebSocket長連接
public static NsApplication getInstance() {
if (instance == null) {
instance = new NsApplication();
}
return instance;
}
private Handler mHandler = new Handler();
private Runnable webSocketRunnable = new Runnable() {
@Override
public void run() {
if (mWebSocketService != null &&
mWebSocketService.client != null && mWebSocketService.client.isOpen()) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("from","");
jsonObject.put("to", "");
LogUtil.e(TAG, "JSONObject:" + jsonObject.toString());
mWebSocketService.sendMsg(jsonObject.toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
//每隔一定的時(shí)間,對長連接進(jìn)行一次心跳檢測
mHandler.postDelayed(this, HEART_BEAT_RATE);
}
};
public WebSocketService getWebSocketService() {
return mWebSocketService;
}
/**
* 開啟并綁定WebSocket服務(wù)
*/
public void startWebSocketService(String deviceToken) {
Intent bindIntent = new Intent(this, WebSocketService.class);
bindIntent.putExtra(DEVICE_TOKEN, deviceToken);
startService(bindIntent);
bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
mHandler.removeCallbacks(webSocketRunnable);
mHandler.postDelayed(webSocketRunnable, HEART_BEAT_RATE);//開啟心跳檢測
}
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//服務(wù)與活動成功綁定
mWebSocketService = ((WebSocketService.JWebSocketClientBinder) iBinder).getService();
LogUtil.e(TAG, "WebSocket服務(wù)與Application成功綁定");
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
//服務(wù)與活動斷開
mWebSocketService = null;
LogUtil.e(TAG, "WebSocket服務(wù)與Application成功斷開");
}
};
}