LINUX.ORG.RU

Расскажите в деталях как пользоваться неблокирующими сокетами

 , ,


0

2

Собственно, сабж. Хочу повысить собственный навык network programming до уровня, достаточно для написания веб сервера, принимающего множество подключений.

Что удалось понять самостоятельно:

  1. Использование блокирующих сокетов и запись/чтение через классические read()/write() в потоке для каждого подключения имеет смысл только если сервер не рассчитан на большое кол-во подключения (ибо большое кол-во потоков будет жрать память) в пользу простоты кода.
  2. Использование неблокирующих сокетов имеет смысл если нужно принимать много подключений, и данных оттуда могут поступать не сразу/медленно/с задержкой. Посему можно набрать пачку подключений и потом poll()-ить их.
  3. Создание потока (posix threads) или процесса (fork()) - довольно затратная операция, и потому для отзывчивого и/или производительного сервера есть смысл создавать потоки/процессы заранее, а потом передавать им дескрипторы.
  4. Использование блокирующих вызовов ввода-вывода (т.е. write() и read()) не позволяет добиться информации о ошибках типа «потеря соединения» или «network is unreachable», и потому приходится использовать неблокирующие сокеты и соответствующие вызовы.

Что не удалось понять и требуются пояснения:

  1. Как передать дескриптор уже существующему потоку? Я так понял, что никаких спец. техник не нужно, поток просто должен знать номер и всё.
  2. Для передачи дескриптора уже существующему процессу (который создали при помощи fork()) нужно что-то шаманить с sendmsg(). Дальше я не копал. Судя по всему, этим не особо активно пользуются в современном софте, хотя хз. К слову, я так и не понял чем именно sendmsg() заставляет получить дескриптор в процессе, а не просто передаёт информацию.
  3. Если send() и другие функции из той же семьи возвращают кол-во переданных/полученных байт, то как они могут что-то вернуть, если они сразу возвращают управление? Что они возвращают в таком случае?
  4. Можно ли использовать буфер сразу после того, как send() вернул управление? Если да, то он, получается, сначала копирует буфер в пространство ядра, а потом возвращает управление (т.е. send() всё-таки не мгновенно возвращает, лол). Если нет, то когда можно снова использовать буфер? Нужно poll()-ить и ждать пока poll() вернёт результат по соответствующему дескриптору?
  5. Если poll()-ить менее пары десятков дескрипторов, можно ли забить болт на epoll() или kqueue()?
  6. Есть ли какой-то годный туториал (от простого к сложному, можно и на английском), который бы посвятил бы во всякие тонкости типа тех, которые будут в следующем пункте:
  7. Если poll() сообщает, что есть входящие данные по дескриптору, но чтение их возвращает 0, означает ли это, что удалённый хост просто сделал close()?
  8. Зачем нужен shutdown(), если есть обычный close()?

Ну и попутный вопрос - заметил, что glibc жруч по части кол-ва системных вызовов, да и вообще, по жиробасности итогового бинарника. Кто-то в продакшне юзал musl libc, и нет ли с ним серьезных подводных камней?

IECTA ()

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

anonymous ()
Ответ на: комментарий от anonymous

маны недостаточно информативны и не очень пригодны для обучения
на некоторые вопросы банально не знаешь как составить поисковый запрос, например на №3 гугл какую-то херню выдаёт

IECTA ()
Ответ на: комментарий от IECTA

не правда, send всегда возвращает количество байт который он отправил(пусть даже себе в буфер), и вернул вам управление, дальше вы должны самостоятельно решать что вам делать с уже отправленными даными в буфере, и с теми что остались не отправлены, обычно send'у скармливают указатель на буфер, который по результату send, двигают вперед

ответы на это есть в любом сетевом проекте или либе

anonymous ()
Ответ на: комментарий от anonymous

send всегда возвращает количество байт который он отправил(пусть даже себе в буфер)

Я так понимаю, что из вашего выражения следует, что
1. send() всегда копируеут данные в пространство ядра, а потом возвращает управление
2. Что, если он забрал себе данные в пространство ядра, а отправить их по сети потом не удалось? Это же полная лажа и я не смогу это отследить!

дальше вы должны самостоятельно решать что вам делать с уже отправленными даными в буфере

Ну это же логично

и с теми что остались не отправлены, обычно send'у скармливают указатель на буфер, который по результату send, двигают вперед

Да, я читал, что с сокетами, в отличие от файлов, вполне нормальна ситуация типа

char buffer[1000];

...

