Используя сокеты и потоки сокета

Эта статья объясняет, как работать с сокетами и потоками сокета на различных уровнях от POSIX до Основы.

На почти каждом уровне сетей программное обеспечение может быть разделено на две категории: клиенты (программы, соединяющиеся с другими приложениями) и службы (программы, которые другие приложения подключают с). На высоком уровне эти строки ясны. Большинство программ записанный использующий высокоуровневый APIs является просто клиентами. На более низком уровне, однако, строки являются часто расплывчатыми.

Сокет и поток, программирующий обычно, попадают в одну из следующих широких категорий:

Эта глава разделена на разделы на основе вышеупомянутых задач:

Выбор семьи API

API, который Вы выбираете для основанных на сокете соединений, зависит от того, делаете ли Вы соединение с другим узлом или получаете соединение от другого узла. Это также зависит от того, используете ли Вы TCP или некоторый другой протокол. Вот несколько факторов для рассмотрения:

Запись основанного на TCP клиента

Путем Вы делаете исходящее соединение, зависит, на каком языке программирования Вы используете, на типе соединения (TCP, UDP, и т.д), и на том, пытаетесь ли Вы совместно использовать код с другим (неMac, не-iOS) платформы.

Подразделы ниже описывают использование NSStream. Кроме, где отмечено, CFStream API имеет функции с аналогичными именами и ведет себя так же.

Для узнавания больше о POSIX снабжают API сокетом, читают Сокет UNIX FAQ в http://developerweb .net/.

Установление соединения

Как правило рекомендуемый способ установить соединение TCP к удаленному узлу с потоками. Потоки автоматически обрабатывают многие проблемы то настоящее соединений TCP. Например, потоки предоставляют возможность для соединения именем хоста, и в iOS, они автоматически активируют сотовый модем устройства или по требованию VPN при необходимости (в отличие от этого CFSocket или сокеты BSD). Потоки являются также более подобным Какао сетевым интерфейсом, чем протоколы нижнего уровня, ведущие себя в пути, который в основном совместим с потоком файла Какао APIs.

Путем Вы получаете потоки ввода и вывода для узла, зависит от того, использовали ли Вы открытие службы для обнаружения узла:

Если Вы не используете автоматический подсчет ссылок, после получения потоков ввода и вывода необходимо сразу сохранить их. Тогда бросьте их к NSInputStream и NSOutputStream объекты, набор их объекты делегата (который должен соответствовать NSStreamDelegate протокол), запланируйте их на текущий цикл выполнения и вызовите их open методы.

Обработка событий

Когда stream:handleEvent: к методу обращаются NSOutputStream делегат объекта и значение streamEvent параметра NSStreamEventHasSpaceAvailable, вызвать write:maxLength: отправить данные. Этот метод возвращает число записанных байтов или отрицательное число на ошибке. Если бы меньше байтов было записано, чем Вы попытались отправить, то необходимо стоять в очереди остающиеся данные и отправить их после того, как метод делегата вызывают снова с NSStreamEventHasSpaceAvailable событие. Если ошибка происходит, необходимо вызвать streamError узнать, что пошло не так, как надо.

Когда stream:handleEvent: к методу обращаются Ваш NSInputStream делегат объекта и значение streamEvent параметра NSStreamEventHasBytesAvailable, Ваш входной поток получил данные, которые можно считать с read:maxLength: метод. Этот метод возвращает число чтения байтов или отрицательное число на ошибке.

Если бы меньше байтов было считано, чем Вам нужно, то необходимо поставить данные в очередь и ожидать, пока Вы не получаете другое потоковое событие с дополнительными данными. Если ошибка происходит, необходимо вызвать streamError узнать, что пошло не так, как надо.

Если другой конец соединения закрывает соединение:

  • Ваш делегат соединения stream:handleEvent: с методом вызывают streamEvent набор к NSStreamEventHasBytesAvailable. Когда Вы читаете из того потока, Вы получаете длину нуля (0).

  • Ваш делегат соединения stream:handleEvent: с методом вызывают streamEvent набор к NSStreamEventEndEncountered.

Когда любое из этих двух событий имеет место, метод делегата ответственен за обнаружение условия конца файла и очистку.

Закрытие соединения

Для закрытия соединения не запланируйте его от цикла выполнения, установите делегата соединения в nil (делегат не сохраняется), близко оба из связанных потоков с close метод, и затем выпускает сами потоки (если Вы не используете ARC), или установите их в nil (если Вы). По умолчанию это закрывает базовое сокетное соединение. Существует две ситуации, в которых необходимо закрыть его сами, однако:

  • Если Вы ранее устанавливаете kCFStreamPropertyShouldCloseNativeSocket к kCFBooleanFalse путем вызова setProperty:forKey: на потоке.

  • Если Вы создали потоки на основе существующего сокета BSD путем вызова CFStreamCreatePairWithSocket.

    По умолчанию потоки, создаваемые из существующего собственного сокета, не закрывают свой базовый сокет. Однако можно включить автоматическое закрытие путем установки kCFStreamPropertyShouldCloseNativeSocket к kCFBooleanTrue с setProperty:forKey: метод.

