iOS 开发之CFSocket
前言
CFSocket是在系统的CFNetwork.framework中,
Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。
Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
Socket的通信过程
每一个应用或服务器都有一个端口,因此需要包含以下的步骤:
- 服务端利用Socket监听端口
- 客户端发起连接
- 服务端返回信息,建立连接,开始通信
- 客户端,服务端断开连接
HTTP和Socket连接的区别
OSI模型把网络通信分成7层, 由低向高分别是: 物理层,数据链路层,网络层,传输层,会话层,表示层和应用层
其中HTTP协议是对应应用层,TCP协议对应传输层,IP协议对应网络层,HTTP协议时基于TCP连接。
TCP/IP是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据
在传输数据时候可以只是用TCP/IP, 但是这样没有应用层,无法识别传输的数据内容,这样是没有意义的,如果想使传输的数据有意义,则必须使用应用层协议,HTTP就是一种,Web使用它,封装HTTP文本信息,然后使用TCP/IP协议传输到网络上
Socket 实际上是对TCP/IP协议的疯转,本身并不是协议,而是调用一个接口(API), 通过Socket, 我们才能使用TCP/IP 协议
建立一次TCP连接需要进行“三次握手”
image.png
SYN(synchronous),同步标志,ACK(Acknowledgement)即确认标志,seq应该是Sequence Number, 序号的意思,另外还有四次握手的fin, 应该是final,表示结束标志
第一次握手: 客户端发送一个TCP的SYN标志位置1的包指明链接的服务器端口,以及初始程序号X,保存在包头的序列号(sequence Number)字段里
第二次握手: 服务器发回确认包(ACK)应答,即SYN标志位和ACK标志位均为1 同时,将确认序号(Acknowledgement Number)设置为客户的序列号加1,即 X + 1
第三次握手: 客户端再次发送确认包(ACK) SYN标志位为0, ACK标志位为1, 并且吧服务器发来的序号字段+1, 放在确定字段中发送给对方,并且在数据段放写序列号的+1
只有进行完三次握手后,才能正式传输数据,理想状态下只要建立起链接,在通信双方主动关闭链接之前,TCP连接会一直保持下去。三次握手能够确保对面已经收到自己的同步序列号,这样就可以保证后续数据包的丢失可以被察觉,这也是TCP流式传输的基础。
断开TCP连接需要发送4个包,客户端和服务端都可以发起这个请求,在socket编程中任何一方执行close()操作就会产生"四次握手"
image.png
关闭是4次,是因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文,ACK用来回应,SYN用来同步。但是当关闭连接的情况下,接收端收到FIN报文时候,很可能不会立即关闭,所以先发送一个ACK报文告诉发送端我收到了,只有等接受端报文全部发送完了,才能发送FIN报文
Socekt
Socket是通信的基石,是支持TCP/IP协议的基本操作单元,包含5种信息:连接使用的协议,本机主机IP地址,本地进程的端口号,远程主机IP地址,远程进程的协议端口。
应用层通过传输层进行数据传输时候,可能会遇到同一个TCP协议端口传输好几种数据,可以通过socket来区分不同应用程序或者网络连接。
建立Socket连接的步骤
1.至少需要一对,一个作用于客户端,一个在服务端
- 链接分为三个步骤:服务器监听,客户端请求,链接确认
- 服务端监听: 并不对应具体的客户端socket,而是处于等待连接状态,实时监听网络状态,等待客户端连接
- 客户端请求:客户端的套接字向服务端套接字发送链接请求,因此需要知道服务端的套接字的地址和端口号,而且需要描述他要连接的服务器的套接字
- 连接确认: 当服务端套接字监听到或者接收到客户端的套接字的链接请求,就响应客户端的套接字,建立一个新的链接,把客户端的套接字的描述发给客户端,一旦确认,双方就正式建立连接,而且服务端的套接字仍在监听状态,继续接受其他客户端的套接字
Socket HTTP TCP区别
socket 连接可以指定传输层协议, 可以是TCP 或者UDP, 当是TCP协议的时候就是TCP连接。而HTTP连接就是请求->响应的方式, 在请求时候需要先建立连接,然后客户端向服务器发出请求之后,服务器才能回复数据。
socket一旦建立连接,服务器可以主动将数据传输给客户端,而HTTP则需要客户端先向服务器发送请求之后才能将数据返回给客户端,但实际上socket建立之后因为种种原因,会导致断开连接,其中一个原因就是防火墙会断开长时间处于非活跃状态的链接,因此需要轮询告诉网络,这个连接是活跃的
应用
iOS 提供了Socket网络编程接口CFSocket, TCP和UDP的socket是有区别的
基于TCP的Socket
image.png
基于UDP的Socket
image.png
常用的socket 分为两种
流式Socket(SOCKET_STREAM)
数据报式(SOCKET_DGRAM)
流式针对面向TCP链接的应用,而数据报式是一种无连接的socket,对应于无连接的UDP服务应用
iOS官方给出的CFSocket,它是基于BSD Socket进行抽象和封装,CFSocket中包含了少数开销,它几乎可以提供BSD sockets 所具有的一切功能,并且把socket集成进一个“运行循环”当中。CFSocket并不仅仅限于流的sockets(比如TCP),它可以处理任何类型的socket
你可以利用CFSocketCreate 功能从头开始创建一个CFSocket对象,或者利用CFSocketCreateWithNative函数从BSD socket创建,然后需要利用函数CFSocketCreateRunLoopSource 创建一个“运行循环”源,并利用函数CFRunLoopAddSource把它加入一个运行循环,这样不论CFSocket对象是否接收到信息,CFSocket回调函数都可以运行。
示例代码:
客户端和服务器端都需要引入的头文件如下:
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#import <unistd.h>
用两个电脑测试,两个端的端口要一样,两个端的地址一定要相同(设置为服务器端代码所在的电脑的端口),且服务器端要用有线连接网络,客户端用
客户端
/** 这个端口可以随便设置*/
#define TEST_IP_PROT 22235
/** 替换成你需要连接服务器绑定的IP地址,不能随便输*/
#define TEST_IP_ADDR "192.168.103.244"
服务端
/** 这个端口可以随便设置*/
#define TEST_IP_PROT 22235
/** 替换成你当前连接的WIFI的IP地址*/
#define TEST_IP_ADDR "192.168.103.244"
客户端
- 连接服务器
- (IBAction)connectServer:(id)sender {
if (!_socketRef) {
// 创建socket关联的上下文信息
/*
typedef struct {
CFIndex version; 版本号, 必须为0
void * info; 一个指向任意程序定义数据的指针,可以在CFSocket对象刚创建的时候与之关联,被传递给所有在上下文中回调
const void *(*retain)(const void *info); info 指针中的retain回调,可以为NULL
void (*release)(const void *info); info指针中的release回调,可以为NULL
CFStringRef (*copyDescription)(const void *info); 回调描述,可以n为NULL
} CFSocketContext;
*/
CFSocketContext sockContext = {0, (__bridge void *)(self), NULL, NULL, NULL};
//创建一个socket
_socketRef = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCF, kCFSocketConnectCallBack, ServerConnectCallBack, &sockContext);
//创建sockadd_in的结构体,改结构体作为socket的地址,IPV6需要改参数
//sockaddr_in
// sin_len; 长度
//sin_family;协议簇, 用AF_INET -> 互联网络, TCP,UDP 等等
//sin_port; 端口号(使用网络字节顺序)htons:将主机的无符号短整形数转成网络字节顺序
//in_addr sin_addr; 存储IP地址, inet_addr()的功能是将一个点分十进制的IP转换成一个长整型数(u_long类型),若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址, 否则为IMADDR_NONE
//sin_zero[8]; 让sockaddr与sockaddr_in 两个数据结构保持大小相同而保留的空字节,无需处理
struct sockaddr_in Socketaddr;
//memset: 将addr中所有字节用0替代并返回addr,作用是一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
memset(&Socketaddr, 0, sizeof(Socketaddr));
Socketaddr.sin_len = sizeof(Socketaddr);
Socketaddr.sin_family = AF_INET;
Socketaddr.sin_port = htons(TEST_IP_PROT);
Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
//将地址转化为CFDataRef
CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&Socketaddr, sizeof(Socketaddr));
//连接
//CFSocketError CFSocketConnectToAddress(CFSocketRef s, CFDataRef address, CFTimeInterval timeout);
//第一个参数 连接的socket
//第二个参数 连接的socket的包含的地址参数
//第三个参数 连接超时时间,如果为负,则不尝试连接,而是把连接放在后台进行,如果_socket消息类型为kCFSocketConnectCallBack,将会在连接成功或失败的时候在后台触发回调函数
CFSocketConnectToAddress(_socketRef, dataRef, -1);
//加入循环中
//获取当前线程的runLoop
CFRunLoopRef runloopRef = CFRunLoopGetCurrent();
//把socket包装成CFRunLoopSource, 最后一个参数是指有多个runloopsource通过一个runloop时候顺序,如果只有一个source 通常为0
CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socketRef, 0);
//加入运行循环
//第一个参数:运行循环管
//第二个参数: 增加的运行循环源, 它会被retain一次
//第三个参数:用什么模式把source加入到run loop里面,使用kCFRunLoopCommonModes可以监视所有通常模式添加source
CFRunLoopAddSource(runloopRef, sourceRef, kCFRunLoopCommonModes);
//之前被retain一次,这边要释放掉
CFRelease(sourceRef);
}
}
- 发送消息
- (IBAction)sendMessage:(id)sender {
if (!_socketRef) {
[[[UIAlertView alloc] initWithTitle:@"对不起" message:@"请先连接服务器" delegate:self cancelButtonTitle:@"确定" otherButtonTitles: nil] show];
return;
}
NSString *stringTosend = [NSString stringWithFormat:@"%@说:%@",self.nameText.text,self.messageText.text];
const char* data = [stringTosend UTF8String];
/** 成功则返回实际传送出去的字符数, 失败返回-1. 错误原因存于errno*/
long sendData = send(CFSocketGetNative(_socketRef), data, strlen(data) + 1, 0);
if (sendData < 0) {
perror("send");
}
}
- 读取数据
- (void)_readStreamData
{
//定义一个字符型变量
char buffer[512];
/*
int recv(SOCKET s, char FAR *buf, int len, int flags);
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据
1. 第一个参数指定接收端套接字描述符
2.第二个参数指明一个缓冲区,改缓冲区用来存放recv函数接受到的数据
3. 第三个参数指明buf的长度
4.第四个参数一般置0
*/
long readData;
//若无错误发生,recv() 返回读入的字节数。如果连接已终止,返回0 如果发生错误,返回-1, 应用程序可通过perror() 获取相应错误信息
while ((readData = recv(CFSocketGetNative(_socketRef), buffer, sizeof(buffer), 0))) {
NSString *content = [[NSString alloc] initWithBytes:buffer length:readData encoding:NSUTF8StringEncoding];
dispatch_async(dispatch_get_main_queue(), ^{
self.infoLabel.text = [NSString stringWithFormat:@"%@\n%@", content, self.infoLabel.text];
});
}
perror("++++++recv+++++++++");
}
- 回调函数
/**
回调函数
@param s socket对象
@param callbackType 这个socket对象的活动类型
@param address socket对象连接的远程地址,CFData对象对应的是socket对象中的protocol family (struct sockaddr_in 或者 struct sockaddr_in6), 除了type 类型 为kCFsocketAcceptCallBack 和kCFSocketDataCallBack ,否则这个值通常是NULL
@param data 跟回调类型相关的数据指针
kCFSocketConnectCallBack : 如果失败了, 它指向的就是SINT32的错误代码
kCFSocketAcceptCallBack : 它指向的就是CFSocketNativeHandle
kCFSocketDataCallBack : 它指向的就是将要进来的Data
其他情况就是NULL
@param info 与socket相关的自定义的任意数据
*/
void ServerConnectCallBack (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info)
{
ViewController *vc = (__bridge ViewController *)(info);
//判断是不是NULL
if (data != NULL)
{
printf("----->>>>>>连接失败\n");
[vc performSelector:@selector(_releseSocket) withObject:nil];
}
else
{
printf("----->>>>>>连接成功\n");
[vc performSelectorInBackground:@selector(_readStreamData) withObject:nil];
}
}
- 清空socket
- (void)_releseSocket
{
if (_socketRef) {
CFRelease(_socketRef);
}
_socketRef = NULL;
self.infoLabel.text = @"----->>>>>>连接失败-----";
}
服务端
- 初始化
- (void)_initSocket
{
@autoreleasepool {
//创建Socket, 指定TCPServerAcceptCallBack
//作为kCFSocketAcceptCallBack 事件的监听函数
//参数1: 指定协议族,如果参数为0或者负数,则默认为PF_INET
//参数2:指定Socket类型,如果协议族为PF_INET,且该参数为0或者负数,则它会默认为SOCK_STREAM,如果要使用UDP协议,则该参数指定为SOCK_DGRAM
//参数3:指定通讯协议。如果前一个参数为SOCK_STREAM,则默认为使用TCP协议,如果前一个参数为SOCK_DGRAM,则默认使用UDP协议
//参数4:指定下一个函数所监听的事件类型
CFSocketRef _socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, TCPServerAcceptCallBack, NULL);
if (_socket == NULL) {
NSLog(@"————————创建socket 失败");
return;
}
BOOL reused = YES;
//设置允许重用本地地址和端口
setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, (const void *)&reused, sizeof(reused));
//定义sockaddr_in类型的变量, 该变量将作为CFSocket的地址
struct sockaddr_in Socketaddr;
memset(&Socketaddr, 0, sizeof(Socketaddr));
Socketaddr.sin_len = sizeof(Socketaddr);
Socketaddr.sin_family = AF_INET;
//设置服务器监听地址
Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
//设置服务器监听端口
Socketaddr.sin_port = htons(TEST_IP_PROT);
//将ipv4 的地址转换为CFDataRef
CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&Socketaddr, sizeof(Socketaddr));
//将CFSocket 绑定到指定IP地址
if (CFSocketSetAddress(_socket, address) != kCFSocketSuccess) {
//如果_socket 不为NULL, 则f释放_socket
if (_socket) {
CFRelease(_socket);
exit(1);
}
_socket = NULL;
}
//启动h循环箭筒客户链接
NSLog(@"----启动循环监听客户端连接---");
//获取当前线程的CFRunloop
CFRunLoopRef cfrunLoop = CFRunLoopGetCurrent();
//将_socket包装成CFRunLoopSource
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
//为CFRunLoop对象添加source
CFRunLoopAddSource(cfrunLoop, source, kCFRunLoopCommonModes);
CFRelease(source);
//运行当前线程的CFrunLoop
CFRunLoopRun();
}
}
- 读取数据
void readStream(CFReadStreamRef readStream, CFStreamEventType eventype, void * clientCallBackInfo) {
UInt8 buff[2048];
NSString *aaa = (__bridge NSString *)(clientCallBackInfo);
NSLog(@"+++++++>>>>>%@", aaa);
//--从可读的数据流中读取数据,返回值是多少字节读到的, 如果为0 就是已经全部结束完毕,如果是-1 则是数据流没有打开或者其他错误发生
CFIndex hasRead = CFReadStreamRead(readStream, buff, sizeof(buff));
if (hasRead > 0) {
NSLog(@"----->>>>>接受到数据:%s \n", buff);
const char *str = "test, test , test \n";
//向客户端输出数据
CFWriteStreamWrite(writeStreamRef, (UInt8 *)str, strlen(str) + 1);
}
}
- 回调函数
void TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
//如果有客户端Socket连接进来
if (kCFSocketAcceptCallBack == type) {
//获取本地Socket的Handle, 这个回调事件的类型是kCFSocketAcceptCallBack,这个data就是一个CFSocketNativeHandle类型指针
CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
//定义一个255数组接收这个新的data转成的socket的地址,SOCK_MAXADDRLEN表示最长的可能的地址
uint8_t name[SOCK_MAXADDRLEN];
//这个地址数组的长度
socklen_t namelen = sizeof(name);
/*
*/
//MARK:获取socket信息
//第一个参数 已经连接的socket
//第二个参数 用来接受地址信息
//第三个参数 地址长度
//getpeername 从已经连接的socket中获的地址信息, 存到参数2中,地址长度放到参数3中,成功返回0, 如果失败了则返回别的数字,对应不同错误码
if (getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen) != 0) {
perror("++++++++getpeername+++++++");
exit(1);
}
//获取连接信息
struct sockaddr_in *addr_in = (struct sockaddr_in *)name;
// inet_ntoa 将网络地址转换成"." 点隔的字符串格式
NSLog(@"-------->>>>%s===%d--连接进来了", inet_ntoa(addr_in-> sin_addr), addr_in->sin_port);
//创建一组可读/可写的CFStream
readStreamRef = NULL;
writeStreamRef = NULL;
//创建一个和Socket对象相关联的读取数据流
//参数1 :内存分配器
//参数2 :准备使用输入输出流的socket
//参数3 :输入流
//参数4 :输出流
CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStreamRef, &writeStreamRef);
//CFStreamCreatePairWithSocket() 操作成功后,readStreamRef和writeStream都指向有效的地址,因此判断是不是还是之前设置的NULL就可以了
if (readStreamRef && writeStreamRef) {
//打开输入流 和输出流
CFReadStreamOpen(readStreamRef);
CFWriteStreamRef(writeStreamRef) = NULL;
//一个结构体包含程序定义数据和回调用来配置客户端数据流行为
NSString *aaa = @" 这 是 一个 测 测试 的 代码";
CFStreamClientContext context = {0, (__bridge void *)(aaa), NULL, NULL };
//指定客户端的数据流, 当特定事件发生的时候, 接受回调
//参数1 : 需要指定的数据流
//参数2 : 具体的事件,如果为NULL,当前客户端数据流就会被移除
//参数3 : 事件发生回调函数,如果为NULL,同上
//参数4 : 一个为客户端数据流保存上下文信息的结构体,为NULL同上
//CFReadStreamSetClient 返回值为true 就是数据流支持异步通知, false就是不支持
if (CFReadStreamSetClient(readStreamRef, kCFStreamEventHasBytesAvailable, readStream, &context)) {
exit(1);
}
//将数据流加入循环
CFReadStreamScheduleWithRunLoop(readStreamRef, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
const char *str = "+++welcome++++\n";
//向客户端输出数据
CFWriteStreamWrite(writeStreamRef, (UInt8 *)str, strlen(str) + 1);
}
else
{
//如果失败就销毁已经连接的socket
close(nativeSocketHandle);
}
}
}
结果如下:
客户端:
image.png
服务端:
2018-09-30 14:36:19.601959+0800 服务端[11483:942439] ----启动循环监听客户端连接---
2018-09-30 14:41:01.910275+0800 服务端[11483:942439] 192.168.108.63:38369连接进来了
2018-09-30 14:41:23.003615+0800 服务端[11483:942439] earth,wind,fire,be my call
接收到数据:Tom说:hello world
参考链接:
http://www.cnblogs.com/QianChia/p/6391989.html
https://blog.csdn.net/potato512/article/details/44001767
https://blog.csdn.net/chang6520/article/details/7874804
.......