Как лучше всего реализовать эхо-сервер с асинхронным вводом-выводом и IOCP?

Как мы все знаем, эхо-сервер — это сервер, который читает из сокета и записывает эти самые данные в другой сокет.

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

Мои классы: Stream, который абстрагирует сокет, именованный канал или что-то еще, и IoRequest, который абстрагирует как структуру OVERLAPPED, так и буфер памяти для выполнения ввода-вывода (конечно, подходит как для чтения, так и для записи). Таким образом, когда я выделяю IoRequest, я просто выделяю память для буфера памяти для структуры данных + OVERLAPPED за один раз, поэтому я вызываю malloc() только один раз. Вдобавок к этому я также реализую в объекте IoRequest причудливые и полезные вещи, такие как атомарный счетчик ссылок и так далее.

Сказав это, давайте рассмотрим способы сделать лучший эхо-сервер:

-------------------------------------------- Метод А. --- ---------------------------------------

1) Сокет «читатель» завершает чтение, обратный вызов IOCP возвращается, и у вас есть IoRequest, только что завершенный с буфером памяти.

2) Скопируем только что полученный буфер с "читателя" IoRequest на "писатель" IoRequest. (это будет включать memcpy() или что-то еще).

3) Давайте снова запустим новое чтение с ReadFile() в «считывателе», с тем же IoRequest, что и для чтения.

4) Давайте запустим новую запись с WriteFile() в «писателе».

-------------------------------------------- Метод Б. --- ---------------------------------------

1) Сокет «читатель» завершает чтение, обратный вызов IOCP возвращается, и у вас есть IoRequest, только что завершенный с буфером памяти.

2) Вместо копирования данных передайте этот IoRequest "писателю" для записи, без копирования данных с помощью memcpy().

3) «Читатель» теперь нуждается в новом IoRequest, чтобы продолжить чтение, выделить новый или передать уже выделенный ранее, возможно, только что завершенный для записи, прежде чем произойдет новая запись.


Итак, в первом случае у каждого Stream объекта есть свой IoRequest, данные копируются memcpy() или подобными функциями, и все работает нормально. Во втором случае 2 объекта Stream действительно передают IoRequest объекты друг другу без копирования данных, но это немного сложнее, вам нужно управлять «перестановкой» IoRequest объектов между 2 Stream объектами, с возможным недостатком, чтобы получить проблемы с синхронизацией (как насчет тех завершений, которые происходят в разных потоках?)

Мои вопросы:

Q1) Действительно ли стоит избегать копирования данных!? Копирование 2 буферов с memcpy() или подобным очень выполняется быстро, в том числе потому, что для этой цели используется кэш ЦП. Давайте рассмотрим, что с первым методом у меня есть возможность эха из сокета «читатель» в несколько сокетов «писатель», но со вторым я не могу этого сделать, так как я должен создать N новых IoRequest объектов для каждого N писатели, так как каждому WriteFile() нужна своя структура OVERLAPPED.

Q2) Я предполагаю, что когда я запускаю новые записи N для N разных сокетов с WriteFile(), я должен предоставить N разных структур OVERLAPPED И N разных буферов, где можно читать данные. Или я могу запустить N WriteFile() вызовов с N разными OVERLAPPED, берущими данные из одного и того же буфера для N сокетов?


person Marco Pagliaricci    schedule 17.10.2014    source источник
comment
Эхо данных — это все, что вам нужно? Рассмотрите возможность использования .NET. .NET memcpy работает так же быстро, как C++. Вероятно, при такой рабочей нагрузке большая часть ЦП не будет расходоваться на управляемый код. Многие из ваших проблем исчезнут с .NET.   -  person usr    schedule 19.10.2014
comment
Спасибо, уср. Эхо данных - это не то, что нужно только мне. Я просто интересовался в общем смысле. Я использую C++, поэтому я не могу использовать .NET и управляемые вещи. Мне было интересно, могу ли я избежать memcpy() для этой цели, поскольку IOCP могут позволить вам это сделать.   -  person Marco Pagliaricci    schedule 19.10.2014