ssize_t bytes_send = send(fd, buffer, sizeof(buffer));
и bytes_send будет скорее всего меньше sizeof(buffer) после первого вызова.

И потому потом надо вызывать send() снова, но уже с смещением (buffer + bytes_send) и в количестве (sizeof(buffer) - bytes_send)

IECTA ()

Последнее исправление: beastie 09.06.2017 18:50:09 (всего исправлений: 2)

beastie, спасибо за редактирование. На этом форуме списки с круглой скобкой не одобряются?.. Просто интересно.

IECTA ()
Ответ на: комментарий от IECTA

2. Что, если он забрал себе данные в пространство ядра, а отправить их по сети потом не удалось? Это же полная лажа и я не смогу это отследить!

send может быть как:
udp - где нет гарантии что они дошли
и
tcp - вы не контролируете канал, за вас это делает tcp, а про надежность доставки это отдельная тема, tcp keep-alive итд, где поверх tcp добавляют свой уровень абстракции который и будет проверять надежность доставки
поэтому отправленный буфер обычно еще держат в запасе, и удаляют только тогда когда ваш уровень абстракции скажет что данные получила та сторона, либо уже истек срок актуальности этих данных в буфере

anonymous ()
Ответ на: комментарий от anonymous

у меня tcp

где поверх tcp добавляют свой уровень абстракции который и будет проверять надежность доставки

омг. Т.е. используя tcp нужно еще ДОПОЛЬНИТЕЛЬНО проверять всё ли пришло на принимающей стороне? В чём тогда смысл этого tcp, если я не могу узнать полученны ли данные на другом конце без доп. костылей? Херня какая-то...

IECTA ()
Ответ на: комментарий от IECTA

tcp это канал передачи данных, если в нем проблемы, есть таймеры(их можно тюнить в системе) которые сообщат потом в дескриптор ошибку errno, которую вы сможете уже расшифровать, и понять как вам реагировать на эту ситуацию
у вас прослеживается незнание сетевой части, а не незнание неблокирующих операций как вы написали
здесь можно посоветовать только брать уже готовые проекты с гитхаба итд, и компилить, разбираться в каждом из них, всякие на libevent построенные к примеру, если нужен неблокирующий ИО

anonymous ()
Ответ на: комментарий от anonymous

у вас прослеживается незнание сетевой части, а не незнание неблокирующих операций как вы написали

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

Я просто думал, что используя tcp и уже имеющиеся средства для работы с этим можно абстрагироваться от внутренних сетевых потрохов, нужно лишь грамотно воспользоваться, собственно, существующим API, которое абстрагирует программиста от деталей. Ведь tcp - якобы надёжный канал. Погодите. Или, по вашим словам, не очень надёжный?

незнание неблокирующих операций как вы написали

Теория и практика - вещи разные. Неблокирующие операции можно знать, но пока реальный софт не напишешь и на грабли не понаступаешь - сложно назвать себя нормальным сетевым программистом. Хотелось бы просто быть заранее готовым к граблям.

здесь можно посоветовать только брать уже готовые проекты с гитхаба итд

Какие есть относительно простые/небольшие проекты, где для изучения не нужно будет вылазить на стену из-за офигевания от сложности кода?

libevent

относительно сложен. Я, к слову, не просто так спросил в шапке темы относительно «гайдов» по сетевому программированию. Beej читал (хоть и не полностью), но там нет деталей.

IECTA ()
Ответ на: комментарий от IECTA

дайте определение надежности ?
tcp надежный, но на разных ИО он ведет себя по разному
на блокирующих он возвращает управление только когда данные доставлены или ошибка
на неблокирующем ИО он возвращает управление всегда
а доставлены ли данные или нет, вы получаете на евент в дискрипторе, когда евент приходит на дискриптор, в нем получаете, или ошибку доставки или разрешение на дальнейший send

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

ну литераДурка она всегда есть в гугле, даже помню где то находил склад всяких книг pdf по сетевом программированию, думаю вы и сами сможете их поискать

anonymous ()
Ответ на: комментарий от anonymous

дайте определение надежности ?

если отправляющая сторона может быть уверена в том, что принимающая сторона получила данные, то надёжность присутствует

на блокирующих он возвращает управление только когда данные доставлены или ошибка

Вот! Т.е. дело не в протоколе. Используя tcp таки можно удостовериться о получении данных получателем.

на неблокирующем ИО он возвращает управление всегда

Угу. Как тогда узнать об ошибке не прибегая к доп. костылям? Не хотелось бы повторять функционал, который уже заолжен в tcp.

а доставлены ли данные или нет, вы получаете на евент в дискрипторе

