iOS Socket,NSStream - 入门

最近公司需要使用长连接完成一些业务,因为业务基本使用React Native(缩写RN)写的,因此准备直接使用RN的WebSocket封装完成。本系列将从《iOS Socket,NSStream - 入门》开始扩展。

1.Socket简介

A network socket is an internal endpoint for sending or receiving data at a single node in a computer network. Concretely, it is a representation of this endpoint in networking software (protocol stack), such as an entry in a table (listing communication protocol, destination, status, etc.), and is a form of system resource.

简要的说:Socket就是可以接收发送数据的网络端点。而我们常说的Socket就是基于TCP/IP的封装的网络通信的编程接口。

2.使用

下面将使用CocoaAsyncSocket写一个简易的服务端的socket服务(后续会直接分析其实现原理),客户端使用苹果原生API完成。

2.1 客户端

#import "ViewController.h"

@interface ViewController ()<UITextViewDelegate,NSStreamDelegate>{
    NSInputStream *inputStream;
    NSOutputStream *outputStream;
    UITextView *textView;
    UIButton *sendMsgButton;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self initUI];
    [self connectServer];
}

- (void)initUI{
    textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
    textView.backgroundColor = [UIColor redColor];
    textView.center = CGPointMake(self.view.center.x, 100);
    [self.view addSubview:textView];
    
    sendMsgButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 200, 50)];
    sendMsgButton.center = CGPointMake(self.view.center.x, 200);
    [sendMsgButton setTitle:@"发送消息" forState:UIControlStateNormal];
    [sendMsgButton addTarget:self action:@selector(sendMsg:) forControlEvents:UIControlEventTouchUpInside];
    sendMsgButton.backgroundColor = [UIColor redColor];
    [self.view addSubview:sendMsgButton];
}

- (void)connectServer{
    NSString *host = @"10.180.186.111";
    int port = 8585;
    
    // 1.创建输入输出流,设置代理
    CFReadStreamRef readStreamRef;
    CFWriteStreamRef writeStreamRef;
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStreamRef, &writeStreamRef);
    inputStream = (__bridge NSInputStream *)(readStreamRef);
    outputStream = (__bridge NSOutputStream *)(writeStreamRef);
    
    inputStream.delegate = self;
    outputStream.delegate = self;
    
    // 2.输入输出流必须加入主运行runLoop中
    [inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    [outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
    [inputStream open];
    [outputStream open];
}

- (void)sendMsg:(id)sender{
    NSString *msg = textView.text;
    NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding];
    [outputStream write:msgData.bytes maxLength:msgData.length];
    textView.text = @"";
    NSLog(@"客户端发送消息:%@",msg);
}

#pragma mark - NSStreamDelegate
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch(eventCode) {
            
        case NSStreamEventOpenCompleted:
            NSLog(@"客户端输入输出流打开完成");
            break;
            
        case NSStreamEventHasBytesAvailable:
            NSLog(@"客户端有字节可读");
            [self readData];
            break;
            
        case NSStreamEventHasSpaceAvailable:
            NSLog(@"客户端可以发送字节");
            break;
            
        case NSStreamEventErrorOccurred:
            NSLog(@"客户端连接出现错误");
            break;
            
        case NSStreamEventEndEncountered:
            NSLog(@"客户端连接结束");
            //关闭输入输出流
            [inputStream close];
            [outputStream close];
            
            //从主运行循环移除
            [inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            [outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            break;
        default:
            break;
            
    }
}

-(void)readData{
    
    //建立一个缓冲区 可以放1024个字节
    uint8_t buf[1024];
    //返回实际装的字节数
    NSInteger len = [inputStream read:buf maxLength:sizeof(buf)];
    //把字节数组转化成字符串
    NSData *data =[NSData dataWithBytes:buf length:len];
    //从服务器接收到的数据
    NSString *recStr =[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Socket:读取从服务端发来的消息:%@",recStr);
}

@end

2.2 服务端

首先导入GCDAsyncSocket.h,GCDAsyncSocket.m

#import <Foundation/Foundation.h>

@interface Server : NSObject

@property(strong,nonatomic)NSMutableArray *clientSocket;

- (void)startChatServer;

@end

#import "GCDAsyncSocket.h"
#import "GCDAsyncSocket.h"

@interface Server ()<GCDAsyncSocketDelegate>
{
    GCDAsyncSocket *_serverSocket;
    dispatch_queue_t _golbalQueue;
}

@end

@implementation Server

- (void)startChatServer{
    NSError *err;
    [_serverSocket acceptOnInterface:@"10.180.186.111" port:8585
                               error:&err];
    if (!err) {
        NSLog(@"服务开启成功");
    }else{
        NSLog(@"服务开启失败");
    }
}
-(instancetype)init{
    if (self = [super init]) {
        _clientSocket = [NSMutableArray array];
        _golbalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        _serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_golbalQueue];
    }
    return self;
}
#pragma mark 有客户端建立连接的时候调用
-(void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
    [self.clientSocket addObject:newSocket];
    
    //newSocket为客户端的Socket。这里读取数据
    [newSocket readDataWithTimeout:-1 tag:100];
}

#pragma mark - GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    [sock readDataWithTimeout:-1 tag:100];
}

