最近项目需要,用到socket长链接来进行数据处理,本来一直用http的我一下子就傻了眼,没关系,只要有心什么都不是事!经过我两天的研究和查找资料,终于对tcp有了一定的初步了解,在此也记录一下!
首先,我们需要AsyncSocket这个第三方库,对此也进行了解一下:
AsyncSocket类是支持TCP的
AsyncUdpSocket是支持UDP的
AsyncSocket是封装了CFSocket和CFSteam的TCP/IP socket网络库。它提供了异步操作,本地cocoa类的基于delegate的完整支持。
在此也附上下载地址:https://github.com/robbiehanson/CocoaAsyncSocket
我们下载完成之只需要把RunLoop文件夹下的AsyncSocket.h, AsyncSocket.m, AsyncUdpSocket.h, AsyncUdpSocket.m 四个文件拷贝到自己的project中即可,不过我建议还是在项目中自己创建一个文件夹方便管理,首先我们新建一个scoket(我英文不好,应该是socket,别见笑)项目,将RunLoop下的四个文件放到项目中如图:
然后我们需要添加一个CFNetwork.framework,在targets中找到build phases然后添加如图:
接着我们创建一个单例对象,命名就为Singleton吧,以后不管在哪里我们需要用到长链接,只需要调用这个单例对象便可进行操作。
单例的创建我就不用多说了吧,直接把我的代码附上了
.h文件
#import <Foundation/Foundation.h>
#import "AsyncSocket.h"
#define DEFINE_SHARED_INSTANCE_USING_BLOCK(block) \
static dispatch_once_t onceToken = 0; \
__strong static id sharedInstance = nil; \
dispatch_once(&onceToken, ^{ \
sharedInstance = block(); \
}); \
return sharedInstance; \
@interface Singleton : NSObject<AsyncSocketDelegate>
+ (Singleton *)sharedInstance;
@end
.m文件
+(Singleton *) sharedInstance
{
static Singleton *sharedInstace = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstace = [[self alloc] init];
});
return sharedInstace;
}
然后我们还需要在.h文件声明socket变量,和打开链接方法,并且继承AsyncSocket的代理协议,建议大家一定要看下代理方法有哪些分别都是什么用处,都是在什么时候调用,这个很重要!
于是.h文件代码:
@interface Singleton : NSObject<AsyncSocketDelegate>//感兴趣的同学可以点击进入AsyncSocketDelegate中看看代理方法
@property (nonatomic, strong) AsyncSocket *socket; // socket
@property (nonatomic, copy ) NSString *socketHost; // socket的Host
@property (nonatomic, assign) UInt16 socketPort; // socket的prot
-(void)socketConnectHost;// socket连接
+ (Singleton *)sharedInstance;
@end
.m文件实现打开链接的方法
/ socket连接
-(void)socketConnectHost{
self.socket = [[AsyncSocket alloc] initWithDelegate:self];
NSError *error = nil;
[self.socket connectToHost:self.socketHost onPort:self.socketPort withTimeout:3 error:&error];
}
使用socket进行即时通讯还有一个必须的操作,即对服务器发送心跳包,每隔一段时间对服务器发送长连接指令(指令不唯一,由服务器端指定,包括使用socket发送消息,发送的数据和格式都是由服务器指定),如果没有收到服务器的返回消息,AsyncSocket会得到失去连接的消息,我们可以在失去连接的回调方法里进行重新连接。
因此在singleton.h中声明一个定时器
@property (nonatomic, retain) NSTimer *connectTimer; // 计时器
在.m中实现连接成功回调方法,并在此方法中初始化定时器,发送心跳在后文向服务器发送数据时说明
#pragma mark - 连接成功回调
-(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
NSLog(@"socket连接成功");
//如果程序需要帐号登陆,一般在这里会先发送注册包
// 每隔30s像服务器发送心跳包
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];// 在longConnectToSocket方法中进行长连接需要向服务器发送的讯息
[[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes];//最好把定时器放在子线程中(假如有一个scrowview,当你一直滚动scrowview时,主线程一直在处理滚动事件,定时器就不会再走)
[self.connectTimer fire];
[self.socket readDataWithTimeout:-1 tag:0];//如果不加这句则可能不会触发回调函数,收不到服务器数据
}
实现longconnecttosocket方法
-(void)longConnectToSocket
{
// 根据服务器要求发送固定格式的数据,假设为指令@"longConnect",但是一般不会是这么简单的指令
NSString *longConnect = @"longConnect(xin tiao bao)\n";
NSLog(@"发送心跳包");
NSData *dataStream = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:dataStream withTimeout:-1 tag:1];
}
socket 断开连接与重连
失去连接有几种情况,服务器断开,用户主动cut,还可能有如QQ其他设备登录被掉线的情况,不管那种情况,我们都能收到socket回调方法返回给我们的讯息,如果是用户退出登录或是程序退出而需要手动cut,我们在cut前对socket的userData赋予一个值来标记为用户退出,这样我们可以在收到断开信息时判断究竟是什么原因导致的掉线
因此我们需要在.h文件声明一个枚举
enum{
SocketOfflineByServer,// 服务器掉线,默认为0
SocketOfflineByUser, // 用户主动cut
};
声明断开连接方法
-(void)cutOffSocket; // 断开socket连接
.m实现此方法
// 切断socket
-(void)cutOffSocket{
self.socket.userData = SocketOfflineByUser;// 声明是由用户主动切断
[self.connectTimer invalidate];
[self.socket disconnect];
}
一旦socket链接出现断开,将会执行代理方法-(void)onSocketDidDisconnect:(AsyncSocket *)sock,因此我们需要在这个方法中判断是否需要重新连接
.m文件实现此代理方法如下:
-(void)onSocketDidDisconnect:(AsyncSocket *)sock
{
NSLog(@"sorry the connect is failure %ld",sock.userData);
if (sock.userData == SocketOfflineByServer) {
// 服务器掉线,重连
[self socketConnectHost];
}
else if (sock.userData == SocketOfflineByUser) {
// 如果由用户断开,不进行重连
return;
}
}
socket 接收到服务器数据时会执行-(void)onSocket:(AsyncSocket )sock didReadData:(NSData )data withTag:(long)tag方法
-(void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSLog(@"收到数据");
// 对得到的data值进行解析与转换即可
NSLog(@"------%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
[self.socket readDataWithTimeout:-1 tag:0];
}
到此,我们的socket算是初步已经完成,现在我把Singleton.h和Singleton.m文件的全部代码贴出来供大家参考:
#import <Foundation/Foundation.h>
#import "AsyncSocket.h"
#define DEFINE_SHARED_INSTANCE_USING_BLOCK(block) \
static dispatch_once_t onceToken = 0; \
__strong static id sharedInstance = nil; \
dispatch_once(&onceToken, ^{ \
sharedInstance = block(); \
}); \
return sharedInstance; \
@interface Singleton : NSObject<AsyncSocketDelegate>
@property (nonatomic, strong) AsyncSocket *socket; // socket
@property (nonatomic, copy ) NSString *socketHost; // socket的Host
@property (nonatomic, assign) UInt16 socketPort; // socket的prot
@property (nonatomic, strong) NSTimer *connectTimer; // 计时器
enum{
SocketOfflineByServer,// 服务器掉线,默认为0
SocketOfflineByUser, // 用户主动cut
};
-(void)cutOffSocket; // 断开socket连接
-(void)socketConnectHost;//开启socket链接
+ (Singleton *)sharedInstance;
@end
//
// Singleton.m
// scoket
//
// Created by huasu on 15/10/12.
// Copyright (c) 2015年 HS. All rights reserved.
//
#import "Singleton.h"
@implementation Singleton
+(Singleton *) sharedInstance
{
static Singleton *sharedInstace = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstace = [[self alloc] init];
});
return sharedInstace;
}
-(void)socketConnectHost{
self.socket = [[AsyncSocket alloc] initWithDelegate:self];
NSError *error = nil;
[self.socket connectToHost:self.socketHost onPort:self.socketPort withTimeout:3 error:&error];
}
//pragma mark - 连接成功回调
-(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
NSLog(@"socket连接成功");
// 每隔30s像服务器发送心跳包
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];// 在longConnectToSocket方法中进行长连接需要向服务器发送的讯息
[[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes];//最好把定时器放在子线程中(假如有一个scrowview,当你一直滚动scrowview时,主线程一直在处理滚动事件,定时器就不会再走)
[self.connectTimer fire];
[self.socket readDataWithTimeout:-1 tag:0];
}
// 切断socket
-(void)cutOffSocket{
self.socket.userData = SocketOfflineByUser;// 声明是由用户主动切断
[self.connectTimer invalidate];
[self.socket disconnect];
}
-(void)onSocketDidDisconnect:(AsyncSocket *)sock
{
NSLog(@"sorry the connect is failure %ld",sock.userData);
if (sock.userData == SocketOfflineByServer) {
// 服务器掉线,重连
[self socketConnectHost];
}
else if (sock.userData == SocketOfflineByUser) {
// 如果由用户断开,不进行重连
return;
}
// NSData *dataStream = [@8 dataUsingEncoding:NSUTF8StringEncoding];
//
// [self.socket writeData:dataStream withTimeout:1 tag:1];
}
-(void)longConnectToSocket
{
// 根据服务器要求发送固定格式的数据,假设为指令@"longConnect",但是一般不会是这么简单的指令
NSString *longConnect = @"longConnect(xin tiao bao)\n";
NSLog(@"发送心跳包");
NSData *dataStream = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[self.socket writeData:dataStream withTimeout:-1 tag:1];
}
- (void)onSocket:(AsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag;
{
NSLog(@"----%ld",partialLength);
}
-(void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSLog(@"收到数据");
// 对得到的data值进行解析与转换即可
NSLog(@"------%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
[self.socket readDataWithTimeout:-1 tag:0];
}
@end
现在我们让创建一个viewcontroller来简单适用一下:
#import "ViewController.h"
#import "Singleton.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor=[UIColor redColor];
[Singleton sharedInstance].socketHost = @"192.168.5.109";// host设定
[Singleton sharedInstance].socketPort = 7701;// port设定
// 在连接前先进行手动断开
[Singleton sharedInstance].socket.userData = SocketOfflineByUser;
[[Singleton sharedInstance] cutOffSocket];
// // 确保断开后再连,如果对一个正处于连接状态的socket进行连接,会出现崩溃
[Singleton sharedInstance].socket.userData = SocketOfflineByServer;
[[Singleton sharedInstance] socketConnectHost];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
此时无论是突然断网,还是服务器主动断开连接,只要网络再次连接 都会进行从新链接
上面的socketHost时服务器地址,而socketPort是服务器端口
ui处理:
//1、发送消息 单例添加一个公共方法,传入一个字符串或Data
//2、接收消息 单例收到Socket消息,发送本地推送。相应的VC收听这个消息,实现后续逻辑