Для получения дополнительной информации

Для узнавания больше считайте Установку Потоков Сокета в Потоковом Руководстве по программированию, Используя NSStreams Для соединения TCP Без NSHost, или см. проекты примера кода SimpleNetworkStreams и RemoteCurrency.

Запись основанного на TCP сервера

Как упомянуто ранее, сервер и клиент подобны, как только установлено соединение. Основное различие - то, что клиенты делают исходящие соединения, тогда как серверы создают сокет слушания (иногда слушают сокет) — сокет, прислушивающийся к входящим соединениям — тогда принимают соединения на том сокете. После этого каждое получающееся соединение ведет себя точно так же, как соединение Вы могли бы сделать в клиенте.

API, который необходимо выбрать для сервера, зависит прежде всего от того, пытаетесь ли Вы совместно использовать код с другим (неMac, не-iOS) платформы. Существует только два APIs, предоставляющий возможность для прислушиваний к входящим сетевым соединениям: Базовая Основа снабжает API сокетом, и POSIX (BSD) снабжает API сокетом. Высокоуровневый APIs не может использоваться для принятия входящих соединений.

Следующие разделы описывают, как использовать этот APIs для прислушиваний к входящим соединениям.

Слушание с базовой основой

Для использования Базовой Основы APIs для прислушиваний к входящим соединениям необходимо сделать следующее:

  1. Добавьте надлежащий, включает:

    #include <CoreFoundation/CoreFoundation.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
  2. Создайте объекты сокета (возвратился как a CFSocketRef объект) с CFSocketCreate или CFSocketCreateWithNative функция. Указать kCFSocketAcceptCallBack как callBackTypes значение параметра. Обеспечьте указатель на a CFSocketCallBack функция обратного вызова как значение параметра выноски.

    CFSocketRef myipv4cfsock = CFSocketCreate(
        kCFAllocatorDefault,
        PF_INET,
        SOCK_STREAM,
        IPPROTO_TCP,
        kCFSocketAcceptCallBack, handleConnect, NULL);
    CFSocketRef myipv6cfsock = CFSocketCreate(
        kCFAllocatorDefault,
        PF_INET6,
        SOCK_STREAM,
        IPPROTO_TCP,
        kCFSocketAcceptCallBack, handleConnect, NULL);
  3. Свяжите сокет с CFSocketSetAddress функция. Обеспечьте a CFData объект, содержащий a sockaddr структура, указывающая информацию о требуемом порте и семье.

    struct sockaddr_in sin;
     
    memset(&sin, 0, sizeof(sin));
    sin.sin_len = sizeof(sin);
    sin.sin_family = AF_INET; /* Address family */
    sin.sin_port = htons(0); /* Or a specific port */
    sin.sin_addr.s_addr= INADDR_ANY;
     
    CFDataRef sincfd = CFDataCreate(
        kCFAllocatorDefault,
        (UInt8 *)&sin,
        sizeof(sin));
     
    CFSocketSetAddress(myipv4cfsock, sincfd);
    CFRelease(sincfd);
     
    struct sockaddr_in6 sin6;
     
    memset(&sin6, 0, sizeof(sin6));
    sin6.sin6_len = sizeof(sin6);
    sin6.sin6_family = AF_INET6; /* Address family */
    sin6.sin6_port = htons(0); /* Or a specific port */
    sin6.sin6_addr = in6addr_any;
     
    CFDataRef sin6cfd = CFDataCreate(
        kCFAllocatorDefault,
        (UInt8 *)&sin6,
        sizeof(sin6));
     
    CFSocketSetAddress(myipv6cfsock, sin6cfd);
    CFRelease(sin6cfd);
  4. Начните слушать на сокете путем добавления сокета к циклу выполнения.

    Создайте источник цикла выполнения для сокета с CFSocketCreateRunLoopSource функция. Затем добавьте сокет к циклу выполнения путем обеспечения его источника цикла выполнения для CFRunLoopAddSource функция.

    CFRunLoopSourceRef socketsource = CFSocketCreateRunLoopSource(
        kCFAllocatorDefault,
        myipv4cfsock,
        0);
     
    CFRunLoopAddSource(
        CFRunLoopGetCurrent(),
        socketsource,
        kCFRunLoopDefaultMode);
     
    CFRunLoopSourceRef socketsource6 = CFSocketCreateRunLoopSource(
        kCFAllocatorDefault,
        myipv6cfsock,
        0);
     
    CFRunLoopAddSource(
        CFRunLoopGetCurrent(),
        socketsource6,
        kCFRunLoopDefaultMode);