вот тут поподробнее пожалуйста. Что за евент? Как его получить? Из независимых от работы ПО событий я знаю только POSIX сигналы.

когда евент приходит на дискриптор, в нем получаете, или ошибку доставки или разрешение на дальнейший send

Хотелось бы на это посмотреть. Если вы о возможных результатах revents из poll(), то тогда вопросов нет и всё хорошо - нужно просто poll()-ить, и никаких костылей с доп. каналом не нужно.

libevent не такой уж и сложный, самый простой я бы сказал, да, не три строчки, но простой

честно говоря, открыв репу libevent, даже не знал с какого файла начинать читать
я так понял, все потроха тут: https://github.com/libevent/libevent/tree/master/include/event2
Но где то, что нужно? Что там делает вообще http? Ну и макросная жесть для совместимости со всем подряд (включая windows) усложняет чтение жутко.

IECTA ()

Как передать дескриптор уже существующему потоку?

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

Для передачи дескриптора уже существующему процессу (который создали при помощи fork()) нужно что-то шаманить с sendmsg(). Дальше я не копал. Судя по всему, этим не особо активно пользуются в современном софте

Т. к. процессы не разделяют общую память, для передачи данных между ними нужно использовать сообщения (mq_*), отображаемую память (mmap), именованные или неименованные каналы fifo, обычные файлы, сокеты или, возможно, что-то ещё, в зависимости от задачи и вкусов программиста.

Но, несмотря на то, что процессы тяжелее, а их взаимодействие сложнее, они надёжнее потоков. Ведь если поток обратится к запрещённому адресу и вызовет segmentation fault, то слетят и все остальные потоки этого процесса. Процесс же может нагадить только самому себе, но не другим. Поэтому там, где важна надёжность, используют fork(). Если же производительность важнее, то используют pthread.

К слову, я так и не понял чем именно sendmsg() заставляет получить дескриптор в процессе, а не просто передаёт информацию.

А я не понял вопрос.

Если send() и другие функции из той же семьи возвращают кол-во переданных/полученных байт, то как они могут что-то вернуть, если они сразу возвращают управление?

Функция send() возвращает количество записанных в сокет байт. Это не значит, что эти байты уже переданы, и тем более не значит, что они получены на той стороне.

Можно ли использовать буфер сразу после того, как send() вернул управление?

Можно. Но, как здесь уже ответили, можно и подождать уведомления о доставке, чтоб можно было отправить повторно. Впрочем, всё зависит от задачи.

Если poll() сообщает, что есть входящие данные по дескриптору, но чтение их возвращает 0, означает ли это, что удалённый хост просто сделал close()?

Да.

Зачем нужен shutdown(), если есть обычный close()?

Чтобы закрывать сокет на чтение или запись, оставляя открытым на запись или чтение. Например, если я закрыл на запись, то на том конце read() после прочтения всего, что было отправлено до этого, вернёт 0, но write() сможет ещё мне ответить. Но после shutdown() в любом случае надо вызывать close(). Так что они не являются полностью взаимозаменяемыми.

beastie, спасибо за редактирование. На этом форуме списки с круглой скобкой не одобряются?.. Просто интересно.

Я так понимаю, что циферки 1, 2, 3 исправлены на разметку «нумерованный list». Подробнее см. www.linux.org.ru/help/lorcode.md .

В чём тогда смысл этого tcp, если я не могу узнать полученны ли данные на другом конце без доп. костылей?

Во-первых, tcp всё-таки пытается отправить данные какое-то время, в отличие от udp. Во-вторых, tcp гарантирует правильный порядок пакетов, в отличие от udp. Но зато udp намного легче, поэтому если порядок пакетов не важен, то лучше использовать udp, а доставку контролировать самостоятельно в рамках пользовательского протокола (если такой контроль вообще нужен).

aureliano15 ()
Ответ на: комментарий от IECTA

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

неблокирующая архитектура строится на демультиплексорах типа select/poll/kevent итд
где весь смысл слушать(крутить) их по кругу(таймауту) и реагировать на события приходящие в их дескрипторы
это я и называют event'ами
создали вы сокет,
запихали его в демультиплексор,
отправили send сколько байт,
и переходите в цикл демультиплексора, где можете делать еще что то свое(если таймаут маленький), либо висеть подольше и ждать когда ос вам сообщит в евен, ошибка в отправке, или она готова что бы вы отправляли данные еще
и в случае с tcp полагались что она данные доставила до получателя



