Как стать автором
Обновить

Комментарии 18

Однако Windows не работает с сокетами так же, как с файлами

Конечно же работает. В Windows по-умлочанию для сокета возвращается точно такой же дескриптор ядра.

Надо понимать две вещи:

  1. в Linux вызов write это системный вызов. В Windows системным вызовом будет ZwWriteFile, на худой конец его win32 обёртка WrteFile.

  2. в Windiws существует концепция Layered Socket Providers (LSP). Объединяющиеся в цепочки, через которые последовательно проходят вызовы сетевого API. Базовый провайдер возвращает описатели. Но вполне возможна ситуация, когда сторонний провайдер в цепочке решит вернуть псевдо-описатель, вместо настоящего, ядерного. И будет перекодировать их "на лету". Разумеется, с таким псевдо-описателем работать напрямую уже не получится, только проходя через всю цепочку. Выходом может быть просмотр цепочки и работа с нижним уровнем, минуя все установленные фильтры.

Далее, select это тормозно. Для производительных приложений надо использовать epoll в ,Linux и completion ports в windiows.

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

Очень плохой совет. Внезапное появление executable памяти в процессе это красная тряпка для средств защиты. Кроме того, приложению может быть вообще запрещено самому выделять исполняемую память (и/или модифицировать атрибуты страниц). Например SELinux отлично различает кейсы по загрузке динамических библиотек, выделению исполняемой памяти, изменению атрибутов страниц, причем для стека отдельно.

Про select дополню что на винде он тоже не рекомендуется, если дескрипторов может быть много (хотя их и можно расширить на этапе компиляции). Помимо completion ports есть специальные WSAEnumNetworkEvents для этого.

Epoll уже можно заменить на io_uring.

Вот, что ещё нужно знать о сокетах:

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

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

Если вы посылаете набор байтов, то на другую сторону они придут не одним куском, а иногда будут разбиты на куски произвольного размера. Всегда стоит рассчитывать на получение только части данных за один recv().

Ну, системные таймауты (по крайней мере в Linux) - вещь вполне себе рабочая и полагаться на них вполне можно. Надо только понимать что информация об ошибке к вам спокойно может прийти только через 5 минут или даже более, это - вполне нормальные величины по умолчанию, и такие оповещения - не самые приоритетные. Если для вашего применения это неприемлемо но автоматический контроль целостности установленного соединения важен - тогда да, действительно надо это все иметь в самом вашем протоколе (том что будет поверх TCP) и учитывать. В большинстве же простых случаев это не обязательно.

Еще стоит упопянуть особенность TCP которая не очевидна новичкам. Количество вызовов send() и recv() могут не совпадать.

Два коротких send() можно вычитать одним recv(), и наоборот большой send() придется вычитывать несколькими recv().

Я недавно по привычке решил что это же правило работает и для вебсокетов. Но там таких проблем нет, на каждый send будет один receive

А это потому что вебсокеты - прикладной протокол поверх TCP.

ошибочно посчитал, что, например, send() блокируется и возвращает успешное выполнение только после успешного получения сообщения на стороне recv()

Зависит от типа сокета - синхронный или асинхронный.

Синхронный вполне себе блокирует.

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

Уже начинают напрягать такие статьи. То, что send() не гарантирует recv() так же очевидно, как и то, что если я отправлю посылку почтой России, то не факт, что на том конце её примут. Ну то есть тут не просто не нужно быть "опытным сетевым программистом", а достаточно всего лишь дружить с логикой. Если это неочевидно, то, возможно, стоит рассмотреть другие области программирования.

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

public struct ChatClientString {
    
    public init() {}
    
    public func work() {
        print("Представьтесь:")
        let message = readLine()
        let name = message ?? "User"
        
        do {
            let dog = try Leash()
        
            let queue = DispatchQueue(label: "keyboard listen")
            queue.async {
                keyinput: while true {
                    let message = readLine()
                    guard let line = message else { continue keyinput }
                    do { try dog.clientSocket.write(from: name + ": " + line) }
                    catch let error { print(error) }
                }
            }
        
            while true {
                let incom = try dog.clientSocket.readString()
                print("from sever: \(incom ?? "")")
            }
        
        } catch let error {
            print(error)
        }
    }
}

Фундаментальная задача, которую я пытался решить, — это чистая обработка нарушения связности сети, то есть когда машина А и машина Б оказываются полностью разъединены.

Для решения такой задачи лучше использовать UDP. И ещё в ARP может понадобиться немножко залезть.

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

Этого недостаточно, потому что потеряться может само подтверждение. Тут нужен двухфазный коммит.

Однако Windows не работает с сокетами так же, как с файлами. Если вы хотите использовать нативные Windows API, то нужно применять специализированные функции сокетов: send(), recv(), closesocket() и так далее.

Странные какие-то вещи вы пишете. Потому что все поставляемые MS провайдеры базовых сервисов (TCP, UDP, RAW для IP и IPv6) используют в качестве описателя (descriptor) сокета Handle файловой системы (IFS), то есть для системы эти сокеты — файлы, и с ними можно работать как с файлами, используя системные функции типа ReadFile/WriteFile. Это легко видеть, если посмотреть в их записях каталога (командой netsh Winsock show catalog) содержимое поля Служебные флаги: оно содержит флаг XP1_IFS_HANDLES (0x00020000)

а в винде есть какая нибудь функция, которая ждёт что в файл что нибудь запишут, чтобы тут же прочитать, по типу let i = readLine() с клавиатуры, только из файла?

Дык, ReadFile в простейшей форме себя именно так и ведет: если данных для чтения нет, то она блокируется на ожидании момента, когда данные появятся. А более сложные формы ReadFile могут вместо блокировки запукать асинхронную операцию чтения с ожиданием. В Windows для этого не надо никаких особых условий — там изначально архитектура ввода-вывода асинхронная, и даже синхроные операции чтения/записи реализованы как надстройка над асинхронными.

Сетевое программирование - это целая область, со своими "механиками". Новичкам может показаться, что мол "я сейчас возьму либы и примеры и начну слать байты по сети, а обо всем остальном позаботятся высокоуровневые интерфейсы, OSI и т.д.". Но суровая действительность такова, что Pipe - это не просто абстрактное название. Это почти буквально труба(трубопровод) и никто не знает, что там с этим трубопроводом происходит на пути TCP/UDB пакета. Он может оборваться и ни клиент ни сервер об этом не узнают "автоматически". Поэтому иногда к сетевому программированию (особенно для подвижных средств связи с изменяемой пропускной способностью канала) я подхожу как к программированию UART/RS232/485.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий