最近公司需要使用长连接完成一些业务,因为业务基本使用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做到的。
文章有任何问题欢迎指正,谢谢。