通过GCDAsyncSocket实现socket链接,直接上代码:
服务端代码
#import "ServerViewController.h"
#import
// 定义消息类型
#define kcTextDataType 0x00000000
#define kcImageDataType 0x00000001
#define kcVideoDataType 0x00000002
@interface ServerViewController ()
@property (nonatomic, strong) GCDAsyncSocket *socket;
@property (nonatomic, strong) NSMutableArray *clientSockets;
@property (weak, nonatomic) IBOutlet UIButton *linkButton;
@property (weak, nonatomic) IBOutlet UIButton *senderFileButton;
@property (weak, nonatomic) IBOutlet UIButton *breakButton;
@property (weak, nonatomic) IBOutlet UIButton *senderButton;
@property (weak, nonatomic) IBOutlet UITextView *msgTextView;
@property (weak, nonatomic) IBOutlet UITextField *inputTextField;
@property (nonatomic, copy) NSString * chackedMessage;
@property (nonatomic, strong) NSMutableData *dataM;
@property (nonatomic, assign) unsigned int totalSize;
@property (nonatomic, assign) unsigned int currentCommandId;
@end
@implementation ServerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.chackedMessage = @"";
}
- (IBAction)linkAction:(id)sender
{
// 创建socket
if (self.socket == nil)
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
// 绑定socket
if (!self.socket.isConnected){
NSError *error;
[self.socket acceptOnPort:8090 error:&error];
if (error) {
NSLog(@"%@",error);
}else {
NSLog(@"服务器socket 开启成功");
[self.linkButton setTitle:@"建立成功" forState:UIControlStateNormal];
}
}
}
- (IBAction)senderFileAction:(UIButton *)sender
{
UIImage * img = [UIImage imageNamed:@"test.jpg"];
NSData *imageData = UIImagePNGRepresentation(img);
unsigned int command = kcImageDataType;
NSMutableData * mData = [self dataHander:imageData type:command];
for (GCDAsyncSocket *clientSocket in self.clientSockets) {
[clientSocket writeData:mData withTimeout:-1 tag:10010];
}
}
- (IBAction)breakLink:(id)sender
{
[self.socket disconnect];
self.socket = nil;
}
- (IBAction)senderAction:(id)sender {
if (self.inputTextField.text.length == 0) {
return;
}
NSData *data = [[self.inputTextField.text stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *mData = [self dataHander:data type:kcTextDataType];
self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"发送消息:%@\n", self.inputTextField.text];
dispatch_async(dispatch_get_main_queue(), ^{
self.msgTextView.text = self.chackedMessage;
self.inputTextField.text = @"";
});
for (GCDAsyncSocket *clientSocket in self.clientSockets) {
[clientSocket writeData:mData withTimeout:-1 tag:10010];
}
}
// 将数据类型及数据本身进行拼接处理
- (NSMutableData *)dataHander:(NSData *)data type:(unsigned int)type
{
NSMutableData *mData = [NSMutableData data];
unsigned int dataLength = 4+4+(int)data.length;
NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
[mData appendData:lengthData];
// 拼接指令类型(4~7:指令)
NSData *typeData = [NSData dataWithBytes:&type length:4];
[mData appendData:typeData];
[mData appendData:data];
return mData;
}
#pragma mark - GCDAsyncSocketDelegate
// 连接状态回调
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
NSLog(@"当前客户端的IP:%@ 端口号%d",newSocket.connectedHost,newSocket.connectedPort);
// 1.存储客户端面的Socket对象
[self.clientSockets addObject:newSocket];
NSLog(@"当前有%ld个客户端连接",self.clientSockets.count);
// 2.客户连接建立后,设置可以读取数据
[newSocket readDataWithTimeout:-1 tag:10010];
[self.socket readDataWithTimeout:-1 tag:10010];
}
// 连接断开
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"断开 socket连接 原因:%@",err);
[self.clientSockets removeObject:sock];
}
//接收客户端发送来的数据并进行粘包处理
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
if (data.length == 0) {
return;
}
if(self.dataM.length == 0){
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
[totalSizeData getBytes:&totalSize length:4];
NSLog(@"接收总数据的大小 %u",totalSize);
self.totalSize = totalSize;
NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
unsigned int commandId = 0;
[commandIdData getBytes:&commandId length:4];
self.currentCommandId = commandId;
}
[self.dataM appendData:data];
if (self.dataM.length == self.totalSize) {
if (self.currentCommandId == kcTextDataType) {
[self showMsg:[self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)]];
self.dataM = nil;
}else if (self.currentCommandId == kcVideoDataType){//图片
}else if (self.currentCommandId == kcVideoDataType){
}
}
[sock readDataWithTimeout:-1 tag:10010];
[self.socket readDataWithTimeout:-1 tag:10010];
}
- (void)showMsg:(NSData *)data
{
NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if ([str isEqualToString:@"\n"]) {
return;
}
self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"接收消息:%@", str];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"历史消息:%@", self.chackedMessage);
self.msgTextView.text = self.chackedMessage;
});
}
//消息发送成功 代理函数 向服务器 发送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
NSLog(@"服务器发送 laile ");
NSLog(@"%ld 发送数据成功",tag);
}
#pragma mark -- lazy
- (NSMutableArray *)clientSockets{
if (!_clientSockets) {
_clientSockets = [NSMutableArray arrayWithCapacity:10];
}
return _clientSockets;
}
- (NSMutableData *)dataM
{
if (!_dataM) {
_dataM = [NSMutableData data];
}
return _dataM;
}
@end
客户端代码
#import "ClientViewController.h"
#import
// 定义消息类型
#define kcTextDataType 0x00000000
#define kcImageDataType 0x00000001
#define kcVideoDataType 0x00000002
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
@interface ClientViewController ()
@property (weak, nonatomic) IBOutlet UIButton *linkButton;
@property (weak, nonatomic) IBOutlet UIButton *reLinkButton;
@property (weak, nonatomic) IBOutlet UIButton *breakButton;
@property (weak, nonatomic) IBOutlet UIButton *senderButton;
@property (weak, nonatomic) IBOutlet UITextView *msgTextView;
@property (weak, nonatomic) IBOutlet UITextField *inputTextField;
@property (nonatomic, strong) GCDAsyncSocket *socket;
@property (nonatomic, copy) NSString * chackedMessage;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSMutableData *dataM;
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) unsigned int totalSize;
@property (nonatomic, assign) unsigned int currentCommandId;
@property (weak, nonatomic) IBOutlet UIImageView * backImageView;
// 心跳timer
@property (nonatomic, strong) NSTimer *heartTimer;
// 重连等待时间
@property (nonatomic, assign) NSInteger reconnectTime;
@end
@implementation ClientViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.chackedMessage = @"";
self.queue = dispatch_get_global_queue(0, 0);
UIBarButtonItem * item = [[UIBarButtonItem alloc] initWithTitle:@"发送" style:UIBarButtonItemStyleDone target:self action:@selector(senderAction:)];
self.navigationItem.rightBarButtonItem = item;
}
- (void)finish
{
[self.inputTextField resignFirstResponder];
}
- (IBAction)linkAction:(id)sender {
if (self.socket == nil) {
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.queue];
}
// 连接socket
if (!self.socket.isConnected){
NSError *error;
[self.socket connectToHost:@"ip地址" onPort:8090 withTimeout:-1 error:&error];
if (error) {
NSLog(@"%@",error);
return;
}else {
[self.linkButton setTitle:@"已连接" forState:UIControlStateNormal];
[self setupHeartBeat];
}
}
}
// 重新链接
- (IBAction)reLinkAction:(id)sender {
// 创建socket
if (self.socket == nil) {
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.queue];
}
// 连接socket
if (!self.socket.isConnected){
NSError *error;
[self.socket connectToHost:@"ip地址" onPort:8090 withTimeout:-1 error:&error];
if (error) NSLog(@"%@",error);
}
}
// 断开链接
- (IBAction)breakAction:(id)sender {
[self.socket disconnect];
self.socket = nil;
[self.linkButton setTitle:@"连接" forState:UIControlStateNormal];
}
- (IBAction)senderAction:(id)sender {
if (self.inputTextField.text.length == 0) {
return;
}
NSData *data = [[self.inputTextField.text stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *mData = [NSMutableData data];
// 计算数据总长度 data
unsigned int dataLength = 4+4+(int)data.length;
NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
[mData appendData:lengthData];
// 数据类型 data
// 2.拼接指令类型(4~7:指令)
unsigned int command = kcTextDataType;
// 将类型标记拼接到数据上
NSData *typeData = [NSData dataWithBytes:&command length:4];
[mData appendData:typeData];
// 最后拼接数据
[mData appendData:data];
NSLog(@"发送数据的总字节大小:%ld",mData.length);
[self.socket writeData:mData withTimeout:-1 tag:10086];
self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"发送消息:%@\n", self.inputTextField.text];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"历史消息:%@", self.chackedMessage);
self.msgTextView.text = self.chackedMessage;
self.inputTextField.text = @"";
});
}
// 关闭socket
- (void)disconnectSocket{
if (self.socket) {
[self.socket disconnect];
self.socket.delegate = nil;
self.socket = nil;
[self destoryHeartBeat];
}
}
// 心跳处理
//在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项:SO_KEEPALIVE。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。而且逻辑层处理断线可能也不是那么好处理。一般,如果只是用于保活还是可以的。
//其实,要判定掉线,只需要send或者recv一下,如果结果为零,则为掉线。但是,在长连接下,有可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更要命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在这个时候,就需要我们的心跳包了,用于维持长连接,保活。
// 在获知了断线之后,服务器逻辑可能需要做一些事情,比如断线后的数据清理呀,重新连接呀……当然,这个自然是要由逻辑层根据需求去做了。
- (void)setupHeartBeat{
dispatch_async(dispatch_get_main_queue(), ^{
[self destoryHeartBeat];
__weak typeof(self) weakSelf = self;
self.heartTimer = [NSTimer scheduledTimerWithTimeInterval:15 repeats:YES block:^(NSTimer * _Nonnull timer) {
__weak typeof(self) strongSelf = weakSelf;
NSData *data = [[self.inputTextField.text stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *mData = [NSMutableData data];
// 计算数据总长度 data
unsigned int dataLength = 4+4+(int)data.length;
NSData *lengthData = [NSData dataWithBytes:&dataLength length:4];
[mData appendData:lengthData];
// 数据类型 data
// 2.拼接指令类型(4~7:指令)
unsigned int command = kcTextDataType;
NSData *typeData = [NSData dataWithBytes:&command length:4];
[mData appendData:typeData];
// 最后拼接数据
[mData appendData:data];
NSLog(@"发送数据的总字节大小:%ld",mData.length);
[strongSelf.socket writeData:mData withTimeout:-1 tag:10086];
}];
});
}
- (void)destoryHeartBeat{
//当然这里还以更加严谨一点: 因为已经在主线程了
dispatch_main_async_safe(^{
if (self.heartTimer && [self.heartTimer respondsToSelector:@selector(isValid)] && [self.heartTimer isValid]) {
[self.heartTimer invalidate];
self.heartTimer = nil;
}
});
}
#pragma mark - GCDAsyncSocketDelegate
//已经连接到服务器
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port{
NSLog(@"连接成功 : %@---%d",host,port);
[self.socket readDataWithTimeout:-1 tag:10086];
}
// 连接断开
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"断开 socket连接 原因:%@",err);
}
/**
粘包中容易出现的问题:
1.要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去;
2.接收数据端的应用层没有及时读取接收缓冲区中的数据;
3.数据发送过快,数据包堆积导致缓冲区积压多个数据后才一次性发送出去(如果客户端每发送一条数据就睡眠一段时间就不会发生粘包);
*/
//已经接收服务器返回来的数据,由于拆包、粘包的问题数据不能发送过快
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
if (data.length == 0) {
NSLog(@"传输数据长度为: 0");
return;
}
// 1.第一次接收数据
if(self.dataM.length == 0){
// 获取总的数据包大小
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
//读取前四个字节
[totalSizeData getBytes:&totalSize length:4];
NSLog(@"接收总数据的大小 %u",totalSize);
self.totalSize = totalSize;
// 获取指令类型
NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
unsigned int commandId = 0;
[commandIdData getBytes:&commandId length:4];
self.currentCommandId = commandId;
}
// 2.拼接二进制数据
[self.dataM appendData:data];
if (self.dataM.length == self.totalSize) {
//丢包的现象
NSLog(@"数据已经接收完成");
if (self.currentCommandId == kcTextDataType) {
[self showMsg:[self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)]];
dispatch_async(dispatch_get_main_queue(), ^{
self.msgTextView.hidden = NO;
self.backImageView.hidden = YES;
});
self.dataM = nil;
}else if (self.currentCommandId == kcImageDataType){//图片
UIImage * img = [UIImage imageWithData:[self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)]];
dispatch_async(dispatch_get_main_queue(), ^{
self.msgTextView.hidden = YES;
self.backImageView.hidden = NO;
self.backImageView.image = img;
});
self.dataM = nil;
}else if (self.currentCommandId == kcVideoDataType){
self.dataM = nil;
}
self.totalSize = 0;
}
[sock readDataWithTimeout:-1 tag:10010];
[self.socket readDataWithTimeout:-1 tag:10010];
}
//消息发送成功 代理函数 向服务器 发送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
NSLog(@"%ld 发送数据成功",tag);
}
- (void)showMsg:(NSData *)data
{
NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if ([str isEqualToString:@"\n"]) {
return;
}
self.chackedMessage = [self.chackedMessage stringByAppendingFormat:@"接收消息:%@", str];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"历史消息:%@", self.chackedMessage);
self.msgTextView.hidden = NO;
self.msgTextView.text = self.chackedMessage;
});
}
#pragma mark -- lazyLoad
- (NSMutableData *)dataM
{
if (!_dataM) {
_dataM = [NSMutableData data];
}
return _dataM;
}
@end
参考文章:拆包粘包