一、Socket
1.网络体系结构和网络协议
在说socket之前,先要简单说一说网络体系结构。OSI(Open System Interconnection Reference, 开放式系统互联通信参考)将计算机网络体系结构划分为以下七层:
其中媒体层是网络工程师所研究的对象,主机层则是用户所面向和关心的内容。
常应用到的传输协议有http协议、tcp/udp协议、ip协议分别对应于应用层、传输层、网络层。TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。
我们在传输数据时,可以只使用传输层(TCP/IP),那样的话由于没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用应用层协议,应用层协议很多,有HTTP、FTP、TELNET等等,也可以自己定义应用层协议。WEB使用HTTP作传输层协议,以封装HTTP文本信息,然 后使用TCP/IP做传输层协议将它发送到网络上。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。
2.Http和Socket连接区别
2.1 socket连接:
建立起一个TCP连接需要经过“三次握手”: 第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认; 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态; 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”。
socket连接就是所谓的长连接,理论上客户端和服务器端一旦加你其连接将不会主动断掉;但是由于各种环境因素可能会连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有传输数据,网络防火墙可能会断开该连接以释放网络资源。所以当一个socket连接中没有数据传输的时候,那么为了维持连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。
2.2 HTTP连接
HTTP协议即超文本传送协议(HypertextTransfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。 HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。 1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。 2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。 由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。
二.GCDAsyncSocket
连接
- 预连接,检查delegate,delegateQueue,是否已经连接,支持IPv4/IPv6,
- lookupHost,获取server地址
- lookup,建立连接
- connectWithAddress4:address6:,通过lookup调用
- 调用createSocket,创建客户端socket
- connectSocket,调用connect()函数连接服务器
服务器开始监听(acceptOnPort)
这里最终调用的是acceptOnInterface方法
- 通过getInterfaceAddress4:address6:获取本地的IPv4/IPv5 地址以及端口
- 创建IPv4/IPv6的socket,并且进行bind本机地址
- 调用listen函数进行监听这个socket
- 获取客户端连接回调,创一个DISPATCH_SOURCE_TYPE_READ的source并且启动,然后设置event hanlder,这里面会调用doAccept方法,在这个方法,通过accept()函数获取客户端新连接的socket,然后会异步到delegateQueue,调用代理方法socket:didAcceptNewSocket:来获取新的连接
发送数据
发送数据的对象是GCDAsyncWritePacket
发送的方法是writeData:
- 异步到socketQueue,把GCDAsyncWritePacket对象加入到writeQueue
- 调用maybeDequeueWrite,取出writeQueue第一个数据为currentWrite
- 写入数据有三种方式
- 通过write()函数正常写入
- TLS方式,通过CFWriteStreamWrite写入
- SSL方式,通过SSLWrite写入,在这里面如果遇到了I/O阻塞(errSSLWouldBlock),会把数据放入缓冲区进行再次写入
在iOS开发中使用socket,一般都是用第三方库GCDAsyncSocket(虽然也有原生CFSocket)。 GCDAsyncSocket 下载地址: GCDAsyncSocket
使用之前需要先在项目引入ASyncSocket库:
- 把ASyncSocket库源码加入项目:只需要增加RunLoop目录中的AsyncSocket.h、AsyncSocket.m、AsyncUdpSocket.h和AsyncUdpSocket.m四个文件。
- 在项目增加CFNetwork框架:在Framework目录右健,选择Add-->Existing Files... , 选择 CFNetwork.framework
一般来说,一个用户只需要建立一个socket长连接,所以可以用单例类方便使用。
单例方法:
// 创建单例
+ (Singleton *) sharedInstance
{
static Singleton *sharedInstace = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstace = [[self alloc] initPrivate];
});
return sharedInstace;
}
// 私有创建方法,不公开
- (instancetype)initPrivate {
if (self = [super init]) {
_lockStr = @"1234";
}
return self;
}
// 废除init创建方法
- (instancetype)init {
@throw [NSException exceptionWithName:@"初始化异常" reason:@"不允许通过init方法创建对象" userInfo:nil];
}
建立socket长连接:
#define TIME_OUT 20
// 建立socket连接
-(void)socketConnectHost{
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
NSLog(@"连接服务器");
NSError *error = nil;
[_socket connectToHost:_socketHost onPort:_socketPort withTimeout:TIME_OUT error:&error];
}
// socket成功连接回调
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"成功连接到%@:%d",host,port);
_bufferData = [[NSMutableData alloc] init]; // 存储接收数据的缓存区
[_socket readDataWithTimeout:-1 tag:99];
}
心跳:
心跳机制是判断客户端与服务端双方是否存活。
好处是:断了之后客户端可以重新建连,而对于服务端来说清理无效连接。
心跳客户端和服务器都可以发起,一般来说客户端发,一般实现步骤如下
- 客户端每隔一个时间发送一个包给服务器,并且设置超时时间
- 服务器收到后回应一个包
- 客户端如果收到包则说明正常,超时的话则说明挂了
@property (nonatomic, retain) NSTimer *heartTimer; // 心跳计时器
在连接成功的回调方法里,启动定时器,每隔2秒向服务器发送固定的消息来检测长连接。
// 心跳连接
-(void)longConnectToSocket{
根据服务器要求发送固定格式的数据,假设为指令@"longConnect",但是一般不会是这么简单的指令
NSString *longConnect = @"longConnect";
NSData *dataStream = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[_socket writeData:dataStream withTimeout:1 tag:1];
}
断开连接:
- 主动断开:
- (void)cutOffSocket {
[_socket disconnect];
_socket.userData = @(SocketOfflineByUser);
NSLog(@"断开连接");
}
- 被动断开:
-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
if (err.code == 57) {
_socket.userData = @(SocketOfflineByWifiCut); // wifi断开
}
else {
_socket.userData = @(SocketOfflineByServer); // 服务器掉线
}
NSLog(@"断开连接,错误:%@",err);
}
错误码请见 sys/errno.h
发送消息:
// 发消息
- (void)sendMessage:(NSData *)data {
[_socket writeData:data withTimeout:TIME_OUT tag:10];
}
// wirte成功
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
// 持续接收数据
// 超时设置为附属,表示不会使用超时
[_socket readDataWithTimeout:-1 tag:tag];
}
接收消息:
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 在这里处理消息
[self disposeBufferData:data];
//持续接收服务端的数据
[sock readDataWithTimeout:-1 tag:tag];
}
TCP粘包
TCP是面向连接的传输层协议,TCP连接只能是一对一的,它提供可靠的交付服务,也就是说,通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达,TCP提供全双工通信,TCP是面向字节流的,无消息保护边界
基于上面TCP所以会有粘包的问题,而UDP基于数据包则不会出现粘包。但是由于UDP传输不可靠,会出现丢包,无序的问题,而TCP则不会有。
TCP粘包的问题和相关处理。粘包是指发送方发送的若干包数据到接收方接收时粘成一包,TCP传输往往会出现粘包。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
一种比较周全的对策是:接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。具体的方法就是在发送数据是在数据前加入包头,接收时首先将待处理的接收数据流(长度为m)强行转换成预定的结构数据形式,并从中取出结构数据长度字段n,而后根据n计算得到第一包数据长度。
1)若n<m,则表明数据流内容超过一段完整的数据结构,将前n长度的数据截取并进行处理,对于剩下的m-n长度数据重复上述解析和判断。 2)若n=m,则表明数据流内容恰好是一完整结构数据,直接将其存入临时缓冲区即可。 3)若n>m,则表明数据流内容尚不够构成一完整结构数据,需留待与下一包数据合并后再行处理。
下面是我和服务器约定好的包头和包的类型
// 定义包头
typedef struct tagNetPacketHead
{
int version; //版本
int eMainType; //包类型主协议
int eSubType; //包类型子协议
unsigned int nLen; //包体长度
} NetPacketHead;
// 定义发包类型
typedef struct tagNetPacket
{
NetPacketHead netPacketHead; //包头
unsigned char *packetBody; //包体
} NetPacket;
收到数据时先将收到的数据放到缓存中,然后进行上述判断。
- (void)disposeBufferData:(NSData *)data {
@synchronized (self.lockStr) {
[_bufferData appendData:data];
while (_bufferData.length >= 16) {
struct tagNetPacketHead head;
[_bufferData getBytes:&head range:NSMakeRange(0, 16)];
while (_bufferData.length >= 16 && !(head.version == 1 && head.eMainType > -10 && head.eMainType < 1000 && head.eSubType > - 10 && head.eSubType < 1000)) {
int a = (int)_bufferData.length - 1;
_bufferData = [_bufferData subdataWithRange:NSMakeRange(1, a)].mutableCopy;
if (_bufferData.length >= 16) {
[_bufferData getBytes:&head range:NSMakeRange(0, 16)];
}
}
BOOL isIn = !(head.nLen > (_bufferData.length - 16));
if (isIn && _bufferData.length >= 16) {
NSMutableData *pendingData = [NSMutableData data];
if (head.eSubType == -1) {
pendingData = [_bufferData subdataWithRange:NSMakeRange(4, 4)].mutableCopy;
[pendingData appendData:[_bufferData subdataWithRange:NSMakeRange(16, head.nLen)]];
}
else {
pendingData = [_bufferData subdataWithRange:NSMakeRange(4, 8)].mutableCopy;
NSLog(@"%d", head.nLen);
[pendingData appendData:[_bufferData subdataWithRange:NSMakeRange(16, head.nLen)]];
}
[DisposeManager disposeData:pendingData num:head.eMainType];
int totalLen = _bufferData.length;
_bufferData = [_bufferData subdataWithRange:NSMakeRange(16 + head.nLen, totalLen - 16 - head.nLen)].mutableCopy;
}
}
}
}
注意:在这里加入线程锁@synchronized (self.lockStr),防止缓冲区同时被多个线程访问发生缓冲区数据混乱。
发送数据时则需要将数据按照约定的结构进行处理,在前边加上包头。
- (NSMutableData *)linkDataWithVersion:(NSData *)versionData mainType:(int)mainType subType:(int)subType packetBody:(NSData *)packetBody{
if (!versionData) {
int version = 1;
versionData = [NSMutableData dataWithBytes:&version length:sizeof(version)];
}
NSMutableData *mainTypeData = [NSMutableData dataWithBytes:&mainType length:sizeof(mainType)];
NSMutableData *subTypeData = [NSMutableData dataWithBytes:&subType length:sizeof(subType)];
unsigned int len;
if (packetBody) {
len = packetBody.length;
}
else {
len = 0;
}
NSMutableData *lenData = [NSMutableData dataWithBytes:&len length:sizeof(len)];
NSMutableData *sendData = [[NSMutableData alloc] init];
[sendData appendData:versionData];
[sendData appendData:mainTypeData];
[sendData appendData:subTypeData];
[sendData appendData:lenData];
[sendData appendData:packetBody];
return sendData.mutableCopy;
}
注:这里的mainTypeData和subTypeData只是和本工程相关的主协议和子协议,并不具备普遍性。
注:version和服务器端约定好是1。
客户端负载均衡
负载均衡(Load Balance)一般指服务端负载均衡,意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。对于服务端负载均衡一般都会维护一个服务器清单,当客户端请求到来时负载均衡服务器会从清单中选出一台处理请求。
而客户端负载均衡最大的区别在于服务器清单是在客户端维护。
实现步骤:
- 获取服务器列表并且缓存在本地
- 对每个服务器进行连接并且发送测速包
- 当全部拿到测速结果(一次来回的时间等)后,找到最优的服务器进行连接