天天看點

Websocket實作即時通訊

Websocket

前言

關于我和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個;

自己可以在對應的函數裡面做響應的處理,比如當連結發生錯誤時要重新去打開該連結,收到消息時即時的儲存聊天記錄和發送系統通知來提醒使用者檢視新消息等等;

[html] view plain copy

  1. <pre name="code" class="html"><span style="font-size:18px;">public class TestClient extends WebSocketClient {  
  2.     public TestClient(URI serverURI) {  
  3.         super(serverURI);  
  4.     }  
  5.     /***  
  6.      * 連結關閉  
  7.      */  
  8.     @Override  
  9.     public void onClose(int arg0, String arg1, boolean arg2) {  
  10.     }  
  11.     /***  
  12.      * 連結發生錯誤  
  13.      */  
  14.     @Override  
  15.     public void onError(Exception arg0) {  
  16.     }  
  17.     /**  
  18.      * 新消息  
  19.      */  
  20.     @Override  
  21.     public void onMessage(String arg0) {  
  22.     }  
  23.     /***  
  24.      * 連結打開  
  25.      */  
  26.     @Override  
  27.     public void onOpen(ServerHandshake arg0) {  
  28.         // TODO Auto-generated method stub  
  29.     }  
  30. }  
  31. </span>  

下面是我聊天部分代碼,有離線消息/PC同步/多對多的聊天;

僅供參考;

[java] view plain copy

  1. /*** 
  2.  * <h2>WebSocket Android 用戶端</h2> 
  3.  * <ol> 
  4.  * <li>Socket連結打開回調 {@link WebSocket#onOpen(ServerHandshake)},此處有 
  5.  * {@link SocketConstant#ON_OPEN} 廣播發出; 
  6.  * <li>Socket連結出現異常錯誤時回調 {@link WebSocket#onError(Exception)},此處有 
  7.  * {@link SocketConstant#ON_ERROR}廣播發出; 
  8.  * <li>Socket連結關閉回調 {@link WebSocket #onClose(int, String, boolean)},此處有 
  9.  * {@link SocketConstant#ON_CLOSES}廣播發出; 
  10.  * <li>Socket連結接收消息回調 {@link WebSocket#onMessage(String)} 
  11.  * ,此處做收消息的邏輯的處理;包括發送消息伺服器傳回的發送結果,PC端同步接收的新消息,及外來的新消息; 
  12.  * <li>檢測是否有消息遺漏 {@link WebSocket#checkMsgWebId(String, String)},參數為聯系人和webId; 
  13.  * <li>取得正在桌面運作的activity的名稱 {@link WebSocket#getRunningActivityName()} 
  14.  * <li>接收到的消息的處理 {@link WebSocket#messageHandle(MessageEntity, String)} 
  15.  * ,參數為消息實體和消息類型 (buyer/server) 
  16.  * <li>發送新消息的系統通知 {@link WebSocket#sendNotification(String)},參數為聯系人; 
  17.  * <li>儲存離線消息 {@link WebSocket#saveOffLineMsg(HashMap)},參數為接收到的離線消息集合; 
  18.  * <li>儲存從服務端擷取的聯系人的webId {@link WebSocket#saveContactsWebID(HashMap)} 
  19.  * ,參數為以聯系人為key以最大webId為值得map集合; 
  20.  * </ol> 
  21.  *  
  22.  * @author li\'mingqi <a> 2014-3-19</a> 
  23.  *  
  24.  */  
  25. public class WebSocket extends WebSocketClient {  
  26.     // 登陸傳回的back_type字段  
  27.     public static final String LOGIN_RETURN_TYPE = "login";  
  28.     // 發送資訊的back_type字段  
  29.     public static final String SEND_RETURN_TYPE = "send_result";  
  30.     // 接收資訊的back_type字段  
  31.     public static final String RECEIVER_RETURN_TYPE = "msg";  
  32.     // 接收客服的資訊的back_type字段  
  33.     public static final String GET_SERVER_RETURN_TYPE = "server_info";  
  34.     // 接收服務端傳回對應聯系人的最大順序ID  
  35.     public static final String CONTACTS_MAX_WEBID_TYPE = "max_id_return";  
  36.     // 接收使用者的離線消息  
  37.     public static final String USER_OFFLINE_MSG_TYPE = "offline";  
  38.     // 上下文對象  
  39.     private Context mContext;  
  40.     // socket傳回json解析類對象  
  41.     private WebSocketParser mParser;  
  42.     // 系統通知管理  
  43.     public NotificationManager mNotificationManager;  
  44.     // 系統通知  
  45.     private Notification mNoti;  
  46.     // 意圖  
  47.     private PendingIntent mIntent;  
  48.     // 該系統通知的 id  
  49.     public static final int NOTIFICATION_ID = 100;  
  50.     @SuppressWarnings("deprecation")  
  51.     public SGWebSocket(Context context, URI serverUri, Draft draft) {  
  52.         super(serverUri, draft);  
  53.         this.mContext = context;  
  54.                 //新消息的解析類  
  55.                 this.mParser = WebSocketParser.getInstance();  
  56.                 //收到新消息發送的通知  
  57.                 this.mNotificationManager = (NotificationManager) this.mContext  
  58.                 .getSystemService(Context.NOTIFICATION_SERVICE);  
  59.         this.mNoti = new Notification(R.drawable.system_info, "您有新消息!",  
  60.                 System.currentTimeMillis());  
  61.     }  
  62.     /*** 
  63.      * send broadcast <SGSocketConstant>ON_CLOSES filter if this socket closed 
  64.      * socket 發生關閉時發送的廣播,若想提示,可以接受并處理 
  65.      *  
  66.      */  
  67.     @Override  
  68.     public void onClose(int arg0, String arg1, boolean arg2) {  
  69.         // 更改儲存的連結狀态  
  70.         UserInfoUtil.saveSocket(mContext, false);  
  71.         mNotificationManager.cancelAll();  
  72.         Intent intent = new Intent(SocketConstant.ON_CLOSES);  
  73.         intent.putExtra(SocketConstant.ON_CLOSES, arg1.toString());  
  74.         mContext.sendBroadcast(intent);  
  75.     }  
  76.     /*** 
  77.      * send broadcast <SGSocketConstant>ON_ERROR filter if this socket has error 
  78.      * socket 發生錯誤發送的廣播,若想提示,可以接受并處理 
  79.      *  
  80.      */  
  81.     @Override  
  82.     public void onError(Exception arg0) {  
  83.         Intent intent = new Intent(SGSocketConstant.ON_ERROR);  
  84.         intent.putExtra(SGSocketConstant.ON_ERROR, arg0.toString());  
  85.         mContext.sendBroadcast(intent);  
  86.         this.close();  
  87.     }  
  88.     // 買家  
  89.     public static final String MSG_BUYER_TYPE = "1";  
  90.     // 客服  
  91.     public static final String MSG_SERVCER_TYPE = "2";  
  92.     // 遊客  
  93.     // public static final String MSG_RANDOM_TYPE = "3";  
  94.     /*** 
  95.      * receiver message from server 1,登陸傳回 type 
  96.      * <WebSocket>LOGIN_RETURN_TYPE; 2,發送傳回 type 
  97.      * <WebSocket>SEND_RETURN_TYPE; 3,接收資訊傳回 type 
  98.      * <WebSocket>RECEIVER_RETURN_TYPE; 
  99.      *  
  100.      * @throws InterruptedException 
  101.      */  
  102.     @Override  
  103.     public void onMessage(String content) {  
  104.         // parser  
  105.         try {  
  106.             JSONObject object = new JSONObject(content);  
  107.             Log.i("json", "賣家--" + object.toString());  
  108.             String back_type = object.getString("back_type");  
  109.             String activity = getRunningActivityName();  
  110.             if (SEND_RETURN_TYPE.equals(back_type)) {// 發送具體消息時傳回發送結果  
  111.                 // json解析  
  112.                 MessageEntity entity = mParser.sendMessageParser(mContext,  
  113.                         content);  
  114.                 if ("true".equals(entity.getSend_state())) {// 發送成功  
  115.                     // 判斷是否是PC端發送的消息,若是PC端發送的消息,則在Android端做同步存儲處理  
  116.                     // 1,首先判斷資料庫中是否包含該條資訊  
  117.                     boolean has = MessageDB.getInstance(mContext)  
  118.                             .findMessageByMsgId(entity.get_id());  
  119.                     if (has) {  
  120.                         // Android端發送  
  121.                         MessageDB.getInstance(mContext).update(entity.get_id(),  
  122.                                 true, entity.getReceiverTime(),  
  123.                                 entity.getWebId());// 更新發送狀态為已發送  
  124.                     } else {  
  125.                         // PC端發送,将該消息同步到Android端資料庫  
  126.                         entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);  
  127.                         MessageDB.getInstance(mContext).insert(entity,  
  128.                                 SocketConstant.MSG_TYPE_BUYER);// 賣家發送給買家的  
  129.                         // 通知聊天首頁面,更新聊天清單  
  130.                         pcSynAndroid(activity);  
  131.                     }  
  132.                     // 檢測是否有消息遺漏  
  133.                     checkMsgWebId(entity.getContacts(), entity.getWebId());  
  134.                     Log.i("miss", "發送傳回或者PC同步--" + entity.getContacts() + "--"  
  135.                             + entity.getWebId());  
  136.                 } else if ("false".equals(entity.getSend_state())) {// 發送失敗  
  137.                     MessageDB.getInstance(mContext).update(entity.get_id(),  
  138.                             false, entity.getReceiverTime(), entity.getWebId());  
  139.                     Toast.makeText(mContext, entity.getErrorText(),  
  140.                             Toast.LENGTH_SHORT).show();  
  141.                 }  
  142.                 // 登陸傳回 記錄session  
  143.             } else if (LOGIN_RETURN_TYPE.equals(back_type)) {  
  144.                 KApplication.session = object.getString("session_id");  
  145.                 String str = object.getString("login_status");  
  146.                 if ("true".equals(str)) {  
  147.                     UserInfoUtil.saveSocket(mContext, true);  
  148.                     // 生成json請求字元串  
  149.                     String maxIdstring = SocketJsonUtil  
  150.                             .getContactsCurrentWebId(UserInfoUtil  
  151.                                     .getUser(mContext)[0], "2", MessageDB  
  152.                                     .getInstance(mContext)  
  153.                                     .findAllContactsAndType());  
  154.                     // 登陸成功,向伺服器索取聯系人的最大webId  
  155.                     send(maxIdstring);  
  156.                     Log.i("send", maxIdstring);  
  157.                 } else if ("false".equals(str)) {  
  158.                     UserInfoUtil.saveSocket(mContext, false);  
  159.                 }  
  160.             } else if (RECEIVER_RETURN_TYPE.equals(back_type)) {// 接收到的具體聊天的資訊  
  161.                 // json解析  
  162.                 MessageEntity entity = mParser.receiverMessagePrser(mContext,  
  163.                         content);  
  164.                 // 判斷資料庫中是否有該條消息,有則不處理,無則處理消息;  
  165.                 if (!MessageDB.getInstance(mContext).findMessageByMsgId(  
  166.                         entity.get_id())) {  
  167.                     // 消息處理  
  168.                     if (MSG_BUYER_TYPE.equals(entity.getSenderType())) {  
  169.                         // 買家  
  170.                         messageHandle(entity, SocketConstant.MSG_TYPE_BUYER);  
  171.                     } else if (MSG_SERVCER_TYPE.equals(entity.getSenderType())) {  
  172.                         // 賣家,客服  
  173.                         messageHandle(entity, SocketConstant.MSG_TYPE_SERVER);  
  174.                     }  
  175.                     Log.i("miss", "沒有該條消息");  
  176.                     // 檢測是否有消息遺漏  
  177.                     checkMsgWebId(entity.getContacts(), entity.getWebId());  
  178.                 }  
  179.             } else if (GET_SERVER_RETURN_TYPE.equals(back_type)) {// 擷取閃聊客服傳回的資料  
  180.                 // 客服  
  181.                 ServerEntity entity = mParser.serverInfoParser(content);// 客服對象  
  182.                 Intent intent = new Intent(SocketConstant.GET_SERVER_INFO);  
  183.                 intent.putExtra("server_info", entity);  
  184.                 mContext.sendBroadcast(intent);  
  185.             } else if (CONTACTS_MAX_WEBID_TYPE.equals(back_type)) {  
  186.                 // 傳回的聯系人最大的消息id  
  187.                 HashMap<String, String> map = mParser.contactsMaxWebId(content);  
  188.                 // 将聯系人和其最大webId存入臨時集合  
  189.                 saveContactsWebID(map);  
  190.                 // 開始請求伺服器,釋放離線消息給用戶端;  
  191.                 send(SocketJsonUtil.getOffLine(  
  192.                         UserInfoUtil.getUser(mContext)[0], "2"));  
  193.                 Log.i("send",  
  194.                         SocketJsonUtil.getOffLine(  
  195.                                 UserInfoUtil.getUser(mContext)[0], "2"));  
  196.             } else if (USER_OFFLINE_MSG_TYPE.equals(back_type)) {  
  197.                 // 使用者的離線消息  
  198.                 HashMap<String, ArrayList<MessageEntity>> map = mParser  
  199.                         .offLineMsg(mContext, content);  
  200.                 // 将離線消息入庫  
  201.                 saveOffLineMsg(map);  
  202.             }  
  203.         } catch (JSONException e) {  
  204.             this.close();  
  205.         }  
  206.     }  
  207.     /*** 
  208.      * send broadcast <SocketConstant>ON_OPEN filter if this socket opened 
  209.      * socket 打開時發送的廣播,若想提示,可以接受并處理 
  210.      *  
  211.      */  
  212.     @Override  
  213.     public void onOpen(ServerHandshake arg0) {  
  214.         Intent intent = new Intent(SGSocketConstant.ON_OPEN);  
  215.         mContext.sendBroadcast(intent);  
  216.     }  
  217.     /*** 
  218.      * 檢測正在運作tasktop的activity 
  219.      * @return current running activity name 
  220.      *  
  221.      */  
  222.     private String getRunningActivityName() {  
  223.         ActivityManager activityManager = (ActivityManager) mContext  
  224.                 .getSystemService(Context.ACTIVITY_SERVICE);  
  225.         String runningActivity = activityManager.getRunningTasks(1).get(0).topActivity  
  226.                 .getClassName();  
  227.         return runningActivity;  
  228.     }  
  229.     /*** 
  230.      * send notification for this contacts 
  231.      * 發送通知 
  232.      * @param contacts 
  233.      *  
  234.      */  
  235.     @SuppressWarnings("deprecation")  
  236.     private void sendNotification(String contacts) {  
  237.         Intent intent = new Intent(mContext, MainActivity.class);  
  238.         mIntent = PendingIntent.getActivity(mContext, 100, intent, 0);  
  239.         mNoti.flags = Notification.FLAG_AUTO_CANCEL;  
  240.         mNoti.defaults = Notification.DEFAULT_VIBRATE;  
  241.         mNoti.setLatestEventInfo(mContext, "标題", "您有新消息!", mIntent);  
  242.         mNoti.contentView = new RemoteViews(mContext.getApplicationContext()  
  243.                 .getPackageName(), R.layout.notification_item);  
  244.         mNoti.contentView.setTextViewText(R.id.noti_message, "收到來自" + contacts  
  245.                 + "的新消息");  
  246.         mNotificationManager.notify(NOTIFICATION_ID, mNoti);  
  247.     }  
  248.     /*** 
  249.      * 具體聊天收到的外來消息處理 
  250.      *  
  251.      * @param entity 
  252.      *            消息實體 
  253.      * @param messageType 
  254.      *            消息類型(買家/客服) 
  255.      */  
  256.     private void messageHandle(MessageEntity entity, String messageType) {  
  257.         String activity = getRunningActivityName();  
  258.         // 處于聊天的頁面  
  259.         if ("com.ui.activity.ManageChartActivity".equals(activity)) {  
  260.             // 處于正在聊天對象的頁面,将資料寫入資料庫,并發送廣播更新頁面資料  
  261.             if (KApplication.crurentContacts.equals(entity.getContacts())) {  
  262.                 /** 
  263.                  * 接收到的消息,消息實體entity的send_state字段狀态設定為 
  264.                  * MSG_SEND_SUCCESS_STATE(即201) 
  265.                  **/  
  266.                 entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);// 收到的資訊,設定資訊的狀态  
  267.                 entity.setRead(SocketConstant.READ_STATE);  
  268.                 MessageDB.getInstance(mContext).insert(entity, messageType);// 将資料寫入資料庫,  
  269.                 Intent intent = new Intent(SocketConstant.NEW_MESSAGE);  
  270.                 intent.putExtra("newmsg", entity);  
  271.                 mContext.sendBroadcast(intent);  
  272.                 // 沒有處于閃聊對象的頁面,将資料寫入資料庫,發送系統通知  
  273.             } else {  
  274.                 entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);// 收到的資訊,設定資訊的狀态  
  275.                 entity.setRead(SocketConstant.DEFAULT_READ_STATE);  
  276.                 MessageDB.getInstance(mContext).insert(entity, messageType);  
  277.                 if (KApplication.sp.getBoolean(RefreshUtils.noteFlag, false)) {  
  278.                     sendNotification(entity.getContacts());  
  279.                 }  
  280.                 Intent intent = new Intent(  
  281.                         SocketConstant.RECEIVER_NEW_MESSAGE);  
  282.                 mContext.sendBroadcast(intent);  
  283.             }  
  284.             // 将資料寫入資料庫,發送系統通知  
  285.         } else {  
  286.             entity.setSend_state(SocketConstant.MSG_SEND_SUCCESS_STATE);// 收到的資訊,設定資訊的狀态  
  287.             entity.setRead(SocketConstant.DEFAULT_READ_STATE);  
  288.             MessageDB.getInstance(mContext).insert(entity, messageType);  
  289.             Intent intent = new Intent();  
  290.             if ("com.ui.activity.ManageConversationActivity"  
  291.                     .equals(activity)  
  292.                     || "com.ui.activity.MainActivity"  
  293.                             .equals(activity)) {  
  294.                 intent.setAction(SocketConstant.RECEIVER_NEW_MESSAGE);// 聊天頁面  
  295.             } else {  
  296.                 intent.setAction(SocketConstant.RECEIVER_NEW_MESSAGE_OTHER);// 其他頁面  
  297.             }  
  298.             mContext.sendBroadcast(intent);  
  299.             if (KApplication.sp.getBoolean(RefreshUtils.noteFlag, false)) {  
  300.                 sendNotification(entity.getContacts());  
  301.             }  
  302.         }  
  303.     }  
  304.     /*** 
  305.      * 電腦與手機同步資訊 
  306.      *  
  307.      * @param currentActivity 
  308.      */  
  309.     public void pcSynAndroid(String currentActivity) {  
  310.         if ("com.iflashseller.ui.activity.ManageChartActivity"  
  311.                 .equals(currentActivity)) {  
  312.             // 正好與該聯系人對話的頁面  
  313.             Intent intent = new Intent(SocketConstant.CHART_ACTIVITY);  
  314.             mContext.sendBroadcast(intent);  
  315.         } else {  
  316.             // 其他頁面  
  317.             Intent intent = new Intent(SocketConstant.GROUPS_ACTIVITY);  
  318.             mContext.sendBroadcast(intent);  
  319.         }  
  320.     }  
  321.     /*** 
  322.      * 檢測是否有消息遺漏 
  323.      *  
  324.      * @param contacts 
  325.      *            聯系人 
  326.      * @param webId 
  327.      *            服務端給出的消息Id 
  328.      */  
  329.     public void checkMsgWebId(String contacts, int webId) {  
  330.         // 集合中含有該聯系人  
  331.         if (KApplication.webIds.containsKey(contacts)) {  
  332.             Log.i("miss", "儲存的--" + KApplication.webIds.get(contacts));  
  333.             // 臨時集合中儲存的webId  
  334.             int c = KApplication.webIds.get(contacts);  
  335.             /*** 
  336.              * 如果新收到的消息的webId大于臨時集合中儲存的改聯系人的webId,且他們之間的內插補點大于1, 
  337.              * 則請求伺服器推送疑似丢失的webId對應的消息 
  338.              */  
  339.             if (webId > c && (webId - 1) != c) {  
  340.                 // id不連續  
  341.                 for (int i = c + 1; i < webId; i++) {  
  342.                     // 向伺服器發送請求,擷取遺漏的消息  
  343.                     String miss = SocketJsonUtil.getMissMsg(  
  344.                             UserInfoUtil.getUser(mContext)[0], "2", contacts,  
  345.                             "1", i + "");  
  346.                     this.send(miss);  
  347.                     Log.i("miss", miss);  
  348.                 }  
  349.                 /*** 
  350.                  * 如果他們之間的內插補點正好為1,則修改臨時集合的改聯系人的webId, 
  351.                  */  
  352.             } else if (webId > c && (webId - 1) == c) {  
  353.                 KApplication.webIds.put(contacts, webId);  
  354.                 Log.i("miss", "修改的--" + contacts + "--" + webId);  
  355.             }  
  356.             /**** 
  357.              * 臨時集合中沒有改聯系人的資訊,則将該聯系人的webId存入臨時集合. 
  358.              */  
  359.         } else {  
  360.             KApplication.webIds.put(contacts, webId);  
  361.             Log.i("miss", "新增--" + contacts + "--" + webId);  
  362.         }  
  363.     }  
  364.     /*** 
  365.      * 将從服務端擷取的聯系人的webId存入臨時集合 
  366.      *  
  367.      * @param map 
  368.      */  
  369.     public void saveContactsWebID(HashMap<String, String> map) {  
  370.         Iterator<Entry<String, String>> iter = map.entrySet().iterator();  
  371.         while (iter.hasNext()) {  
  372.             Entry<String, String> es = iter.next();  
  373.             String contacts = es.getKey();  
  374.             String maxWebID = es.getValue();  
  375.             KApplication.webIds.put(contacts, Integer.parseInt(maxWebID));  
  376.         }  
  377.     }  
  378.     /*** 
  379.      * 将離線消息入庫 
  380.      *  
  381.      * @param map 
  382.      */  
  383.     public void saveOffLineMsg(HashMap<String, ArrayList<MessageEntity>> map) {  
  384.         Iterator<Entry<String, ArrayList<MessageEntity>>> iter = map.entrySet()  
  385.                 .iterator();  
  386.         while (iter.hasNext()) {  
  387.             ArrayList<MessageEntity> msgs = iter.next().getValue();  
  388.             for (int i = 0; i < msgs.size(); i++) {  
  389.                 threadSleep(100);  
  390.                 MessageDB.getInstance(mContext).insert(msgs.get(i),  
  391.                         SocketConstant.MSG_TYPE_BUYER);  
  392.                 Log.i("write", "離線資料入庫---" + msgs.get(i).toString());  
  393.             }  
  394.             /*** 
  395.              * 如果服務端一次釋放的離線消息大于等于10條,則繼續請求釋放離線消息. 
  396.              */  
  397.             if (msgs.size() >= 10) {  
  398.                 send(SocketJsonUtil.getOffLine(  
  399.                         UserInfoUtil.getUser(mContext)[0], "2"));  
  400.                 Log.i("send",  
  401.                         SocketJsonUtil.getOffLine(  
  402.                                 UserInfoUtil.getUser(mContext)[0], "2"));  
  403.             }  
  404.         }  
  405.         // 一輪消息入庫結束,發送通知,更新UI;  
  406.         mContext.sendBroadcast(new Intent(  
  407.                 SocketConstant.OFFLINE_MSG_RECEIVER_SUCCESS));  
  408.     }  
  409.     private void threadSleep(long time) {  
  410.         try {  
  411.             Thread.currentThread();  
  412.             Thread.sleep(time);  
  413.         } catch (InterruptedException e) {  
  414.             e.printStackTrace();  
  415.         }  
  416.     }  
  417. }  

