用socket可以實現像QQ那樣傳送立即訊息的功能。用戶端和服務端需要建立長串連,在長串連的情況下,發送訊息。用戶端可以發送心跳包來檢測長串連。
在iOS開發中使用socket,一般都是用第三方庫AsyncSocket,不得不承認這個庫確實很強大。下載地址CocoaAsyncSocket
。
特性
AsyncSocket類是支援TCP的。
AsyncUdpSocket是支援UDP的。
AsyncSocket是封裝了CFSocket和CFSteam的TCP/IP socket網路程式庫。它提供了非同步作業,本地cocoa類的基於delegate的完整支援。主要有以下特性:
- 隊列的非阻塞的讀和寫,而且可選逾時。你可以調用它讀取和寫入,它會當完成後告知你。
- 自動的socket接收。如果你調用它接收串連,它將為每個串連啟動新的執行個體,當然,也可以立即關閉這些串連。
- 委託(delegate)支援。錯誤、串連、接收、完整的讀取、完整的寫入、進度以及中斷連線,都可以通過委託模式調用。
- 基於run loop的,而不是線程的。雖然可以在主線程或者背景工作執行緒中使用它,但你不需要這樣做。它非同步調用委託方法,使用NSRunLoop。委託方法包括socket的參數,可讓你在多個執行個體中區分。
- 自包含在一個類中。你無需操作流或者socket,這個類幫你做了全部。
- 支援基於IPV4和IPV6的TCP流。
AsyncUdpSocket是UDP/IP socket網路程式庫,封裝自CFSocket。它的工作很像TCP版本,只不過是用於處理UDP的。它包括基於非阻塞隊列的發送接收操作,完整的委託支援,基於runloop,自包含的類,以及支援IPV4和IPV6。
使用AsyncSocket的時候可以做一層封裝,根據需求提供幾個介面出來。比如:串連、中斷連線、發送訊息等等。還有接受訊息,接受到的訊息可以通過通知、代理、block等傳出去。
下面簡單介紹一下對AsyncSocket使用.一般來說,一個使用者只需要建立一個socket長串連,所以可以用單例類方便使用。
定義單列類:LGSocketServe
LGSocketServe.h
複製代碼 代碼如下:
//
// LGSocketServe.h
// AsyncSocketDemo
//
#import <Foundation/Foundation.h>
#import "AsyncSocket.h"
@interface LGSocketServe : NSObject<AsyncSocketDelegate>
+ (LGSocketServe *)sharedSocketServe;
@end
LGSocketServe.m
複製代碼 代碼如下:
//
// LGSocketServe.m
// AsyncSocketDemo
//
#import "LGSocketServe.h"
@implementation LGSocketServe
static LGSocketServe *socketServe = nil;
#pragma mark public static methods
+ (LGSocketServe *)sharedSocketServe {
@synchronized(self) {
if(socketServe == nil) {
socketServe = [[[self class] alloc] init];
}
}
return socketServe;
}
+(id)allocWithZone:(NSZone *)zone
{
@synchronized(self)
{
if (socketServe == nil)
{
socketServe = [super allocWithZone:zone];
return socketServe;
}
}
return nil;
}
@end
建立socket長串連
LGSocketServe.h
複製代碼 代碼如下:
@property (nonatomic, strong) AsyncSocket *socket; // socket
// socket串連
- (void)startConnectSocket;
LGSocketServe.m
//自己設定
#define HOST @"192.168.0.1"
#define PORT 8080
//設定連線逾時
#define TIME_OUT 20
- (void)startConnectSocket
{
self.socket = [[AsyncSocket alloc] initWithDelegate:self];
[self.socket setRunLoopModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
if ( ![self SocketOpen:HOST port:PORT] )
{
}
}
- (NSInteger)SocketOpen:(NSString*)addr port:(NSInteger)port
{
if (![self.socket isConnected])
{
NSError *error = nil;
[self.socket connectToHost:addr onPort:port withTimeout:TIME_OUT error:&error];
}
return 0;
}
宏定義一下HOST、PORT、TIME_OUT,實現startConnectSocket方法。這個時候要設定一下AsyncSocket的代理AsyncSocketDelegate。當長串連成功之後會調用:
複製代碼 代碼如下:
- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
//這是非同步返回的串連成功,
NSLog(@"didConnectToHost");
}
心跳
LGSocketServe.h
複製代碼 代碼如下:
@property (nonatomic, retain) NSTimer *heartTimer; // 心跳計時器
LGSocketServe.m
- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
//這是非同步返回的串連成功,
NSLog(@"didConnectToHost");
//通過定時器不斷髮送訊息,來檢測長串連
self.heartTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(checkLongConnectByServe) userInfo:nil repeats:YES];
[self.heartTimer fire];
}
// 心跳串連
-(void)checkLongConnectByServe{
// 向伺服器發送固定可是的訊息,來檢測長串連
NSString *longConnect = @"connect is here";
NSData *data = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:data withTimeout:1 tag:1];
}
在串連成功的回調方法裡,啟動定時器,每隔2秒向伺服器發送固定的訊息來檢測長串連。(這個根據伺服器的需要就可以了)
中斷連線
1,使用者手動中斷連線
LGSocketServe.h
複製代碼 代碼如下:
// 斷開socket串連
-(void)cutOffSocket;
LGSocketServe.m
-(void)cutOffSocket
{
self.socket.userData = SocketOfflineByUser;
[self.socket disconnect];
}
cutOffSocket是使用者中斷連線之後,不在嘗試重新串連。
2,wifi斷開,socket中斷連線
LGSocketServe.m
複製代碼 代碼如下:
- (void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err
{
NSLog(@" willDisconnectWithError %ld err = %@",sock.userData,[err description]);
if (err.code == 57) {
self.socket.userData = SocketOfflineByWifiCut;
}
}
wifi斷開之後,會回調onSocket:willDisconnectWithError:方法,err.code == 57,這個時候設定self.socket.userData = SocketOfflineByWifiCut。
重新串連
socket斷開之後會回調:
LGSocketServe.m
複製代碼 代碼如下:
- (void)onSocketDidDisconnect:(AsyncSocket *)sock
{
NSLog(@"7878 sorry the connect is failure %ld",sock.userData);
if (sock.userData == SocketOfflineByServer) {
// 伺服器掉線,重連
[self startConnectSocket];
}
else if (sock.userData == SocketOfflineByUser) {
// 如果由使用者斷開,不進行重連
return;
}else if (sock.userData == SocketOfflineByWifiCut) {
// wifi斷開,不進行重連
return;
}
}
在onSocketDidDisconnect回調方法裡面,會根據self.socket.userData來判斷是否需要重新串連。
發送訊息
LGSocketServe.h
複製代碼 代碼如下:
// 發送訊息
- (void)sendMessage:(id)message;
LGSocketServe.m
//設定寫入逾時 -1 表示不會使用逾時
#define WRITE_TIME_OUT -1
- (void)sendMessage:(id)message
{
//像伺服器發送資料
NSData *cmdData = [message dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:cmdData withTimeout:WRITE_TIME_OUT tag:1];
}
//發送訊息成功之後回調
- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag
{
}
發送訊息成功之後會調用onSocket:didWriteDataWithTag:,在這個方法裡可以進行讀取訊息。
接受訊息
LGSocketServe.m
複製代碼 代碼如下:
//設定讀取逾時 -1 表示不會使用逾時
#define READ_TIME_OUT -1
#define MAX_BUFFER 1024
//發送訊息成功之後回調
- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag
{
//讀取訊息
[self.socket readDataWithTimeout:-1 buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
}
//接受訊息成功之後回調
- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
//服務端返回訊息資料量比較大時,可能分多次返回。所以在讀取訊息的時候,設定MAX_BUFFER表示每次最多讀取多少,當data.length < MAX_BUFFER我們認為有可能是接受完一個完整的訊息,然後才解析
if( data.length < MAX_BUFFER )
{
//收到結果解析...
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
NSLog(@"%@",dic);
//解析出來的訊息,可以通過通知、代理、block等傳出去
}
[self.socket readDataWithTimeout:READ_TIME_OUT buffer:nil bufferOffset:0 maxLength:MAX_BUFFER tag:0];
接受訊息後去解析,然後可以通過通知、代理、block等傳出去。在onSocket:didReadData:withTag:回調方法裡面需要不斷讀取訊息,因為資料量比較大的話,伺服器會分多次返回。所以我們需要定義一個MAX_BUFFER的宏,表示每次最多讀取多少。當data.length < MAX_BUFFER我們認為有可能是接受完一個完整的訊息,然後才解析 。
出錯處理
LGSocketServe.m
複製代碼 代碼如下:
- (void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err
{
NSData * unreadData = [sock unreadData]; // ** This gets the current buffer
if(unreadData.length > 0) {
[self onSocket:sock didReadData:unreadData withTag:0]; // ** Return as much data that could be collected
} else {
NSLog(@" willDisconnectWithError %ld err = %@",sock.userData,[err description]);
if (err.code == 57) {
self.socket.userData = SocketOfflineByWifiCut;
}
}
}
socket出錯會回調onSocket:willDisconnectWithError:方法,可以通過unreadData來讀取未來得及讀取的buffer。
使用
匯入#import “LGSocketServe.h”
複製代碼 代碼如下:
LGSocketServe *socketServe = [LGSocketServe sharedSocketServe];
//socket串連前先中斷連線以免之前socket串連沒有斷開導致閃退
[socketServe cutOffSocket];
socketServe.socket.userData = SocketOfflineByServer;
[socketServe startConnectSocket];
//發送訊息 @"hello world"只是舉個列子,具體根據服務端的訊息格式
[socketServe sendMessage:@"hello world"];