CocoaAsyncSocket 文档3:介绍GCDAsyncSocket

仲璞瑜
2023-12-01

原文地址:https://github.com/robbiehanson/CocoaAsyncSocket/wiki/Intro_GCDAsyncSocket

GCDAsyncSocket is a TCP library. It’s built atop Grand Central Dispatch.

This page provides an introduction to the library.

GCDAsyncSocket是一个TCP库。它建立在GCD技术之上。本页就是介绍这个库。

Initialization


The most common way to initialize an instance is simply like this:

socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

The delegate and delegate_queue are required in order for GCDAsyncSocket to invoke your delegate methods. The code above specifies “self” as the delegate, and instructs the library to invoke all delegate methods on the main thread.

Setting a delegate is likely a familiar operation. However, providing a delegateQueue may be a new concept. Most typical libraries are single-threaded. When it’s time to invoke a delegate method, they just call it. The libraries assume your delegate code is also single-thread. Or the libraries may be multi-threaded internally, but they assume your delegate code is only single-threaded, and designed to run only on the main thread. So they simply always invoke all delegate methods on the main thread.

GCDAsyncSocket, on the other hand, was designed for performance. It allows you to receive delegate callbacks on dedicated gcd queues of your choosing. This allows it to be used in high-performance servers, and can support thousands upon thousands of concurrent connections. But it also helps in typical applications. Want your UI to be a bit snappier? Ever considered moving that network processing code off the UI thread? Even today’s mobile devices have multiple CPU cores… perhaps it’s time to start taking advantage of them.

初始化


通常,初始化一个实例用如下方法:

socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

用到delegate和delegate_queue是为了GCDAsyncsocket调用你的委托方法。上面的代码指定self为delegate(代理),然后通知库在主线程调用委托方法。

设置代理可能是一个熟悉的操作。然而,使用delegateQueue可能是一个新的概念。大部分典型的库是单线程的。当该调用委托方法的时候,调用就是了。库假定你的委托代码也是单线程。或库可能是内部的多线程,但他们认为你的delegate代码只是单线程,并设计为只在主线程中运行。所以他们在主线程中调用所有的委托方法。

另一方面,GCDAsyncsocket被设计为高性能的。它允许在你指定的GCD队列中,接受委托方法的回调。这允许它被用于高性能服务器,并且可以支持成千上万的并发连接。同时,这对特定的应用也有帮助。想让你的界面更加流畅么?你考虑过把网络线程代码移出界面线程么?甚至在如今移动设备都具有多核CPU的今天……也许是时候开始利用他们的优势了。

Configuration


Most of the time no configuration is necessary. There are various configuration options (as described in the header file), but they’re mainly for advanced use cases.

Note: Security (TLS/SSL) is something you setup later. These protocols actually run on top of TCP (they’re not part of TCP itself.)

配置


大多数的时间没有配置是必要的。有多种配置选项(如头文件中所述),但它们主要用于高级用例。
注:安全(TLS / SSL)是你安装后。这些协议是运行在TCP之上(他们不是TCP本身的一部分。)

Connecting


The most common way to connect is:

NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // Asynchronous!
{
    // If there was an error, it's likely something like "already connected" or "no delegate set"
    NSLog(@"I goofed: %@", err);
}

The connect methods are asynchronous. What does this mean? It means when you call the connect methods, they start a background operation to connect to the desired host/port, and then immediately return. This asynchronous background operation will eventually either succeed or fail. Either way, the associated delegate method will be called:

- (void)socket:(GCDAsyncSocket *)sender didConnectToHost:(NSString *)host port:(UInt16)port
{
    NSLog(@"Cool, I'm connected! That was easy.");
}

So if the connect method is asynchronous, why does it return a boolean and error? The only time this method will return NO is if something obvious prevents it from starting the connect operation. For example, if the socket is already connected, or if the delegate was never set.