#pragma mark - GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    
    //接收到数据
    NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"服务端收到的数据:%@",receiverStr);

    if ([[receiverStr lowercaseString] hasPrefix:@"msg:"]) {
        [sock writeData:[@"你也好!!" dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
    }
}

@end

2.3 图解

图片转载至关于iOS socket都在这里了

2.4 测试结果

客户端:
2017-06-13 14:54:54.910 SocketClient[14967:459438] 客户端输入输出流打开完成
2017-06-13 14:54:54.910 SocketClient[14967:459438] 客户端输入输出流打开完成
2017-06-13 14:54:54.910 SocketClient[14967:459438] 客户端可以发送字节
2017-06-13 14:55:03.593 SocketClient[14967:459438] 客户端发送消息:你好
2017-06-13 14:55:03.593 SocketClient[14967:459438] 客户端可以发送字节
2017-06-13 14:55:03.593 SocketClient[14967:459438] 客户端有字节可读
2017-06-13 14:55:03.594 SocketClient[14967:459438] 客户端读取从服务端发来的消息:你也好!!
服务端:
2017-06-13 14:54:45.661 SocketServer[14956:458965] 服务开启成功
2017-06-13 14:55:03.589 SocketServer[14956:459916] 服务端收到的数据:你好
2017-06-13 14:55:03.589 SocketServer[14956:459916] 服务端回复数据:你也好!!

3.NSInputStream,NSOutputStream

我们先看看二者的虚基类NSStream的描述:

NSStream is an abstract class for objects representing streams. Its interface is common to all Cocoa stream classes, including its concrete subclasses NSInputStream and NSOutputStream.
NSStream objects provide an easy way to read and write data to and from a variety of media in a device-independent way. You can create stream objects for data located in memory, in a file, or on a network (using sockets), and you can use stream objects without loading all of the data into memory at once.
By default, NSStream instances that are not file-based are non-seekable, one-way streams (although custom seekable subclasses are possible). Once the data has been provided or consumed, the data cannot be retrieved from the stream.

  • 1.NSStream有两个子类NSInputStream和NSOutputStream。
  • 2.NSStream提供了很方便的方式读取,写入各种内存,文件,网络套接字的方式中的数据。
  • 3.不是基于文件的NSStream实例是不可见的,是单向;且一旦被消耗 掉不会再恢复。

在2中我们了解了NSInputStream和NSOutputStream在Socket中的使用,下面我们直接利用它们来读写文件。

3.1 NSInputStream 读取工程目录的文件写入沙盒

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"txt"];
[self readDataFromFile:filePath];

- (void)readDataFromFile:(NSString *)filePath{
    inputStream = [[NSInputStream alloc] initWithFileAtPath:filePath];
    inputStream.delegate = self;
    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [inputStream open];
}

- (void)writeDataToFile:(NSString *)filepath{
    if(!mutableData){
        return;
    }
    
    [mutableData writeToFile:filepath atomically:YES];
}