ну ничего, посидите недельку, поизучаете libevent, и разберетесь, покомпилируете, порежете не нужное для вас, порефакторите до минимума, так и оттачиваются навыки быстрого разбора кода, а там смотришь через пару лет сможете легко читать хоть весь код qt хоть все линукс ядро

anonymous ()

Ща тебе тут насоветуют libevent и прочей жести.

Не нужно этим пользоваться. Просто открывай сокет и жди коннекта (select). Приконнектились — запускай отдельный поток на каждого клиента. Там тоже при помощи select элементарно все делается.

Вот если клиентов ожидается туева хуча, тогда уж нужно будет выпендриваться...

anonymous ()
Ответ на: комментарий от anonymous

кто сказал про пользоваться ?
совет был в том что бы понять как это работает на практике
а не тыркаться с голыми апи, не зная как их применить на практике

anonymous ()
Ответ на: комментарий от IECTA

Т.е. используя tcp нужно еще ДОПОЛЬНИТЕЛЬНО проверять всё ли пришло на принимающей стороне?

Не, прикол в том, что если ты через UDP сделал send() пакета на 100 байтов, то с другой стороны recv() либо получит ровно эти 100 байтов, либо какой-то другой пакет (если отправлял), либо ничего. TCP же реализует поток байтов: если ты сделал send() на 100 байтов, то recv() с другой стороны может взять и вернуть 50 байтов, а потом следующий recv() --- остальные 50, потому что так вышло.

ilammy ★★★ ()
Ответ на: комментарий от aureliano15

Благодарю за развёрнутый ответ

А я не понял вопрос.

имелось ввиду, что, судя по всему, sendmsg() для передачи дескриптора передаёт данные в первую очередь ядру. Ведь если бы мы банально передачи числовое значение нового дескриптора существующему процессу (как с потоками), то он бы ни к чему не вёл, и попытки работать с ним приводили бы к ebadfd

Функция send() возвращает количество записанных в сокет байт. Это не значит, что эти байты уже переданы, и тем более не значит, что они получены на той стороне.

А чтобы узнать о состоянии дел достаточно использовать poll()/select()/..., верно?

Чтобы закрывать сокет на чтение или запись, оставляя открытым на запись или чтение. Например, если я закрыл на запись, то на том конце read() после прочтения всего, что было отправлено до этого, вернёт 0, но write() сможет ещё мне ответить. Но после shutdown() в любом случае надо вызывать close(). Так что они не являются полностью взаимозаменяемыми.

Дело в том, что мне сложно представить ситуацию, когда нужно закрыть сокет только на чтение или запись. Только лишь чтобы другая сторона при read() получила 0? В общем, я так понял что shutdown() скорее взаимодополняет close().

Я так понимаю, что циферки 1, 2, 3 исправлены на разметку «нумерованный list». Подробнее см. www.linux.org.ru/help/lorcode.md .

Да, были исправлены. Не понятно только зачем.

IECTA ()
Ответ на: комментарий от anonymous

ну если по бумаге, то да, можно быть уверенным что данные доставлены получателю

если о практике, оно в целом бывает совсем по разному
Если «надёжность» tcp рассматривать не бинарно (присутствует-отсутствует), а уровнем, то практический уровень tcp в моей задаче устраивает.

но это уже высший пилотаж, вам оно пока не надо

Да уж, я тут дополнительно читал вот еще о некотором «высшем пилотаже», и тоже понял, что пока рановато: https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or...

неблокирующая архитектура строится на демультиплексорах типа select/poll/kevent итд

Да, я как раз о poll() спрашивал.

qt

он на плюсах, я терпеть их не могу, к сожалению.

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

хм, я так понял, туториалов нормальных в интернете таки нет, придется колупать libevent...

IECTA ()
Ответ на: комментарий от anonymous

Ща тебе тут насоветуют libevent и прочей жести.

ну, вообще мб проще взять чей-то легковесный, но грамотно написанный веб сервер

Приконнектились — запускай отдельный поток на каждого клиента

с таким подходом можно и забить на неблокирующие сокеты

Вот если клиентов ожидается туева хуча, тогда уж нужно будет выпендриваться...

но как именно?..

IECTA ()
Ответ на: комментарий от anonymous

на блокирующих он возвращает управление только когда данные доставлены

4.2

LamerOk ★★★★★ ()
Ответ на: комментарий от aureliano15