Ответы (1)


Стоит ли избегать копирования данных!?

Зависит от того, сколько вы копируете. 10 байт, не так уж и много. 10Мб, то да, стоит избегать копирования!

В этом случае, поскольку у вас уже есть объект, содержащий данные rx и блок OVERLAPPED, копировать его кажется несколько бессмысленным — просто перевыпустите его в WSASend() или как-то еще.

but with the second one I can't do that

Вы можете, но вам нужно абстрагировать класс «IORequest» от класса «Buffer». Буфер содержит данные, счетчик ссылок atomic int и любую другую управляющую информацию для всех вызовов, IOrequest блок OVERLAPPED и указатель на данные и любую другую управляющую информацию для каждого вызова. Эта информация может иметь атомарный счетчик ссылок int для объекта буфера.

IOrequest — это класс, который используется для каждого вызова отправки. Поскольку он содержит только указатель на буфер, нет необходимости копировать данные, поэтому он достаточно мал и равен O(1) размеру данных.

Когда приходит завершение tx, потоки обработчика получают IOrequest, удаляют ссылку из буфера и уменьшают атомарный int в нем до нуля. Поток, которому удается достичь 0, знает, что объект буфера больше не нужен, и может удалить его (или, что более вероятно, на высокопроизводительном сервере, перераспределить его для последующего повторного использования).

Или я могу запускать N вызовов WriteFile() с N различными OVERLAPPED, берущими данные из одного и того же буфера для N сокетов?

Да, ты можешь. См. выше.

Ре. threading - конечно, если ваши «данные управления» могут быть доступны из нескольких потоков обработчика завершения, тогда да, вы можете защитить их с помощью критической секции, но атомарный int должен подойти для счетчика ссылок буфера.

person Martin James    schedule 18.10.2014
comment
Перечитав мой ответ сегодня утром, кажется, не все так ясно. Тем не менее, он был принят, так что вы, должно быть, получили от него некоторую помощь :) - person Martin James; 19.10.2014
comment
Да, это было очень полезно. Единственный момент, о котором я подумал, это то, что обычно я размещаю структуру OVERLAPPED в той же памяти буферной памяти, просто чтобы иметь только 1 вызов malloc(). Как вы думаете, лучше иметь ref-счетчик в IORequest классе или только в буфере, и оставить IORequest классу управлять только OVERLAPPED структурами? В последнем случае мне приходится звонить malloc() 2 раза. - person Marco Pagliaricci; 19.10.2014
comment
«Я размещаю структуру OVERLAPPED в той же памяти буфера», да, это часто делается. Я писал только серверы «веб-стиля», а не «трансляции/чаты», так что это новая область для меня. (например, я использую WSASend(), который принимает массив буферов :). Я как бы думаю, что должен быть способ избежать дополнительного malloc. Я подумаю об этом еще. Было бы лучше, если бы у меня не было этого вонючего похмелья от вчерашнего пивного фестиваля :( - person Martin James; 19.10.2014
comment
Ржу не могу! Кстати, разве это не проблема веб-серверов, с которыми вы работали в прошлом? Я имею в виду, например, асинхронное чтение файлов с помощью ReadFile(), который заполняет некоторые пакеты OVERLAPPED+buffer, а затем вы передаете эти большие пакеты в WSASend(), который будет отправлять данные в клиентский сокет, например, через протокол HTTP. - person Marco Pagliaricci; 19.10.2014
comment
Для широковещательной ситуации у меня есть отдельный класс «дескриптор буфера», который реализует тот же интерфейс, что и обычный «буфер», но в то время как обычный буфер представляет собой блок памяти, WSABUF и OVERLAPPED, «дескриптор буфера» является указателем на буфер , WSABUF и OVERLAPPED. Чтобы передать буфер, вы просто прикрепляете его к дескрипторам буфера для каждой отправки. Дескрипторы буфера подсчитываются, как и буферы, и содержат ссылку на исходный буфер. Это позволяет избежать копирования в широковещательном случае 1->N и не усложняет случай 1->1. - person Len Holgate; 03.11.2014