一,前言
前段时间一直在开发并维护一个关于收银系统的项目,其中使用了CocoaAsyncSocket.framework这一类库,着重使用的是该类库里面的GCDAsyncSocket类。这里做个梳理。
该类是socket套接字的封装,可用与iOS平台的socket开发。第一次接触socket要始于大学时候了,也仅了解这么一个东西(那什么,出来混总是要还的)......
- Socket底层是纯C语言,跨平台使用,Socket网络编程在任意一门语言中都很重要,熟悉这些底层交互是提高编程能力的重要一部分。
二,认识Socket
要了解Socket,有些东西想必是绕不过去的...... 0_0
2.1,socket所在的位置
在七层网络模型中的位置:socket是连接应用层与传输层之间的桥梁
著名的网络七层从下往上依次是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
2.2,概念
- http协议 对应于应用层
- tcp协议 对应于传输层
- ip协议 对应于网络层
TCP/IP是传输层协议,主要解决数据如何在网络中传输;
HTTP是应用层协议,主要解决如何包装数据;
其实我们在传输数据的时候,可以只使用传输层,数据同样能到达接收方,但是那样的话由于没有应用层,便无法识别数据内容,那传输的数据就没有意义了,要使传输有意义,则就必须使用应用层协议。
应用层协议很多,有HTTP、FTP、TELNET、等等,或者自己定义应用层协议。WEB使用HTTP作为传输层协议,用来封装HTTP文本信息,然后使用TCP/IP做传输协议将它发送到网络上。
Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket我们可以使用TCP/IP协议。
2.3,HTTP协议
HTTP协议就是超文本传输协议,属于应用层面向对象的协议,常基于TCP连接方式,建立在TCP协议上的一种应用,是web联网的基础,也是手机应用终端常用的协议。
HTTP连接最显著的特点是客户端发送的每次请求都需要服务器返回响应,在请求结束之后,会主动释放连接,俗称“短连接”,从建立连接到关闭连接的过程称为“一次连接”。
由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客 户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。
2.4,TCP协议
TCP协议是传输控制协议,提供面向连接,可靠地字节流服务,提供超时重发,丢弃重复数据,检验数据,流量控制等功能。在正式收发数据前,必须建立可靠的连接,即“三次握手”。
三次握手:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ackj+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
四次挥手:
第一次挥手:数据传输结束以后,客户端的应用进程发出连接释放报文段,并停止发送数据,其首部:FIN=1,seq=u。
第二次挥手:服务器端收到连接释放报文段之后,发出确认报文,其首部:ack=u+1,seq=v。此时本次连接就进入了半关闭状态,客户端不再向服务器发送数据。而服务器端仍会继续发送。
第三次挥手:若服务器已经没有要向客户端发送的数据,其应用进程就通知服务器释放TCP连接。这个阶段服务器所发出的最后一个报文的首部应为:FIN=1,ACK=1,seq=w,ack=u+1。
第四次挥手:客户端收到连接释放报文段之后,必须发出确认:ACK=1,seq=u+1,ack=w+1。 再经过2MSL(最长报文端寿命)后,本次TCP连接真正结束,通信双方完成了他们的告别。
在这个过程中,通信双方的状态如下图,其中:ESTAB-LISHED:连接建立状态、FIN-WAIT-1:终止等待1状态、FIN-WAIT-2:终止等待2状态、CLOSE-WAIT:关闭等待状态、LAST-ACK:最后确认状态、TIME-WAIT:时间等待状态、CLOSED:关闭状态
2.5,UDP协议
用户数据报协议,面向非连接,不保证可靠性的数据传输服务,没有超时重发等机制,故而传输速度很快。
- 特点:它不与对方建立连接,而是直接的把数据包发送过去,UDP适用于一次只传送少量数据、可靠性要求不高的应用环境。
三,Socket的原理
3.1,Socket概念
Socket(套接字)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。包含进行网络通信的必须的五种信息:双方连接使用的协议、本地主机IP地址,本地进程的协议端口、远地主机的IP地址、远地进程的协议端口。
3.2,建立一个socket连接
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
- 服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
- 客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
- 连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户 端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
3.3,Socket与TCP连接的关系
创建Socket连接时候,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP、UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。
3.4,Socket连接与HTTP连接关系
通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。
但在实际网络应用 中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。
而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。
很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给客户端;
若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以 保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。
四,Socket使用
首先,根据项目实际需求,这里定义主机(也就是server)是iPad,子机(也就是client)是iPad,iPhone,Android终端等;同时,当设备是iPad时,可以切换主子机角色。
1,基于TCP,GCDAsyncSocket
Server端:
- 比如在登录成功之后,初始化、设置代理、准备开启连接服务:
server->serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:server delegateQueue:[server serverQueue]]; //初始化、设置代理
BOOL isAccept = [serverSocket acceptOnPort:kPort error:nil];//监测端口
... 表示其他处理的其他操作,
- 收到连接之后便会走代理
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
...
[clientSocketArray addObject:model];//使用一个数组保存连接的socket,保证此链接是长连接,如果不保存,则连接完会自动断开,从而导致无法互相发送消息
...
TRMessageModel *msg = [[TRMessageModel alloc] init];
msg.protocol = TRInnerProtocolOfWelcome;
msg.queueType = TRQueueTypeOfSenderLinear;
msg.data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
msg.isSuccess = YES;
msg.hashCode = msg.hash;
...
[self sendMessageWithSocket:newSocket messageModel:msg];// 发送一些业务信息,其实就是调用[socket writeData:contentData withTimeout:-1 tag:0];方法,根据不同业务发送不同消息
[sock readDataWithTimeout:-1 tag:0];
}
接下来根据不同状态,会走不同的代理方法:
- 连接之后
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
[sock readDataWithTimeout:-1 tag:tag];
}
- 发送完数据之后触发代理方法:
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
}
- 收到数据之后触发代理方法:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
LSLog(@"%s",__func__);
...
[self receiveMessageWithData:data socketModel:client];// 业务逻辑处理
...
[sock readDataWithTimeout:-1 tag:tag];
}
- 客户端断开连接之后
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
//断开之后需要的一些操作
...
// [self detectionHeartbeat];
}
client端:
client.socketModel.socket = [[GCDAsyncSocket alloc] initWithDelegate:client delegateQueue:[client clientQueue]];
与主机端一样,都分别走不同的代理方法
- 连接之后
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
TRMessageModel *msgModel = [[TRMessageModel alloc] init];
msgModel.protocol = TRInnerProtocolOfVerification;
msgModel.data = [@"connect" dataUsingEncoding:NSUTF8StringEncoding];
msgModel.queueType = TRQueueTypeOfSenderLinear;
msgModel.deviceCode = self.deviceCode;
msgModel.deviceName = self.deviceName;
msgModel.isSuccess = YES;
msgModel.hashCode = msgModel.hash;
...
[self sendMessageWithSocket:sock messageModel:msgModel];
self.timestamp = TimeStamp();
...
[self postSynergismHeartbeat];// 发送心跳包
[self detectionHeartbeat];// 监测心跳
[sock readDataWithTimeout:-1 tag:0];
}
2,基于UDP,GCDAsyncUDPSocket
GCDAsyncUDPSocket类是iOS下支持UDP协议的socket编程类,使用也很容易,通信的收发过程都有代理可以调用。
五,遇到的一些问题
本项目中使用socket过程中还涉及到一些处理粘包问题、消息等待处理、线程问题、队列、消息广播,设备辨认、心跳、网络环境强弱,数据效率,数据安全,数据格式,跨平台,等等;
有的是业务上的,有的是客观环境下的,有的是技术上的,可见一个稳定的项目要客服很多技术问题,所以程序猿们都很棒有木有。
这些问题这里先不一一写过,在后续总结里面一一探讨
希望各路大佬不吝赐教,指出不足。拍砖!拍砖!拍砖!