至于資料庫的一部分代碼就不貼出了,無非是增删改查。

下面貼出部分監控連結狀态的代碼,以保證能即時的收到消息;

[java] view plain copy

  1. public class LApplication extends Application {  
  2.     public static String TAG = LApplication.class.getSimpleName();  
  3.     /** 接收消息廣播 **/  
  4.     private LPullReceiver mPullReceiver;  
  5.     /** 是否是正在登入 **/  
  6.     public static boolean isLoging = false;  
  7.     /** socket管理類 **/  
  8.     private LPushManager mWebSocket;  
  9.     @Override  
  10.     public void onCreate() {  
  11.         super.onCreate();  
  12.         /*** 
  13.          * 注冊接收消息的廣播 
  14.          */  
  15.         mPullReceiver = new LPullReceiver();  
  16.         // 廣播過濾  
  17.         IntentFilter filter = new IntentFilter();  
  18.         // 時鐘資訊發生變化  
  19.         filter.addAction(Intent.ACTION_TIME_TICK);  
  20.         // 開機廣播  
  21.         filter.addAction(Intent.ACTION_BOOT_COMPLETED);  
  22.         // 網絡狀态發生變化  
  23.         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);  
  24.         // 螢幕打開  
  25.         filter.addAction(Intent.ACTION_SCREEN_ON);  
  26.         // 注冊廣播  
  27.         registerReceiver(mPullReceiver, filter);  
  28.         // 執行個體化socket管理類  
  29.         mWebSocket = new LPushManager(getApplicationContext());  
  30.         // 應用重新開機一次,預設socket為關閉狀态  
  31.         LPushUser.saveSocket(getApplicationContext(), false);  
  32.         // 預設連結沒有被拒絕  
  33.         LPushUser.saveConnect(getApplicationContext(), false);  
  34.         // 1,擷取目前時間  
  35.         long currentTime = System.currentTimeMillis();  
  36.         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  37.         Date date = new Date(currentTime);  
  38.         String time = sdf.format(date);  
  39.         // 修改标記的時間,保證5分鐘内連結一次  
  40.         LPushUser.saveOpenTime(getApplicationContext(), time);  
  41.     }  
  42.     /** 
  43.      * 廣播接口類 
  44.      * <ol> 
  45.      * <li>接收時鐘發生變化的廣播 
  46.      * <li>接收網絡發生變化的廣播 
  47.      * <li>接收開機發送廣播 
  48.      * <li>接收使用者登入後發送的廣播 
  49.      * </ol> 
  50.      *  
  51.      * @author li\'mingqi 
  52.      *  
  53.      */  
  54.     class LPullReceiver extends BroadcastReceiver {  
  55.         @SuppressLint("SimpleDateFormat")  
  56.         @Override  
  57.         public void onReceive(Context context, Intent intent) {  
  58.             String action = intent.getAction();  
  59.             if (Intent.ACTION_TIME_TICK.equals(action)  
  60.                     || ConnectivityManager.CONNECTIVITY_ACTION.equals(action)  
  61.                     || Intent.ACTION_BOOT_COMPLETED.equals(action)  
  62.                     || Intent.ACTION_SCREEN_ON.equals(action)) {  
  63.                 if (LPushUser.GETLOG(getApplicationContext()))  
  64.                     Log.i("lpush",  
  65.                             "socket的連結狀态----"  
  66.                                     + LPushUser  
  67.                                             .getSocket(getApplicationContext())  
  68.                                     + "是否正在連結--" + isLoging + "----" + action);  
  69.                 // 當時鐘或者網絡發生變化或者socket關閉或者發生異常時或者開機 時,判斷socket連接配接是否出現異常  
  70.                 if (!LPushUser.getSocket(getApplicationContext()) && !isLoging  
  71.                         && LPushUser.getSocketEnable(getApplicationContext())  
  72.                         && !"".equals(IP) && !"".equals(PORT)  
  73.                         && !LPushUser.getConnectState(getApplicationContext())) {  
  74.                     // 掉線,執行登入動作  
  75.                     if (LNetworkUtil.netIsEnable(getApplicationContext())) {  
  76.                         mWebSocket.secondMethod(IP, PORT);  
  77.                         // 開始登入了,标記打開時間  
  78.                         // 1,擷取目前時間  
  79.                         long currentTime = System.currentTimeMillis();  
  80.                         SimpleDateFormat sdf = new SimpleDateFormat(  
  81.                                 "yyyy-MM-dd HH:mm:ss");  
  82.                         Date date = new Date(currentTime);  
  83.                         String time = sdf.format(date);  
  84.                         // 修改标記的時間,保證5分鐘嗅探連結一次  
  85.                         LPushUser.saveOpenTime(getApplicationContext(), time);  
  86.                     }  
  87.                 } else {  
  88.                     // APP端已經處于連結正常狀态 -----5分鐘嗅探連結一次  
  89.                     // 1,擷取目前時間  
  90.                     long currentTime = System.currentTimeMillis();  
  91.                     SimpleDateFormat sdf = new SimpleDateFormat(  
  92.                             "yyyy-MM-dd HH:mm:ss");  
  93.                     Date date = new Date(currentTime);  
  94.                     String time = sdf.format(date);  
  95.                     // 2,比對連結打開時間  
  96.                     long minTime = LStringManager.dateDifference(  
  97.                             LPushUser.getOpenTime(getApplicationContext()),  
  98.                             time);  
  99.                     if (LPushUser.GETLOG(getApplicationContext())) {  
  100.                         Log.i("lpush",  
  101.                                 "連結時長----現在時間:"  
  102.                                         + time  
  103.                                         + ";儲存時間:"  
  104.                                         + LPushUser  
  105.                                                 .getOpenTime(getApplicationContext())  
  106.                                         + ";時差" + minTime + "分鐘");  
  107.                     }  
  108.                     if (minTime >= 5) {  
  109.                         // 大于等于5分鐘,則重新連結  
  110.                         // 5分鐘之後重新連結  
  111.                         // 修改被拒絕狀态  
  112.                         if (LPushUser.getConnectState(getApplicationContext())) {  
  113.                             LPushUser.saveConnect(getApplicationContext(),  
  114.                                     false);  
  115.                         }  
  116.                         if (LNetworkUtil.netIsEnable(getApplicationContext())  
  117.                                 && LPushUser  
  118.                                         .getSocketEnable(getApplicationContext())) {  
  119.                             mWebSocket.secondMethod(IP, PORT);  
  120.                             // 修改标記的時間,保證5分鐘嗅探連結一次  
  121.                             LPushUser.saveOpenTime(getApplicationContext(),  
  122.                                     time);  
  123.                         }  
  124.                     }  
  125.                 }  
  126.             }  
  127.         }  
  128.     }  
  129.     /*** 
  130.      * 設定推送功能的使用與否,預設使用推送功能,若是關閉推送功能請設定false; 
  131.      *  
  132.      * @param enable 
  133.      *            是否使用 
  134.      *  
  135.      *            li\'mingqi  
  136.      */  
  137.     protected void setLPushEnable(boolean enable) {  
  138.         LPushUser.saveSocketEnable(getApplicationContext(), enable);  
  139.     }  
  140.     /*** 
  141.      *  
  142.      *  
  143.      * @param ip 
  144.      *            ip資訊 
  145.      * @param port 
  146.      *            端口資訊 
  147.      *  
  148.      *            li\'mingqi  
  149.      */  
  150.     protected void setSocketIPInfo(String ip, String port) {  
  151.         this.IP = ip;  
  152.         this.PORT = port;  
  153.     }  
  154.     /** 
  155.      * 設定使用者的Uid 
  156.      *  
  157.      * @param uid 
  158.      *            li\'mingqi  
  159.      */  
  160.     public void setUserInfo(int uid, String code) {  
  161.         /*** 
  162.          * 資料驗證 
  163.          */  
  164.         // if (0 == uid || null == code || "".equals(code)) {  
  165.         // Log.e(TAG, "您輸入的使用者ID或者CODE值為空");  
  166.         // new NullPointerException("您輸入的使用者ID或者CODE值為空").printStackTrace();  
  167.         // return;  
  168.         // }  
  169.         // 儲存使用者ID  
  170.         LPushUser.saveUserID(getApplicationContext(), uid);  
  171.         // 儲存使用者CODE  
  172.         LPushUser.saveUserCode(getApplicationContext(), code);  
  173.         // 重新開機連結  
  174.         mWebSocket.close();  
  175.     }  
  176.     /*** 
  177.      * 設定是否檢視日志 
  178.      *  
  179.      * @param flag 
  180.      *            是否檢視日志 
  181.      * @version 1.2 li\'mingqi 
  182.      */  
  183.     public void openLogInfo(boolean flag) {  
  184.         LPushUser.SAVELOG(getApplicationContext(), flag);  
  185.     }  
  186.     /*** 
  187.      * socket連結重置,伺服器一直處于拒絕連結狀态,用戶端連結一次遭拒後标記了遭拒的狀态,重置之後可進行再次開啟連結; 
  188.      *  
  189.      * @version 1.3 li\'mingqi  
  190.      */  
  191.     private void reset() {  
  192.         LPushUser.saveConnect(getApplicationContext(), false);  
  193.     }  
  194. }  

UI界面顯示部分就不貼出了,後面貼出Application類的目的就是監控各種手機系統的廣播來嗅探自己的websocket連結的狀态;用來維持它以保證能即時的收到消息;