Сетевое программирование с IOLib

Материал из wiki.lissyara.su
Перейти к: навигация, поиск

Сетевое программирование с IOLib в ANSI Common Lisp. автор: Питер Келлер (Peter Keller) (psilord@cs.wisc.edu). Версия: 0.0. 4/02/2010. Перевод: Charlz_Klug. Дата: 11.05.2012.

Что такое IOLib?

IOLib - это переносимая библиотека Ввода/Вывода для ANSI Common Lisp. Она включает в себя интерфейс сокетов для сети с поддержкой IPV4, IPV6, TCP, UDP, мультиплексор Ввода/Вывода с не блокируемым Вводом/Выводом, библиотеку для работы с DNS и библиотеку путей.

Где Я могу получить IOLib?

Текущая версия IOLib'а находится здесь.

Введение

Это учебное пособие является свободным толкованием книги "Сетевое программирование в UNIX, сетевые API: Сокеты и XTI. 2-е издание" ("UNIX Network Programming, Networking APIs: Sockets and XTI 2nd Edition") за авторством Ричарда Стивенса (W. Richard Stevens). Многие примеры были взяты из исходного кода этой книги. Первое отличие от исходного кода в языке C - это использование пакета Bordeaux Threads, более структурированная реализация таких идей как буферы данных и обработка ошибок. Ещё одно отличие - это стиль кодирования с точки зрения Common Lisp'а.

Главные цели этого обучающего пособия следующие:

  • Основы ANSI Common Lisp для программистов не знакомых с этим языком.
  • IPV4 TCP.
  • Клиент/Серверная архитектура.
  • Виды построения сервера: Итеративный, Конкурентный и Смешанный методы.
  • Блокируемый и не блокируемый Ввод/Вывод.

Это общие намеченные цели, однако, это руководство будет расширено через описание API IOLib, как это описано в секции "Будущие Планы" данного учебного пособия. Отсутствие описания IOLib API будет исправлено в следующих ревизиях.

И наконец, образцы кода в данном учебном пособии взяты из реальных программ и вставлены в это руководство через метод генерации шаблонов. Образцы кода содержат встроенный язык разметки призванный облегчить работу. Этот язык выглядит так (в виде одной строки): ';; ex-NNNb' в начале секции примера и ';; ex-NNNe' в конце секции. Число --NNN является целочисленным порядковым номером и в начале и конце каждой секции он должен совпадать.

Благодарность

Я хочу выразить огромную благодарность Стелиану Ионеску (Stelian Ionescu), автору IOLib за объяснения различных возможностей IOLib'а и за его терпение в наших, иногда, долгих беседах.

Поддерживаемый код

Файл package.lisp содержит маленькую библиотеку кода, широко используемую в качестве примеров. Реализации поддерживаемого кода:

  • Пакет содержит примеры, вызываемые так: :iolib.examples.
  • Переменные *host* и *port*, устанавливаются в "localhost" и 9999 соответственно. Это имя по умолчанию и порт к которому подключается клиент и который слушает сервер. Впрочем, серверы обычно привязываются к адресу 0.0.0.0.
  • Маленькая, но эффективная реализация очередей из "ANSI Common Lisp" за авторством Пола Грэма (Paul Graham). Интерфейс вызывается так:
       (make-queue)
       (enqueue obj q)
       (dequeue q)
       (empty-queue q)
  •  :iolib.examples зависит только от IOLib и использует пакеты :common-lisp, :iolib и :bordeaux-threads.

Запуск примеров

Эти примеры были разработаны и протестированы на SBCL 1.0.33.30 и запускались на x86 машине с операционной системой Ubuntu 8.10. Они (примеры) запускались с двумя запущенных экземпляров SBCL, одна из которых выступает в роли клиента, а другая в роли сервера.

Предположим, что нам нужно запустить первый пример сервера времени и подключиться к нему с клиента. В начале, сервер будет привязываться к *host*'у и *port*'у и ожидать подключения клиента. Мы подключаемся к *host*'у и *port*'у, получаем время и завершаем работу.

Вначале запускаем сервер:

Linux black > sbcl
This is SBCL 1.0.33.30, an implementation of ANSI Common Lisp.
More information about SBCL is available at .
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
* (require :iolib.examples) ; much output!
* (in-package :iolib.examples)
#
* (run-ex1-server)
Created socket: #[fd=5]
Bound socket: #
Listening on socket bound to: 0.0.0.0:9999
Waiting to accept a connection...
[ server is waiting for the below client to connect! ]
Got a connection from 127.0.0.1:34794!
Sending the time...Sent!
T
*

Теперь запускаем клиента, который будет подключаться к вышеуказанному серверу:

Linux black > sbcl
This is SBCL 1.0.33.30, an implementation of ANSI Common Lisp.
More information about SBCL is available at .
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
* (require :iolib.examples) ; much output!
* (in-package :iolib.examples)
#
* (run-ex1-client)
Connected to server 127.0.0.1:9999 via my local connection at 127.0.0.1:34794!
2/27/2010 13:51:48
T
*

В каждом клиентском примере, мы можем указать хост и порт к которому нужно производить подключение:

* (run-ex1-client :host "localhost" :port 9999)
Connected to server 127.0.0.1:9999 via my local connection at 127.0.0.1:34798!
2/27/2010 13:53:7
T
*

Серверы могут слушать различные порты, но в данном случае мы слушаем 0.0.0.0:9999, что означает слушать все интерфейсы на машине и порт 9999.

Обзор Примеров

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

Соответствие протоколов клиента к протоколам серверов предоставлено ниже:

Клиенты: ex1-client, ex2-client, ex3-client, могут работать с серверами: ex1-server, ex2-server, ex3-server, ex4-server.

Клиенты:: ex4-client, ex5a-client, могут работать с серверами: ex5-server, ex6-server.

Клиенты: ex5b-client, могут работать с серверами: ex7-server, ex8-server.

Некоторые клиенты и сервера используют серию "временных" протоколов, например: ex1-client, ex2-client, ex3-client, и ex1-server, ex2-server, ex3-server, и ex4-server.

Некоторые клиенты и сервера используют серию "эхо в строку" ("echo a line") протоколов, это следующие примеры: ex4-client, ex5a-client, ex5b-client, ex5-server, ex6-server, ex7-server, и ex8-server.

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

Клиенты, получающие время

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

Клиент, получающий время по IVP4/TCP: ex1-client.lisp

Это очень простой пример клиентской программы получающей время. По умолчанию эта программа работает с сервером через *host* и *port*, возвращает простую текстовую строку с текущим временем и датой, после чего завершает работу. Данная программа написана в стиле языка C для сравнения с примерами на других языках. Она использует блокировку и строчно ориентированный Ввод/Вывод.

Программа производит следующие шаги:

1. Запуск ex1-client.lisp:

(defun run-ex1-client (&key (host *host*) (port *port*))

2. Создаёт активный TCP сокет:

Функция создания сокета (MAKE-SOCKET ...) - это метод с помощью которого создаются сокеты в IOLib. Эта функция очень гибкая и может использоваться для создания и инициализации сокетов с помощью одного - единственного вызова.
В этом случае мы используем самый простой способ вызова этой функции. В результате вызова создаётся и активизируется сокет IPv4 интернет поток, в который можно писать и читать utf8 текст, в частности, поддерживается переход на новую строку.
Одно маленькое, но важное отличие IOLib сокетов от Berkely сокетов - это их предопределённость и постоянность в виде активного или пассивного состояния. Активные сокеты используются для подключения к серверу, пассивные сокеты используются для создания сокетов принимающих соединения с клиентов.
  ;; Создать интернет TCP сокет под IPV4
  (let ((socket
         (make-socket
          :connect :active
          :address-family :internet
          :type :stream
          :external-format '(:utf-8 :eol-style :crlf)
          :ipv6 nil)))

3. Определяет IP адрес и порт Сервера для установления соединения с сокетом:

Этот маленький участок кода содержит много вызовов, относящихся к IOLib и мы рассмотрим каждый из них.
Функция LOOKUP-HOSTNAME принимает строку DNS имени машины и возвращает 4 значения:
  1. адрес.
  2. список дополнительных адресов (если они существуют).
  3. каноничное имя хоста.
  4. alist всех имён хоста с их соответствующими адресами.
Мы используем только первое возвращаемое значение, адресную составляющую, для последующей передачи её функции CONNECT.
Функция CONNECT подключает сокет к адресу, в случае отсутствия ключевого слова :port подключение производится к случайному порту. Обычно код клиента использует :wait t что означает ожидание либо подключения, либо ошибки. :wait t не используется в тех случаях, когда клиенту нужны подключения к многим серверам, например web клиенты или если серверу так же, как и клиенту не нужны блокировки.
Функции REMOTE-HOST и REMOTE-PORT возвращают ip адрес и порт удалённого соединения, связанного с подключённым сокетом. LOCAL-HOST и LOCAL-PORT возвращает информацию о стороне клиентского сокета. Аналогично вызывается REMOTE-NAME и LOCAL-NAME, возвращаются два значения, где первое значение - это эквивалент *-host и второе значение - это эквивалент *-port.
    ;; создать блокируемое соединение для сервера времени на заданном порту.
    (connect socket (lookup-hostname host) :port port :wait t)
    (format t "Connected to server ~A:~A via my local connection at ~A:~A!~%"
            (remote-host socket) (remote-port socket)
            (local-host socket) (local-port socket))

4. Чтение и отображение ответа сервера:

Теперь, после того, как сокет подключился к серверу, сервер отправляет строку с текстом клиенту. Клиент использует стандартную функцию Common Lisp'а READ-LINE для чтения информации с сокета. Функция READ-LINE блокируется и производит выход в случае прочтения *всей строки*. Будучи однажды прочитанной, строка выводится в *стандартный вывод* через вызов функции FORMAT.
    ;; прочитать одну строку информации с сервераread the one line of information I need from the daytime
    ;; времени. Здесь можно использовать read-line, поскольку это TCP сокет.
    (let ((line (read-line socket)))
      (format t "~A" line))

5. Конец программы:

Мы закрываем сокет с помощью стандартной функции CLOSE и возвращаем true, таким образом возвращаемое значение этого примера является t.
    ;; всё завершено
    (close socket)
    t))

Эта программа работает, но она имеет несколько значительных недостатков. Первый и самый главный недостаток - это отсутствие обработки каких-либо состояний о которых сигнализирует IOLib в общих случаях. Пример может быть запущен с помощью ex1-client.lisp без запуска сервера времени. В большинстве случаев, Вы будете выброшены в отладчик с необработанным состоянием SOCKET-CONNECTION-REFUSED-ERROR (ОШИБКА-ПОДКЛЮЧЕНИЯ-К-СОКЕТУ). Второй недостаток - эта программа не написана в стиле Common Lisp.

Клиент получающий время по IVP4/TCP: ex2-client.lisp

В этом примере, мы просто сделаем ex1-server.lisp чуть лучше в плане очистки объекта сокета с помощью использования IOLib форм. Он также будет использовать строчно ориентированную блокировку Ввода/Вывода.

Макрос WITH-OPEN-SOCKET вызывает MAKE-SOCKET с аргументами и привязывает сокет к переменной 'socket'. При выходе из этой формы происходит автоматическое закрытие сокета.

Эта программа настолько короткая, что данный пример можно привести здесь:

(defun run-ex2-client (&key (host *host*) (port *port*))
 
  ;; Здесь мы знакомим Вас с with-open-socket. Это лёгкий способ создать обёртку 
  ;; для синхронных и блокируемых соединений с формой
  ;; для того, чтобы быть уверенными что сокет закроетсе при любом выходе из программы.
  (with-open-socket
      (socket :connect :active
              :address-family :internet
              :type :stream
              :external-format '(:utf-8 :eol-style :crlf)
              :ipv6 nil)
 
    ;; Создать блокируемое соединение к серверу времени на заданном порту. Мы
    ;; вводим функцию lookup-hostname, которая конвертирует имя хоста в
    ;; 4 значения, но в нашем случае нужно только первое значение, являющееся
    ;; адресом.
    (connect socket (lookup-hostname host) :port port :wait t)
    (format t "Connected to server ~A:~A from my local connection at ~A:~A!~%"
            (remote-name socket) (remote-port socket)
            (local-name socket) (local-port socket))
 
    ;; прочесть одну строку необходимой информации с сервера времени.
    ;; Здесь возможно использование read-line, поскольку это TCP
    ;; сокет. Этот сокет будет заблокирован после прочтения всей строки.
    (let ((line (read-line socket)))
      (format t "~A" line)
      t)))

Этот пример можно использовать в будущем, если вы будете использовать его с флагами WITH-OPEN-SOCKET

    :remote-host (lookup-hostname host)
    :remote-port port

вызов MAKE-SOCKET, лежащий в основе этого примера, подключает сокет прямо к серверу до того как он будет доступен в теле макроса, позволяя нам полностью удалять вызов соединения! Однако, в ранних примерах мы не использовали сокращённую нотацию IOLib'а по причине демонстрации того, как библиотека отображает традиционную концепцию сокетов. После нашего первого знакомства с API IOLib, использование сокращений будет только плюсом в плане улучшения читабельности кода.

Клиент получающий время по IVP4/TCP: ex3-client.lisp

Теперь мы перейдём к обработке состояний, что положительно скажется на Вашей IOLib программе. Все промышленные программы использующие IOLib должны обрабатывать сигналы IOLib'а о состояниях. Такие состояния являются общими для всех сетевых программ. До сих пор мы видели только одно состояние, когда пытались подключиться к не запущенному серверу времени. Сигнал о состоянии был следующим: SOCKET-CONNECTION-REFUSED-ERROR. Интерфейс потока установливает состояния о которых сигнализировал IOLib, а другой низкоуровневый слой IOLib - отвечающий за неблокируемый Ввод/Вывод имеет другой набор состояний. Между ними есть некоторая взаимосвязь, эту взаимосвязь мы осветим позднее. Ну а теперь, мы будем использовать состояния связанные с потоком.

Наша переделка ex2-client.lisp в ex3-client.lisp (по прежнему используется линейно ориентированный блокируемый Ввод/Вывод) будет следующей:

0. Мы создадим вспомогательную функцию, которая производит подключение к серверу и считывает строку времени и даты:

Помните, что макрос HANDLER-CASE является частью функции считывающей время и дату с сервера. Здесь мы можем получить состояние END-OF-FILE в том случае, если клиент был подключён, но до того как сервер распознал отключение, клиент закрыл подключение. После получение END-OF-FILE внутри формы WITH-OPEN-SOCKET мы должны помнить очистить эту форму и закрыть соединение. Если мы не поймаем это состояние, программа будет остановлена и передана в отладчик, а это нам не нужно. Является спорным следующий момент: обрабатывать состояние сразу после возникновения или спустя некоторое время после вызова. В нашем простом примере, этот вопрос не существенен. Со временем, когда Ваша IOLib программа будет более сложной, перед Вами встанет вопрос когда обрабатывать сигнализированные состояния.
(defun run-ex3-client-helper (host port)
 
  ;; Создать интернет TCP сокет под IPV4
  (with-open-socket
      (socket :connect :active
              :address-family :internet
              :type :stream
              :external-format '(:utf-8 :eol-style :crlf)
              :ipv6 nil)
 
    ;; создать блокируемое соединение к серверу времени по указанному порту.
    (connect socket (lookup-hostname host) :port port :wait t)
    (format t "Connected to server ~A:~A from my local connection at ~A:~A!~%"
            (remote-name socket) (remote-port socket)
            (local-name socket) (local-port socket))
 
    (handler-case
        ;; прочесть одну строку нужной информации о времени с
        ;; сервера. Я могу использовать read-line поскольку это TCP
        ;; сокет. Он будет заблокирован после прочтения всей строки.
        (let ((line (read-line socket)))
          (format t "~A" line)
          t)
 
      ;; Всё же стоит выдать уведомление о преждевременном
      ;; отключении сервера...
      (end-of-file ()
        (format t "Got end-of-file. Server closed connection!")))))

1. Некоторые состояния, приводящие к остановке выполнения программ и передаче программы в отладчик, находятся в более высоком уровне:

Помните, что мы можем перехватывать SOCKET-CONNECTION-REFUSED-ERROR из соединения внутри функции run-ex3-client-helper.
;; Главная точка входа в ex3-client
(defun run-ex3-client (&key (host *host*) (port *port*))
  (handler-case
 
      (run-ex3-client-helper host port)
 
    ;; Обработка общих сигналов о ошибках...
    (socket-connection-refused-error ()
      (format t "Connection refused to ~A:~A. Maybe the server isn't running?~%"
              (lookup-hostname host) port))))

Здесь находятся некоторые общие состояния IOLib (также некоторые состояния из ANSI Common Lisp) и ситуации из которых они сигнализируются. * По крайней мере * в любой IOLib программе, при необходимости, следует обрабатывать эти состояния.

END-OF-FILE:

Когда функция потока такая как READ, READ-LINE и т.д. (но не RECEIVE-FROM) пытается считать из сокета данные, а получает конец файла.

HANGUP:

При попытке записи в сокет с помощью функции потока, такой как WRITE, FORMAT и т.д. (но не SEND-TO) при закрытом сокете.

SOCKET-CONNECTION-RESET-ERROR:

Возникает при исполнении Ввода/Вывода в сокет. А другая сторона сокета посылает RST пакет. Кроме того, это состояние происходит в функции ACCEPT IOLib и ей подобных.

SOCKET-CONNECTION-REFUSED-ERROR:

Это состояние сигнализирует соединение, если не было найдено серверов ожидающих входящее соединение.

Сервер Времени

На этом мы закончили рассмотрение эволюции клиентов времени и теперь обратим взор на серверы времени.

Порядок рассмотрения серверов будет соответствовать порядку рассмотрения клиентов.

Сервер Времени IVP4/TCP: ex1-server.lisp

Это первый пример итеративного сервера, обслуживающего одного-единственного клиента и завершающего свою работу. Выполняется блокировка Ввода/Вывода и не выполняется обработка ошибок. По функциональности этот пример подобен ex1-client.lisp

0. Создать сокет сервера:

Мы видим, что этот сокет установлен в :passive. Каждый сокет в IOLib устанавливается или в активное состояние или в пассивное, в нашем случае это сокет сервера и он является пассивным. Также мы видим что возможно обращение к нижележащим fd сокета с помощью функции SOCKET-OS-FD.
(defun run-ex1-server (&key (port *port*))
  ;; Создать пассивный (серверный) TCP сокет под IPV4 сокеты.
  ;; Это минимальное
  ;; различие от интерфейса сокетов Беркли.
  (let ((socket
         (make-socket
          :connect :passive
          :address-family :internet
          :type :stream
          :external-format '(:utf-8 :eol-style :crlf)
          :ipv6 nil)))
    (format t "Created socket: ~A[fd=~A]~%" socket (socket-os-fd socket))

1. Связка сокета

Связывание сокета задаёт конечную точку к которой может подключаться клиент. Константа IOLib +IPV4-UNSPECIFIED+ содержит 0.0.0.0 и означает, что клиент может соединяться с любого интерфейса, и подключение будет производиться к указанному порту :port. Ключевое слово :reuse-addr означает опцию SO_REUSEADDR для сокета, что в свою очередь обозначает следующее: если сокет находится в состоянии TIME_WAIT, то он сразу же может использоваться повторно. Использование :reuse-addr на слушающем сокете рекомендуется для всех серверов.
    ;; Привязывает сокет ко всем интерфейсам с определённым портом.
    (bind-address socket
                  +ipv4-unspecified+ ; which means INADDR_ANY or 0.0.0.0
                  :port port
                  :reuse-addr t)
    (format t "Bound socket: ~A~%" socket)

2. Прослушивание сокета

Прослушивание сокета позволяет клиентам производить подключения. В этом примере, мы указываем, что можно ставить в очередь 5 соединений до начала их подключения.
    ;; Конвертирование сокета в слушающий сокет
    (listen-on socket :backlog 5)
    (format t "Listening on socket bound to: ~A:~A~%"
            (local-host socket) (local-port socket))

3. Приём клиентских соединений.

Здесь мы наконец вызываем функцию IOLib ACCEPT-CONNECTION. Так как мы хотели бы сделать её блокируемой, мы передаём этой функции следующие аргументы: :wait t. Когда ACCEPT-CONNECTION завершит свою работу, то возвратится новый сокет, предоставляющий подключения к клиенту. В некоторых ситуациях ACCEPT-CONNECTION может вернуть nil, например на медленных серверах, когда клиент посылает TCP пакет RST в промежутке между ответом ядра о принятии подключения и вызовом ACCEPT-CONNECTION. Также мы можем использовать функцию REMOTE-NAME, возвращающую два значения, ip адрес и порт удалённой стороны сокета.
    ;; Блокирование входящего соединения
    (format t "Waiting to accept a connection...~%")
    (let ((client (accept-connection socket :wait t)))
      (when client
        ;; When we get a new connection, show who it is from.
        (multiple-value-bind (who rport)
            (remote-name client)
          (format t "Got a connection from ~A:~A!~%" who rport))

4. Отправка времени клиенту.

Здесь мы создаём строку, содержащую время и отсылаем эту строку клиенту. Учтите, что мы используем вызов функции FINISH-OUTPUT. Использование этой функции гарантирует что весь вывод будет записан в сокет клиента. Для потоков использующих блокированный ввод/вывод, это означает, что каждая запись в блокированный сокет будет сопровождаться вызовом FINISH-OUTPUT.
        ;; Использование интернет TCP потока, даёт нам возможность
        ;; использования функции format. Однако, нужно вызывать finish-output
        ;; для завершения отправки всех данны.
        ;; Кроме того, это является блокированной записью.
 
        (multiple-value-bind (s m h d mon y)
            (get-decoded-time)
          (format t "Sending the time...")
          (format client "~A/~A/~A ~A:~A:~A~%" mon d y h m s)
          (finish-output client))

5. Закрываем подключение к клиенту.

Мы завершили вывод на клиента, теперь надо закрыть соединение и предупредить клиента.
        ;; Говорим клиенту что передача данных завершена.
        (close client)
        (format t "Sent!~%"))

6. Закрываем сокет сервера.

Это одноразовый сервер, мы закрыли слушающий сокет и завершили работу программы. В этом и в остальных серверах мы вызываем FINISH-OUTPUT для очистки всех сообщений ожидаемых в *standard-output*.
      ;; Программа завершается закрытием сокета сервера.
      (close socket)
      (finish-output)
      t)))

Вышеприведенный код является базовой идеей для очень простого TCP сервера с блокированным Вводом/Выводом. Подобно ex1-client, этот сервер не обладает обработкой состояний (таких как HANGUP) от клиента - это означает, что клиент может отключиться до того, как сервер начнет передачу времени.

Однако, одна главная и весьма щекотливая проблема этого кода - это то, что сокет клиента *не закрывается немедленно* если сервер завершил работу, в свою очередь это приводит к переходу в отладчик или в сигнализированное состояние, перед передачей даты клиенту. Если произойдет эта ситуация, то пройдет много времени в процессе сбора мусора и завершения работы. В этом сценарии, клиент будет ожидать приема данных которые никогда не придут, до тех пор, пока реализация Lisp не закроет сокет. Сборка мусора - это очень удобная особенность Common Lisp, но есть и другая сторона медали - если мы ограничены в использовании памяти, то сборка мусора должна происходить сразу же. Остающиеся открытыми и не управляемыми сокеты могут причинить неудобства клиентам.

Все клиенты или серверы написанные с помощью IOLib должны использовать макросы IOLib для управления закрытием сокета, или систему состояний Common Lisp для работы с сигнализированными состояниями или другое подобное решение.

Сервер Времени IVP4/TCP: ex2-server.lisp

Также как и ex2-client этот сервер использует макрос WITH-OPEN-SOCKET для открытия сокета сервера. Мы вводим WITH-ACCEPT-CONNECTION для приёма клиента и преобразования этого сервера из одноразового в итеративный сервер, который может, только последовательно, обрабатывать несколько клиентов.

0. Последовательный приём и обслуживание клиентов:

Эта часть из ex2-server демонстрирует бесконечный цикл приёма подключений. Макрос WITH-ACCEPT-CONNECTION использует сокет сервера и вводит в обращение новую связку: клиент и принятое подключение. Мы должны сказать подключению, что собираемся его блокировать. Если по какой-либо причине мы покинем тело, то клиентский сокет будет автоматически очищен.
    ;; Продолжать бесконечный прием подключений.
    (loop
       (format t "Waiting to accept a connection...~%")
 
       ;; Пользуясь with-accept-connection, мы обеспечим автоматическое
       ;; закрытие клиентского подключение при выходе из формы.
       (with-accept-connection (client server :wait t)
         ;; При получении нового подключения, показать кто это из формы.
         (multiple-value-bind (who rport)
             (remote-name client)
           (format t "Got a connnection from ~A:~A!~%" who rport))
 
         ;; Использование интернет TCP потока, мы воспользуемся функцией format.
         ;; Однако, мы должны быть уверены в вызове finish-output после передачи всех данных.
         (multiple-value-bind (s m h d mon y)
             (get-decoded-time)
           (format t "Sending the time...")
           (format client "~A/~A/~A ~A:~A:~A~%" mon d y h m s)
           (finish-output client)
           (format t "Sent!~%")
           (finish-output)
           t)))))

Для очень простой блокировки Ввода/Вывода серверов, подобно этому серверу, последовательный прием и обработка клиентских подключений не является серьезной проблемой, но если сервер выполняет что-то требующее много времени или возвращает некоторое количество данных нескольким активным клиентам, то становится очевидным, что дизайн этого сервера все еще плох. Это означает что сервер вернется в верхний уровень если вы прекратите его работу. Когда это случится, формы WITH-* автоматически закроют подключение к клиенту.

Сервер Времени IVP4/TCP: ex3-server.lisp

В этой версии примера сервера с блокируемым Вводом/Выводом мы добавим обработку самых распространённых сигналов о состояних сети. Эти сигналы состояний являются общими при работе с сокетами. Также как и в предыдущем клиенте мы введём HANDLER-CASE, что приведёт к маленькой реструктуризации кода.

0. Вспомогательная функция открывает пассивный сокет, связывается с ним и начинает прослушивание сокета:

Ничего нового в этом участке кода нет. Мы уже видели этот код ранее. В промышленном коде мы укоротим этот код с помощью WITH-OPEN-SOCKET, это даст нам удобную возможность связываться и подключаться с соответствующими ключевыми аргументами.
(defun run-ex3-server-helper (port)
  (with-open-socket
      (server :connect :passive
              :address-family :internet
              :type :stream
              :ipv6 nil
              :external-format '(:utf-8 :eol-style :crlf))
 
    (format t "Created socket: ~A[fd=~A]~%" server (socket-os-fd server))
 
    ;; Привязать сокет ко всем интерфейсам с определённым портом.
    (bind-address server +ipv4-unspecified+ :port port :reuse-addr t)
    (format t "Bound socket: ~A~%" server)
 
    ;; начать прослушивание серверного сокета
    (listen-on server :backlog 5)
    (format t "Listening on socket bound to: ~A:~A~%"
            (local-host server)
            (local-port server))

1. Повторять обработку клиентов в конвейерном виде:

Новый материал в этой функции - HANDLER-CASE отсылающий клиенту информацию о времени. Пограничное состояние при передаче данных клиенту - это получение сервером reset (RST) от клиента, отключение клиента и ситуация когда нет принимающих данные клиентов. Если передача данных находится внутри формы WITH-ACCEPT-CONNECTION и возникнут вышеперечисленные состояния, то мы можем очистить форму при выходе из неё. Если мы не сможем отловить эти состояния, то мы попадём в отладчик.
Один из сложных вопросов - что за значение мы получаем при перехвате этих состояний и почему мы ничего не можем с этим поделать (кроме вывода уведомления и предотвращения попадания кода в отладчик). Решения этого вопроса мы оставим читателю, пусть этот читатель сам поставит эксперименты приводящие к таким состояниям, проследит работу кода и сам найдёт решение. В промышленном коде, где автор не может охватить все состояния, одним из сильных решений является игнорирование всех состояний.
Конечно, игнорирование состояний является лучшим решением вытекающим из контекста.
    ;; осуществляем бесконечную обработку соединений.
    (loop
       (format t "Waiting to accept a connection...~%")
 
       ;; Здесь мы видим with-accept-connection что облегчает нам жизнь
       ;; через закрытие сокета после работы с ним.
       (with-accept-connection (client server :wait t)
         ;; Когда получено новое соединение, покажем
         ;; кто это.
         (multiple-value-bind (who rport)
             (remote-name client)
           (format t "Got a connnection from ~A:~A!~%" who rport))
 
         ;; Использование интернет TCP потока, даёт возможность использования format
         ;; Однако, мы должны быть уверены, что весь вывод ушёл к клиенту (finish-output).
         (multiple-value-bind (s m h d mon y)
             (get-decoded-time)
           (format t "Sending the time...")
 
           ;; Перехват состояния, при котором клиент закрыл соединение.
           ;; После выхода из with-accept-connection
           ;; сокет должен автоматически закрыться.
           (handler-case
               (progn
                 (format client "~A/~A/~A ~A:~A:~A~%" mon d y h m s)
                 (finish-output client))
 
             (socket-connection-reset-error ()
               (format t "Client reset connection!~%"))
 
             (hangup ()
               (format t "Client closed conection!~%")))
 
           (format t "Sent!~%"))))

2. Конец вспомогательной функции возвращает T к месту вызова:

    t))

3. Точка входа в этот пример:

Мы обрабатываем состояние SOCKET-ADDRESS-IN-USE-ERROR, это состояние сигнализируется больше всего в случае, когда мы пытаемся связать сокет к занятому адресу, это происходит при уже запущенному сервере или когда адрес находится в состоянии TIME_WAIT. Последняя ситуация происходит тогда, когда один сервер уже завершил работу а другой сервер только запускается. Здесь мы должны использовать ключевой аргумент :reuse-addr со значением "истина" для функции BIND-ADDRESS, что даст нам возможность связывать сокет к адресу в состоянии TIME_WAIT.
;; Это главная точка входа в пример 3 сервера.
(defun run-ex3-server (&key (port *port*))
  (handler-case
 
      (run-ex3-server-helper port)
 
    (socket-address-in-use-error ()
      ;; Здесь мы перехватываем состояние, возникающее при связывании некоторого
      ;; порта до его освобождения ядром.
      ;; В общем случае это означает что Вы забыли вставить ':reuse-addr t' в качестве
      ;; аргумента для связывания адресов.
      (format t "Bind: Address already in use, forget :reuse-addr t?")))
 
  (finish-output))

Сервер Времени IVP4/TCP: ex4-server.lisp

Это наш первый многопользовательский сервер и последний сервер времени. Обычно параллельные вычисления реализуются (в среде UNIX) с помощью вызова библиотеки fork() которая создаёт новый процесс с семантикой копирование-на-запись (copy-on-write) для обработки подключения к клиенту. В среде этого руководства мы решили реализовать эту идею с помощью переносимой библиотеки Bordeaux Threads. Ввод/Вывод, по прежнему, остаётся строчно ориентированным и блочным, но, когда нить блокирует другую нить создаётся иллюзия обработки сервером нескольких клиентов в не-блокируемом стиле.

Также мы введём функцию UNWIND-PROTECT, обеспечивающую закрытие различных сокетов при возникновении различных состояний при работе сервера. UNWIND-PROTECT выполняется как единая форма и после вычисления или прерывания работы этой формы запускается специальная очистительная форма. Очистительная форма выполняется *всегда* и мы можем использовать её для очистки системных ресурсов не связанных с ОЗУ, например - сокеты.

Нити создают свои специфические проблемы в дизайне сервера. Есть две важные проблемы: гонка данных и завершение работы нитей. Руководство попытается осветить гонку данных в примерах и эта проблема окончательно решается через использование мьютексов Bordeaux-Threads или через переменные состояний. Наш простой пример не нуждается в мьютексах, так как они не используют обмен данными между собой.

Более сложная проблема - это завершение работы нитей. Данное руководство поощряет эксперименты с клиентами и серверами в REPL, нити могут утекать при инициализации сервером процесса остановки нити и возвращаются в REPL. Мы будем использовать три вызовов API из Bordeaux Threads: THREAD-ALIVE-P, ALL-THREADS и DESTROY-THREAD - они не используются в обычном программировании нитей. Мы будем использовать их с целью попытки очищения утёкших нитей, таким образом клиенты будут знать когда остановился процесс сервера и мы не будем загрязнять REPL всё более растущим числом исполняемых нитей. Действующий метод разрушения нитей, особенно в SBCL, использует обращение к форме очистки нитей UNWIND-PROTECT, закрывающих сокет к клиенту перед разрушением нити. Мы не даём никаких гарантий что в других реализациях Common Lisp форма UNWIND-PROTECT будет работать также.

Но этим методом очень сложно управлять поскольку он использует функцию IGNORE-ERRORS для игнорирования любых состояний о которых может сигнализировать DESTROY-THREAD библиотеки Bordeaux Thread, включая такие важные состояния как HEAP-EXHAUSTED-ERROR в специфическом состоянии SBCL. В настоящем нитевом сервере завершение инициализирующей нити (обозначающей завершение работы и закрытие LISP процесса) будет разрушать все остальные нити как процессы и в итоге завершит работу. This is the recommended way a threaded server should exit.

Реализация нитей зависит от реализации языка и не игрушечные серверы использующие нити должны использовать родную реализацию нитей специфичную для конкретной реализации Common Lisp. В данном примере сложной ситуацией является ситуация завершения работы нити в блокировочном Вводе/Выводе. Различные реализации управляют этим процессом с помощью различных путей.

Два предоставленных примера, ex4-server и ex5-server показывают общую идею для структуризации кода в утилизации нитей.

Ниже представлен разбор ex4-server:

0. Специальная переменная позволяющая инициализированной нити передать клиентскому сокету нить управляющую выводом клиенту:

;; Эта переменная обозначает то, с помощью чего мы передаём клиентский сокет с
;; инициализурующей нити в подробную нить, управляющую выводом к клиенту.
(defvar *ex4-tls-client* nil)

1. Вспомогательная функция, которую можно использовать как заготовку для сервера:

(defun run-ex4-server-helper (port)
  (with-open-socket
      (server :connect :passive
              :address-family :internet
              :type :stream
              :ipv6 nil
              :external-format '(:utf-8 :eol-style :crlf))
 
    (format t "Created socket: ~A[fd=~A]~%" server (socket-os-fd server))
 
    ;; Связать сокет на всех интерфейсах с определённым портом.
    (bind-address server +ipv4-unspecified+ :port port :reuse-addr t)
    (format t "Bound socket: ~A~%" server)
 
    ;; начать прослушивание сокета сервера
    (listen-on server :backlog 5)
    (format t "Listening on socket bound to: ~A:~A~%"
            (local-host server)
            (local-port server))

2. Идём дальше, приём клиентского соединения на слушающем сокете и запуск нити обслуживающей его:

В этом куске кода есть несколько особенностей. Первая вещь - это уведомление UNWIND-PROTECT и его очищающей формы. Форма, которую сторожит UNWIND-PROTECT - это бесконечный цикл производящий блокировочный доступ к клиентскому сокету, связывает *default-special-bindings* через добавление assoc списка связывающего *ex4-tls-client* и создающего нить обслуживающую клиента.
Форма очистки проходит по всем активным клиентским нитям и разрушает их, игнорируя все возникающие и существующие состояния. Разрушение нитей предотвращает от их пакетирования и их опустошения в случае множественного запуска и остановки серверов в единицу времени. В дополнение, эта форма принудительно производит закрытие, тем самым немедленно уведомляя клиентов о закрытии.
    ;; Здесь мы вводим понятие unwind-protect для того, что бы убедиться в правильном закрытии
    ;; всех оставшихся нитей в случае ухода сервера по каким либо причинам.
    ;; продолжаем бесконечный приём подключений, но если по каким либо причинам произошёл выход
    ;; то мы должны убедиться в разрушении всех запущенных нитей.
    (unwind-protect
         (loop                         ; продолжаем приём соединений...
            (format t "Waiting to accept a connection...~%")
            (finish-output)
            (let* ((client (accept-connection server :wait t))
                   ;; устанавливаем специальную переменную нужную для
                   ;; пакета Bordeaux Threads, передачи
                   ;; и приёма клиентского сокета от созданной нити
                   ;; *default-special-bindings* не
                   ;; должен модифицироваться, здесь мы введём новые рамки
                   ;; для него.
                   (*default-special-bindings*
                    (acons '*ex4-tls-client* client
                           *default-special-bindings*)))
 
              ;; ...и обслуживание подключения!
              (when client
                (make-thread #'process-ex4-client-thread
                             :name 'process-ex4-client-thread))))
 
      ;; Очистка формы для uw-p.
      ;; Очистка всех клиентских нитей по завершению.
      ;; Этот код полезно использовать в REPL поскольку
      ;; предполагается интерактивная работа с руководством. В настоящем 
      ;; нитевом сервере, сервер должен только выходить - разрушая 
      ;; процесс сервера и дав сигнал выхода для всех нитей, которые в свою очередь уведомляют
      ;; клиентов.
      (format t "Destroying any active client threads....~%")
      (mapc #'(lambda (thr)
                (when (and (thread-alive-p thr)
                           (string-equal "process-ex4-client-thread"
                                         (thread-name thr)))
                  (format t "Destroying: ~A~%" thr)
                  ;; Игнорируем все состояния могущие произойти если
                  ;; нить дойдёт до финиша гонки между
                  ;; жизнью и разрушением.
                  (ignore-errors
                    (destroy-thread thr))))
            (all-threads)))))

3. Начало обработки нити клиента:

При зарождении нити, вышеупомянутая привязка сокета клиента к *ex4-tls-client* начинает работать через механизм *специальных-обвязок-по-умолчанию* (*default-special-bindings*). Через объявление об игнорировании *ex4-tls-client* мы информируем компилятор о том, что эта переменная будет установлена "в другом месте" и не надо выдавать предупреждения о том, что этой переменной не назначено значение. В нашем случае, это определение будет использоваться в процессе работы сервера.
;;; Нить, которая обслуживает клиентское подключение.
(defun process-ex4-client-thread ()
  ;; Это значение устанавливается за пределами контекста этой нити.
  (declare (ignorable *ex4-tls-client*))

4. Послать время к сокету:

В этой форме UNWIND-PROTECT обслуживает каждый возможный случай выхода из вычисляемой функции: нормальный выход, сигнал о состоянии или разрушение нити -- на SBCL! Во всех случаях, сокет к клиенту будет закрываться, что вызовет очистку ресурсов Операционной Системы и извещает клиента о том, что сервер закрыл подключение. В данном случае форма HANDLER-CASE только информирует нас о возможном сигнале, возникшем при записи времени клиенту.
  ;; Убедимся о том, что сокет клиента будет всегда закрытым!
  (unwind-protect
       (multiple-value-bind (who port)
           (remote-name *ex4-tls-client*)
         (format t "A thread is handling the connection from ~A:~A!~%"
                 who port)
 
         ;; Подготовка времени и отправка его к клиенту.
         (multiple-value-bind (s m h d mon y)
             (get-decoded-time)
           (handler-case
               (progn
                 (format t "Sending the time to ~A:~A..." who port)
                 (format *ex4-tls-client*
                         "~A/~A/~A ~A:~A:~A~%"
                         mon d y h m s)
                 (finish-output *ex4-tls-client*)
                 (format t "Sent!~%"))
 
             (socket-connection-reset-error ()
               (format t "Client ~A:~A reset the connection!~%" who port))
 
             (hangup ()
               (format t "Client ~A:~A closed connection.~%" who port)))))
 
    ;; Очистка формы для uw-p.
    (format t "Closing connection to ~A:~A!~%"
            (remote-host *ex4-tls-client*) (remote-port *ex4-tls-client*))
    (close *ex4-tls-client*)))
Это маленькие хитрости для создания устойчивой обработки закрытия клиентского сокета в нити. Например, если мы используем специальную переменную *ex4-tls-client* для лексически расширенной переменной и затем применим форму UNWIND-PROTECT для закрытия лексически расширенной переменной, то в случае пробуждения и разрушения нити после лексической связки, но до UNWIND-PROTECT, то мы потеряем сокет клиента в сборщике мусора.
Такой неправильный код выглядит примерно так:
    ;; Этот код не правилен!
    (defun process-ex4-client-thread ()
      (declare (ignorable *ex4-tls-client*))
      (let ((client *ex4-tls-thread*))
        ;; нить будет разрушена прямо здесь! клиентский сокет остаётся открытым!
        (unwind-protect
          ( [evaluable form] )
          (close client))))

5. Точка входя в этот пример:

Подобно ранним серверам, мы вызываем вспомогательную функцию и посмотрим что получится если :reuse-addr не true в вызове функции BIND-ADDRESS.
;; Точка входа в этот пример.
(defun run-ex4-server (&key (port *port*))
  (handler-case
 
      (run-ex4-server-helper port)
 
    ;; обработка некоторых общих сигналов
    (socket-address-in-use-error ()
      (format t "Bind: Address already in use, forget :reuse-addr t?")))
 
  (finish-output))

Комментарий для Клиентов/Серверов времени

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

Эхо Строчные Клиенты и Серверы

Следующие примеры будут фокусироваться на протоколе эхо. Это простой сервер, который возвращает клиенту всё, что он написал. Клиент может запрашивать прекращение работы с сервером (исключение ex8-server, где эта функциональность не реализована) через отправку слова "quit", в качестве строки. Эта команда говорит серверу закрыть подключение к клиенту после прекращения эхо для строки. Закрытие клиентского считывающего сокета даёт клиенту знать о том, что подключение к серверу прекращено и что наступило время для выхода. Также мы реализуем интерфейс мультиплексора сокета, что позволит работать с множественными подключениями сокета. Эта реализация будет похажа на UNIX'овский select(), epoll() или kqueue(). По причине переносимости не блокируемых операций на *стандартном-вводе* (*standard-input*) и *стандартном-выводе* (*standard-output*) (мы не сможем так-просто реализовать это) мы обязаны некоторым формам блочного Ввода/Вывода в наших клиентских приложениях, поскольку они интерактивны и работают с человеком. Мы будем использовать настоящий не-блочный Ввод/Вывод в примере ex8-server после подключения сервера к клиенту.

Эхо Клиенты

Эхо клиенты - это группа программ, выполняющих чтение строки со *стандартного-ввода* (*standard-input*) и записывающих считанную строку на сервер, далее идёт считывание ответа с сервера и вывод результата на *стандартный-вывод* (*standard-output*). Пока есть портируемый метод чтения "всего, что только возможно" из *стандартного-ввода* (*standard-input*), не будет симметричного метода записи "всего, что Я могу" в *стандартный-вывод* (*standard-output*). Для дизайна нашего клиента, это означает что все наши клиенты будут строчно ориентированы и будут исполнять блочный Ввод/Вывод при чтении из *стандартного-ввода* (*standard-input*) и записывать в *стандартный-вывод* (*standard-output*).

Эхо клиент IPV4/TCP: ex4-client.lisp

Это программа эхо клиента, выполняющая только базовые функции, обрабатывающая только самые общие состояния при работе с сервером:

0. Подключение к серверу и запуск эха строк:

Здесь мы будем использовать WITH-OPEN-SOCKET для создания активного сокета, который в последствии будет использоваться для подключения к серверу. Мы будем обрабатывать HANGUP, для ситуации, когда сервер уйдёт до того, как клиент начнёт что-либо писать в него, и END-OF-FILE, когда сервер закроет подключение.
Учтите что мы вызываем функцию ex4-str-cli взамен макроса HANDLER-CASE. Этот приём позволяет нам не проверять каждое сигнализированное состояние в ex4-str-cli и значительно проще в реализации.
В этом специфичном примере, мы не делаем ничего кроме уведомления о произошедшем состоянии, которое произошло после закрытии сокета через WITH-OPEN-SOCKET.
(defun run-ex4-client-helper (host port)
 
  ;; Создать интернет TCP сокет под IPV4
  (with-open-socket
      (socket :connect :active
              :address-family :internet
              :type :stream
              :external-format '(:utf-8 :eol-style :crlf)
              :ipv6 nil)
 
    ;; создать блокируемое подключение к серверу времени на заданном порту.
    (connect socket (lookup-hostname host) :port port :wait t)
 
    (format t "Connected to server ~A:~A from my local connection at ~A:~A!~%"
            (remote-host socket) (remote-port socket)
            (local-host socket) (local-port socket))
 
    (handler-case
        (ex4-str-cli socket)
 
      (socket-connection-reset-error ()
        (format t "Got connection reset. Server went away!"))
 
      (hangup ()
        (format t "Got hangup. Server closed connection on write!~%"))
 
      (end-of-file ()
        (format t "Got end-of-file. Server closed connection on read!~%")))))

1. Эхо строки для сервера:

Пока пользователь не введёт "quit" в строку, мы читаем строку, отправляем её на сервер, читаем с сервера и печатаем на стандартный вывод. Если будет сигнализировано какое-либо состояние, то мы получим ошибку в Шаге номер 0 и произведём обработку ошибки.
Когда будет введён "quit", строка как обычно будет отправлена на сервер, но в этот раз сервер закроет подключение к клиенту. К несчастью, пока клиент работает с блокировочным Вводом/Выводом, мы должны считывать каждую строку из *стандартного-ввода* (*standard-input*) прежде чем мы получим сигнал о состоянии от IOLib, уведомляющий нас о том, что сервер закрыл сокет.
В практике, это означает то, что после закрытия соединения сервером, пользователь должен продолжать Ввод/Вывод столько раз, сколько это потребуется для получения сигнала о состоянии.
;; прочесть строку из стандартного ввода, отправить её на сервер, прочесть ответ, записать его
;; его в стандартный вывод. Если мы прочитаем 'quit' то его эхо будет 
;; возвращено нам с последующим закрытием соединения.
(defun ex4-str-cli (socket)
  (loop
     (let ((line (read-line)))
       ;; send it to the server, get the response.
       (format socket "~A~%" line)
       (finish-output socket)
       (format t "~A~%" (read-line socket)))))

2. Точка входа в пример:

Мы обрабатываем отменённые состояния как обычное подключение, но в этом шаге нет ничего примечательного.
;; Это точка входа в этот пример
(defun run-ex4-client (&key (host *host*) (port *port*))
  (unwind-protect
       (handler-case
 
           (run-ex4-client-helper host port)
 
         ;; обрабатываем общие просигнализированные ошибки...
         (socket-connection-refused-error ()
           (format t "Connection refused to ~A:~A. Maybe the server isn't running?~%"
                   (lookup-hostname host) port)))
 
    ;; Очистка формы
    (format t "Client Exited.~%")))

Эхо Клиент IPV4/TCP: ex5a-client.lisp

Это первый клиент использующий мультиплексор сокета для уведомления сокета о готовности сервера для чтения и записи. Пока мультиплексор используется в единственном нитевом сервере, его можно использовать для клиентов - особенно для клиентов, которые общаются с несколькими серверами подобно web клиентам. Использование API мультиплексора требует значительного изменения структуры кода. Не рекомендуется одновременное использование мультиплексора и нитей для обработки сетевых подключений.

Держите в голове тот факт, что мы ВСЕГДА можем блокировать чтение из *стандартного-ввода* (*standard-input*) или запись в *стандартный-вывод* (*standard-output*), мы только можем попытаться читать/писать в стандартные потоки в то время когда мультиплексор думает что он может читать/писать в сервер без блокировки. Это меняет форму из традиционных примеров в форму подобную С, поскольку в С можно определить готов ли STDIN или STDOUT для работы на манер сетевого файлового дескриптора.

Первое большое изменение, которое будет отличать новый код от прежних примеров - это прекращение использования WITH-OPEN-SOCKET в связи с переходом на ручной контроль закрытия сокета к серверу. Это особенно важно для клиентов, использующих активные сокеты. Второе изменение - это способ создания и регистрации обработчиков для чтения и записи в сокет сервера. Третье изменение - это снятие регистрации с обработчика и закрытие ассоциации сокета с этим обработчиком под правильными состояниями. Другие изменения будут описаны тогда, когда мы столкнёмся с ними.

Главные функции API мультиплексора следующие:

(make-instance 'iomux:event-base ....)
Создать экземпляр базы-событий и ассоциировать некоторые свойства, такие как event-dispatch для последующего возвращения если мультиплексор не управляет никакими сокетами.
Передаётся так:
 :exit-when-empty - при отсутствии зарегистрированных обработчиков, возвращается event-dispatch.
(event-dispatch ...)
По умолчанию, всегда находится в бесконечном цикле мультиплексора и обрабатывает запросы Ввода/Вывода. Передаётся в связку event-base в дополнение:
 :once-only - единожды запускает готовый обработчик.
 :timeout - если нет Ввода/Вывода в течении определённого таймаута.
(set-io-handler ...)
Ассоциирует обработчик с состоянием вызванного определённого сокета.
Передаётся как:
связка event-base
ключевые слова :read или :write или :error
замыкание обработчика
(remove-fd-handlers ...)
Удаляет обработчик для определённого состояния с определённым сокетом.
Передаётся так:
связка event-base
и fd
один или более :read t, :write t, :error t.

Здесь будет пример использования этого API.

0. База событий:

База-событий - это объект, который содержит состояние мультиплексора. Он должен быть инициализирован и закрыт так, как мы видим в функции этого примера.
;; Это будет экземпляр мультиплексора.
(defvar *ex5a-event-base*)

1. Вспомогательная функция в которой мы создадим активный сокет:

Вместо использования WITH-OPEN-SOCKET, мы вручную создадим сокет. Мы сделаем это для более лучшего контроля закрытия сокета. WITH-OPEN-SOCKET пробует выполнять FINISH-OUTPUT на сокете до его закрытия. Это нехорошо в случае когда сокет уже был закрыт или был сигнал состояния подобный HANGUP. Попытка записи данных в закрытый сокет просто повлечёт за собой сигнал другого состояния. Для предотвращения слоёв состояний обслуживаемого кода, мы будем сами управлять закрытием сокета.
(defun run-ex5a-client-helper (host port)
  ;; Создать интернет TCP сокет под IPV4
  ;; Мы не будем использовать with-open-socket
  ;; поскольку код предназначен для синхронного ввода/вывода на один сокет.
  ;; Поскольку мы не будем использовать эту
  ;; форму, решение удаления и закрытия сокета  при закрытии соединения к серверу
  ;; будет перенесено на обработчики.
  (let ((socket (make-socket :connect :active
                             :address-family :internet
                             :type :stream
                             :external-format '(:utf-8 :eol-style :crlf)
                             :ipv6 nil)))

2. Подключение к серверу, регистрация обработчиков сокета:

Мы защищаем закрытие сокета через UNWIND-PROTECT. Мы будем говорить об ответвлении этого решения в следующем шаге, когда будем описывать форму очистки UNWIND-PROTECT'а. В этой секции кода, мы устанавливаем обработчики чтения и записи для сокета, и вызываем функцию диспетчера, которая продолжит вызов обработчиков ассоциированных с сокетом до тех пор, пока сокет не будет закрыт и с обработчиков не будет снята регистрация. Когда это случится (смотрите главную функцию для выяснения причин), будет возвращён EVENT-DISPATCH и мы продолжим очистку формы для UNWIND-PROTECT.
Установка обработчика в мультиплексоре требует нескольких аргументов для функции set-io-handler. Ниже перечислены аргументы нужные для этой функции:
1. *ex5a-event-base*
Это экземпляр мультиплексора для которого мы устанавливаем обработчик.
2. (socket-os-fd socket)
Этот вызов возвращает нижележащий дескриптор файла операционной системы, ассоциированный с сокетом.
3. :read
Это ключевое слово обозначает то, что мы собираемся вызывать обработчик при готовности сокета для чтения. Возможны следующие варианты: :write и :error.
4.
(make-ex5a-str-cli-read    socket (make-ex5a-client-disconnector socket))
Функция make-ex5a-str-cli-read возвращает замыкание на сокет и другое замыкание возвращаемое функцией make-ex5a-client-disconnector. Эта функция будет вызываться при готовности сокета для чтения. Мы будем использовать короткое обозначение этой функции и что будет передаваться через мультиплексор. Отключение функции будет вызываться через возвращённую функцию считывания, если считывающая функция думает что ей нужно закрыть сокет на сервер.
    (unwind-protect
         (progn
           ;; создать блокируемое подключение на эхо сервер по заданному порту.
           (connect socket (lookup-hostname host) :port port :wait t)
 
           (format t "Connected to server ~A:~A from my local connection at ~A:~A!~%"
                   (remote-host socket) (remote-port socket)
                   (local-host socket) (local-port socket))
 
           ;; установить обработчик для чтения и записи
           (set-io-handler *ex5a-event-base*
                           (socket-os-fd socket)
                           :read
                           (make-ex5a-str-cli-read
                            socket
                            (make-ex5a-client-disconnector socket)))
 
           (set-io-handler *ex5a-event-base*
                           (socket-os-fd socket)
                           :write
                           (make-ex5a-str-cli-write
                            socket
                            (make-ex5a-client-disconnector socket)))
 
           (handler-case
               ;; продолжать ввод и вывод на fd через
               ;; вызов соответствующего обработчика как сокета, пригодного для
               ;; чтения. Соответствующий обработчик должен заботится о
               ;; закрытии сокета в нужное время.
               (event-dispatch *ex5a-event-base*)
 
             ;; Уведомим пользователя клиента в случае если обработчик не 
             ;; поймал общее состояние.
             (hangup ()
               (format t "Uncaught hangup. Server closed connection on write!%"))
             (end-of-file ()
               (format t "Uncaught end-of-file. Server closed connection on read!%"))))

3. Очистка формы для UNWIND-PROTECT:

В форме очистки, мы всегда закрываем сокет и мы передаём функции close :abort t для попытки закрытия сокета если это возможно. Если мы только пытаемся закрыть сокет, то мы должны использовать другое состояние для сигнализирования предыдущего состояния, на пример: HANGUP, воздействующий на сокет. :abort t производит уход от этого случая. Если сокет уже закрыт обработчиком, то повторное закрытие ни на что не будет воздействовать.
      ;; Выражение очистки для uw-p.
      ;; Пытаемся произвести очистку, в случае когда клиент не корректно завершил работу
      ;; и сокет остался открытым.
      ;; Множественный вызов закрытия сокета является безопасным.
      ;; Однако, мы не должны использовать finish-output на сокете, сигнализирующем
      ;; другое состояние, если
      ;; обработчик ввода/вывода уже закрыл сокет.
      (format t "Client safely closing open socket to server.~%")
      (close socket :abort t))))

4. Создать функцию записи для сокета готового для записи:

Эта функция возвращает замыкание, которое вызывается мультиплексором, при готовности к чтению чего-либо с сервера. Аргументы для замыкания - fd, нижележащий файловый дескриптор для готового сокета, событие, которое может быть следующим: :read, :write или :error, если обработчик зарегистрирован несколько раз, и исключение, равное nil в нормальных условиях, :error под ошибкой с сокетом, или :timeout, если мы используем операции таймаута при работе с сокетом.
Замыкание будет читать строку с помощью функции READ-LINE и писать её в сервер. Чтение будет блочным, но надеюсь запись не будет блочной. Очевидно, что при записи громадных строк, мы будем опять блокироваться, в этом случае FINISH-OUTPUT будет проталкивать данные в блокируемый Ввод/Вывод, пока они (данные) не закончатся и мы вернёмся с обработчика. Таким образом это замыкание пишет при готовности, но всё же есть случаи, когда оно по прежнему блокируется.
В этом обработчике, есть сигнализируемые состояния, возникающие при чтении из *стандартного-ввода* (состояние END-OF-FILE) или записи на сокет сервера (состояние HANGUP), мы вызываем разъединение замыкания и передаём ему :close. Когда мы получим описание функции дисконнектора, Вы увидите что это обозначает.
Разъединение замыкания, будучи вызванным, перемещает обработчик и закрывает сокет. Вызывает возвращение EVENT-DISPATCH как только сокет будет находится под управлением мультиплексора будет закрытым - поскольку мы говорим мультиплексору делать то, что он делает!
(defun make-ex5a-str-cli-write (socket disconnector)
  ;; Когда эта следующая функция будет вызвана, поскольку диспетчер событий
  ;; знает что сокет сервера доступен на запись.
  (lambda (fd event exception)
    ;; Получить строку со стандартного ввода, и отправить её на сервер
    (handler-case
        (let ((line (read-line)))
          (format socket "~A~%" line)
          (finish-output socket))
 
      (end-of-file ()
        (format t "make-ex5a-str-cli-write: User performed end-of-file!~%")
        (funcall disconnector :close))
 
      (hangup ()
        (format t
                "make-ex5a-str-cli-write: server closed connection on write!~%")
        (funcall disconnector :close)))))

5. Сделаем считывающую функцию для сокета готового для чтения:

Этот кусок кода подобен коду предыдущего шага, мы только управляем соответствующими состояниями и после чтения строки с сервера выводим её в *стандартный-вывод*. Опять же мы продолжаем чтение с сервера без блокировок, но если количество информации велико, мы будем считывать её блочно, до тех пор пока read-line не прочитает все данные и символ новой строки.
(defun make-ex5a-str-cli-read (socket disconnector)
  ;; Когда эта следующая функция будет вызвана, поскольку диспетчер событий
  ;; знает что сокет с сервера доступен для чтения.
  (lambda (fd event exception)
    ;; получить строку с сервера, и отправить её в *стандартный-вывод*
    (handler-case
        ;; Если мы отправляем "quit" на сервер, он закроет соединение к
        ;; нам и мы получим уведомление о достижении конца-файла.
        (let ((line (read-line socket)))
          (format t "~A~%" line)
          (finish-output))
 
      (end-of-file ()
        (format t "make-ex5a-str-cli-read: server closed connection on read!~%")
        (funcall disconnector :close)))))

6. Функция дисконнектора:

Эта функция возвращает замыкание, которое принимает произвольное число аргументов. Если аргументы к вызванному замыканию содержат :read, :write или :error, то удаляется соответствующий обработчик ассоциированного сокета. Если нет ни одного из этих трёх аргументов, то удаляются все обработчики для этого сокета. В дополнение: если определён :close, то сокет будет закрыт. В данном примере, не используются все свойства этой функции. Эта функция (или другие подобные функции использующие корректирующую специальную переменную event-base) будет использована каждый раз, когда мы будем использовать мультиплексор.
Замыкание вызывается каждый раз, когда обработчик верит, что может снять регистрацию с себя или другого обработчика, или закрыть сокет. Поскольку мы можем часто закрывать сокет в замыкании дисконнектора, мы не можем использовать WITH-OPEN-SOCKET для автоматического закрытия сокета, поскольку WITH-OPEN-SOCKET может попробовать очистить данные в сокете, по сигналу другого состояния.
(defun make-ex5a-client-disconnector (socket)
  ;; Когда вызывается эта функция, она может указать на удаление callback'а, если
  ;; никакие callback'и не определены, то будут удалены все callback'и! Дополнительно возможно
  ;; закрытие сокета.
  (lambda (&rest events)
    (format t "Disconnecting socket: ~A~%" socket)
    (let ((fd (socket-os-fd socket)))
      (if (not (intersection '(:read :write :error) events))
          (remove-fd-handlers *ex5a-event-base* fd :read t :write t :error t)
          (progn
            (when (member :read events)
              (remove-fd-handlers *ex5a-event-base* fd :read t))
            (when (member :write events)
              (remove-fd-handlers *ex5a-event-base* fd :write t))
            (when (member :error events)
              (remove-fd-handlers *ex5a-event-base* fd :error t)))))
    ;; и наконец если мы запросили закрытие сокета, мы сделаем это здесь
    (when (member :close events)
      (close socket :abort t))))

7. Точка входа для этого примера и установка event-base:

Эта функция больше чем просто пример не использующий мультиплексор. Защищённый с помощью UNWIND-PROTECT, мы первым делом инициализируем базу событий через вызов make-instance 'iomux:event-base. Здесь мы пропускаем ключевой аргумент :exit-when-empty t, который указывает на то, что функция event-dispatch должна вернуться когда не останется ни одного зарегистрированного обработчика. Когда это случится, мы вызовем помощника, который поймает общие состояния и будет ждать нашего возвращения.
;; Это точка входа в этот пример.
(defun run-ex5a-client (&key (host *host*) (port *port*))
  (let ((*ex5a-event-base* nil))
    (unwind-protect
         (progn
           ;; Когда соединение будет закрыто, специально от клиента или по причине отключения сервером,
           ;; мы должны прервать цикл событий мультиплексора. Таким образом, при создании event-base,
           ;; мы должны точно указать это поведение.
           (setf *ex5a-event-base*
                 (make-instance 'iomux:event-base :exit-when-empty t))
           (handler-case
               (run-ex5a-client-helper host port)
 
             ;; обработка общих сигналов об ошибках...
             (socket-connection-refused-error ()
               (format t "Connection refused to ~A:~A. Maybe the server isn't running?~%"
                       (lookup-hostname host) port))))

8. Форма очистки для UNWIND-PROTECT:

Эта форма очистки закрывает экземпляр *ex5a-event-base*. IOLib определяет метод для общей функции CLOSE, которая принимает event-base и выполняет необходимую работу для его завершения.
      ;; Очищающая форма для uw-p
      ;; для того, чтобы убедиться что базу событий можно закрыть и покинуть клиентский алгоритм.
      (when *ex5a-event-base*
        (close *ex5a-event-base*))
      (format t "Client Exited.~%")
      (finish-output))))

Эта программа хорошо работает с вводом, осуществляемым человеком, но не работает с пакетным вводом. Недочёт заключается в том, что как только мы получим состояние END-OF-FILE при закрытии *стандартного-ввода*, мы незамедлительно снимем регистрацию с обработчиков чтения/записи на сервер, закроем сокет и выйдем из программы. Это разрушит другие данные идущие с/на сервер(а), что может привести к тому, что строки могут остаться без эха.

Эхо Клиент IPV4/TCP: ex5b-client.lisp

Продолжая исправление проблемы пакетного ввода в ex5a-client, мы будем использовать функцию shutdown, которая позволит нам информировать сервер о том, что мы закончили запись данных, но оставляем сокет открытым, таким образом мы можем читать ответ от сервера. Этот приём позволяет эффективно закрывать только половину TCP подключения.Сервер понимает этот вид протокола и не считает что клиент окончательно вышел из сеанса связи при получении END-OF-FILE от клиента и закрывает всё соединение, отбрасывая все оставшиеся данные для клиента.

Этот клиент очень близок к ex5a-client за исключением того, что мы завершаем соединение при окончании записи на сервер по получении END-OF-FILE со *стандартного-ввода* и ожидаем возврат всех данных от сервера. Сервер сигнализирует нам о завершении пересылки всех данных через закрытие его соединения. Клиент видит завершение записи сервера как END-OF-FILE на сокете подключенном к серверу.

Мы покажем этот пример как разницу ex5aq-client.

0. Выключение записи конца в сокет на сервере:

Здесь мы используем расширенную функциональность замыкания дисконнектора. После выключения записи конца нашего TCP соединения, мы вызываем (funcall disconnector :write), что позволяет удалять обработчики записи (на сервере), но оставляет соединение открытым. После того, как это случится, у нас не остаётся пути для повторного чтения со *стандартного-ввода*. Как только сервер отправит конечные данные и закроет его подключение к этому клиенту, мы удалим обработчик чтения, что в свою очередь уберёт последний обработчик и будет причиной возвращения функции EVENT-DISPATCH, что и будет концом работы клиента.
(defun make-ex5b-str-cli-write (socket disconnector)
  ;; Будет вызвана следующая функция, поскольку диспетчер событий
  ;; знает что сокет сервера доступен на запись.
  (lambda (fd event exception)
    ;; Получаем строку со стандартного ввода, и отправляем её на сервер
    (handler-case
        (let ((line (read-line)))
          (format socket "~A~%" line)
          (finish-output socket))
 
      (end-of-file ()
        (format t
                "make-ex5b-str-cli-write: User performed end-of-file!~%")
        ;; Shutdown отправляет конец в нашу трубу для получения данных на лету
        ;; с сервера!
        (format t
                "make-ex5b-str-cli-write: Shutting down write end of socket!~%")
        (shutdown socket :write t)
        ;; после отправки конца записи в трубу, удаляем этот обработчик
        ;; так мы не сможем прочитать больше данных со *стандартного-ввода* и попытаемся записать их
        ;; в сервер.
        (funcall disconnector :write))
 
      (hangup ()
        (format t
                "make-ex5b-str-cli-write: server closed connection on write!~%")
        (funcall disconnector :close)))))

Будьте осторожны, в случае закрытия обоих концов соединения необходимо вызвать close для сокета с целью освобождения ресурсов операционной системы.

Эхо Серверы

Эхо серверы спарены с клиентами, как в начале этого руководства, в дальнейшем они (эхо серверы) будут использовать мультиплексор и избавятся от блокировок при работе с Вводом/Выводом при различном размере данных чтения/записи.

Эхо Сервер IPV4/TCP: ex5-server.lisp

Этот сервер использует нити и подобен серверу ex4-server, но вместо отправки только времени, каждая нить обрабатывает эхо протокол для клиента. Пока этот сервер работает на блочном Вводе/Выводе, и только единственная нить остаётся блочной, но не весь сервер. Пакетный ввод от клиентов, пока, не работает так, как надо. В целом это общая модель для класса серверов с не блочным поведением.

0. Используется специальная переменная для подключения сокета клиента к нити:

;; Используется специальная переменная для удержания клиентского сокета в нити,
;; обрабатывающий этот сокет.
(defvar *ex5-tls-client* nil)

1. Обычное начало для сервера:

(defun run-ex5-server-helper (port)
  (with-open-socket
      (server :connect :passive
              :address-family :internet
              :type :stream
              :ipv6 nil
              :external-format '(:utf-8 :eol-style :crlf))
 
    (format t "Created socket: ~A[fd=~A]~%" server (socket-os-fd server))
 
    ;; Привязать сокет ко всем интерфейсам с определённым портом.
    (bind-address server +ipv4-unspecified+ :port port :reuse-addr t)
    (format t "Bound socket: ~A~%" server)
 
    ;; запустить прослушивание сокета сервера
    (listen-on server :backlog 5)
    (format t "Listening on socket bound to: ~A:~A~%"
            (local-host server)
            (local-port server))

2. Первая половина кода, создающего нити для клиента:

    ;; бесконечно поддерживать приём соединений, но в случае их пропадания, по какой либо причине, 
    ;; убедиться в разрушении любых запущенных нитей.
    (unwind-protect
         (loop
            (format t "Waiting to accept a connection...~%")
            (finish-output)
            (let* ((client (accept-connection server :wait t))
                   ;; set up the special variable to store the client
                   ;; we accepted...
                   (*default-special-bindings*
                    (acons '*ex5-tls-client* client
                           *default-special-bindings*)))
 
              ;; ...и обрабатывать подключение!
              (when client
                (make-thread #'process-ex5-client-thread
                             :name 'process-ex5-client-thread))))

3. Вторая половина, форма очистки для UNWIND-PROTECT:

Мы должны быть уверены что очищаем только нити клиента!
      ;; Форма очистки для uw-p.
      ;; По завершению, очистить все нити клиента.
      ;; Этот код предназначен для использования с REPL'ом, поскольку
      ;; предполагается, что пользователь будет работать с этим руководством в интерактивном режиме.
      ;; В настоящем
      ;; нитевом сервере, сервер должен завершить работу с разрушением
      ;; процесса сервера, и всех нитей, которые в свою очередь предупреждают о завершении работы
      ;; всех клиентов.
      (format t "Destroying any active client threads....~%")
      (mapc #'(lambda (thr)
                (when (and (thread-alive-p thr)
                           (string-equal "process-ex5-client-thread"
                                         (thread-name thr)))
                  (format t "Destroying: ~A~%" thr)
                  ;; Игнорируем все возникающие состояния, могущие появится,
                  ;; при неожиданном завершении нити.
                  (ignore-errors
                    (destroy-thread thr))))
            (all-threads)))))

4. Обслуживать клиента и сигнализируемые состояния:

В этой функции, мы будем работать под любыми происходящими состояниями, если что-то пойдёт не так, мы закроем сокет к клиенту, и таким образом избежим его утечку в сборщик мусора. Также мы обслуживаем некоторое количество состояний клиента, могущих возникнуть при работе функции str-ex5-echo.
;; Нить, обслуживающая соединение с клиентом.
(defun process-ex5-client-thread ()
  ;; декларируется игнорируемой, поскольку значение динамической переменной
  ;; присваивается за границей этой функции.
  (declare (ignorable *ex5-tls-client*))
  ;; вне зависимости от того в каком месте цикла нас покинет клиент, мы всегда
  ;; закроем подключение.
  (unwind-protect
       (multiple-value-bind (who port)
           (remote-name *ex5-tls-client*)
         (format t "A thread is handling the connection from ~A:~A!~%"
                 who port)
 
         (handler-case
             ;; здесь актуальный алгоритм эха
             (str-ex5-echo *ex5-tls-client* who port)
 
           (socket-connection-reset-error ()
             (format t "Client ~A:~A: connection reset by peer.~%"
                     who port))
 
           (end-of-file ()
             (format t "Client ~A:~A closed connection for a read.~%"
                     who port)
             t)
 
           (hangup ()
             (format t "Client ~A:~A closed connection for a write.~%"
                     who port)
             t)))
 
    ;; очищающая форма unwind-protect
    ;; Мы всегда закроем подключение к клиенту, если эта
    ;; нить будет разрушена (в SBCL эта очищающая форма
    ;; запускается в случае разрушения нити).
    (format t "Closing connection to ~A:~A!~%"
            (remote-host *ex5-tls-client*) (remote-port *ex5-tls-client*))
    (close *ex5-tls-client*)
    t))

5. Здесь идёт предоставление эхо протокола для клиента:

Читаем строки от клиента и возвращаем их эхо. Весь этот Ввод/Вывод является блокируемым. Если мы увидим "quit" от клиента, происходит выход из цикла, далее происходит шаг 4-ый: очистка формы UNWIND-PROTECT и закрытие подключения к клиенту.
;; Эта функция будет разговаривать с клиентом.
(defun str-ex5-echo (client who port)
  ;; здесь мы получаем сигналы о граничных состояниях
  ;; клиента (обозначающие закрытие его подключений к нам при чтении или
  ;; записи) что для нас является знаком для выхода из бесконечного цикла
  (let ((done nil))
    (loop until done
       do
       (let ((line (read-line client)))
         (format t "Read line from ~A:~A: ~A~%" who port line)
         (format client "~A~%" line)
         (finish-output client)
         (format t "Wrote line to ~A:~A: ~A~%" who port line)
 
         ;; Выход из нити, в случае получения запроса 'quit' от пользователя.
         ;; Принудительное завершение сокета клиента.
         (when (string= line "quit")
           (setf done t))
         t))))

6. Входная функция для этого примера:

;; Это только проверка на некоторые состояния, мы будем выводить
;; сообщения о них.
(defun run-ex5-server (&key (port *port*))
  (handler-case
 
      (run-ex5-server-helper port)
 
    ;; обработка некоторых общих состояний
    (socket-address-in-use-error ()
      (format t "Bind: Address already in use, forget :reuse-addr t?")))
 
  (finish-output))

Эхо Сервер IPV4/TCP: ex6-server.lisp

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

Мы введём новый концепт с мультиплексором в котором прослушивающая часть сокета сервера зарегистрирована с мультиплексором. Обработчик чтения (в данном контексте вызывается обработчиком прослушивания) ассоциируется с этим сокетом, когда клиент подключился к адресу сервера. Следовательно, после подготовки прослушивающего сокета обработчик прослушивания принимает клиентов и ассоциирует обратный вызов протокола эхо с клиентским сокетом в мультиплексоре.

Дизайн Ввода/Вывода этого сервера такой, что если клиентское подключение готово для чтения, мы читаем строку, затем немедленно возвращаем строку клиенту в той же функции без подтверждения готовности на запись. Этот способ стал доступен после использования блокируемого Ввода/Вывода. Причиной для такого дизайна стала минимизация сложности в использовании мультиплексора в обработчике прослушивателя. Поздние примеры будут более сложными, так как мы добавим API мультиплексора.

0. Переменная в которой хранится экземпляр мультиплексора:

;; Эта переменная представляет состояние мультиплексора.
(defvar *ex6-server-event-base*)

1. Хэш таблица клиентских подключений:

Мы записываем каждого подключающегося к серверу клиента в хэш таблицу сокета, ключом является список (ip адрес порт) и ассоциируем со значением клиентского сокета. С помощью этой таблицы мы можем закрыть любые открытые клиентом соединения через форму очистки вне зависимости от любых состояний.
;; Храним все открытые клиентом соединения в виде ключей в таблице. Значения
;; являются списком, содержащим хост и порт подключения. Мы можем использовать их для
;; закрытия всех соединений к клиентам, в случае завершения работы сервера.
;; Такой приём позволяет нам уведомить клиентов о завершении работы сервера.
(defvar *ex6-server-open-connections*)

2. Создаём и привязываем сокет сервера:

Мы защищаем сокет сервера с помощью UNWIND-PROTECT для закрытия сокета в конце работы сервера или в случае когда что-то в работе сервера пойдёт не так.
;; Настроить сервер и клиентов сервера для работы с мультиплексором
(defun run-ex6-server-helper (port)
 
  ;; Мы не должны использовать with-open-socket, так как нам нужен
  ;; более лучший контроль при закрытии сокета сервера.
  (let ((server (make-socket :connect :passive
                             :address-family :internet
                             :type :stream
                             :ipv6 nil
                             :external-format '(:utf-8 :eol-style :crlf))))
    (unwind-protect
         (progn
           (format t "Created socket: ~A[fd=~A]~%" server (socket-os-fd server))
           ;; Привязать сокет ко всем интерфейсам с определённым портом.
           (bind-address server +ipv4-unspecified+ :port port :reuse-addr t)
           (format t "Bound socket: ~A~%" server)
 
           ;; запустить прослушивание на сокете сервера
           (listen-on server :backlog 5)
           (format t "Listening on socket bound to: ~A:~A~%"
                   (local-host server)
                   (local-port server))

3. Регистрация обработчика прослушивателя на сокете сервера и запуск диспетчеризации событий с мультиплексором:

           ;; Настройка инициализации обработчика прослушивателя для всех входящих клиентов
           (set-io-handler *ex6-server-event-base*
                           (socket-os-fd server)
                           :read
                           (make-ex6-server-listener-handler server))
 
           ;; продолжать бесконечный приём подключений.
           (handler-case
               (event-dispatch *ex6-server-event-base*)
 
             ;; В случае потери обработчиком этих состояний, мы
             ;; будем отлавливать их здесь.
             (socket-connection-reset-error ()
               (format t "~A~A~%"
                       "Caught unexpected reset by peer! "
                       "Client connection reset by peer!"))
             (hangup ()
               (format t "~A~A~%"
                       "Caught unexpected hangup! "
                       "Client closed connection on write!"))
             (end-of-file ()
               (format t "~A~A~%"
                       "Caught unexpected end-of-file! "
                       "Client closed connection on read!"))))

4. Когда сервер прекратит обслуживание клиентов, мы закроем сокет сервера:

      ;; Очищающее выражение для uw-p.
      ;; Убедившись в закрытости сокета сервера, мы покидаем
      ;; сервер.
      (close server))))

5. Обработчик прослушивателя:

После замыкания от этой функции, вызванной мультиплексором на готовом сокете сервера, мы принимаем клиента с блокируемым доступом. Мы сохраняем соединение клиента в нашей таблице и регистрируем замыкание эхо строки с сокетом. Замыкание эхо строки содержит функцию дисконнектора, как было показано в предыдущих примерах мультиплексора.
;; Готовность мультиплексора для чтения
;; даёт нам сигнал о возможности приёма данных от клиентов. Мы будем принимать их и
;; затем регистрировать принятые сокеты клиентов в мультиплексоре
;; с соответствующей функцией эхо протокола.
(defun make-ex6-server-listener-handler (socket)
  (lambda (fd event exception)
 
    ;; осуществлять блокируемый приём, возвращать nil в случае отсутствия сокета
    (let* ((client (accept-connection socket :wait t)))
      (when client
        (multiple-value-bind (who port)
            (remote-name client)
          (format t "Accepted a client from ~A:~A~%" who port)
 
          ;; сохранить соединение клиента для последующего закрытия
          ;; после завершении работы сервера.
          (setf (gethash `(,who ,port) *ex6-server-open-connections*) client)
 
          ;; настроить функцию эха строки для сокета клиента.
          (set-io-handler *ex6-server-event-base*
                          (socket-os-fd client)
                          :read
                          (make-ex6-server-line-echoer
                           client
                           who
                           port
                           (make-ex6-server-disconnector client))))))))

6. Генератор замыкания эхо строки:

Эта функция возвращает замыкание, связывающееся с клиентским сокетом в мультиплексоре. Когда сокет будет готов, мы читаем строку от клиента и записываем её обратно клиенту. После этого следует блокировка Ввода/Вывода в течении которого сервер ожидает окончания транзакции. Это означает что клиент отправивший один ASCII байт не являющийся признаком строки может заблокировать сервер для всех остальных клиентов. Этот серьёзный дефект излечивается не-блокируемым Вводом/Выводом, продемонстрированным на примере ниже.
;; Эта функция возвращает функцию, читающую строку, с последующим
;; возвращением эха в тот сокет откуда было произведено чтение. Это блокируемый Ввод/Вывод.
;; Этот код уязвим для атаки в обслуживании описанной на странице
;; 167 в "Unix Network Programming 2nd Edition: Sockets and XTI" за авторством
;; Ричарда Стивенса (Richard Stevens).
(defun make-ex6-server-line-echoer (socket who port disconnector)
  (format t "Creating line-echoer for ~A:~A~%" who port)
  (lambda (fd event exception)
    (handler-case
        (let ((line (read-line socket))) ;; read a line from the client
          (format t "Read ~A:~A: ~A~%" who port line)
          (format socket "~A~%" line) ;; write it the client
          (finish-output socket)
          (format t "Wrote ~A:~A: ~A~%" who port line)
 
          ;; закрыть подключение к клиенту если он запросил выход
          (when (string= line "quit")
            (format t "Client requested quit!~%")
            (funcall disconnector who port)))
 
      (socket-connection-reset-error ()
        ;; Обрабатывать состояния пока мы будем
        ;; разговаривать с клиентом.
        (format t "Client's connection was reset by peer.~%")
        (funcall disconnector who port))
 
      (hangup ()
        (format t "Client went away on a write.~%")
        (funcall disconnector who port))
 
      (end-of-file ()
        (format t "Client went away on a read.~%")
        (funcall disconnector who port)))))

7. Генератор замыканий дисконнектора:

Эта функция возвращает замыкания, удаляющие все обработчики с сокета и закрывающие их (сокеты). Это означает, что сервер не может обрабатывать пакетный ввод с клиентов, при получении END-OF-FILE от клиента в процессе чтения, поскольку произойдёт нарушение соединения и разрушение всех данных. После закрытия сокета, мы также удаляем его из нашей таблицы открытых соединений.
;; Если мы решил отключить себя от клиента, это приведёт
;; к удалению всех обработчиков и удалению нашего соединения с
;; *ex6-server-open-connections*.
(defun make-ex6-server-disconnector (socket)
  (lambda (who port)
    (format t "Closing connection to ~A:~A~%" who port)
    (remove-fd-handlers *ex6-server-event-base* (socket-os-fd socket))
    (close socket)
    (remhash `(,who ,port) *ex6-server-open-connections*)))

8. Инициализация базы-событий, таблицы подключений и запуск сервера:

Этот код начинается с формы UNWIND-PROTECT, которая защищает ресурсы серверного сокета.
;; Это вводная функция в этот пример.
(defun run-ex6-server (&key (port *port*))
  (let ((*ex6-server-open-connections* nil)
        (*ex6-server-event-base* nil))
    (unwind-protect
         (handler-case
             (progn
               ;; Clear the open connection table and init the event base
               (setf *ex6-server-open-connections*
                     (make-hash-table :test #'equalp)
 
                     *ex6-server-event-base*
                     (make-instance 'event-base))
 
               (run-ex6-server-helper port))
 
           ;; handle a common signal
           (socket-address-in-use-error ()
             (format t "Bind: Address already in use, forget :reuse-addr t?")))

9. Очистка соединений клиента и закрытие базы-событий:

Когда сервер завершает работу мы идём в хэш *ex6-server-open-connections* и закрываем каждого найденного в этом хэше клиента. После завершения этого действия, мы закрываем базу-событий. Такой приём гарантирует правильную очистку мусора.
      ;; Форма очистки для uw-p
      ;; Закрываем все открытые подключения к клиентам. Мы делаем это
      ;; для того, чтобы клиенты знали что сервер прекращает работу
      ;; немедленно. Сокеты не являются памятью, и их нельзя очистить сборщиком
      ;; мусора по первому желанию. Первым делом они должны быть закрыты.
      (maphash
       #'(lambda (k v)
           (format t "Closing a client connection to ~A~%" k)
           ;; We don't want to signal any conditions on the close...
           (close v :abort t))
       *ex6-server-open-connections*)
 
      ;; также мы очищаем мультиплексор!
      (when *ex6-server-event-base*
        (close *ex6-server-event-base*))
      (format t "Server Exited~%")
      (finish-output))))

Этот сервер использует мультиплексор самым простым способом, поскольку зарегистрирован только один обработчик для клиента. Этот обработчик читает, затем пишет эти же данные клиенту. Объём данных, прочитанных от клиента, никогда не покинет обрабатывающую функцию.

Эхо Сервер IPV4/TCP: ex7-server.lisp

Этот пример отличается от ex6-server поскольку этот пример полностью разделяет чтение и запись данных к клиенту в различные обрабатывающие функции. Естественно, это повлечёт за собой архитектурные изменения в порядке работы сервера, поскольку данные от клиентов надо сохранять "где нибудь" пока мультиплексор определит что он может записать данные к клиенту. Мы введём понятие объекта буфера-ввода-вывода (io-buffer), реализованного через замыкание и по одному на каждого клиента, этот буфер будет сохранять данные на лету, пока клиент будет готовиться для получения данных от сервера.

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

При создании буфера-ввода-вывода мы будем принимать ограниченное количество данных от клиента. Конечно, пока мы используем read-line с блочным Вводом/Выводом и клиент может записать огромное количество данных до символа новой строки на сервер, мы не применять какую-либо политику хранения данных на сервере. Клиент может вести себя благоразумно и может отсылать разумное количество информации - но такое поведение большая редкость в настоящем мире и здесь будет уместна наша политика. Когда мы напишем пример сервера с не блокируемым Вводом/Выводом, придёт время разработки политики хранения клиентских данных.

Этот сервер будет поддерживать пакетный ввод от клиента. Когда он увидит END-OF-FILE от клиента, и будут доступны данные для записи, сервер будет ожидать сигнала от мультиплексора о готовности клиента к приёму данных и попытается произвести запись данных на клиента.

Поскольку этот пример состоит из большого количества кода мы будем показывать лишь его различие от ex6-server.

0. Обработчик прослушивателя:

Важный код в этой функции - это вызов make-ex7-io-buffer. Эта функция возвращает замыкание, называемое как io-buffer, принимающий один аргумент: или :read-a-line или :write-a-line. Когда производится funcall io-buffer'а с соответствующим аргументом, возвращается *другое* замыкание и это замыкание регистрируется с соответствующим состоянием готовности в мультиплексоре.
Возвращаемое замыкание связывается в лексическом пространстве хранилища предназначенного для клиента.
Замыкания, возвращённые с :read-a-line и :write-a-line имеют доступ к некоторому пространству хранилища уникальному для объекта io-buffer. Это означает то, что обработчик записи к клиенту может получить доступ к данным, прочитанным обработчиком чтения клиента.
;; Создать замыкание прослушивателя, принимающее клиента и регистрирующее
;; функции буфера с ним.
(defun make-ex7-server-listener-handler (socket)
  (lambda (fd event exception)
    ;; создать блокируемый приём, возвращающий nil при отсутствии сокета
    (let* ((client (accept-connection socket :wait t)))
      (when client
        (multiple-value-bind (who port)
            (remote-name client)
          (format t "Accepted a client from ~A:~A~%" who port)
 
          ;; сохранить соединение клиента в случае когда нам нужно закрыть его позже.
          (setf (gethash `(,who ,port) *ex7-open-connections*) client)
 
          ;; Мы создаём io-buffer, принимающий прочтённые данные с
          ;; сокета и возвращающее эхом прочтённую информацию в
          ;; сокет. Буфер принимает это через два внутренних
          ;; обработчика, обработчик чтения и обработчик записи.
          (let ((io-buffer
                 (make-ex7-io-buffer client who port
                                     (make-ex7-server-disconnector client))))
 
            ;; устанавливаем функцию эха строки для клиентского сокета.
            ;; внутренности буфер будут выполнять соответствующую
            ;; регистрацию/снятие с регистрации требуемых обработчиков
            ;; в нужное время, зависящее от доступности данных.
 
            (set-io-handler *ex7-event-base*
                            (socket-os-fd client)
                            :read
                            (funcall io-buffer :read-a-line))
 
            (set-io-handler *ex7-event-base*
                            (socket-os-fd client)
                            :write
                            (funcall io-buffer :write-a-line))))))))

1. Функция дисконнектора:

Эта функция, в основном, идентична предыдущему примеру используемому в ex5a-client. Единственная разница - это использование специальной переменной.
После того, как io-buffer знает под какими состояниями он должен регистрировать или снимать регистрацию специфичные обработчики для сокета клиента, мы должны выборочно удалять их без воздействия на остальные обработчики.
(defun make-ex7-server-disconnector (socket)
  ;; Когда вызывается эта функция, она должна сообщать какой обратный вызов следует удалить, если
  ;; обратные вызовы не определены, будут удалены все доступные обратные вызовы! Дополнительно
  ;; удаляемый сокет должен быть информирован.
  (lambda (who port &rest events)
    (let ((fd (socket-os-fd socket)))
      (if (not (intersection '(:read :write :error) events))
          (remove-fd-handlers *ex7-event-base* fd :read t :write t :error t)
          (progn
            (when (member :read events)
              (remove-fd-handlers *ex7-event-base* fd :read t))
            (when (member :write events)
              (remove-fd-handlers *ex7-event-base* fd :write t))
            (when (member :error events)
              (remove-fd-handlers *ex7-event-base* fd :error t)))))
    ;; и наконец, если было запрошено закрытие сокета, мы делаем это так
    (when (member :close events)
      (format t "Closing connection to ~A:~A~%" who port)
      (finish-output)
      (close socket)
      (remhash `(,who ,port) *ex7-open-connections*))))

Теперь мы подходим к описанию кодовой базы ex7-io-buffer. Эта кодовая база воздействует напрямую с базой-событий экземпляра мультиплексора с целью регистрации и дерегистрации обработчиков клиента. Обработчики регистрируются только тогда, когда доступны данные на запись или есть свободное место для чтения данных превышающих размер буфера.

0. Генератор замыканий io-buffer и ассоциированного лексического хранилища:

Это переменные, содержащие внутреннее состояния замыкания и принимающие данные от клиента. В расширенном описании - эти переменные хранят информацию о времени регистрации обработчика (с того момента как эти объекты могут регистрировать и дерегистрировать обработчики внутри и снаружи себя) и получили или нет END-OF-FILE от клиента. Функция line-queue получает актуальные данные от клиента.
(defun make-ex7-io-buffer (socket who port disconnector &key (max-bytes 4096))
  (let ((line-queue (make-queue))
        (bytes-left-to-write 0)
        (read-handler-registered nil)
        (write-handler-registered nil)
        (eof-seen nil))

1. Замыкание read-a-line:

Эта функция, которая в конце концов будет зарегистрирована с мультиплексором с момента ожидания аргументов. Её работа - чтение строки с клиента, когда мультиплексор сообщает о том, что клиент доступен для чтения и затем сохраняет строку в очередь-строк (line-queue). Если мы будем читать строку, мы немедленно зарегистрируем обработчик write-a-line с мультиплексором, такой приём даёт нам возможность узнавать когда клиент будет доступен для получения данных. Если данные перейдут заданный пик данных (который устанавливается нами), мы дерегистрируем обработчик чтения и прекратим получение данных. Если мы получим END-OF-FILE, но ничего не останется для записи, то в этом случае обработчик произведёт маленькую оптимизацию и закроет сокет на клиента и дерегистрирует всё. Это защита от ненужных циклов через мультиплексор в данном случае.
Обработка END-OF-FILE - это интересно в случае дерегистрации считывающего обработчика, означает что нам ничего больше не нужно и достижение END-OF-FILE. В этой точке, единственная вещь, которую может сделать мультиплексор с соответствующим клиентом - это записать все строки сохранённые в очереди-строк (line-queue) клиенту и закрыть подключение к клиенту.
Из всех сигнализируемых состояний, только состояние SOCKET-CONNECTION-RESET-ERROR может закрыть соединение через удаление всех обработчиков в мультиплексоре для этого клиента и окончательно выкинуть все оставшиеся данные.
    (labels
        ;; Если от этой функции поступит сигнал о существовании данных на запись, будет
        ;; установлен io-handler на сокет для обработчика записи.
        ;; Если функция уведомит о том что она прочитала >= максимальному количеству байтов
        ;; данные будут удалены из неё с помощью обработчика *после* того, как
        ;; обработчик записи будет правильно установлен.
        ((read-a-line (fd event exception)
           (handler-case
               (let ((line (format nil "~A~%" (read-line socket)))) ; add a \n
                 (format t "Read from ~A:~A: ~A" who port line)
                 (enqueue line line-queue)
                 (incf bytes-left-to-write (length line))
 
                 (when (> bytes-left-to-write 0)
                   ;; Если обработчик записи не зарегистрирован, то
                   ;; регистрация произойдёт с момента получения данных на запись.
                   (unless write-handler-registered
                     (set-io-handler *ex7-event-base*
                                     (socket-os-fd socket)
                                     :write
                                     #'write-a-line)
                     (setf write-handler-registered t)))
 
                 ;; Теперь, если есть больше данных, чем возможно для чтения,
                 ;; удалить себя с обработчика ввода-вывода. Когда
                 ;; обработчик записи уведомит об этом, после записи некоторых
                 ;; данных, оставшиеся за пределами чтения, будет произведена перерегистрация
                 ;; обработчика ввода-вывод для сокета чтения.
                 (when (>= bytes-left-to-write max-bytes)
                   (funcall disconnector who port :read)
                   (setf read-handler-registered nil)))
 
             (socket-connection-reset-error ()
               ;; Если клиент сбрасывает свои соединения, мы выключим
               ;; всё.
               (format t "Client ~A:~A: Connection reset by peer~%" who port)
               (funcall disconnector who port :close))
 
             (end-of-file ()
               ;; Когда мы получим обозначение конца файла, это не обязательно
               ;; означает что клиент покинул нас, это обозначает что
               ;; клиент выполнил завершение записи на
               ;; своём сокете и теперь ожидает данные находящиеся на
               ;; сервере для приёма. Однако, если
               ;; ничего не осталось для записи и наш считывающий конец закрыт,
               ;; мы рассматриваем это как уход клиента и
               ;; закрываем подключение.
               (format t "Client ~A:~A produced end-of-file on a read.~%"
                       who port)
               (if (zerop bytes-left-to-write)
                   (funcall disconnector who port :close)
                   (progn
                     (funcall disconnector who port :read)
                     (setf read-handler-registered nil)
                     (setf eof-seen t))))))

2. Замыкание write-a-line:

Эта функция - немного симметрична к read-a-line. Она регистрирует и дерегистрирует себя или обработчик чтения в зависимости от данных доступных к чтению/записи. Если достигнут END-OF-FILE и нет данных на запись, эта функция закроет соединение к клиенту и дерегистрирует всё остальное.

         ;; Эта функция проверяет и при наличии достаточного количества байтов к
         ;; байтам-оставшихся-на-запись свыше максимального-количества-байтов, она перерегистрирует
         ;; обработчик ввода/вывода считывателя. Если нет данных на запись, эта функция
         ;; проверит обработчик чтения на зарегистрированность, дерегистрирует себя
         ;; и не будет вызываться на сокете готовом к чтению
         ;; без данных на запись.
         (write-a-line (fd event exception)
           (handler-case
               (progn
                 ;; If we have something to write to the client, do so.
                 (when (> bytes-left-to-write 0)
                   (let ((line (dequeue line-queue)))
                     (format socket "~A" line) ;; newline is in the string.
                     (finish-output socket)
                     (format t "Wrote to ~A:~A: ~A" who port line)
                     (decf bytes-left-to-write (length line))))
 
                 ;; Если мы увидим, что мы упали ниже отметки максимального-количества-байтов,
                 ;; обработчик чтение перерегистрируется для получения следующих данных для
                 ;; нас. Однако, перерегистрация обработчика записи не произойдёт 
                 ;; если клиент закрыл считывающий конец
                 ;; нашего сокета.
                 (when (< bytes-left-to-write max-bytes)
                   (unless (or eof-seen read-handler-registered)
                     (set-io-handler *ex7-event-base*
                                     (socket-os-fd socket)
                                     :read
                                     #'read-a-line)
                     (setf read-handler-registered t)))
 
                 ;; Если мы увидим, что нет достаточно данных на запись
                 ;; И достигли конца файла с клиента,
                 ;; то мы закроем соединение к клиенту
                 ;; поскольку он ничего нам не сообщает и мы завершим
                 ;; общение с этим клиентом. 
                 ;;
                 ;; Если мы увидим, что записали все наши данные и
                 ;; должны ещё что-то сделать позже, то излишне
                 ;; будет производить дерегистрацию.
                 ;; Это означает, что временами мы
                 ;; нам нужно произвести проход через
                 ;; диспетчер-событий для осуществления записи если мы прочитали
                 ;; что-то больше от клиента и это дерегистрировало нас.
                 (when (zerop bytes-left-to-write)
                   (if eof-seen
                       (funcall disconnector who port :close)
                       (progn
                         (funcall disconnector who port :write)
                         (setf write-handler-registered nil)))))
 
             (socket-connection-reset-error ()
               ;; Если получилось так, что Я получил сброс, убедимся что соединение
               ;; закрыто. Я не получу этого здесь, но если Вы
               ;; столкнётесь с подобным, это будет хорошей
               ;; защитой.
               (format t "Client ~A:~A: connection reset by peer.~%" who port)
               (funcall disconnector who port :close))
 
             (hangup ()
               ;; В этом сервера, если клиент не получает данных,
               ;; это означает что он никогда снова не отправит нам данные. Таким образом
               ;; будет лучше закрыть соединение.
               (format t "Client ~A:~A got hangup on write.~%" who port)
               (funcall disconnector who port :close)))))

3. Возвращённое замыкание, представляющее io-buffer:

Это замыкание возвращается с помощью make-ex7-io-buffer и используется для получения доступа к функциям read-a-line и write-a-line. Оно берёт единственный аргумент, либо :read-a-line, либо :write-a-line и возвращает ссылку на внутреннюю функцию.
      ;; Эта функция возвращается из make-ex7-io-buffer,
      ;; что позволяет нам получить доступ к чтению/записи в пределах
      ;; замыкания. Мы должны запрашивать корректную функцию при установке
      ;; обработчиков ввода/вывода. ПОМНИТЕ: С помощью простого запроса к обработчику,
      ;; Я вставляю это в обработчик iolib
      ;; событий. Вот почему они должны регистрироваться в этой точке.
      (lambda (msg)
        (cond
          ((equalp msg :read-a-line)
           (setf read-handler-registered t)
           #'read-a-line)
          ((equalp msg :write-a-line)
           (setf write-handler-registered t)
           #'write-a-line)
          (t
           (error "make-ex7-buffer: Please supply :read-a-line or :write-a-line~%")))))))

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

Эхо Сервер IPV4/TCP: ex8-server.lisp

Этот сервер использует неблокируемый Ввод/Вывод и мультиплексор для одновременной работы с несколькими клиентами.

Архитектурно этот сервер очень похож на ex7-server, но есть внутренние отличия в реализации io-buffer. Для начала: ex7-server осуществляет чтение с клиента через использование функции потока READ-LINE, запись использует функцию FORMAT с сохранением строк в очереди, теперь же мы будем использовать RECEIVE-FROM и SEND-TO с массивом без знаковых байтов в качестве буфера для чтения/записи байтов с сокета.

Доступ к сокету через API потока отличается от доступа через API сырого сокета (мы будем использовать API сырого сокета). RECEIVE-FROM и SEND-TO не являются частью интерфейса потока. Они нижележащий уровень API в IOLib и находится ближе к абстракции Операционной Системы, как следствие, имеют другой набор сигнализируемых состояний. Эти отличающиеся состояния имеют следующие формы: isys: like: isys:epipe, isys:ewouldblock и т.д. Есть некоторые пересечения с именами состояний, сигнализируемым от API потока, например: SOCKET-CONNECTION-RESET-ERROR и SOCKET-CONNECTION-REFUSED.

Пример ответвления этих API - это RECEIVE-FROM. Отличие от интерфейса потока выражается в том, что READ-LINE сигнализирует о END-OF-FILE при чтении из сокета, закрытого клиентом, а функция RECEIVE-FROM вернёт 0, обозначая этим конец файла. Функция потока FORMAT сигнализирует HANGUP в случае если она пытается записать в сокет уже ушедшего клиента. SEND-TO не выдаст никакого сигнала или какого-либо уведомления об ошибке при записи в сокет уже ушедшего клиента - обычно ошибка проявляется при следующем RECEIVE-FROM, который обнаруживает, что клиент отключился от сервера. Байты переданные через SEND-TO попросту исчезнут!

Возможно это будет для Вас сюрпризом, с IOLib все основные fds в предыдущих примерах были не блокируемым! Вот почему мы назначали :wait t для ACCEPT-CONNECTION и CONNECT.

Библиотека IOLib определяет соответствует ли блокировки интерфейса потока требованиям ANSI Common Lisp. Однако, когда мы используем SEND-TO и RECEIVE-FROM мы автоматически получаем статус не-блочной работы нижележащей fd. По этой причине мы явно не устанавливаем нижележащий fd в статус не-блочной работы - статус уже установлен!

Код сервера представлен как отличие от ex7-server, но io-buffer для этого не-блочного сервера (в файле ex8-buffer.lisp) будет описан полностью. Также, этот сервер поддерживает требование пакетного ввода от клиента ex-5b-client, именно этот клиент следует использовать совместно с данным сервером.

Код ex8-server:

0. Обработчик прослушивателя (первая половина):

Принять и сохранить клиентское соединение.
(defun make-ex8-server-listener-handler (socket)
  (lambda (fd event exception)
    ;; установить блокируемый приём, вернуть nil в случае отсутствия сокета
    (let* ((client (accept-connection socket :wait t)))
      (when client
        (multiple-value-bind (who port)
            (remote-name client)
          (format t "Accepted a client from ~A:~A~%" who port)
 
          ;; сохранить клиентское соединение для случая, когда это соединение нам понадобится позднее.
          (setf (gethash `(,who ,port) *ex8-open-connections*) client)

1. Обработчик прослушивателя (вторая половина):

Подобно ex7-server, мы регистрируем обработчики чтения и записи. Но всё же помните, что мы изменили ключевые слова для замыкания io-buffer на :read-some-bytes и :write-some-bytes. Будет лучше если мы покажем что делает io-buffer.
          ;; Мы создаём io-buffer, он берёт на себя задачу по считыванию данных из
          ;; сокета и дублированию информации обратно в
          ;; сокет. Буфер выполняет эту задачу с помощью двух внутренних
          ;; обработчиков: обработчика чтения и обработчика записи.
          (let ((io-buffer
                 (make-ex8-io-buffer client who port
                                     (make-ex8-server-disconnector client))))
 
            ;; устанавливаем функцию эха без знаковых байтов для
            ;; клиентского сокета. Буфер, будет производить внутри себя
            ;; соответствующие регистрации/дерегистрации
            ;; требуемых обработчиков в нужное время и при
            ;; наличии данных.
 
            (set-io-handler *ex8-event-base*
                            (socket-os-fd client)
                            :read
                            (funcall io-buffer :read-some-bytes))
 
            (set-io-handler *ex8-event-base*
                            (socket-os-fd client)
                            :write
                            (funcall io-buffer :write-some-bytes))))))))

В остальном сервер чрезвычайно похож на ex7-server.

Теперь, мы покажем io-buffer этого сервера.

0. Внутреннее состояние замыкания io-buffer:

Связка echo-buf - это массив без знаковых байтов размером равным max-bytes. Это место в котором сохраняются данные от клиента до передачи их обратно отправителю.
Связка чтение-индекс (read-index) содержит указание на свободное пространство в буфере echo-buf, где можно хранить данные во время чтения.
Связка запись-индекс (write-index) содержит указание на количество прочитанных данных от клиента. Она перемещается к read-index и когда он имеет некоторое значение в виде read-index - это означает что не осталось данных на передачу к клиенту.
Связки чтение-обработчик-регистрация (read-handler-registered) и запись-обработчик-регистрация (write-handler-registered) позволяет io-buffer'у узнавать когда зарегистрирован обработчик для чтения и записи данных.
Связка eof-seen ставит пометку о том, когда клиент закрыл своё соединение для записи на сервер. После этого сервер вытолкнет все данные клиенту и закроет сокет на клиента.
(defun make-ex8-io-buffer (socket who port disconnector &key (max-bytes 16384))
  (let ((echo-buf (make-array max-bytes :element-type 'unsigned-byte))
        (read-index 0)
        (write-index 0)
        (read-handler-registered nil)
        (write-handler-registered nil)
        (eof-seen nil))

1. Считывание байтов с клиента:

В этой функции, мы будем конвертировать значение 0 возвращаемое функцией RECEIVE-FROM после считывания данных с закрытого сокета в сигнализируемое состояние END-OF-FILE для сохранения структуры нашего кода. После прочтения некоторого количества байтов, мы инкрементируем указатель read-index для гарантии регистрации обработчика записи предназначенного для возвращения данных. Мы оптимизируем процесс записи и попытаемся осуществить немедленную запись данных без проверки готовности сокета. Далее, если свободное место в массиве echo-buf закончится, мы дерегистрируем себя, это приведёт к невозможности дальнейших попыток чтения данных от клиента, до тех пор, пока мы не будем готовы принять их (с возможностью возврата всех данных клиенту). Мы пометим флаг END-OF-FILE и дерегистрируем обработчик чтения если мы увидим что клиент закрыл своё соединение. Мы оптимизируем поведение программы так, чтобы после окончания данных на запись соединение к клиенту закрывалось.
    (labels
        ;; Это функция ответственная за чтение байтов с клиента.
        ((read-some-bytes (fd event exception)
           (handler-case
               (progn
                 ;; Читать столько, сколько мы сможем.
                 (multiple-value-bind (buf bytes-read)
                     (receive-from socket
                                   :buffer echo-buf
                                   :start read-index
                                   :end max-bytes)
 
                   ;; В отличие от чтения с потока, receive-from
                   ;; возвращает нуль при чтении end-of-file, поэтому мы возвращаемся
                   ;; и сигнализируем о наступлении этого состояния для
                   ;; того, чтобы handler-case решил что делать с этим состоянием как это было в наших
                   ;; других примерах.
                   (when (zerop bytes-read)
                     (error 'end-of-file))
 
                   (format t "Read ~A bytes from ~A:~A~%" bytes-read who port)
                   (incf read-index bytes-read))
 
                 ;; Регистрируем обработчик записи в случае наличия данных на
                 ;; запись.
                 ;;
                 ;; Затем, попытаемся сразу записать некоторые данные в сокет,
                 ;; хотя сокет может быть неготовым, 
                 ;; этот трюк - способ сэкономить код.
                 ;; Функция write-some-bytes avoid another go around.
                 ;; должна обрабатывать econnreset поскольку
                 ;; это соединение может быть закрыто в момент данного
                 ;; вызова. Обычно, если мультиплексор сказал мне
                 ;; о том, что Я могу писать, то всё должно сработать хорошо, но, поскольку эта запись
                 ;; находится за пределом мультиплексора и оптимизации,
                 ;; нужно производить проверку.
                 (when (/= write-index read-index)
                   (unless write-handler-registered
                     (set-io-handler *ex8-event-base*
                                     (socket-os-fd socket)
                                     :write
                                     #'write-some-bytes)
                     (setf write-handler-registered t))
 
                   ;; Если Я могу писать - Я делаю это немедленно!
                   (write-some-bytes fd :write nil))
 
                 ;; Если Я нахожусь за пределами свободного места для хранения данных, то удалю
                 ;; себя из обработчика ввода/вывода. Когда обработчик записи
                 ;; уведомит что он завершил запись всех данных,
                 ;; все индексы установятся в 0 и обработчик
                 ;; записи удалит себя. Если в вызове выше
                 ;; работает write-some-bytes, то read-index не должен
                 ;; равняться max-bytes при исполнении этих строк кода. 
                 (when (= read-index max-bytes)
                   (funcall disconnector who port :read)
                   (setf read-handler-registered nil)))
 
             (socket-connection-reset-error ()
               ;; Обрабатывать отправку reset'а клиентом.
               (format t "Client ~A:~A: connection reset by peer.~%" who port)
               (funcall disconnector who port :close))
 
             (end-of-file ()
               ;; Когда он получит конец файла, это не обязательно означает что
               ;; клиент ушёл, возможно что
               ;; клиент выполнил выключение записи на конце
               ;; своего сокета и ожидает записи сохранённых от
               ;; сервера. Однако, если
               ;; не осталось данных на запись и наш конец закрыт,
               ;; мы делаем вывод что клиент покинул нас и
               ;; закрываем соединение.
               (format t "Client ~A:~A produced end-of-file on a read.~%"
                       who port)
               (if (= read-index write-index)
                   (funcall disconnector who port :close)
                   (progn
                     (funcall disconnector who port :read)
                     (setf read-handler-registered nil)
                     (setf eof-seen t))))))

2. Запись байтов на клиента:

Пока есть байты на запись, мы записываем их, одновременно отслеживая количество записанных байтов. Когда байты закончатся, мы дерегистрируем обработчик записи, так как не следует вызывать функцию лишний раз - обычно клиентский сокет всегда готов на запись. Если мы увидим метку конца-файла и у нас нет данных, мы закрываем клиентское соединение и на этом работа закончена. Если мы не видим конец файла, то выясняем находимся ли мы в конце буфера, если это так и есть, мы сбрасываем индексы на начало. В противном случае, мы перерегистрируем обработчик чтения для получения дополнительных данных.
Здесь мы обрабатываем некоторые новые состояния: isys:ewouldblock - нужен, поскольку иногда нижележащая Операционная Система помечает fd как готовый к записи, когда по факту он не готов для записи и это обнаруживается при записи на него. Мы должны следить за этим состоянием при попытке оптимизации записи данных в считывающий обработчик, так как мы делаем это за пределами мультиплексора - это идиоматичный случай и экономит проход через мультиплексор. Слежение за isys:ewouldblock просто прекращает запись и мы должны повторить попытку записи позже. Под некоторыми состояниями, send-to сигнализирует об ошибке isys:epipe, что в свою очередь служит обозначением того, что клиент закрыл своё соединение. Это похоже на состояние HANGUP в формате вызова с API потока. Мы будем рассматривать это состояние так же как и классический HANGUP.
         ;; Эта функция ответственна за запись байтов к клиенту.
         (write-some-bytes (fd event exception)
           (handler-case
               (progn
                 ;; Если есть данные на запись, записываем их. ПОМНИТЕ:
                 ;; Нет средств определения ошибок случившихся при записи
                 ;; через send-to. Если Я пишу в закрытый
                 ;; клиентом) сокет, send-to может ничего
                 ;; не сказать о произошедших неполадках и вернуть число записанных
                 ;; байтов. В этом случае, ничего записано не будет но мы
                 ;; не узнаем. Обычно в этом случае,
                 ;; обработчик считывания вернёт количество прочитанных байтов равный 0 на данном сокете
                 ;; и мы можем узнать что соединение было нарушено.
                 (when (> read-index write-index)
                   (let ((wrote-bytes (send-to socket echo-buf
                                               :start write-index
                                               :end read-index)))
                     (format t "Wrote ~A bytes to ~A:~A~%" wrote-bytes who port)
                     (incf write-index wrote-bytes)))
 
                 ;; Если мы увидим что у нас есть данные на запись и достигли конца файла,
                 ;; то закрываем соединение, и всё. Если мы не видим
                 ;; конца файла, то дерегистрируем обработчик записи и перерегистрируем
                 ;; обработчик чтения для получения следующих данных. Если индексы буфера
                 ;; находятся в самом конце, мы сбрасываем их на начало.
                 (when (= read-index write-index)
                   (if eof-seen
                       (funcall disconnector who port :close)
                       (progn
 
                         ;; если нет ничего на запись, то дерегистрируем запись
                         (funcall disconnector who port :write)
                         (setf write-handler-registered nil)
 
                         ;; Если мы находимся в конце буфера, то перемещаемся
                         ;; к началу и получаем больше пространства для данных
                         (when (= read-index write-index max-bytes)
                           (setf read-index 0
                                 write-index 0))
 
                         ;; Перерегистрируем обработчик чтения для получения следующих данных
                         (unless read-handler-registered
                           (set-io-handler *ex8-event-base*
                                           (socket-os-fd socket)
                                           :read
                                           #'read-some-bytes)
                           (setf read-handler-registered t))))))
 
             (socket-connection-reset-error ()
               ;; Если по каким-либо причинам клиент сбросит сетевое подключение
               ;; мы получим этот сигнал.
               (format t "Client ~A:~A: connection reset by peer.~%" who port)
               (funcall disconnector who port :close))
 
             (isys:ewouldblock ()
               ;; Иногда это случается при записи даже если
               ;; помечено как готовый. Также мы должны запрашивать
               ;; разрешение на запись у сокета с ниезвестным статусом. Игнорируем
               ;; и повторяем попытку снова.
               (format t "write-some-bytes: ewouldblock~%")
               nil)
 
             (isys:epipe ()
               ;; В этом сервере, если клиент не принимает данные,
               ;; то это означает, что клиент не отправит нам данные снова. В этом случае
               ;; закроем соединение от греха подальше.
               (format t "Client ~A:~A got hangup on write.~%" who port)
               (funcall disconnector who port :close)))))

3. Возвращённое замыкание io-buffer'а:

Так же как и в make-ex7-io-buffer, мы возвращаем один из внутренних замыканий, соответствующих для чтения или записи мультиплексором.
      ;; Эта функция возвращается из make-ex8-io-buffer, что позволяет
      ;; нам получать доступ на чтение/запись в области
      ;; замыкания. Мы будем запрашивать правильные функции при установке
      ;; обработчиков. ПОМНИТЕ: С помощью простого запроса к обработчику,
      ;; Я предполагаю его немедленную вставку в обработчик iolib
      ;; событий. Именно поэтому они считаются зарегистрированными в этой
      ;; точке.
      (lambda (msg)
        (cond
          ((equalp msg :read-some-bytes)
           (setf read-handler-registered t)
           #'read-some-bytes)
          ((equalp msg :write-some-bytes)
           (setf write-handler-registered t)
           #'write-some-bytes)
          (t
           (error "make-ex8-buffer: Please supply :read-some-bytes or :write-some-bytes~%")))))))