1.什么是socket?
-网络上的两个程序通过一个双向的通信连接实现数据的交换,这个链接的一端称为一个socket(又叫套接字)
-应用程序通过套接字向网络发出请求或者应答网络请求,常用的协议TCP/UDP
连接过程,具体可分三步:
(1)服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态
(2)客户端请求:客户端的套接字提出连接请求,连接的目标是服务器端的套接字。客户端怎么确保自己能正确连接服务器端呢,客户端的套接字必须首先描述他要连接的服务器的套接字,包括服务器端的地址和端口号,然后向服务器端套接字提出连接请求。
(3)连接确认:当接收到来自客户端套接字的连接请求后,服务器端会响应这个请求,建立一个新的线程,把服务器端的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
2.GCDAsyncSocket的用法
-创建Socket对象。
GCDAsyncSocket中socket连接后的事件处理都是在代理里面的
// 创建Socket对象
// 设置代理,让代理在全局队列中调用
GCDAsyncSocket *clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
// 用强指针引用着Socket对象
self.clientSocket = clientSocket;
NSString *host = nil;//host,主机地址
uint16_t port = 8288;//port,端口号
// 连接服务器
NSError *error = nil;
[clientSocket connectToHost:host onPort:port error:&error];
if (error) {
NSLog(@"%@", error.localizedDescription);
}
-常用的几个代理GCDAsyncSocketDelegate.
注意:在连接成功之后要监听数据的读取,接收到数据后也要监听数据的读取,接收数据的代理不会被回调
// 连接成功后的回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
// 监听读取数据
[sock readDataWithTimeout:-1 tag:0];
}
// 与服务器断开连接后的回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
// 可以调用该方法查看断开连接的原因
NSLog(@"%@", err);
}
// 接收数据的回调
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
// 接收到的消息
NSString *messageStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", messageStr);
// 准备读取下次的数据
[sock readDataWithTimeout:-1 tag:0];
-发送消息
/**
* 发送消息
*
* @param sendMessage 发送的消息内容
*/
- (void)sendMessage:(NSString *)sendMessage{
if (sendMessage == nil || sendMessage.length == 0) {
return;
}
// 发送
[self.clientSocket writeData:[sendMessage dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
}
// 发送消息成功后的回调,消息发送成功后可以在这个代理里面做一些处理
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
-GCDAsyncSocket的服务端
服务端和客户端的接收和发送数据的方法是一样的,不同的地方就是服务端是开启一个服务被动的等待客户端的连接。
(1)调用GCDAsyncSocket的接口开启服务
/**
* Tells the socket to begin listening and accepting connections on the given port.
* When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it,
* and the socket:didAcceptNewSocket: delegate method will be invoked.
*
* The socket will listen on all available interfaces (e.g. wifi, ethernet, etc)
**/
- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr;
此方法绑定一个端口号port, errPtr是错误变量,可通过该变量判断服务开启是否成功
// 定义一个标志位,用于判断当前服务是否开启
BOOL isRunning;
/**
* @brief 开启服务
*/
- (void)startServer
{
if (!isRunning)
{
[self initServer];
[_serverSocket readDataWithTimeout:-1 tag:0];
isRunning = YES;
}
}
/**
* @brief 服务端初始化
*/
- (void)initServer
{
allClientArray = [NSMutableArray array];
dispatch_queue_t socketQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:socketQueue];
NSError *error = nil;
[_serverSocket acceptOnPort:_socketPort error:&error];
if (error != nil)
{
NSLog(@"error --> %@", error);
}
else
{
NSLog(@"server start...");
}
}
(2)关闭服务
停止服务直接调用- (void)disconnect方法即可
/**
* @brief 停止服务
*/
- (void)stopServer
{
if (isRunning)
{
[_serverSocket disconnect];
isRunning = NO;
NSLog(@"server stop...");
}
}
在有客户端接入的服务器时,会调用下面三个委托方法:
/**
* This method is called immediately prior to socket:didAcceptNewSocket:.
* It optionally allows a listening socket to specify the socketQueue for a new accepted socket.
* If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue.
*
* Since you cannot autorelease a dispatch_queue,
* this method uses the "new" prefix in its name to specify that the returned queue has been retained.
*
* Thus you could do something like this in the implementation:
* return dispatch_queue_create("MyQueue", NULL);
*
* If you are placing multiple sockets on the same queue,
* then care should be taken to increment the retain count each time this method is invoked.
*
* For example, your implementation might look something like this:
* dispatch_retain(myExistingQueue);
* return myExistingQueue;
**/
- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock
/**
* Called when a socket accepts a connection.
* Another socket is automatically spawned to handle it.
*
* You must retain the newSocket if you wish to handle the connection.
* Otherwise the newSocket instance will be released and the spawned connection will be closed.
*
* By default the new socket will have the same delegate and delegateQueue.
* You may, of course, change this at any time.
**/
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
/**
* Called when a socket connects and is ready for reading and writing.
* The host parameter will be an IP address, not a DNS name.
**/
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;
总结:上面两段代码都是关于如何使用GCDAsyncSocket的,具体的步骤1.初始化socket, 2.建立连接,3.发送接收数据 ,4.断开连接
补充知识:
1.初始化socket源码的四种方法
- (instancetype)init;
- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq;
aDelegate是socket的代理
dq是delegate的线程
sq是socket的线程,这个是可选的设置,可以写为null,这时GCDAsyncSocket内部会帮你创建一个它自己的socket线程,如果要自己提供一个socket线程,不要提供一个并发线程,频繁的socket通信过程中可能会阻塞掉,一般不用自己创建。
注意:在使用套接字前,必须设置委托和委托调度队列,否则将出现错误
2.建立连接时
如果建立连接成功,会收到socket成功的回调
- (void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(NSError*)err
如果失败了,则会收到以下回调
- (void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(NSError*)err
3.发送数据
[self.socket writeData:data withTimeout:-1 tag:0];
发送数据的回调
- (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag;
4.读取数据的回调
- (void)socket:(GCDAsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag;
5.断开连接,重连
[self.socket disconnect];//断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
//这里可以做重连操作
}
6.主动读取消息
在发送消息后,需要主动调取didReadDataWithTimeOut方法读取消息,这样才能接收到你发出请求后从服务器端收到的数据
- (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag
{
[self.socket readDataWithTimeout:-1 tag:tag];
}
7.关于tag参数
是为了在回调方法中匹配发起调用的方法的,不会加在传输数据中。
调用write方法,收到didWriteData回调,调用writeDataWithTimeOut读取数据。收到消息后,会调用didReadData的delegate方法。这是一次数据发送,在接受服务端回应的过程。
例如
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag;
该方法和它的代理回调中的tag参数是对应的。源码中tag的传递是包含在当前的写的数据包 GCDAsyncWritePacket currentWrite 中。
同理,read的tag和readDataWithTimeout代理回调中的tag是一致的,tag传递包含在GCDAsyncReadPacket* currentRead数据包中
需要注意,根据tag做消息回执的标识,可能会出现错乱的问题