#pragma mark - NSStreamDelegate
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch(eventCode) {
            
        case NSStreamEventOpenCompleted:
            NSLog(@"客户端输入输出流打开完成");
            break;
            
        case NSStreamEventHasBytesAvailable:
            NSLog(@"客户端有字节可读");
            [self readDataFromStream:inputStream];
            break;
            
        case NSStreamEventHasSpaceAvailable:
            NSLog(@"客户端可以发送字节");
            break;
            
        case NSStreamEventErrorOccurred:
            NSLog(@"客户端连接出现错误");
            break;
            
        case NSStreamEventEndEncountered:{
            NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
            NSString *writeFilePath = [documentsDir stringByAppendingPathComponent:@"data.txt"];
            [self writeDataToFile:writeFilePath];
            
            //关闭输入输出流
            [inputStream close];
            [outputStream close];
            
            //从主运行循环移除
            [inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            [outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            break;
        }
        default:
            break;
            
    }
}

以上读数据写入沙盒的方式还是需要临时的mutableData存储数据,达不到减少内存占用的意图,这里我们考虑如何将NSOutputStream利用进去,避免使用临时的数据,占用内存(数据过大的话)。

3.2 NSOutputStream 绑定输出

NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *writeFilePath = [documentsDir stringByAppendingPathComponent:@"data.txt"];
[self bindOutputStreamWithFile:writeFilePath];

- (void)bindOutputStreamWithFile:(NSString *)filePath{
   outputStream = [[NSOutputStream alloc] initToFileAtPath:filePath append:YES];
    outputStream.delegate = self;
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [outputStream open];
}


- (void)readDataFromStream:(NSInputStream *)stream{
    uint8_t buf[1024];
    NSInteger len = 0;
    len = [(NSInputStream *)stream read:buf maxLength:1024];  // 读取数据
    [outputStream write:buf maxLength:len]; // 注意此处
}

以上的例子只是说明NSInputStream,NSOutputStream读写文件的使用(真实场景直接使用文件操作copyItemAtPath:toPath:error:即可实现)。

4.NSStream 与 RunLoop

RunLoop此处不做深入分析,这里的一篇文章讲解的很清楚了,有兴趣可以看看。

那为什么NSStream提供了scheduleInRunLoop:forMode:,removeFromRunLoop:forMode:的接口需要将对应的输入,输出流实例绑定,解绑到指定的RunLoop中呢?

我们参考AFNetworking中RunLoop的使用去联想,其实道理是一样的:

  • 我们可以创建自己的RunLoop并能保证即使处于退到后台证输入,输出流回调依旧能执行。
  • 同时可以指定运行的mode,为了保证在转动ScrollView时,主线程的Run Loop会运行在UITrackingRunLoopMode模式,那么NSStream的回调就无法运行,设置为NSRunLoopCommonModes,都可以保证NSStream的回调正常被调用。

本文通过一个简单的Socket的例子,延伸分析了NSStream,NSInputStream,NSOutputStream以及与RunLoop的关系,下篇文章我们将围绕React Native中WebSocket使用,讲解React Native的热加载,远程调试是如何利用WebSocket做到的。

文章有任何问题欢迎指正,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 大纲 一.Socket简介 二.BSD Socket编程准备 1.地址 2.端口 3.网络字节序 4.半相关与全相...
    y角阅读 2,380评论 2 11
  • 一、概念 首先,理清一些概念 TCP/IP和UDP,HTTP协议,Socket 1.TCP/IP和UDP,是网络中...
    _AJH阅读 4,157评论 0 18
  • 还在默默寻找最强撩妹技能么,在如今的直播圈里流行着一句话:爱她,就送她帝王套!别以为只有国民老公思聪才会撩妹,来看...
    丫播Live阅读 877评论 0 0
  • 羽毛轻轻地落了一地,你回过头,再也没有转过来。 你离开了,带走了我的全世界。 你走之后,我的世界一片荒芜,长满了杂...
    卿九久阅读 542评论 2 2