Как мы все знаем, эхо-сервер — это сервер, который читает из сокета и записывает эти самые данные в другой сокет.
Поскольку порты завершения ввода-вывода 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 сокетов?