前言
關於我和WebSocket的緣:我從大二在計算機網絡課上聽老師講過之后,第一次使用就到了畢業之后的第一份工作。直到最近換了工作,到了一家是含有IM社交聊天功能的app的時候,我覺得我現在可以談談我對WebSocket/Socket的一些看法了。要想做IM聊天app,就不得不理解WebSocket和Socket的原理了,聽我一一道來。
目錄
1.WebSocket使用場景
2.WebSocket誕生由來
3.談談WebSocket協議原理
4.WebSocket 和 Socket的區別與聯系
5.iOS平台有哪些WebSocket和Socket的開源框架
6.iOS平台如何實現WebSocket協議
一.WebSocket的使用場景
1.社交聊天
最著名的就是微信,QQ,這一類社交聊天的app。這一類聊天app的特點是低延遲,高即時。即時是這里面要求最高的,如果有一個緊急的事情,通過IM軟件通知你,假設網絡環境良好的情況下,這條message還無法立即送達到你的客戶端上,緊急的事情都結束了,你才收到消息,那么這個軟件肯定是失敗的。
2.彈幕
說到這里,大家一定里面想到了A站和B站了。確實,他們的彈幕一直是一種特色。而且彈幕對於一個視頻來說,很可能彈幕才是精華。發彈幕需要實時顯示,也需要和聊天一樣,需要即時。
3.多玩家游戲
4.協同編輯
現在很多開源項目都是分散在世界各地的開發者一起協同開發,此時就會用到版本控制系統,比如Git,SVN去合並沖突。但是如果有一份文檔,支持多人實時在線協同編輯,那么此時就會用到比如WebSocket了,它可以保證各個編輯者都在編輯同一個文檔,此時不需要用到Git,SVN這些版本控制,因為在協同編輯界面就會實時看到對方編輯了什么,誰在修改哪些段落和文字。
5.股票基金實時報價
金融界瞬息萬變——幾乎是每毫秒都在變化。如果采用的網絡架構無法滿足實時性,那么就會給客戶帶來巨大的損失。幾毫秒錢股票開始大跌,幾秒以后才刷新數據,一秒鍾的時間內,很可能用戶就已經損失巨大財產了。
6.體育實況更新
全世界的球迷,體育愛好者特別多,當然大家在關心自己喜歡的體育活動的時候,比賽實時的賽況是他們最最關心的事情。這類新聞中最好的體驗就是利用Websocket達到實時的更新!
7.視頻會議/聊天
視頻會議並不能代替和真人相見,但是他能讓分布在全球天涯海角的人聚在電腦前一起開會。既能節省大家聚在一起路上花費的時間,討論聚會地點的糾結,還能隨時隨地,只要有網絡就可以開會。
8.基於位置的應用
越來越多的開發者借用移動設備的GPS功能來實現他們基於位置的網絡應用。如果你一直記錄用戶的位置(比如運行應用來記錄運動軌跡),你可以收集到更加細致化的數據。
9.在線教育
在線教育近幾年也發展迅速。優點很多,免去了場地的限制,能讓名師的資源合理的分配給全國各地想要學習知識的同學手上,Websocket是個不錯的選擇,可以視頻聊天、即時聊天以及其與別人合作一起在網上討論問題...
10.智能家居
這也是我一畢業加入的一個偉大的物聯網智能家居的公司。考慮到家里的智能設備的狀態必須需要實時的展現在手機app客戶端上,毫無疑問選擇了Websocket。
11.總結
從上面我列舉的這些場景來看,一個共同點就是,高實時性!
二.WebSocket誕生由來
1.最開始的輪詢Polling階段

這種方式下,是不適合獲取實時信息的,客戶端和服務器之間會一直進行連接,每隔一段時間就詢問一次。客戶端會輪詢,有沒有新消息。這種方式連接數會很多,一個接受,一個發送。而且每次發送請求都會有Http的Header,會很耗流量,也會消耗CPU的利用率。
2.改進版的長輪詢Long polling階段

長輪詢是對輪詢的改進版,客戶端發送HTTP給服務器之后,有沒有新消息,如果沒有新消息,就一直等待。當有新消息的時候,才會返回給客戶端。在某種程度上減小了網絡帶寬和CPU利用率等問題。但是這種方式還是有一種弊端:例如假設服務器端的數據更新速度很快,服務器在傳送一個數據包給客戶端后必須等待客戶端的下一個Get請求到來,才能傳遞第二個更新的數據包給客戶端,那么這樣的話,客戶端顯示實時數據最快的時間為2×RTT(往返時間),而且如果在網絡擁塞的情況下,這個時間用戶是不能接受的,比如在股市的的報價上。另外,由於http數據包的頭部數據量往往很大(通常有400多個字節),但是真正被服務器需要的數據卻很少(有時只有10個字節左右),這樣的數據包在網絡上周期性的傳輸,難免對網絡帶寬是一種浪費。
3.WebSocket誕生
現在急需的需求是能支持客戶端和服務器端的雙向通信,而且協議的頭部又沒有HTTP的Header那么大,於是,Websocket就誕生了!

