学习 CocoaAsyncSocket

Socket

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

大部分系统都提供了一组基于TCP或者UDP的应用程序编程接口(API),该接口通常以一组函数的形式出现,也称为套接字(Socket)

环境说明

python2.7
macOS Catalina
Xcode 11.3.1
CocoaAsyncSocket ea517e0cc1b33b4f706a20f521ed298adbb05378

实验

使用python分别创建Socket服务器与客户端[2]

服务端socket_service.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding: utf-8 -*-

import socket

ip_port = ('192.168.31.136', 9999)

sk = socket.socket() # 创建套接字
sk.bind(ip_port) # 绑定服务地址
sk.listen(5) # 监听连接请求
print('启动socket服务,等待客户端连接...')
conn, address = sk.accept() # 等待连接,此处自动阻塞
while True: # 一个死循环,直到客户端发送‘exit’的信号,才关闭连接
client_data = conn.recv(1024).decode('utf-8') # 接收信息
if client_data == "exit": # 判断是否退出连接
exit("通信结束")
print("来自%s的客户端向你发来信息:%s" % (address, client_data.encode('utf-8')))
conn.sendall('recv:'+client_data.encode('utf-8')) # 回馈信息给客户端
conn.close() # 关闭连接

用户端socket_client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-

import socket

ip_port = ('192.168.31.136', 9999) # 192.168.31.136为本机地址 ifconfig 查看

s = socket.socket() # 创建套接字

s.connect(ip_port) # 连接服务器

while True: # 通过一个死循环不断接收用户输入,并发送给服务器
inp = input("请输入要发送的信息: ").strip()
if not inp: # 防止输入空信息,导致异常退出
continue
s.sendall(inp.encode())

if inp == "exit": # 如果输入的是‘exit’,表示断开连接
print("结束通信!")
break

server_reply = s.recv(1024).decode()
print(server_reply)

s.close() # 关闭连接

在终端中启动socket_service.py

1
2
-> python socket_service.py
启动socket服务,等待客户端连接...

启动socket_client.py

输入"hello"

1
2
3
4
5
6
7
8
# 客户端
-> python socket_client.py
请输入要发送的信息: "hello"
recv:hello
请输入要发送的信息:

# 服务器
来自('192.168.31.136', 58160)的客户端向你发来信息:hello

新开一个服务端socket窗口,使用浏览器访问

1
2
3
4
5
6
7
8
9
10
11
# 服务器

来自('192.168.31.136', 58258)的客户端向你发来信息:GET / HTTP/1.1
Host: 192.168.31.136:9999
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7

使用浏览器访问,使用HTTP协议访问,传输层实现了端到端的传输(能力),但是信息要被正确的理解,需要一些说明,比如上面HTTP 请求头,操作系统拥有传输层的能力,并包装了一组抽象的编程接口(Socket),应用层协议通过调用Socket实现信息的传输并制定格式用于理解信息。

Socket的流程

来自网络

CocoaAsyncSocket

CocoaAsyncSocket为macOS,iOS和tvOS提供了易于使用且功能强大的异步套接字库。

客户端Socket

实现客户端socket,参考上图,主要为创建socket,连接,发送,接收,关闭流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 创建``Socket``
/// 指定代理及代理回调的队列
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

// 连接
NSError *err = nil;
if (![_socket connectToHost:@"192.168.31.136" onPort:9999 error:&err]) {
// If there was an error, it's likely something like "already connected" or "no delegate set"
NSLog(@"I goofed: %@", err);
}

// 发送
NSString *requestString = @"AsyncSocket request string 1";
NSData *requestData = [requestString dataUsingEncoding:NSUTF8StringEncoding];

[_socket writeData:requestData withTimeout:-1 tag:1];

// 接收
[_socket readDataWithTimeout:10 tag:1];

代理回调

连接成功

1
2
3
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"%@ - %@ - %hu",sock,host,port);
}

发送成功

1
2
3
4
5
6
7
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
if (tag == 1) {
NSLog(@"First request sent");
}else if(tag == 2) {
NSLog(@"Second request sent");
}
}

接收成功

1
2
3
4
5
6
7
8
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
if (tag == 1) {
NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}else if (tag == 2) {
NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}
}

确保真机和mac处于同一局域网,输出效果如下:

能成功连接,发送信息给服务端且接收服务端返回的信息

连接

在连接的回调中设置断点,并进行追踪

对于连接操作,采用异步的并发队列进行管理,提高连接效率,调用系统提供socket.h中的connect方法

1
int     connect(int, const struct sockaddr *, socklen_t) __DARWIN_ALIAS_C(connect);

1
2
3
4
5
6
7
8
connect - initiate a connection on a socket

The connect() system call connects the socket referred to by the file
descriptor sockfd to the address specified by addr. The addrlen
argument specifies the size of addr.

If the connection or binding succeeds, zero is returned. On error,
-1 is returned, and errno is set appropriately.

连接完成后,统一回调到socketQueue队列,调用didConnect:方法

SetupStreamPart1()

经过一些容错判断后,调用CFStreamCreatePairWithSocket

创建连接到套接字的可读和可写流

includeReadWrite参数为NO,对Stream在读写操作中发生错误和完成注册回调函数

dispatch_async(delegateQueue,...

异步回调到代理队列(初始化时可指定,此处为主队列)执行代理方法

SetupStreamsPart2()

回到socketQueue

startCFStreamThreadIfNeeded方法中创建一个串行队列,以同步的方式新建一条子线程并开启运行循环

同时在addStreamsToRunLoop中,将创建的读写流作为事件源顺序添加到刚创建的子线程的runloop中,确保发生对应事件时,runloop能顺利派发。

回到didConnect:方法中,

socket开启非阻塞IO,注册读写操作的回调,同时尝试进行读写操作

执行原来的两个读操作

参考

  1. 百科 - 套接字
  2. socket编程
  3. connect2
  4. iOS网络编程之CFNetwork
  5. Introduction to non-blocking I/O
开始
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×