Единственно, надо позаботиться о синхронизации (чтобы один поток не читал во время записи другим или чтобы 2 потока одновременно не писали в одну ячейку

Летние каникулы на ЛОРе в самом разгаре.

LamerOk ★★★★★ ()
Ответ на: комментарий от LamerOk

что 4.2 ? выдираем предложение из контекста ?

anonymous ()
Ответ на: комментарий от IECTA

Если «надёжность» tcp рассматривать не бинарно (присутствует-отсутствует), а уровнем, то практический уровень tcp в моей задаче устраивает.

для лучшего понимания берем какой нибудь веб броузер
нажали открыть страницу, страница открылась, в идеальном случае
в худшем случае, ждем рефрешь по несколько раз, а она еле еле грузится
тсп будет стараться доставить данные(все таки ретрансмиты у него есть) но гарантии он никакой не дает

anonymous ()
Ответ на: комментарий от IECTA

имелось ввиду, что, судя по всему, sendmsg() для передачи дескриптора передаёт данные в первую очередь ядру. Ведь если бы мы банально передачи числовое значение нового дескриптора существующему процессу (как с потоками), то он бы ни к чему не вёл, и попытки работать с ним приводили бы к ebadfd

sendmsg просто отправляет какие-то данные в сокет, ассоциированный с дескриптором, указанным в первом параметре. Если в данных передаются какие-то номера дескрипторов, то это действительно не имеет смысла, т. к. для каждого процесса они уникальны. Процесс должен сам открыть файл/сокет. Однако дочерний процесс может унаследовать открытые дескрипторы от родительского процесса, если они были открыты до вызова fork().

Функция send() возвращает количество записанных в сокет байт. Это не значит, что эти байты уже переданы, и тем более не значит, что они получены на той стороне.

А чтобы узнать о состоянии дел достаточно использовать poll()/select()/..., верно?

Точно не скажу, но не уверен. На самом деле это легко проверить: написать клиент/серверную tcp программу со всеми интересующими функциями, которая передаёт и принимает данные с большими задержками в несколько секунд, запустить клиент на одном компе, а сервер на другом, и в разные моменты времени (до передачи, во время передачи, после передачи, но до приёма, во время приёма и т. д.) просто вытаскивать ethernet-кабель или опускать сетевой интерфейс то на одном компе, то на другом. Потом можно в некоторых случаях снова включать сеть до истечения тайм-аутов, а в других - нет. И наблюдать за реакцией обеих частей. Скорее всего отключение сетки на localhost'е сразу будет замечено, а вот при отключении сетки на удалённом компе посередине сеанса связи, - на локалхосте могут проявляться интересные особенности реализации tcp.

Дело в том, что мне сложно представить ситуацию, когда нужно закрыть сокет только на чтение или запись.

Легко. Клиент передал серверу запрос и закрыл запись, сообщая таким способом, что готов к приёму ответа.

В общем, я так понял что shutdown() скорее взаимодополняет close().

Нет. Хотя бы потому, что после shutdown() всегда необходимо вызывать close(), даже если shutdown() закрыла сокет и на запись, и на чтение.

хм, я так понял, туториалов нормальных в интернете таки нет, придется колупать libevent...

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

aureliano15 ()
Ответ на: комментарий от LamerOk

Летние каникулы на ЛОРе в самом разгаре.

Ну, троли на лоре цветут и пахнут круглый год, так что вам грех жаловаться. :-)

aureliano15 ()
Ответ на: комментарий от IECTA

на блокирующих он возвращает управление только когда данные доставлены или ошибка

Вот! Т.е. дело не в протоколе. Используя tcp таки можно удостовериться о получении данных получателем.

Не слушай анонимуса, он лезет в то, в чём не разбирается. Вот в man 3 send написано по белому:

The send() function shall initiate transmission...

Successful completion of a call to send() does not guarantee delivery of the message. A return value of −1 indicates only locally-detected errors.

То есть, send только начинает отправку данных, и никаких гарантий тебе вообще не даёт.

thesame ★★★★ ()
Ответ на: комментарий от thesame

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

anonymous ()

Лучший способ научиться программировать (в т.ч. сокеты), это свести теорию к минимуму. Слишком много текста - слишком мало кода. Что-то неясно - пиши код.

Приходится собирать информацию по крупицам.

Гугли «Beej's Guide to Network Programming»

Nietzsche ()

8. В типичном случае не нужен. Разница есть либо если нужно закрыть только одно направление, либо если есть копии дескриптора.

Неблокирующие сокеты - какая-то древняя и сумрачная техника, зачем она тебе? select/etc не сложнее, но технически лучше.

anonymous ()
Ответ на: комментарий от IECTA

Это же полная лажа и я не смогу это отследить!

А ты думаешь зачем существуют все остальные протоколы поверх tcp?

anonymous ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.