上圖就是Websocket和Polling的區別,從圖中可以看到Polling里面客戶端發送了好多Request,而下圖,只有一個Upgrade,非常簡潔高效。至於消耗方面的比較就要看下圖了

上圖中,我們先看藍色的柱狀圖,是Polling輪詢消耗的流量,
Use case A: 1,000 clients polling every second: Network throughput is (871 x 1,000) = 871,000 bytes = 6,968,000 bits per second (6.6 Mbps)
Use case B: 10,000 clients polling every second: Network throughput is (871 x 10,000) = 8,710,000 bytes = 69,680,000 bits per second (66 Mbps)
Use case C: 100,000 clients polling every 1 second: Network throughput is (871 x 100,000) = 87,100,000 bytes = 696,800,000 bits per second (665 Mbps)
而Websocket的Frame是 just two bytes of overhead instead of 871,僅僅用2個字節就代替了輪詢的871字節!
Use case A: 1,000 clients receive 1 message per second: Network throughput is (2 x 1,000) = 2,000 bytes = 16,000 bits per second (0.015 Mbps)
Use case B: 10,000 clients receive 1 message per second: Network throughput is (2 x 10,000) = 20,000 bytes = 160,000 bits per second (0.153 Mbps)
Use case C: 100,000 clients receive 1 message per second: Network throughput is (2 x 100,000) = 200,000 bytes = 1,600,000 bits per second (1.526 Mbps)
相同的每秒客戶端輪詢的次數,當次數高達10W/s的高頻率次數的時候,Polling輪詢需要消耗665Mbps,而Websocket僅僅只花費了1.526Mbps,將近435倍!!
三.談談WebSocket協議原理
Websocket是應用層第七層上的一個應用層協議,它必須依賴 HTTP 協議進行一次握手 ,握手成功后,數據就直接從 TCP 通道傳輸,與 HTTP 無關了。
Websocket的數據傳輸是frame形式傳輸的,比如會將一條消息分為幾個frame,按照先后順序傳輸出去。這樣做會有幾個好處:
1)大數據的傳輸可以分片傳輸,不用考慮到數據大小導致的長度標志位不足夠的情況。
2)和http的chunk一樣,可以邊生成數據邊傳遞消息,即提高傳輸效率。

四.WebSocket 和 Socket的區別與聯系
首先,Socket 其實並不是一個協議。它工作在 OSI 模型會話層(第5層),是為了方便大家直接使用更底層協議(一般是 TCP 或 UDP )而存在的一個抽象層。Socket是對TCP/IP協議的封裝,Socket本身並不是協議,而是一個調用接口(API)。

