socket
需求 :
客户端 跟 服务器 如何进行通信 (传输信息) ?
套接字(socket)可以看成是两个网络应用程序进行通信时,各自通信连接中的一
个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机
的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机
的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据
传输要通过套接字(socket)来完成。
端与端之间就像两个不同国家的陌生人, 由于语言不同无法直接沟通
因此双方之间都聘请了一个翻译,而沟通就像这样 :
定义:
socket 在哪里 ?
Socket : 对数据传输协议TCP/UDP IP 等进行封装的API 接口 , 而应用层直接使
用socket 接口来完成传输
(1)套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对
其进行(2)像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O
插入到网络中,并与网络中的其他应用程序进行通信。(3)网络套接字是IP地址与
端口的组合
(1) : 专门是帮助发送和接收数据的作用
(2) : 像读写文件一样去传输数据
文件 读写 :
打开 -> 读写 -> 关闭
socket 操作 :
请求确认 -> 传输数据 -> 结束传输
请求确认(三次握手) -> 传输数据 -> 结束传输(四次分手)
(3) : 如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是 socket
=(210.37.145.1:23)
来源 :
Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后
来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口 (socket 函
数),也是在Internet上进行应用开发最为通用的API。
但是, 在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如
果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现
意思是 : 购买的系统 已经把socket 之间沟通的过程封装好了, 并生成一套公开
API ,公开给 用户去使用, 只需要按照 接口规则使用即可.
socket 有哪些 (系统封装了三种套接字) ?
3种
1. 流式套接字 :
使用TCP协议传输 :
一种可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发送
内设流量控制
选择 : 数据大,对数据准确等要求高的时候
2. 数据报套接字 :
使用UDP协议 :
一种无连接、不可靠的双向数据传输服务
数据包以独立的形式被发送,并且保留了记录边界,不提供可靠性保证
数据在传输过程中可能会丢失(丢了就算了,不会再次发送)或重复,并且不能保证
在接收端按发送顺序接收数据
选择 :
数据校验要求低, 效率高
直播,视频聊天等 掉帧不影响使用的功能需求
3. 原始套接字 :
对较低层协议(如IP或ICMP)进行直接访问
常用于网络协议分析,检验新的网络协议实现,也可用于测试新配置或安装的网
络设备
何时调用socket ?
开始通信时 : (握手)
任何用户要进行通信都必须创建套接字,而创建套接字是通过系统调用socket()
函数实现的
结束通信时 : (分手)
当对某个套接字的通信结束时,必须调用关闭socket函数关闭指定的套接字。
端口
一个端口
同一时刻只能建立一次连接
同一时刻可以监听多个请求
数据缓冲区
数据请求
缓冲区数据-粘包
多个数据因为网络原因同时进入缓冲区没有发送,而进行了累计, 但是不同请求的
数据混合后进行发送会导致无法区分
解决粘包
设计数据格式
数据长度(4字节) + 数据类型(4字节) + data
通过上面格式区分数据
数据流
二进制数据
套接字调用流程 ?
socket数据传输流程:
流程解析 :
https://www.jianshu.com/p/066d99da7cbd 部分整理自
1、使用socket()函数创建套接字
int socket(int af, int type, int protocol);
参数 :
Af : IP 地址类型 (IPv4 或者 IPv6 等)
type : 为数据传输方式,常用的有 SOCK_STREAM(数据准确传输) 和
SOCK_DGRAM (数据可能丢失)
protocol : 表示传输协议,常用的 有TCP 和 UDP 传输协议
2、bind() 绑定套接字
3、使用listen()
于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字
进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。
int listen(int sock, int backlog);
sock : 待监听的sock
backlog : 请求队列的最大长度
listen() :
是被动监听,当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户
端请求时,套接字才会被“唤醒”来响应请求
请求队列 :
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,
只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如
果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。
这个缓冲区,就称为请求队列(Request Queue)
缓冲区的长度 :
能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟
为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20
, 大一点几百上千
4、 accept() 函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
accept() 会阻塞函数执行,直到有新的请求到来 (没有新请求就歇着)
5、socket缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由
TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可
以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这
些都是TCP协议负责的事情
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网
络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决
于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制
6、I/O缓冲区特性 :
输入输出缓冲区的默认大小一般都是 8K
(1)I/O缓冲区在每个TCP套接字中单独存在;
(2)I/O缓冲区在创建套接字时自动生成;
(3)即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
(4)关闭套接字将丢失输入缓冲区中的数据。
7、write()/send()
对于TCP套接字(默认阻塞模式 ) :
1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么
write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机
器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,
write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才
会被唤醒。
3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
4) 直到所有数据被写入缓冲区 write()/send() 才能返回。
8、read()/recv()
对于TCP套接字(默认阻塞模式 ) :
1) 首先会检查缓冲区,如果输入缓冲区中有数据,那么就读取,否则函数会被阻
塞(暂停执行,等待),直到网络上有数据到来。
2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区
中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读
取。
3) 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动
作将暂停,直到上一步动作完成后才能继续,以保持同步性。
8、connect()
客户端向服务器建立连接,使用该函数 :
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
对应服务器的IP地址
三次握手 :
注意 : 在回应对方收到信息时,都要把对方发过来的随机数据包序号 加一 (5000
+ 1) , 设置 ACK 标志位 返回 , 对方根据该值是否加一来判断是否真的接收到了
客户端发起请求(三次握手过程):
1) 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置
SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字
1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向
服务器端发送数据包,客户端就进入了SYN-SEND状态。
2) 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户
端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和
ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户
端发送的数据包
服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数
据包没有关系。
服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填
充“确认号(Ack)”字段。
服务器将数据包发出,进入SYN-RECV状态
3) 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是
服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为
1000+1,如果是就说明连接建立成功。
数据传输 : 接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示
客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序
号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。
客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。
服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发
来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为
2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来
就可以收发数据了。
9、close()
数据传输结束了需要断开连接
断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,
不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发
量高,服务器压力堪忧。
四次分手 :
参考网上一段话 :
[Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
[Shake 3] 套接字B:“我准备好了,可以断开连接了。”
[Shake 4] 套接字A:“好的,谢谢合作。”
关于 TIME_WAIT
MSL ?
数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,
并通知源主机。这称为报文最大生存时间为 MSL(MSL,Maximum Segment
Lifetime)
ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL
是数据包往返的最大时间 ,如果 2MSL 后还未收到服务器重传的 FIN 包,就说
明服务器已经收到了 ACK 包
长连接 :
说明 :
长连接简单解释就是一直保持通信连接状态, 直到一方要求断开为止
Socket 如何 保持长连接 ?
Socket 是 TCP/UDP 层的封装,通过 socket,我们就能进行 TCP 通信。
而socket封装一个setKeepAlive方法保持长连接, 但是时间间隔是2小时发送一
次心跳, 但是时间太长
所以,需要通过一个短时间间隔来发送心跳保持socket长连接
何为心跳 ?
心跳就是每隔一段时间就向服务端发送一次请求,以保持socket一直连接, 返回数
据简单使用0, 1来表示状态, 比如0 下线,1上线
返回状态要区别正常请求
CocoaAsyncSocket 实现长连接
#import "GCDSocketManager.h"
#define SocketHost @"地址"
#define SocketPort 端口
@interface GCDSocketManager()<GCDAsyncSocketDelegate>
//握手次数
@property(nonatomic,assign) NSInteger pushCount;
//断开重连定时器
@property(nonatomic,strong) NSTimer *timer;
//重连次数
@property(nonatomic,assign) NSInteger reconnectCount;
@end
@implementation GCDSocketManager
//全局访问点
+ (instancetype)sharedSocketManager {
static GCDSocketManager *_instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
//可以在这里做一些初始化操作
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
#pragma mark 请求连接
//连接
- (void)connectToServer {
self.pushCount = 0;
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_main_queue()];
NSError *error = nil;
[self.socket connectToHost:SocketHost onPort:SocketPort
error:&error];
if (error) {
DLog(@"SocketConnectError:%@",error);
}
}
#pragma mark 连接成功
//连接成功的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString
*)host port:(uint16_t)port {
DLog(@"socket连接成功");
[self sendDataToServer];
}
部分3:
//连接成功后向服务器发送数据
- (void)sendDataToServer {
//发送数据代码省略...
//发送
[self.socket writeData:jsonData withTimeout:-1 tag:1];
//读取数据
[self.socket readDataWithTimeout:-1 tag:200];
}
//连接成功向服务器发送数据后,服务器会有响应
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data
withTag:(long)tag {
[self.socket readDataWithTimeout:-1 tag:200];
//服务器推送次数
self.pushCount++;
//在这里进行校验操作,情况分为成功和失败两种,成功的操作一般都是拉取数
据
}
#pragma mark 连接失败
//连接失败的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError
*)err {
DLog(@"Socket连接失败");
self.pushCount = 0;
NSUserDefaults *userDefaults = [NSUserDefaults
standardUserDefaults];
NSString *currentStatu = [userDefaults valueForKey:@"Statu"];
//程序在前台才进行重连
if ([currentStatu isEqualToString:@"foreground"]) {
//重连次数
self.reconnectCount++;
//如果连接失败 累加1秒重新连接 减少服务器压力
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 *
self.reconnectCount target:self selector:@selector(reconnectServer)
userInfo:nil repeats:NO];
self.timer = timer;
}
}
部分4:
//如果连接失败,5秒后重新连接
- (void)reconnectServer {
self.pushCount = 0;
self.reconnectCount = 0;
//连接失败重新连接
NSError *error = nil;
[self.socket connectToHost:SocketHost onPort:SocketPort
error:&error];
if (error) {
DLog(@"SocektConnectError:%@",error);
}
}
#pragma mark 断开连接
//切断连接
- (void)cutOffSocket {
DLog(@"socket断开连接");
self.pushCount = 0;
self.reconnectCount = 0;
[self.timer invalidate];
self.timer = nil;
[self.socket disconnect];
}
@end