После этого можно получить доступ к базовому дескриптору сокета BSD с CFSocketGetNative функция.

Когда Вы заканчиваете с сокетом, необходимо закрыть его путем вызова CFSocketInvalidate.

В функции обратного вызова Вашего слушающего сокета (handleConnect в этом случае), необходимо проверить, чтобы удостовериться, что значение callbackType параметра kCFSocketAcceptCallBack, что означает, что было принято новое соединение. В этом случае параметр данных обратного вызова является указателем на a CFSocketNativeHandle значение (целочисленное число сокета) представление сокета.

Для обработки новых входящих соединений можно использовать CFStream, NSStream, или CFSocket APIs. APIs На основе потоков строго рекомендуется.

Сделать это:

  1. Создайте чтение и потоки записи для сокета с CFStreamCreatePairWithSocket функция.

  2. Бросьте потоки к NSInputStream возразите и NSOutputStream возразите, работаете ли Вы в Какао.

  3. Используйте потоки, как описано в письменной форме Основанный на TCP Клиент.

Для получения дополнительной информации см. Ссылку CFSocket. Для примера кода см. проекты примера кода RemoteCurrency и WiTap.

Слушание с POSIX снабжает APIS СОКЕТОМ

Сети POSIX довольно подобны CFSocket API, за исключением того, что необходимо записать собственный код обработки цикла выполнения.

Вот основные шаги для создания сервера уровня POSIX:

  1. Создайте сокет путем вызова socket. Например:

    int ipv4_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    int ipv6_socket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
  2. Свяжите его с портом.

    • При учитывании определенного порта в виду используйте это.

    • Если Вы не имеете определенного порта в виду, нуля передачи для номера порта, и операционная система присвоит Вас эфемерный порт. (Если Вы собираетесь распространить свою службу с Добрый день, необходимо почти всегда использовать эфемерный порт.)

    Например:

        struct sockaddr_in sin;
        memset(&sin, 0, sizeof(sin));
        sin.sin_len = sizeof(sin);
        sin.sin_family = AF_INET; // or AF_INET6 (address family)
        sin.sin_port = htons(0);
        sin.sin_addr.s_addr= INADDR_ANY;
     
        if (bind(listen_sock, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
            // Handle the error.
        }
  3. При использовании эфемерного порта вызвать getsockname узнать, какой порт Вы используете. Можно тогда зарегистрировать этот порт в Добрый день. Например:

        socklen_t len = sizeof(sin);
        if (getsockname(listen_sock, (struct sockaddr *)&sin, &len) < 0) {
            // Handle error here
        }
        // You can now get the port number with ntohs(sin.sin_port).
  4. Вызвать listen начать прислушиваться к входящим соединениям на том порту.

Следующие шаги зависят от того, намереваетесь ли Вы использовать чистый код сокета POSIX или высокоуровневую абстракцию.

Обработка событий с базовой основой

Вызвать CFSocketCreateWithNative. Тогда следуйте за направлениями в Слушании с Базовой Основой, начинающейся на шаге 3.

Обработка событий с центральной отгрузкой

GCD позволяет Вам выполнять операции асинхронно и обеспечивает механизм очереди событий для определения, когда считать данные из сокета. После создания сокета слушания должен основанный на GCD сервер:

  1. Вызвать dispatch_source_create создать источник отгрузки для сокета слушания, указания DISPATCH_SOURCE_TYPE_READ как исходный тип.

  2. Вызвать dispatch_source_set_event_handler (или dispatch_source_set_event_handler_f и dispatch_set_context) установить обработчик, который вызывают каждый раз, когда новое соединение поступает в сокет.

  3. Когда слушать обработчик сокета вызывают (на новое соединение), он должен:

    • Вызвать accept. Эта функция заполняет новое sockaddr структура с информацией о соединении и возвратах новый сокет для того соединения.

      При желании вызовите ntohl(my_sockaddr_obj.sin_addr.s_addr) определить IP-адрес клиента.

    • Вызвать dispatch_source_create создать источник отгрузки для клиентского сокета, указывая DISPATCH_SOURCE_TYPE_READ как исходный тип.

    • Вызвать setsockopt установить SO_NOSIGPIPE флаг на сокете.

    • Вызвать dispatch_source_set_event_handler (или dispatch_source_set_event_handler_f и dispatch_set_context) установить обработчик, который вызывают каждый раз, когда изменяется состояние соединения.

  4. В клиентском обработчике сокета вызвать dispatch_async или dispatch_async_f и передайте вызывающий блок read на сокете для захвата любых новых данных затем обработайте те данные соответственно. Этот блок может также отправить ответы путем вызова write на сокете.

Обработка событий с чистым кодом POSIX

  1. Создайте набор дескриптора файла и добавьте новые сокеты к тому набору, поскольку входят новые соединения.

    fd_set incoming_connections;
    memset(&incoming_connections, 0, sizeof(incoming_connections));
  2. Если необходимо периодически выполнять действия с сетевым потоком, создайте a timeval структура для select тайм-аут.

        struct timeval tv;
        tv.tv_sec = 1; /* 1 second timeout */
        tv.tv_usec = 0; /* no microseconds. */

    Важно выбрать тайм-аут, который разумен. Короткие значения тайм-аута срывают систему, заставляя Ваш процесс работать более часто, чем необходимо. Если Вы не делаете что-то очень необычное, Ваш select цикл не должен будить больше, чем несколько раз в секунду, самое большее, и на iOS, необходимо попытаться избежать делать это вообще. Для альтернатив читайте, Избегают Сокетов POSIX и CFSocket на iOS, Где Возможно в Сетевом Обзоре.

    Если Вы не должны выполнять периодические действия, передачу NULL.

  3. Вызвать select в цикле, передавая две отдельных копии того набора дескриптора файла (создаваемый путем вызова FD_COPY) для чтения и наборов дескриптора записи. select системный вызов изменяет эти наборы дескриптора, очищая любые дескрипторы, которые не готовы к чтению или записи.

    Для timeout параметр, передайте timeval структура Вы создали ранее. Несмотря на то, что OS X и iOS не изменяют эту структуру, некоторые другие операционные системы заменяют это значение остающимся количеством времени. Таким образом, для межплатформенной совместимости, необходимо сбросить это значение каждый раз, когда Вы вызываете select.

    Для nfds параметр, передайте число, которое является один выше, чем фактически использующийся дескриптор файла с самым высоким номером.

  4. Считайте данные из сокетов, вызвав FD_ISSET определить, имеет ли данный сокет незаконченные данные.

    Запишите данные в вызов FD_ISSET определить, имеет ли данный сокет пространство для новых данных.

    Поддержите соответствующие очереди для поступления и выхода данных.

Как альтернатива POSIX select функция, специфичное для BSD kqueue API может также использоваться для обработки событий сокета.

Для получения дополнительной информации

Для узнавания больше о сетях POSIX читайте socket, listen, FD_SET, и select страницы руководства.

Работа с основанными на пакете сокетами

Рекомендуемый способ отправить и получить пакеты UDP путем объединения POSIX API и любой CFSocket или GCD APIs. Для использования этого APIs необходимо выполнить следующие шаги:

  1. Создайте сокет путем вызова socket.

  2. Свяжите сокет путем вызова bind. Обеспечьте a sockaddr структура, указывающая информацию о требуемом порте и семье.

  3. Подключите сокет путем вызова connect (дополнительный).

    Обратите внимание на то, что подключенный сокет UDP не является соединением в самом чистом значении слова. Однако это обеспечивает два преимущества перед несвязанным сокетом. Во-первых, это устраняет необходимость указать адрес назначения каждый раз, когда Вы отправляете новое сообщение. Во-вторых, когда пакет не может быть поставлен, Ваше приложение может получить ошибки. Эта ошибочная поставка не гарантируется с UDP, однако; это зависит от состояния сети, находящегося вне контроля Вашего приложения.

Оттуда, можно работать с соединением тремя способами:

Получение собственного дескриптора сокета для потока сокета

Иногда при работе с основанными на сокете потоками (NSInputStream, NSOutputStream, CFReadStream, или CFWriteStream), Вы, возможно, должны получить базовый дескриптор сокета, связанный с потоком. Например, Вы могли бы хотеть узнать IP-адрес и номер порта для каждого конца потока с getsockname и getpeername, или набор снабжает опции сокетом с setsockopt.

Для получения собственного дескриптора сокета для входного потока вызовите следующий метод:

-(int) socknumForNSInputStream: (NSStream *)stream
{
    int sock = -1;
    NSData *sockObj = [stream propertyForKey:
               (__bridge NSString *)kCFStreamPropertySocketNativeHandle];
    if ([sockObj isKindOfClass:[NSData class]] &&
         ([sockObj length] == sizeof(int)) ) {
        const int *sockptr = (const int *)[sockObj bytes];
        sock = *sockptr;
    }
    return sock;
}

Можно сделать ту же вещь с потоком вывода, но только необходимо сделать это с одним или другим, потому что потоки ввода и вывода для данного соединения всегда совместно используют тот же базовый собственный сокет.