There are actually several different connect methods available to you. They afford you different options such as:

  • Optionally specify a connect timeout.
    E.g. Fail if it doesn’t connect in 5 seconds
  • Optionally specify the interface to connect with E.g. Connect using
    bluetooth, or Connect using WiFi regardless of whether a wired
    connection is available.
  • Supply a raw socket address instead of a name/port pair E.g. I
    resolved an address using NSNetService, and I just want to connect to
    that address.

连接


通常是这样连接的:

NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // Asynchronous!
{
    // If there was an error, it's likely something like "already connected" or "no delegate set"
    NSLog(@"I goofed: %@", err);
}

连接方法是异步的。这是什么意思?这意味着当你调用连接方法,他们开始一个后台操作连接到所需的主机/端口,然后立即返回。这种异步的后台操作最终会成功或失败。无论哪种方式,关联的委托方法都将被调用:

- (void)socket:(GCDAsyncSocket *)sender didConnectToHost:(NSString *)host port:(UInt16)port
{
    NSLog(@"Cool, I'm connected! That was easy.");
}

这里连接方法是异步的,什么情况它返回布尔值和错误?唯一可情况返回NO的是,有一些明显的阻碍,组织它开始连接操作。比如,如果Socket已连接,或者Delegate没有设置。

实际上有几种不同的连接方法可供您选择。他们提供给你不同的选择,如:

  • 可选择指定连接超时 例如:超过五秒断开连接
  • 可选择指定连接的接口 例如:使用蓝牙连接,或使用WiFi,无论此时有线连接是可用的。
  • 使用一个原始的套接字地址,而不是一个名称/端口 例如:我决定用nsnetservice地址,我只是想连接到那个地址。

Reading & Writing


One of the best features of the library is “queued read/write operations”. What does that mean? A quick code example may explain it best:

NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // Asynchronous!
{
    // If there was an error, it's likely something like "already connected" or "no delegate set"
    NSLog(@"I goofed: %@", err);
    return;
}

// At this point the socket is NOT connected.
// But I can start writing to it anyway!
// The library will queue all my write operations,
// and after the socket connects, it will automatically start executing my writes!
[socket writeData:request1 withTimeout:-1 tag:1];

// In fact, I know I have 2 requests.
// Why not just get them both out of the way now?
[socket writeData:request2 withTimeout:-1 tag:2];

// Heck, while I'm at it, I might as well queue up the read for the first response.
[socket readDataToLength:responseHeaderLength withTimeout:-1 tag:TAG_RESPONSE_HEADER];

You may have noticed the tag parameter. What’s that all about? Well, it’s all about convenience for you. The tag parameter you specify is not sent over the socket or read from the socket. The tag parameter is simply echo’d back to you via the various delegate methods. It is designed to help simplify the code in your delegate method.

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

Tags are most helpful when it comes to reading:

#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == TAG_WELCOME)
    {
        // Ignore welcome message
    }
    else if (tag == TAG_CAPABILITIES)
    {
        [self processCapabilities:data];
    }
    else if (tag == TAG_MSG)
    {
        [self processMessage:data];
    }
}

You see, the TCP protocol is modeled on the concept of a single continuous stream of unlimited length. It’s critical to understand this - and is, in fact, the number one cause of confusion that we see.

Imagine that you’re trying to send a few messages over the socket. So you do something like this (in pseudocode):

socket.write("Hi Sandy.");
socket.write("Are you busy tonight?");

How does the data show up on the other end? If you think the other end will receive two separate sentences in two separate reads, then you’ve just fallen victim to a common pitfall! Gasp! But fear not! Your condition isn’t life threatening; it’s just a common cold. The cure can be found by reading the Common Pitfalls page.

Now that we have that out of the way, you may be wondering about those read methods. Here’s a few of them:

- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

The first method, readDataToLength, reads and returns data of the given length. Let’s take a look at an example:

You’re writing the client-side of a protocol where the server sends responses with a fixed-length header. The header for all responses is exactly 8 bytes. The first 4 bytes contain various flags, etc. And the second 4 bytes contain the length of the response data, which is variable. So you might have code that looks like this:

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == TAG_FIXED_LENGTH_HEADER)
    {
        int bodyLength = [self parseHeader:data];
        [socket readDataToLength:bodyLength withTimeout:-1 tag:TAG_RESPONSE_BODY];
    }
    else if (tag == TAG_RESPONSE_BODY)
    {
        // Process the response
        [self handleResponseBody:data];

        // Start reading the next response
        [socket readDataToLength:headerLength withTimeout:-1 tag:TAG_FIXED_LENGTH_HEADER];
    }
}

Let’s look at another example. After all, not all protocols use a fixed length header. HTTP is one such protocol.

A typical HTTP response looks something like this:

HTTP/1.1 200 OK
Date: Thu, 24 Nov 2011 02:18:50 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.1.6
Content-Length: 5233
Content-Type: text/html; charset=UTF-8

That’s just an example. There could be any number of header fields. In other words, the HTTP header has a variable length. How do we read it?

Well the HTTP protocol explains how. Each line in the header is terminated with a CRLF (carriage-return, line-feed : “\r\n”). Furthermore, the end of the header is marked with 2 back-to-back CRLF’s. And the length of the body is specified via the “Content-Length” header field. So we could do something like this:

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == HTTP_HEADER)
    {
        int bodyLength = [self parseHttpHeader:data];
        [socket readDataToLength:bodyLength withTimeout:-1 tag:HTTP_BODY];
    }
    else if (tag == HTTP_BODY)
    {
        // Process response
        [self processHttpBody:data];

        // Read header of next response
        NSData *term = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
        [socket readDataToData:term withTimeout:-1 tag:HTTP_HEADER];
    }
}

I’ve listed 2 available read methods. There are close to 10 different read methods available. They provide more advanced options such as specifying a maxLength, or providing your own read buffer.

读 写


这个库的最佳功能之一是“读队列/写操作”。这是什么意思呢?一下这段代码是最好的诠释:

NSError *err = nil;
if (![socket connectToHost:@"deusty.com" onPort:80 error:&err]) // Asynchronous!
{
    // If there was an error, it's likely something like "already connected" or "no delegate set"
    NSLog(@"I goofed: %@", err);
    return;
}

// At this point the socket is NOT connected.
// 这时Socket没有连接
// But I can start writing to it anyway!
// 但是我已经可以进行写操作
// The library will queue all my write operations,
// 库会将我的写操作排成队列
// and after the socket connects, it will automatically start executing my writes!
// 当Socket连接之后,它将自动执行我的写操作

[socket writeData:request1 withTimeout:-1 tag:1];

// In fact, I know I have 2 requests.
// Why not just get them both out of the way now?
[socket writeData:request2 withTimeout:-1 tag:2];

// Heck, while I'm at it, I might as well queue up the read for the first response.
[socket readDataToLength:responseHeaderLength withTimeout:-1 tag:TAG_RESPONSE_HEADER];

你可能已经注意到tag参数。这是干啥的呢?对于你来说这很方便。tag参数并不通过Socket发送或者接收,tag参数通过各种委托方法回调给你。它的目的是有助于简化您的委托方法中的代码。

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

当信息过来时,tag非常有用

#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == TAG_WELCOME)
    {
        // Ignore welcome message
    }
    else if (tag == TAG_CAPABILITIES)
    {
        [self processCapabilities:data];
    }
    else if (tag == TAG_MSG)
    {
        [self processMessage:data];
    }
}

你知道,TCP协议是一个单一连续的无限长度的流的概念。理解这一事实的关键是,事实上,我们所看到的数字造成混乱的原因。
想象一下你想在Socket上发送一些消息。所以你这样做(伪代码):

