前言
开发的很多项目都用到了Socket长链接,使用场景是即时性和主动性的请求需求。
本文的学习过程
1.Socket的位置和理解
2.Socket通讯过程 && Stream流的学习
3.iOS的Socket开发
1.Socket的位置和理解
Socket的理解
socket的位置
网络七层架构
Socket的位置
socket(套接字)是抽象出来的接口,是一个通信链的句柄,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
2.Socket的两种协议
TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。
UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。(类似广播)
Socket通讯过程图解
Socket运行中的C函数
1.创建套接字
Socket(af,type,protocol)//建立地址和套接字的联系
bind(sockid, local addr, addrlen)//服务器端侦听客户端的请求
listen( Sockid ,quenlen)//建立服务器/客户端的连接 (面向连接TCP)
2.客户端请求连接
Connect(sockid, destaddr, addrlen)//服务器端等待从编号为Sockid的Socket上接收客户连接请求
newsockid=accept(Sockid,Clientaddr, paddrlen)//发送/接收数据
3.面向连接:
send(sockid, buff, bufflen)
recv()
4.面向无连接: 5.释放套接字 close(socked)
endto(sockid,buff,…,addrlen)
recvfrom()
3.iOS的Socket开发
在iOS中以NSStream(流)来发送和接收数据,可以设置流的代理,对流状态的变化做出相应的动作(连接建立,接收到数据,连接关闭)。
NSStream:数据流的父类,用于定义抽象特性,例如:打开、关闭代理,NSStream继承自CFStream(CoreFoundation)
NSInputStream:NSStream的子类,用于读取输入
NSOutputStream:NSSTream的子类,用于写输出。
1.CFNetwork理解(OC底层的流)
CFNetWork是苹果提供的位于 Core Foundation 中的一个基于C的底层框架,是对更底层OS层BSD socket的封装
CFNetWork主要依赖俩个API,CFSocket 和 CFStream,CFSocket主要用于网络底层的通信,而CFStream包括CFReadStream 和 CFWriteStream,分别用于对Socket的读取和写入。
CFStream,流,是一个在搭建的通讯通道里连续传送的字节序列。steam是单向的,所以有必要建立input(read) stream和output(write) stream。
PS:
AFNetworking 使用NSStream
WebSocket 使用NSStream(H5一种协议,SocketRocket)
NSStream是一个基类,在Cocoa中它有两个子类NSInputStream和INOutputStream。分别对应输入和输出流。流提供了一种简单的方式在不同和介质中交换数据,这种交换方式是与设备无关的。你可以创建一个流通过NSData,File,SocketData,在处理大数据的时候,使用流就可以边读边处理。Cocoa中的流对象与Core Foundation中的流对象是对应的。我们可以通过toll-free桥接方法来进行相互转换。NSStream、NSInputStream和NSOutputStream分别对应CFStream、CFReadStream和CFWriteStream。
NSStream比CFStream有更高的扩展性,例如创建一个获得它读到的数据的长度,或者你能够做一个子类,它的实例化对象能够从它们的流里寻找,发回已读字节。NSStream有它自己一套的必需重写方法,如NSInputStream和NSOutputStream。
NSStream的使用过程
1.Input流(读,从流中读出数据)
1.从数据源创建和初始化一个NSInputStream实例
2.将输入流对象配置到一个run loop,open the stream
3. 通过流对象的delegate函数处理事件
4. 当所有数据读完,关闭流
一,使用流对象的准备工作
在使用NSInputStream对象之前你必须有流的数据源,数据源的类型可以是文件,NSData对象,或者一个网络套接字。(前3步)如下
data = [NSMutableData data]; NSString *filePath = [[NSBundle mainBundle]pathForResource:@"test1" ofType:@"txt"]; NSInputStream *inputStream = [[NSInputStream alloc] initWithFileAtPath:filePath]; inputStream.delegate = self; [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [inputStream open];
在你open stream之前,将该对象配置到一个run loop接收stream events。这样,当流中没有数据可读时可以避免delegate阻塞。如果流是发生在另一个线程,你需要确认该流对象是配置在哪个线程的run loop中,不应该在别的线程来处理流!最后Open,开始流!!!
二,处理Stream Events(协议方法)
open 流对象后,流对象会一直向其delegate发送stream:handleEvent:
消息直到到达了流对象的末尾。这些消息的参数中包含一个指示流事件类型的NSStreamEvent常量。对NSInputStream对象而言,最常用的事件类型是NSStreamEventOpenCompleted,NSStreamEventHasBytesAvailable,NSStreamEventEndEncountered。我们尤其感兴趣的应该是NSStreamEventHasBytesAvailable事件。下面的例子就是一个处理NSStreamEventHasBytesAvailable事件的好的方法:
以下例子包括了输入流和输出流的处理方法(便于同学们直接拷贝使用&整体了解)。
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{ switch (eventCode){ case NSStreamEventHasBytesAvailable:{ uint8_t buf[2048]; NSInteger len = 0; len = [(NSInputStream *)aStream read:buf maxLength:2048]; // 读取数据 if (len) { [data appendBytes:buf length:len]; } else { NSLog(@"没有buffer"); } } break; case NSStreamEventEndEncountered:{ [aStream close]; } break; case NSStreamEventHasSpaceAvailable:{ NSInteger bufSize = 2048; uint8_t buf[bufSize]; [data getBytes:buf length:bufSize]; NSOutputStream *writeStream = (NSOutputStream *)aStream; NSInteger len = [writeStream write:buf maxLength:sizeof(buf)]; [data setLength:0]; self.location += len; if (self.location>self.fileSize) { NSLog(@"NSStreamEventEndEncountered"); [aStream close]; } } break; default: break; } }
这里声明了一个大小为2048的buf,(PS:然后将它缓存在一个可变的data里(viewdidload的时候实例化了),这个data为演示使用!!!)调用read:maxLength:函数从stream中读取指定大小的数据到buf中,如果读取成功,delegate将会将读取到的数据添加到NSMutableData对象_data中。
三,处理stream object
当NSInputStream对象到达steam的末尾的时候,它会向stream:handleEvent:函数发送一个NSStreamEventEndEncountered事件类型常量,这里应该需要关闭流!!
case NSStreamEventEndEncountered:{ [aStream close]; }
2.output流(写,将数据写入流)
1,创建和初始化一个NSOutputSteam实例,设置写的路径,并且设置它的delegate。
2,将这个流对象布置在一个runloop上并且open the stream。
3,处理流对象向其delegate发送的事件消息。
4,如果流对象向内存中写入了数据,那么可以通过使用NSStreamDataWrittenToMemoryStreamKey属性获取数据。
5,当没有数据可供写入时,清理流对象。
使用NSOutputStream对象之前你必须指定数据写入的流的目标位置,输出流对象的目标位置可以是file,C buffer, application memory,network socket
NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) lastObject]; NSString *outputPath = [doc stringByAppendingPathComponent:@"local.txt"]; NSLog(@"----path--->%@",doc); NSOutputStream *writeStream = [[NSOutputStream alloc] initToFileAtPath:outputPath append:YES]; [writeStream setDelegate:self]; [writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; [writeStream open];
本例可以获取文件数据的长度
NSDictionary *fileAttributes = [[NSFileManager defaultManager]attributesOfItemAtPath:filePath error:nil]; unsigned long long length = [fileAttributes fileSize]; self.fileSize = length;
当流对象有 有空间可供数据写入 之类的与流有关的事件消息发送时,delegate会收到从NSOutputStream对象发送来的消息。
将流放在一个runloop上,当流对象不能接收更多数据的时候,可以使delegate避免阻塞。如果流是发生在另一个线程,你需要确认该流对象是配置在哪个线程的run loop中,不应该在别的线程来处理流!最后Open,开始流!!!开始数据项NSOutputStream对象传送。
二,处理 Stream Events
开启NSOutputStream后,只要delegate持续向流对象写入数据,流对象就是一直向其delegate发送stream:handleEvent:消息,直到到达了流的末尾。
全部代码已在上面演示
case NSStreamEventHasSpaceAvailable:{
…}
//判断长度,已经没有
if (self.location>self.fileSize) {
…
}
当在向stream中写数据时NSOutputStream对象发生错误,它会停止streaming并且使用NSStreamEventErrorOccurred消息通知其delegate。
通过向NSOutputStream对象发送propertyForKey:消息获取从流向内存中写入的数据,设定key的值为NSStreamDataWrittenToMemoryStreamKey,该stream object将数据返回到一个NSData对象中。
仿Socket通讯,NSStreamSocket(Stream实现Socket的传输功能)
CFReadStreamRef readStream; CFWriteStreamRef writeStream; CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"172.16.4.26", PORT, &readStream, &writeStream); _inputStream = (__bridge_transfer NSInputStream *)readStream; _outputSream = (__bridge_transfer NSOutputStream *)writeStream; [_inputStream setDelegate:self]; [_outputSream setDelegate:self]; [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_outputSream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_inputStream open]; [_outputSream open];
在iOS开发中,一般使用第三方的Socket框架实现Socket通讯功能。
CocoaAsyncSocket
CocoaAsyncSocket支持tcp和udp。其中:
AsyncSocket类是支持TCP的
AsyncUdpSocket是支持UDP的
AsyncSocket是封装了CFSocket和CFSteam的TCP/IP socket网络库
支持基于IPV4和IPV6的TCP流
AsyncUdpSocket是UDP/IP socket网络库,包装自CFSocket
使用过程
链接成功回调协议方法
- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port{
NSLog(@"did connect to host");
}
协议中的回调方法获取数据。。。。
- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"did read data");
}
发送数据
AsyncSocket *socket=[[AsyncSocket alloc] initWithDelegate:self];
[socket connectToHost:@"www.baidu.com" onPort:80 error:nil];
[socket readDataWithTimeout:3 tag:1];
[socket writeData:[@"GET / HTTP/1.1\n\n" dataUsingEncoding:NSUTF8StringEncoding] withTimeout:3 tag:1];
网络数据传输
Wireshark捉包
仔细观察,我们能找到源IP,源端口,目标IP,目标端口 和数据 等信息
那么在实际的应用过程中,前端需要做什么?
1.编译数据/序列化,反编译数据/逆序列化
2.数据缓冲,防止丢包,粘包导致数据错误
3.异常断开重连机制(包括网络错误,网络环境变化)
4.发送心跳包,保证长链接
5.过滤心跳包响应数据
6.申请链接&&主动断开链接
普通的数据包格式(参考)
总结
本文从Socket认识开始,进入了流Stream的学习,再返回实际使用中,常用的第三方框架CocoaSocket的学习!增加了对网络Socket通讯过程的认识!
实际使用过程,还是用CocoaSocket框架!!!!