Socket通常也稱作”套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。網絡上的兩個程序通過一個雙向的通訊連接實現數據的交換,這個雙向鏈路的一端稱為一個Socket,一個Socket由一個IP地址和一個端口號唯一確定。應用程序通常通過”套接字”向網絡發出請求或者應答網絡請求。
Socket在通訊過程中,服務端監聽某個端口是否有連接請求,客戶端向服務端發送連接請求,服務端收到連接請求向客戶端發出接收消息,這樣一個連接就建立起來了。客戶端和服務端也都可以相互發送消息與對方進行通訊,直到雙方連接斷開。
所以基於WebSocket和基於Socket都可以開發出IM社交聊天類的app
五.iOS平台有哪些WebSocket和Socket的開源框架
Socket開源框架有:CocoaAsyncSocket,socketio/socket.io-client-swift
WebSocket開源框架有:facebook/SocketRocket,tidwall/SwiftWebSocket
六.iOS平台如何實現WebSocket協議
Talk is cheap。Show me the code ——Linus Torvalds
我們今天來看看facebook/SocketRocket的實現方法
首先這是SRWebSocket定義的一些成員變量
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@property (nonatomic, weak) id delegate;
/**
A dispatch queue for scheduling the delegate calls. The queue doesn't need be a serial queue.
If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls.
*/
@property (nonatomic, strong) dispatch_queue_t delegateDispatchQueue;
/**
An operation queue for scheduling the delegate calls.
If `nil` and `delegateOperationQueue` is `nil`, the socket uses main queue for performing all delegate method calls.
*/
@property (nonatomic, strong) NSOperationQueue *delegateOperationQueue;
@property (nonatomic, readonly) SRReadyState readyState;
@property (nonatomic, readonly, retain) NSURL *url;
@property (nonatomic, readonly) CFHTTPMessageRef receivedHTTPHeaders;
// Optional array of cookies (NSHTTPCookie objects) to apply to the connections
@property (nonatomic, copy) NSArray *requestCookies;
// This returns the negotiated protocol.
// It will be nil until after the handshake completes.
@property (nonatomic, readonly, copy) NSString *protocol;
|
下面這些是SRWebSocket的一些方法
|
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
|
// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol.
- (instancetype)initWithURLRequest:(NSURLRequest *)request;
- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols;
- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
// Some helper constructors.
- (instancetype)initWithURL:(NSURL *)url;
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols;
- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;
// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.
- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
// SRWebSockets are intended for one-time-use only. Open should be called once and only once.
- (void)open;
- (void)close;
- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason;
///--------------------------------------
#pragma mark Send
///--------------------------------------
//下面是4個發送的方法
/**
Send a UTF-8 string or binary data to the server.
@param message UTF-8 String or Data to send.
@deprecated Please use `sendString:` or `sendData` instead.
*/
- (void)send:(id)message __attribute__((deprecated(
"Please use `sendString:` or `sendData` instead."
)));
- (void)sendString:(NSString *)string;
- (void)sendData:(NSData *)data;
- (void)sendPing:(NSData *)data;
@end
|
對應5種狀態的代理方法
|
1
2
3
4
5
6
7
8
9
10
11
12
|
///--------------------------------------
#pragma mark - SRWebSocketDelegate
///--------------------------------------
@protocol SRWebSocketDelegate - (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;
@optional
- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
// Return YES to convert messages sent as Text to an NSString. Return NO to skip NSData -> NSString conversion for Text messages. Defaults to YES.
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket;
@end
|
didReceiveMessage方法是必須實現的,用來接收消息的。
下面4個did方法分別對應着Open,Fail,Close,ReceivePong不同狀態的代理方法
方法就上面這些了,我們實際來看看代碼怎么寫
先是初始化Websocket連接,注意此處ws://或者wss://連接有且最多只能有一個,這個是Websocket協議規定的
|
1
2
3
4
|
self.ws = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString
stringWithFormat:@
"%@://%@:%zd/ws"
, serverProto, serverIP, serverPort]]]];
self.ws.delegate = delegate;
[self.ws open];
|
發送消息
|
1
|
[self.ws send:message];
|
接收消息以及其他3個代理方法
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//這個就是接受消息的代理方法了,這里接受服務器返回的數據,方法里面就應該寫處理數據,存儲數據的方法了。
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSDictionary *data = [NetworkUtils decodeData:message];
if
(!data)
return
;
}
//這里是Websocket剛剛Open之后的代理方法。就想微信剛剛連接中,會顯示連接中,當連接上了,就不顯示連接中了,取消顯示連接的方法就應該寫在這里面
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
// Open = silent ping
[self.ws receivedPing];
}
//這是關閉Websocket的代理方法
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
[self failedConnection:NSLS(Disconnected)];
}
//這里是連接Websocket失敗的方法,這里面一般都會寫重連的方法
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
[self failedConnection:NSLS(Disconnected)];
}
|
最后
以上就是我想分享的一些關於Websocket的心得,文中如果有錯誤的地方,歡迎大家指點!一般沒有微信QQ那么大用戶量的app,用Websocket應該都可以完成IM社交聊天的任務。當用戶達到億級別,應該還有很多需要優化,優化性能各種的吧。
最后,微信和QQ的實現方法也許並不是只用Websocket和Socket這么簡單,也許是他們自己開發的一套能支持這么大用戶,大數據的,各方面也都優化都最優的方法。如果有開發和微信和QQ的大神看到這篇文章,可以留言說說看你們用什么方式實現的,也可以和我們一起分享,我們一起學習!我先謝謝大神們的指點了!
1,Android 客戶端使用需要配置網絡權限;
2,需要寫一個自己的client類來繼承WebsocketClient;實現websocket的狀態回調和新消息的解析動作;
3,需要監控自己的client的鏈接狀態,維持長鏈接;
4,發送和接收
下面貼出部分相關代碼;
網絡權限的不用說了吧!
client類:
常用狀態回調方法有4個;
自己可以在對應的函數里面做響應的處理,比如當鏈接發生錯誤時要重新去打開該鏈接,收到消息時即時的保存聊天記錄和發送系統通知來提醒用戶查看新消息等等;
- <pre name="code" class="html"><span style="font-size:18px;">public class TestClient extends WebSocketClient {
- public TestClient(URI serverURI) {
- super(serverURI);
- }
- /***
- * 鏈接關閉
- */
- @Override
- public void onClose(int arg0, String arg1, boolean arg2) {
- }
- /***
- * 鏈接發生錯誤
- */
- @Override
- public void onError(Exception arg0) {
- }
- /**
- * 新消息
- */
- @Override
- public void onMessage(String arg0) {
- }
- /***
- * 鏈接打開
- */
- @Override
- public void onOpen(ServerHandshake arg0) {
- // TODO Auto-generated method stub
- }
- }
- </span>
下面是我聊天部分代碼,有離線消息/PC同步/多對多的聊天;
僅供參考;
- /***
- * <h2>WebSocket Android 客戶端</h2>
- * <ol>
- * <li>Socket鏈接打開回調 {@link WebSocket#onOpen(ServerHandshake)},此處有
- * {@link SocketConstant#ON_OPEN} 廣播發出;
- * <li>Socket鏈接出現異常錯誤時回調 {@link WebSocket#onError(Exception)},此處有
- * {@link SocketConstant#ON_ERROR}廣播發出;
- * <li>Socket鏈接關閉回調 {@link WebSocket #onClose(int, String, boolean)},此處有
- * {@link SocketConstant#ON_CLOSES}廣播發出;
- * <li>Socket鏈接接收消息回調 {@link WebSocket#onMessage(String)}
- * ,此處做收消息的邏輯的處理;包括發送消息服務器返回的發送結果,PC端同步接收的新消息,及外來的新消息;
- * <li>檢測是否有消息遺漏 {@link WebSocket#checkMsgWebId(String, String)},參數為聯系人和webId;
- * <li>取得正在桌面運行的activity的名稱 {@link WebSocket#getRunningActivityName()}
- * <li>接收到的消息的處理 {@link WebSocket#messageHandle(MessageEntity, String)}
- * ,參數為消息實體和消息類型 (buyer/server)
- * <li>發送新消息的系統通知 {@link WebSocket#sendNotification(String)},參數為聯系人;
- * <li>保存離線消息 {@link WebSocket#saveOffLineMsg(HashMap)},參數為接收到的離線消息集合;
- * <li>保存從服務端獲取的聯系人的webId {@link WebSocket#saveContactsWebID(HashMap)}
- * ,參數為以聯系人為key以最大webId為值得map集合;
- * </ol>
- *
- * @author li'mingqi <a> 2014-3-19</a>
- *
- */
- public class WebSocket extends WebSocketClient {
- // 登陸返回的back_type字段
- public static final String LOGIN_RETURN_TYPE = "login";
- // 發送信息的back_type字段
- public static final String SEND_RETURN_TYPE = "send_result";
- // 接收信息的back_type字段
- public static final String RECEIVER_RETURN_TYPE = "msg";
- // 接收客服的信息的back_type字段
- public static final String GET_SERVER_RETURN_TYPE = "server_info";
- // 接收服務端返回對應聯系人的最大順序ID
- public static final String CONTACTS_MAX_WEBID_TYPE = "max_id_return";
- // 接收用戶的離線消息
- public static final String USER_OFFLINE_MSG_TYPE = "offline";
- // 上下文對象
- private Context mContext;
- // socket返回json解析類對象
- private WebSocketParser mParser;
- // 系統通知管理
- public NotificationManager mNotificationManager;
- // 系統通知
- private Notification mNoti;
- // 意圖
- private PendingIntent mIntent;
- // 該系統通知的 id
- public static final int NOTIFICATION_ID = 100;
- @SuppressWarnings("deprecation")
- public SGWebSocket(Context context, URI serverUri, Draft draft) {
- super(serverUri, draft);
- this.mContext = context;
- //新消息的解析類
- this.mParser = WebSocketParser.getInstance();
- //收到新消息發送的通知
- this.mNotificationManager = (NotificationManager) this.mContext
- .getSystemService(Context.NOTIFICATION_SERVICE);
- this.mNoti = new Notification(R.drawable.system_info, "您有新消息!",
- System.currentTimeMillis());
- }
- /***
- * send broadcast <SGSocketConstant>ON_CLOSES filter if this socket closed
- * socket 發生關閉時發送的廣播,若想提示,可以接受並處理
- *
- */
- @Override
- public void onClose(int arg0, String arg1, boolean arg2) {
- // 更改保存的鏈接狀態
- UserInfoUtil.saveSocket(mContext, false);
- mNotificationManager.cancelAll();
- Intent intent = new Intent(SocketConstant.ON_CLOSES);
- intent.putExtra(SocketConstant.ON_CLOSES, arg1.toString());
- mContext.sendBroadcast(intent);
- }
- /***
- * send broadcast <SGSocketConstant>ON_ERROR filter if this socket has error
- * socket 發生錯誤發送的廣播,若想提示,可以接受並處理
- *
- */
- @Override
- public void onError(Exception arg0) {
- Intent intent = new Intent(SGSocketConstant.ON_ERROR);
- intent.putExtra(SGSocketConstant.ON_ERROR, arg0.toString());
- mContext.sendBroadcast(intent);
- this.close();
- }
- // 買家
- public static final String MSG_BUYER_TYPE = "1";
- // 客服
- public static final String MSG_SERVCER_TYPE = "2";
- // 游客
- // public static final String MSG_RANDOM_TYPE = "3";
- /***
- * receiver message from server 1,登陸返回 type
- * <WebSocket>LOGIN_RETURN_TYPE; 2,發送返回 type
- * <WebSocket>SEND_RETURN_TYPE; 3,接收信息返回 type
- * <WebSocket>RECEIVER_RETURN_TYPE;
- *
- * @throws InterruptedException
- */
- @Override
- public void onMessage(String content) {
- // parser
- try {
- JSONObject object = new JSONObject(content);
- Log.i("json", "賣家--" + object.toString());
- String back_type = object.getString("back_type");
- String activity = getRunningActivityName();
- if (SEND_RETURN_TYPE.equals(back_type)) {// 發送具體消息時返回發送結果
- // json解析
- MessageEntity entity = mParser.sendMessageParser(mContext,
- content);
- if ("true".equals(entity.getSend_state())) {// 發送成功
- // 判斷是否是PC端發送的消息,若是PC端發送的消息,則在Android端做同步存儲處理
- // 1,首先判斷數據庫中是否包含該條信息
- boolean has = MessageDB.getInstance(mContext)
- .findMessageByMsgId(entity.get_id());
- if (has) {
- // Android端發送
- MessageDB.getInstance(mContext).update(entity.get_id(),
- true, entity.getReceiverTime(),
- entity.getWebId());// 更新發送狀態為已發送
- } else {
- // PC端發送,將該消息同步到Android端數據庫
- entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);
- MessageDB.getInstance(mContext).insert(entity,
- SocketConstant.MSG_TYPE_BUYER);// 賣家發送給買家的
- // 通知聊天主頁面,更新聊天列表
- pcSynAndroid(activity);
- }
- // 檢測是否有消息遺漏
- checkMsgWebId(entity.getContacts(), entity.getWebId());
- Log.i("miss", "發送返回或者PC同步--" + entity.getContacts() + "--"
- + entity.getWebId());
- } else if ("false".equals(entity.getSend_state())) {// 發送失敗
- MessageDB.getInstance(mContext).update(entity.get_id(),
- false, entity.getReceiverTime(), entity.getWebId());
- Toast.makeText(mContext, entity.getErrorText(),
- Toast.LENGTH_SHORT).show();
- }
- // 登陸返回 記錄session
- } else if (LOGIN_RETURN_TYPE.equals(back_type)) {
- KApplication.session = object.getString("session_id");
- String str = object.getString("login_status");
- if ("true".equals(str)) {
- UserInfoUtil.saveSocket(mContext, true);
- // 生成json請求字符串
- String maxIdstring = SocketJsonUtil
- .getContactsCurrentWebId(UserInfoUtil
- .getUser(mContext)[0], "2", MessageDB
- .getInstance(mContext)
- .findAllContactsAndType());
- // 登陸成功,向服務器索取聯系人的最大webId
- send(maxIdstring);
- Log.i("send", maxIdstring);
- } else if ("false".equals(str)) {
- UserInfoUtil.saveSocket(mContext, false);
- }
- } else if (RECEIVER_RETURN_TYPE.equals(back_type)) {// 接收到的具體聊天的信息
- // json解析
- MessageEntity entity = mParser.receiverMessagePrser(mContext,
- content);
- // 判斷數據庫中是否有該條消息,有則不處理,無則處理消息;
- if (!MessageDB.getInstance(mContext).findMessageByMsgId(
- entity.get_id())) {
- // 消息處理
- if (MSG_BUYER_TYPE.equals(entity.getSenderType())) {
- // 買家
- messageHandle(entity, SocketConstant.MSG_TYPE_BUYER);
- } else if (MSG_SERVCER_TYPE.equals(entity.getSenderType())) {
- // 賣家,客服
- messageHandle(entity, SocketConstant.MSG_TYPE_SERVER);
- }
- Log.i("miss", "沒有該條消息");
- // 檢測是否有消息遺漏
- checkMsgWebId(entity.getContacts(), entity.getWebId());
- }
- } else if (GET_SERVER_RETURN_TYPE.equals(back_type)) {// 獲取閃聊客服返回的數據
- // 客服
- ServerEntity entity = mParser.serverInfoParser(content);// 客服對象
- Intent intent = new Intent(SocketConstant.GET_SERVER_INFO);
- intent.putExtra("server_info", entity);
- mContext.sendBroadcast(intent);
- } else if (CONTACTS_MAX_WEBID_TYPE.equals(back_type)) {
- // 返回的聯系人最大的消息id
- HashMap<String, String> map = mParser.contactsMaxWebId(content);
- // 將聯系人和其最大webId存入臨時集合
- saveContactsWebID(map);
- // 開始請求服務器,釋放離線消息給客戶端;
- send(SocketJsonUtil.getOffLine(
- UserInfoUtil.getUser(mContext)[0], "2"));
- Log.i("send",
- SocketJsonUtil.getOffLine(
- UserInfoUtil.getUser(mContext)[0], "2"));
- } else if (USER_OFFLINE_MSG_TYPE.equals(back_type)) {
- // 用戶的離線消息
- HashMap<String, ArrayList<MessageEntity>> map = mParser
- .offLineMsg(mContext, content);
- // 將離線消息入庫
- saveOffLineMsg(map);
- }
- } catch (JSONException e) {
- this.close();
- }
- }
- /***
- * send broadcast <SocketConstant>ON_OPEN filter if this socket opened
- * socket 打開時發送的廣播,若想提示,可以接受並處理
- *
- */
- @Override
- public void onOpen(ServerHandshake arg0) {
- Intent intent = new Intent(SGSocketConstant.ON_OPEN);
- mContext.sendBroadcast(intent);
- }
- /***
- * 檢測正在運行tasktop的activity
- * @return current running activity name
- *
- */
- private String getRunningActivityName() {
- ActivityManager activityManager = (ActivityManager) mContext
- .getSystemService(Context.ACTIVITY_SERVICE);
- String runningActivity = activityManager.getRunningTasks(1).get(0).topActivity
- .getClassName();
- return runningActivity;
- }
- /***
- * send notification for this contacts
- * 發送通知
- * @param contacts
- *
- */
- @SuppressWarnings("deprecation")
- private void sendNotification(String contacts) {
- Intent intent = new Intent(mContext, MainActivity.class);
- mIntent = PendingIntent.getActivity(mContext, 100, intent, 0);
- mNoti.flags = Notification.FLAG_AUTO_CANCEL;
- mNoti.defaults = Notification.DEFAULT_VIBRATE;
- mNoti.setLatestEventInfo(mContext, "標題", "您有新消息!", mIntent);
- mNoti.contentView = new RemoteViews(mContext.getApplicationContext()
- .getPackageName(), R.layout.notification_item);
- mNoti.contentView.setTextViewText(R.id.noti_message, "收到來自" + contacts
- + "的新消息");
- mNotificationManager.notify(NOTIFICATION_ID, mNoti);
- }
- /***
- * 具體聊天收到的外來消息處理
- *
- * @param entity
- * 消息實體
- * @param messageType
- * 消息類型(買家/客服)
- */
- private void messageHandle(MessageEntity entity, String messageType) {
- String activity = getRunningActivityName();
- // 處於聊天的頁面
- if ("com.ui.activity.ManageChartActivity".equals(activity)) {
- // 處於正在聊天對象的頁面,將數據寫入數據庫,並發送廣播更新頁面數據
- if (KApplication.crurentContacts.equals(entity.getContacts())) {
- /**
- * 接收到的消息,消息實體entity的send_state字段狀態設置為
- * MSG_SEND_SUCCESS_STATE(即201)
- **/
- entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);// 收到的信息,設置信息的狀態
- entity.setRead(SocketConstant.READ_STATE);
- MessageDB.getInstance(mContext).insert(entity, messageType);// 將數據寫入數據庫,
- Intent intent = new Intent(SocketConstant.NEW_MESSAGE);
- intent.putExtra("newmsg", entity);
- mContext.sendBroadcast(intent);
- // 沒有處於閃聊對象的頁面,將數據寫入數據庫,發送系統通知
- } else {
- entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);// 收到的信息,設置信息的狀態
- entity.setRead(SocketConstant.DEFAULT_READ_STATE);
- MessageDB.getInstance(mContext).insert(entity, messageType);
- if (KApplication.sp.getBoolean(RefreshUtils.noteFlag, false)) {
- sendNotification(entity.getContacts());
- }
- Intent intent = new Intent(
- SocketConstant.RECEIVER_NEW_MESSAGE);
- mContext.sendBroadcast(intent);
- }
- // 將數據寫入數據庫,發送系統通知
- } else {
- entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);// 收到的信息,設置信息的狀態
- entity.setRead(SocketConstant.DEFAULT_READ_STATE);
- MessageDB.getInstance(mContext).insert(entity, messageType);
- Intent intent = new Intent();
- if ("com.ui.activity.ManageConversationActivity"
- .equals(activity)
- || "com.ui.activity.MainActivity"
- .equals(activity)) {
- intent.setAction(SocketConstant.RECEIVER_NEW_MESSAGE);// 聊天頁面
- } else {
- intent.setAction(SocketConstant.RECEIVER_NEW_MESSAGE_OTHER);// 其他頁面
- }
- mContext.sendBroadcast(intent);
- if (KApplication.sp.getBoolean(RefreshUtils.noteFlag, false)) {
- sendNotification(entity.getContacts());
- }
- }
- }
- /***
- * 電腦與手機同步信息
- *
- * @param currentActivity
- */
- public void pcSynAndroid(String currentActivity) {
- if ("com.iflashseller.ui.activity.ManageChartActivity"
- .equals(currentActivity)) {
- // 正好與該聯系人對話的頁面
- Intent intent = new Intent(SocketConstant.CHART_ACTIVITY);
- mContext.sendBroadcast(intent);
- } else {
- // 其他頁面
- Intent intent = new Intent(SocketConstant.GROUPS_ACTIVITY);
- mContext.sendBroadcast(intent);
- }
- }
- /***
- * 檢測是否有消息遺漏
- *
- * @param contacts
- * 聯系人
- * @param webId
- * 服務端給出的消息Id
- */
- public void checkMsgWebId(String contacts, int webId) {
- // 集合中含有該聯系人
- if (KApplication.webIds.containsKey(contacts)) {
- Log.i("miss", "保存的--" + KApplication.webIds.get(contacts));
- // 臨時集合中保存的webId
- int c = KApplication.webIds.get(contacts);
- /***
- * 如果新收到的消息的webId大於臨時集合中保存的改聯系人的webId,且他們之間的差值大於1,
- * 則請求服務器推送疑似丟失的webId對應的消息
- */
- if (webId > c && (webId - 1) != c) {
- // id不連續
- for (int i = c + 1; i < webId; i++) {
- // 向服務器發送請求,獲取遺漏的消息
- String miss = SocketJsonUtil.getMissMsg(
- UserInfoUtil.getUser(mContext)[0], "2", contacts,
- "1", i + "");
- this.send(miss);
- Log.i("miss", miss);
- }
- /***
- * 如果他們之間的差值正好為1,則修改臨時集合的改聯系人的webId,
- */
- } else if (webId > c && (webId - 1) == c) {
- KApplication.webIds.put(contacts, webId);
- Log.i("miss", "修改的--" + contacts + "--" + webId);
- }
- /****
- * 臨時集合中沒有改聯系人的信息,則將該聯系人的webId存入臨時集合.
- */
- } else {
- KApplication.webIds.put(contacts, webId);
- Log.i("miss", "新增--" + contacts + "--" + webId);
- }
- }
- /***
- * 將從服務端獲取的聯系人的webId存入臨時集合
- *
- * @param map
- */
- public void saveContactsWebID(HashMap<String, String> map) {
- Iterator<Entry<String, String>> iter = map.entrySet().iterator();
- while (iter.hasNext()) {
- Entry<String, String> es = iter.next();
- String contacts = es.getKey();
- String maxWebID = es.getValue();
- KApplication.webIds.put(contacts, Integer.parseInt(maxWebID));
- }
- }
- /***
- * 將離線消息入庫
- *
- * @param map
- */
- public void saveOffLineMsg(HashMap<String, ArrayList<MessageEntity>> map) {
- Iterator<Entry<String, ArrayList<MessageEntity>>> iter = map.entrySet()
- .iterator();
- while (iter.hasNext()) {
- ArrayList<MessageEntity> msgs = iter.next().getValue();
- for (int i = 0; i < msgs.size(); i++) {
- threadSleep(100);
- MessageDB.getInstance(mContext).insert(msgs.get(i),
- SocketConstant.MSG_TYPE_BUYER);
- Log.i("write", "離線數據入庫---" + msgs.get(i).toString());
- }
- /***
- * 如果服務端一次釋放的離線消息大於等於10條,則繼續請求釋放離線消息.
- */
- if (msgs.size() >= 10) {
- send(SocketJsonUtil.getOffLine(
- UserInfoUtil.getUser(mContext)[0], "2"));
- Log.i("send",
- SocketJsonUtil.getOffLine(
- UserInfoUtil.getUser(mContext)[0], "2"));
- }
- }
- // 一輪消息入庫結束,發送通知,更新UI;
- mContext.sendBroadcast(new Intent(
- SocketConstant.OFFLINE_MSG_RECEIVER_SUCCESS));
- }
- private void threadSleep(long time) {
- try {
- Thread.currentThread();
- Thread.sleep(time);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
至於數據庫的一部分代碼就不貼出了,無非是增刪改查。
下面貼出部分監控鏈接狀態的代碼,以保證能即時的收到消息;
- public class LApplication extends Application {
- public static String TAG = LApplication.class.getSimpleName();
- /** 接收消息廣播 **/
- private LPullReceiver mPullReceiver;
- /** 是否是正在登錄 **/
- public static boolean isLoging = false;
- /** socket管理類 **/
- private LPushManager mWebSocket;
- @Override
- public void onCreate() {
- super.onCreate();
- /***
- * 注冊接收消息的廣播
- */
- mPullReceiver = new LPullReceiver();
- // 廣播過濾
- IntentFilter filter = new IntentFilter();
- // 時鍾信息發生變化
- filter.addAction(Intent.ACTION_TIME_TICK);
- // 開機廣播
- filter.addAction(Intent.ACTION_BOOT_COMPLETED);
- // 網絡狀態發生變化
- filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
- // 屏幕打開
- filter.addAction(Intent.ACTION_SCREEN_ON);
- // 注冊廣播
- registerReceiver(mPullReceiver, filter);
- // 實例化socket管理類
- mWebSocket = new LPushManager(getApplicationContext());
- // 應用重啟一次,默認socket為關閉狀態
- LPushUser.saveSocket(getApplicationContext(), false);
- // 默認鏈接沒有被拒絕
- LPushUser.saveConnect(getApplicationContext(), false);
- // 1,獲取當前時間
- long currentTime = System.currentTimeMillis();
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- Date date = new Date(currentTime);
- String time = sdf.format(date);
- // 修改標記的時間,保證5分鍾內鏈接一次
- LPushUser.saveOpenTime(getApplicationContext(), time);
- }
- /**
- * 廣播接口類
- * <ol>
- * <li>接收時鍾發生變化的廣播
- * <li>接收網絡發生變化的廣播
- * <li>接收開機發送廣播
- * <li>接收用戶登錄后發送的廣播
- * </ol>
- *
- * @author li'mingqi
- *
- */
- class LPullReceiver extends BroadcastReceiver {
- @SuppressLint("SimpleDateFormat")
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (Intent.ACTION_TIME_TICK.equals(action)
- || ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
- || Intent.ACTION_BOOT_COMPLETED.equals(action)
- || Intent.ACTION_SCREEN_ON.equals(action)) {
- if (LPushUser.GETLOG(getApplicationContext()))
- Log.i("lpush",
- "socket的鏈接狀態----"
- + LPushUser
- .getSocket(getApplicationContext())
- + "是否正在鏈接--" + isLoging + "----" + action);
- // 當時鍾或者網絡發生變化或者socket關閉或者發生異常時或者開機 時,判斷socket連接是否出現異常
- if (!LPushUser.getSocket(getApplicationContext()) && !isLoging
- && LPushUser.getSocketEnable(getApplicationContext())
- && !"".equals(IP) && !"".equals(PORT)
- && !LPushUser.getConnectState(getApplicationContext())) {
- // 掉線,執行登錄動作
- if (LNetworkUtil.netIsEnable(getApplicationContext())) {
- mWebSocket.secondMethod(IP, PORT);
- // 開始登錄了,標記打開時間
- // 1,獲取當前時間
- long currentTime = System.currentTimeMillis();
- SimpleDateFormat sdf = new SimpleDateFormat(
- "yyyy-MM-dd HH:mm:ss");
- Date date = new Date(currentTime);
- String time = sdf.format(date);
- // 修改標記的時間,保證5分鍾嗅探鏈接一次
- LPushUser.saveOpenTime(getApplicationContext(), time);
- }
- } else {
- // APP端已經處於鏈接正常狀態 -----5分鍾嗅探鏈接一次
- // 1,獲取當前時間
- long currentTime = System.currentTimeMillis();
- SimpleDateFormat sdf = new SimpleDateFormat(
- "yyyy-MM-dd HH:mm:ss");
- Date date = new Date(currentTime);
- String time = sdf.format(date);
- // 2,比對鏈接打開時間
- long minTime = LStringManager.dateDifference(
- LPushUser.getOpenTime(getApplicationContext()),
- time);
- if (LPushUser.GETLOG(getApplicationContext())) {
- Log.i("lpush",
- "鏈接時長----現在時間:"
- + time
- + ";保存時間:"
- + LPushUser
- .getOpenTime(getApplicationContext())
- + ";時差" + minTime + "分鍾");
- }
- if (minTime >= 5) {
- // 大於等於5分鍾,則重新鏈接
- // 5分鍾之后重新鏈接
- // 修改被拒絕狀態
- if (LPushUser.getConnectState(getApplicationContext())) {
- LPushUser.saveConnect(getApplicationContext(),
- false);
- }
- if (LNetworkUtil.netIsEnable(getApplicationContext())
- && LPushUser
- .getSocketEnable(getApplicationContext())) {
- mWebSocket.secondMethod(IP, PORT);
- // 修改標記的時間,保證5分鍾嗅探鏈接一次
- LPushUser.saveOpenTime(getApplicationContext(),
- time);
- }
- }
- }
- }
- }
- }
- /***
- * 設置推送功能的使用與否,默認使用推送功能,若是關閉推送功能請設置false;
- *
- * @param enable
- * 是否使用
- *
- * li'mingqi
- */
- protected void setLPushEnable(boolean enable) {
- LPushUser.saveSocketEnable(getApplicationContext(), enable);
- }
- /***
- *
- *
- * @param ip
- * ip信息
- * @param port
- * 端口信息
- *
- * li'mingqi
- */
- protected void setSocketIPInfo(String ip, String port) {
- this.IP = ip;
- this.PORT = port;
- }
- /**
- * 設置用戶的Uid
- *
- * @param uid
- * li'mingqi
- */
- public void setUserInfo(int uid, String code) {
- /***
- * 數據驗證
- */
- // if (0 == uid || null == code || "".equals(code)) {
- // Log.e(TAG, "您輸入的用戶ID或者CODE值為空");
- // new NullPointerException("您輸入的用戶ID或者CODE值為空").printStackTrace();
- // return;
- // }
- // 保存用戶ID
- LPushUser.saveUserID(getApplicationContext(), uid);
- // 保存用戶CODE
- LPushUser.saveUserCode(getApplicationContext(), code);
- // 重啟鏈接
- mWebSocket.close();
- }
- /***
- * 設置是否查看日志
- *
- * @param flag
- * 是否查看日志
- * @version 1.2 li'mingqi
- */
- public void openLogInfo(boolean flag) {
- LPushUser.SAVELOG(getApplicationContext(), flag);
- }
- /***
- * socket鏈接重置,服務器一直處於拒絕鏈接狀態,客戶端鏈接一次遭拒后標記了遭拒的狀態,重置之后可進行再次開啟鏈接;
- *
- * @version 1.3 li'mingqi
- */
- private void reset() {
- LPushUser.saveConnect(getApplicationContext(), false);
- }
- }
UI界面顯示部分就不貼出了,后面貼出Application類的目的就是監控各種手機系統的廣播來嗅探自己的websocket鏈接的狀態;用來維持它以保證能即時的收到消息;
