目录
一、网络七层模型及五层模型
1、网络七层模型
2、网络五层模型
二、各种协议
1、IP协议
2、TCP协议与UDP协议的区别及TCP链接
3、HTTP协议、HTTP链接及HTTPS协议
三、socket
1、socket是什么?
2、如何建立一个socket链接?
3、socket链接与HTTP链接的区别
4、使用CocoaAsyncSocket实现socket编程
一、网络七层模型及五层模型
1、网络七层模型
网络七层模型从下往上分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
网络七层模型网络七层模型的推出是循序渐进的,下面逐个按需求简单地了解下网络七层模型,注意只是简单了解。
- 需求一:
网络七层模型的最开始,首先要解决的问题就是两个终端之间该如何进行通信,具体来说就是一个终端上发送二进制数据,另一个终端怎么收到它传输的二进制数据。比如一台设备发送的是“你好”(二进制的1),另一台设备接收到之后就应该是“你好”(二进制的1),而不是“再见”(二进制的0),那么这个“你好”为什么用二进制的1表示,而“再见”用二进制的0表示,这都是需要标准的,比如多少伏电压用1来表示,多少伏电压用0来表示等等。
于是出现了物理层。物理层主要解决的问题就是:制定一些标准,确保两个终端之间能进行比特流的传输。
- 需求二:
现在我们能在两个终端之间进行比特流的传输了,但是传输的比特流有可能出错啊,所以就需要有检错与纠错功能。
于是出现了数据链路层。数据链路层主要解决的问题就是:提供了检错与纠错功能,来保证终端与终端之间比特流的正确传输。
- 需求三:
现在我们能进行终端与终端之间比特流的正确传输了,但是随着网络的发展,我们的终端不再是直接点对点的了,而是两个终端之间有很多个中继设备,由这很多个中继设备也可以组合出很多条数据传输路径,那么信息传输的时候该如何选择最佳路径呢?这就需要路由,路由就是一个进程来实现最佳路径的选择。
于是出现了网络层。网络层主要解决的问题就是:定义了IP地址,通过IP地址寻址来确保信息传输时可以选择到最佳的传输路径。因此,IP协议是网络层协议。
- 需求四:
现在我们已经能给指定的终端、传输正确的比特流,但是如果我们要传的东西非常大,比如一个视频,在视频传输过程中网断了怎么办,因此我们还要考虑文件传输准确性的问题。因此,我们就需要对数据进行封装、打包,一个一个地发出去。
例如,TCP协议,你一个终端发出了10000个包,另一个终端就必须收到10000个包,如果丢包了,就要告诉我你没接到哪个包,我再给发一遍,这样就可以保证数据传输的完整性。TCP协议是会绑定IP地址和端口的协议。而UDP协议的话,不管你丢不丢包,主机发出去就行了,如果丢包的话,下次发送再说呗,但是效率高。
于是出现了传输层。传输层主要解决的问题就是:对数据进行封装和打包。TCP和UDP协议就是传输层协议。
- 需求五:
现在我们已经能给指定的终端、传输正确的、封装和打包过的数据了。但是难道我们每传输一次数据都要调用TCP去打包,然后调用IP协议寻找最佳路径去发送吗?这太麻烦了吧。
于是出现了会话层。会话层主要解决的问题就是:实现自动收发包和自动寻址。
- 需求六:
现在我们能实现自动收发包和自动寻址了,但是如果是基于Linux系统的终端给基于Windows系统的终端传输信息呢,两个系统的语法是不一样的,传不了,怎么办?
于是出现了表示层。表示层主要解决的问题就是:不同系统之间通信的语法问题。
- 应用层:
应用层是网络七层模型的最高层,是用户与网络连接的接口,如用户通过应用程序来完成传输文件、收发邮件等网络需求。**HTTP协议就是应用层协议。 **
2、网络五层模型
后来又有了一个五层模型,是把会话层、表达层和应用层合并成了应用层,应用层一个层做了原来三个层的事。这样从下至上IP协议属于网络层协议,TCP/UDP属于传输层协议,HTTP属于应用层协议。
网络五层模型二、各种协议
总的来说,IP协议、TCP/UDP协议还有HTTP/HTTPS协议它们是分别属于网络层、传输层以及应用层的协议,所以它们根本就是存在于不同层的协议,用途就不一样,不必去探讨它们之间的区别,不具备可比性。
1、IP协议
IP协议是一个负责寻址的协议,它是一个网络层协议。
2、TCP协议与UDP协议的区别及TCP链接
TCP协议和UDP协议是一个负责数据封装与打包的协议,它们都是传输层协议。
(1)TCP协议与UDP协议的区别
-
TCP是面向链接的,即使用TCP协议传输数据需要先建立链接,如打电话需要先拨号接通电话;而UDP是不面向链接的,传输数据之前不需要建立链接,如写信直接发出去了。
-
TCP提供可靠的通信,即TCP会保证数据的不丢包、不重复、且有序的传输;而UDP会尽可能地保证数据传输的可靠性,但无法确保数据传输不丢包。
-
每一个TCP链接仅支持点对点的通信;而一个UDP链接可以支持一对一、一对多和多对多等多种通信方式。
总的来说,TCP支持面向链接的、可靠的、点对点的通信,而UDP支持不面向链接的、不可靠的、多种通信方式的通信。
但是UDP的传输速度极快,对系统资源的要求也极少,因此使用TCP还是UDP需要根据实际情况来选择。比如现在很多游戏,对实时性要求很高,就采用UDP协议来传输数据,当然也得益于网速的提高使得丢包的概率极低。
(2)TCP链接
-
建立一个TCP链接需要三次握手:
第一次握手:客户端向服务端发送报文发起链接请求,客户端进入(链接请求_已发送)的状态;
第二次握手:服务端收到客户端发起的请求,会向客户端发送一个报文来确认下这个链接请求,服务端进入(已接收到客户端链接请求)的状态;
第三次握手:客户端在收到服务端确认的报文后,还会向服务端发送一个确认链接的报文,发送完毕后,客户端和服务端就进入了(链接建立)的状态,就可以传递数据了,握手过程是不能包含数据的。 -
断开一次TCP链接需要经过四次握手
3、HTTP协议、HTTPS协议及HTTP链接
(1)HTTP协议
HTTP协议是超文本传输协议的缩写,它是基于TCP协议实现的一个应用层的协议,所以一个HTTP链接其实就是一个标准的TCP链接。最初设计HTTP协议是为了从万维网传输HTML页面到本地浏览器,那么到现在的的话HTTP协议就是指客户端向服务端发起一个请求,存储着资源的服务器把数据传输给客户端的一种传输协议。
一个HTTP请求的URLhttp://www.yiyi.com:8080/articles/index.asp?articleID=1&articleName=weicheng
可分为以下几个部分:
- 协议部分:
http:
,代表该网页使用的是HTTP协议,协议的后面要用//
作分隔符号; - 域名或IP地址部分:
www.yiyi.com
,是URL的域名部分,当然这个地方也可以是IP地址; - 端口部分:
8080
,是URL的端口部分,端口部分和域名部分需要用:
隔开,端口不是一个URL必须的部分,如果不写的话会采用默认端口80
; - 虚拟目录部分:
/articles/
,URL中从第一个/
到最后一个/
之间的内容就是URL的虚拟目录部分; - 文件名部分:
index.asp
,URL中最后一个/
和?
之间的部分是URL的文件名部分; - 参数部分:
articleID=1&articleName=weicheng
,?
后面的部分是参数部分,多个参数用&
连接。
(2)HTTPS协议
HTTP协议以明文的方式传输,不提供任何方式的数据加密,所以攻击者如果攻击了客户端和服务端之间的传输报文,就可以直接读取其中的信息,因此传递一些敏感信息用HTTP协议是不安全的。
那么HTTPS协议是HTTP的安全版,它是在HTTP协议的基础上增加了SSL安全协议的形成的一个加密传输协议,来保证数据传输的安全性。
(3)HTTP链接
上面说了HTTP协议是基于TCP协议实现的一个应用层的协议,所以一个HTTP链接其实就是一个标准的TCP链接。
HTTP链接最显著的特点就是客户端每发送一次请求,服务端都会返回一个响应(返回成功的数据也好、失败也好),这次请求结束后,这个HTTP链接就会主动释放掉,因此HTTP链接是一种“短链接”。
对于HTTP链接来说,从建立链接到关闭链接的这一段过程称为一次链接。如果服务端长时间没收到客户端的请求,就会认为客户端下线了,同样的如果客户端长时间无法收到服务端的响应,就会认为网络已断开,因此如果想要保持客户端持续在线的状态,就需要不断地向服务端发起HTTP请求。
三、socket
1、socket是什么?
-
概述:socket翻译过来就是“插座”嘛,装逼叫法是“套接字”,它是针对TCP和UDP协议封装的一套接口,它内部包含了进行网络数据传输必须的五种信息:数据传输使用的协议、客户端的IP地址、客户端的端口号、服务端的IP地址和服务端的端口号,因此可以使用socket来完成端到端的双向通信。
-
本质:要记住socket本身并不是协议,也就是说它本身并没有规定两个终端之间该怎样传输数据,它仅仅是对TCP和UDP协议封装的一套接口,记住socket只是一套API,我们使用这套API(即使用socket)来间接地使用TCP协议或UDP协议实现数据的传输。
2、如何建立一个socket链接?
前面我们提到了socket其实就是对TCP和UDP协议封装的一套接口,因此当我们使用TCP协议进行链接时,我们建立的链接就是一个TCP链接,当我们使用UDP协议进行链接时,我们建立的链接就是一个UDP链接。因此,每建立一个socket链接就会在内部调用一下TCP链接建立的三次握手,每断开一个socket链接就会在内部调用一下TCP链接断开的四次握手。
建立一个socket链接至少需要两个socket,一个运行于服务端(即ServerSocket),一个运行于客户端(即ClientSocket)。
准备好了两个socket之后,建立socket链接还需要三步:
- 服务端监听:服务端socket并不定位具体的客户端socket,而是处于等待链接的状态,实时监控网络状态;
- 客户端请求:客户端socket提出链接请求,链接的目标就是服务端socket。客户端socket必须首先描述它要链接的服务端socket的IP地址和端口号,然后再向服务器端socket提出链接请求。
- 连接确认:当服务端socket监听到或者说接收到客户端socket的链接请求,它就响应客户端socket的请求,建立一个新的线程,把服务端socket的描述发给客户端,一旦客户端确认了此描述,链接就建立好了。而服务端socket继续处于监听状态,继续接收其他客户端socket的链接请求。
3、socket链接与HTTP链接的区别
socket链接通常情况下就是TCP链接(因为我们通常选择使用TCP协议来完成链接),所以一旦链接建立成功,这个链接就会一直存在那里,客户端和服务端就可以相互通信,直到一方主动断开链接,也就是说socket链接是个长链接。某些情况下,如果我们需要服务端主动向客户端传东西,我们就可以用socket链接来实现。
而HTTP链接是个一次性的链接,就是客户端发起请求,服务端给个响应,然后这个请求就算完成,这个链接就会主动地断掉,所以HTTP链接是个短链接,因此如果我们想要保证客户端一直在线上,就需要不断地向服务端发送请求。大多数情况下,我们开发用的是HTTP链接。
其实,上面我们说到socket链接一旦建立之后就一直挂在那,除非某一端主动断开,但实际情况中数据传输往往要经过路由器、网关和防火墙等多个中间结点,而防火墙会自动断开长时间处于非活跃状态的链接,所以socket链接就可能断开,所以我们在使用socket的时候一定要通过轮循做心跳链接,来告诉网络该链接处于活跃状态。
4、使用CocoaAsyncSocket实现socket编程
参考博客:里面有数据粘包处理的方案。
(1)CocoaAsyncSocket介绍
-
CocoaAsyncSocket是一个开源的三方库,能够帮助我们很快、很简单地实现socket编程。
-
CocoaAsyncSocket主要包含两个类:
- GCDAsyncSocket:用GCD搭建的基于TCP协议的socket网络库
- GCDAsyncUdpSocket:用GCD搭建的基于UDP协议的socket网络库
- 我们会使用基于TCP协议的socket来完成编程。
(2)客户端socket的搭建
- 把GCDAsyncSocket文件拖进项目中:
- 代码:
//
// ViewController.m
// SocketClientDemo
//
// Created by 意一yiyi on 2018/3/26.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "ViewController.h"
#import "GCDAsyncSocket.h"
@interface ViewController ()<GCDAsyncSocketDelegate>// 遵守GCDAsyncSocketDelegate协议
@property (strong, nonatomic) GCDAsyncSocket *clientSocket;// 客户端socket
@property (assign, nonatomic) BOOL isConnected;// 客户端已链接服务端
@property (strong, nonatomic) NSTimer *connectTimer;// 长链接计时器
@property (weak, nonatomic) IBOutlet UITextField *ipAddressTF;// IP地址
@property (weak, nonatomic) IBOutlet UITextField *portTF;// 端口号
@property (weak, nonatomic) IBOutlet UITextField *sendMessageTF;// 发送信息
@property (weak, nonatomic) IBOutlet UITextView *showMessageTV;// 展示信息
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
#pragma mark - 链接服务端
- (IBAction)connectServer:(id)sender {
if (!self.isConnected) {// 客户端未链接服务端
// 创建socket,并指定代理对象为self,代理队列必须为主队列
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
// 链接指定IP地址(域名也可以)和端口的服务端socket,注意是IP地址而不是DNS名称,可以设定链接的超时时间,如果不想设置超时时间可设置为负数
NSError *error = nil;
self.isConnected = [self.clientSocket connectToHost:self.ipAddressTF.text onPort:[self.portTF.text integerValue] viaInterface:nil withTimeout:-1 error:&error];
if(!self.isConnected) {// 如果检测到错误,此方法将返回NO,并设置错误指针。可能的错误是无主机,无效接口或套接字已链接
self.isConnected = NO;
[self showMessageWithStr:[NSString stringWithFormat:@"客户端链接服务端失败,错误信息为:%@", error]];
}else {// 如果未检测到错误,则此方法将启动后台链接操作并立即返回YES。但这里未检测到错误不一定是链接成功了,也有可能是主机无法访问,主机无法访问的时候也会返回YES的,所以链接成功与否是要看下面的回调的
[self showMessageWithStr:@"客户端尝试链接服务端"];
}
}else {// 客户端已链接服务端
[self showMessageWithStr:@"客户端已链接服务端"];
}
}
#pragma mark - GCDAsyncSocketDelegate
// 客户端链接服务端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
self.isConnected = YES;
[self showMessageWithStr:@"客户端链接服务端成功"];
[self showMessageWithStr:[NSString stringWithFormat:@"服务端的地址和端口为:%@===%d", host, port]];
// 由于CocoaAsyncSocket支持排队读写,所以我们在客户端链接服务端成功后,立马读取服务端的数据,所有读/写操作将排队,并且在socket链接时,操作将按顺序出列和处理
[self.clientSocket readDataWithTimeout:-1 tag:0];
// 链接成功后添加定时器,来做心跳链接,保证socket一直处于活跃状态
[self addTimer];
}
// 客户端读取服务端数据成功后的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self showMessageWithStr:text];
// 注意:我们在第一次读取到数据后,需要在这里再次调用[self.clientSocket readDataWithTimeout:-1 tag:0];方法来读取数据,框架本身就是这么设计的,否则我们就只能接收一次数据,之后再也接收不到数据
[self.clientSocket readDataWithTimeout:-1 tag:0];
}
// 客户端与服务端断开链接的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
[self showMessageWithStr:[NSString stringWithFormat:@"客户端与服务端断开链接,断开原因为:%@", err]];
// sokect断开链接时,需要清空代理和客户端本身的socket
self.clientSocket.delegate = nil;
self.clientSocket = nil;
self.isConnected = NO;
[self.connectTimer invalidate];
}
#pragma mark - 心跳链接
- (void)addTimer {
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes];
}
// 心跳链接,表明链接还活着
- (void)longConnectToSocket {
// 心跳链接中发送给服务端的数据只是作为测试代码,根据你们公司需求,或者和后台商定好心跳包的数据以及发送心跳的时间间隔。
float version = [[UIDevice currentDevice] systemVersion].floatValue;
NSString *longConnect = [NSString stringWithFormat:@"%f这个链接还活着呢", version];
NSData *data = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[self.clientSocket writeData:data withTimeout:-1 tag:0];// 心跳链接的本质就是搁一段时间给服务端写一条数据,让防火墙知道这个socket链接还活着
}
#pragma mark - 客户端发送消息给服务端
- (IBAction)sendMessage:(id)sender {
NSData *data = [self.sendMessageTF.text dataUsingEncoding:NSUTF8StringEncoding];
[self.clientSocket writeData:data withTimeout:-1 tag:0];// tag:消息标记
}
#pragma mark - 客户端主动断开与服务端的链接
- (IBAction)disconnect:(id)sender {
[self showMessageWithStr:@"客户端主动断开与服务端的链接了"];
// sokect断开链接时,需要清空代理和客户端本身的socket
self.clientSocket.delegate = nil;
self.clientSocket = nil;
self.isConnected = NO;
[self.connectTimer invalidate];
}
#pragma mark - 信息展示
- (void)showMessageWithStr:(NSString *)str {
self.showMessageTV.text = [self.showMessageTV.text stringByAppendingFormat:@"%@\n", str];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
(3)服务端socket的搭建
- 把GCDAsyncSocket文件拖进项目中:
- 代码:
//
// ViewController.m
// SocketSeverDemo
//
// Created by 意一yiyi on 2018/3/26.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "ViewController.h"
#import "GCDAsyncSocket.h"
@interface ViewController ()<GCDAsyncSocketDelegate>// 遵守GCDAsyncSocketDelegate协议
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;// 服务端socket
@property (strong, nonatomic) NSMutableArray *clientSockets;// 保存客户端socket
@property (strong, nonatomic) NSTimer *checkTimer;// 检测心跳计时器
@property (strong, nonatomic) NSMutableDictionary *clientPhoneTimeDicts;// 客户端标识和心跳接收时间的字典
@property (weak, nonatomic) IBOutlet UITextField *portTF;// 端口号
@property (weak, nonatomic) IBOutlet UITextField *sendMessageTF;// 发送信息
@property (weak, nonatomic) IBOutlet UITextView *showMessageTV;// 展示信息
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initialize];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
#pragma mark - 服务端监听
- (IBAction)startNotice:(id)sender {
// 创建socket,并指定代理对象为self,代理队列必须为主队列
self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
// 服务端开始监听指定端口来自客户端的链接请求
NSError *error = nil;
BOOL result = [self.serverSocket acceptOnPort:[self.portTF.text integerValue] error:&error];
if (result && error == nil) {
[self showMessageWithStr:@"服务端开始监听指定端口来自客户端的链接请求"];
}else {
[self showMessageWithStr:[NSString stringWithFormat:@"服务端监听失败,失败原因:%@", error]];
}
}
#pragma mark - GCDAsyncSocketDelegate
// 服务端链接客户端成功的回调
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
// 保存客户端的socket
[self.clientSockets addObject:newSocket];
[self showMessageWithStr:@"服务端链接客户端成功"];
[self showMessageWithStr:[NSString stringWithFormat:@"客户端的地址和端口为:%@===%d", newSocket.connectedHost, newSocket.connectedPort]];
// 链接成功后读取来自客户端的数据
[newSocket readDataWithTimeout:-1 tag:0];
// 链接成功后添加定时器
[self addTimer];
}
// 服务端读取客户端数据成功后的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 客户端做了心跳链接,每隔5s中向服务端写一条数据,服务端能读取到
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self showMessageWithStr:text];
// 服务端把读取到的每条数据作为key,读取到数据的时间作为value存储起来,因为可能有很多个客户端的socket在链接服务端,这里的心跳链接做个标识存下来,是为了下面服务端每隔10s中做链接断开的判断
if (self.clientPhoneTimeDicts.count == 0) {// 第一次读取到的数据直接添加
[self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text];
}else {
[self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text];
}];
}
[sock readDataWithTimeout:-1 tag:0];
}
#pragma mark - 服务端发送消息给客户端
- (IBAction)sendMessage:(id)sender {
if(self.clientSockets == nil) {
return;
}
NSData *data = [self.sendMessageTF.text dataUsingEncoding:NSUTF8StringEncoding];
[self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj writeData:data withTimeout:-1 tag:0];// tag : 消息标记
}];
}
#pragma mark - 心跳链接
- (void)addTimer {
self.checkTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(checkLongConnect) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.checkTimer forMode:NSRunLoopCommonModes];
}
// 检测心跳
- (void)checkLongConnect {
[self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 获取当前时间
NSString *currentTimeStr = [self getCurrentTime];
// 如果某个客户端socket发送心跳链接的时间和当前时间相差10s以上,服务端则认为该链接断开了
if (([currentTimeStr doubleValue] - [obj doubleValue]) > 10.0) {
[self showMessageWithStr:[NSString stringWithFormat:@"%@已断开与服务端的链接,链接时差%f", key, [currentTimeStr doubleValue] - [obj doubleValue]]];
[self.clientPhoneTimeDicts removeObjectForKey:key];
[self showMessageWithStr:[NSString stringWithFormat:@"服务端移除了与%@的链接", key]];
}else {
[self showMessageWithStr:[NSString stringWithFormat:@"%@和服务端处于链接状态,链接时差%f", key, [currentTimeStr doubleValue] - [obj doubleValue]]];
}
}];
}
// 获取当前时间
- (NSString *)getCurrentTime {
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval currentTime = [date timeIntervalSince1970];
NSString *currentTimeStr = [NSString stringWithFormat:@"%.0f", currentTime];
return currentTimeStr;
}
#pragma mark - 信息展示
- (void)showMessageWithStr:(NSString *)str {
self.showMessageTV.text = [self.showMessageTV.text stringByAppendingFormat:@"%@\n", str];
}
#pragma mark - 初始化
- (void)initialize {
self.clientSockets = [NSMutableArray array];
self.clientPhoneTimeDicts = [NSMutableDictionary dictionary];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
(4)效果
1.gif
网友评论