socket.write("Hi Sandy.");
socket.write("Are you busy tonight?");

数据如何显示在另一端?如果你认为对方会在两个独立的读取接收两个单独的句子,你就掉沟里啦!但是别害怕,你的情况不足以威胁生命,你只是普通感冒。对付的方法在前文可以找到: Common Pitfalls page;

既然我们已经有了这样的方式,你可能会对那些read方法感兴趣。这里就是两个:

- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

第一种方法,readdatatolength,读取并返回给定长度的数据。让我们来看一个例子:
你正在编写一个客户端的协议,服务器发送带有固定长度的头的响应。所有响应的头文件正是8字节。前4个字节包含各种标志,4字节包含响应数据的长度,这是可变的。本文由B9班的真高兴发布于CSDN博客,所以你可能会写出如下代码:

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == TAG_FIXED_LENGTH_HEADER)
    {
        int bodyLength = [self parseHeader:data];
        [socket readDataToLength:bodyLength withTimeout:-1 tag:TAG_RESPONSE_BODY];
    }
    else if (tag == TAG_RESPONSE_BODY)
    {
        // Process the response
        [self handleResponseBody:data];

        // Start reading the next response
        [socket readDataToLength:headerLength withTimeout:-1 tag:TAG_FIXED_LENGTH_HEADER];
    }
}

让我们再看一个例子。毕竟,不是所有的协议使用一个固定长度的头。HTTP是一个这样的协议。

一个典型的HTTP响应看起来像这样:

HTTP/1.1 200 OK
Date: Thu, 24 Nov 2011 02:18:50 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.1.6
Content-Length: 5233
Content-Type: text/html; charset=UTF-8

这只是一个例子。可以有任意长度的头字段。换句话说,这个HTTP头有是变长的。我们怎样read呢?

现在我们看看协议的说明。标题中的每行是一个CRLF终止(回车,换行:“\r\n”)。此外,最终的标题标记2个背靠背的CRLF,和body的长度是通过指定的“Content-Length”头字段表示的。所以我们会这样做:

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == HTTP_HEADER)
    {
        int bodyLength = [self parseHttpHeader:data];
        [socket readDataToLength:bodyLength withTimeout:-1 tag:HTTP_BODY];
    }
    else if (tag == HTTP_BODY)
    {
        // Process response
        [self processHttpBody:data];

        // Read header of next response
        NSData *term = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
        [socket readDataToData:term withTimeout:-1 tag:HTTP_HEADER];
    }
}

我已经列出了2种可供read的方法。有接近10种不同的读取方法。他们提供更高级的选项,如指定最大长度,或提供自己的读缓冲区。

Writing a server


GCDAsyncSocket also allows you to create a server, and accept incoming connections. It looks something like this:

listenSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

NSError *error = nil;
if (![listenSocket acceptOnPort:port error:&error])
{
    NSLog(@"I goofed: %@", error);
}

- (void)socket:(GCDAsyncSocket *)sender didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
    // The "sender" parameter is the listenSocket we created.
    // The "newSocket" is a new instance of GCDAsyncSocket.
    // It represents the accepted incoming client connection.

    // Do server stuff with newSocket...
}

It’s as simple as that! For a more concrete example, see the “EchoServer” sample project that comes with the repository.

写服务端


GCDAsyncSocket还允许您创建一个服务器,并接受传入的连接。它看起来像这样:

listenSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

NSError *error = nil;
if (![listenSocket acceptOnPort:port error:&error])
{
    NSLog(@"I goofed: %@", error);
}

- (void)socket:(GCDAsyncSocket *)sender didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
    // The "sender" parameter is the listenSocket we created.
    // The "newSocket" is a new instance of GCDAsyncSocket.
    // It represents the accepted incoming client connection.

    // Do server stuff with newSocket...
}

这很简单!一个更具体的例子,看看自带的库”echoserver”示例项目。

 类似资料: