Haskell дейтаграмма UDP-сокеты: прием работает, а отправка - нет - PullRequest
1 голос
/ 05 апреля 2020

Чтобы понять, как работает сетевое взаимодействие в Haskell, я реализую простой эхоподобный UDP-сервер. Предполагается прослушивать через IPv6 на локальном хосте (порт 7331), получать сообщения и передавать их обратно отправителю, соединенному с другой строкой.

{-# LANGUAGE OverloadedStrings #-}

module Main where

import Network.Socket hiding (send, sendTo, recv, recvFrom)
import Network.Socket.ByteString
import Control.Concurrent
import Control.Exception
import Control.Monad (forever)
import System.IO (
        IOMode (ReadWriteMode)
        , hPutStrLn
                 )
import qualified Data.ByteString as BS

main = do
    sock <- socket AF_INET6 Datagram defaultProtocol
    let hints = defaultHints { addrFamily = AF_INET6, addrSocketType = Datagram}
    serverAddr <- addrAddress . head <$> getAddrInfo (Just hints) (Just "::1") (Just "7331")
    print serverAddr
    bind sock serverAddr
    print sock
    forever $ do
        receivedStuff <- recvFrom sock 65535    -- blocks
        forkIO $ bracket newSendSocket close (serveReceive receivedStuff)

serveReceive :: (BS.ByteString, SockAddr) -> Socket -> IO ()
serveReceive (msg, fromAddr) sendSocket = do
    putStrLn $ "Got message " ++ show msg ++ " from " ++ show fromAddr
    sendTo sendSocket ("Hi, thx for " `BS.append` msg) fromAddr
    putStrLn "sent response"
    return ()

newSendSocket :: IO Socket
newSendSocket = socket AF_INET6 Datagram defaultProtocol

Я проверяю функциональность сервера с помощью netcat: nc -u -6 "localhost" 7331

Сервер получает сообщения и может поместить их в стандартный вывод. Но ответы никогда не отображаются в netcat.

Есть идеи, что я здесь не так делаю? Насколько я знаю, сокеты датаграмм не нужно связывать (bind) или connect ed перед отправкой данных с использованием sendTo.

Ответы [ 2 ]

3 голосов
/ 05 апреля 2020

Почему вы открываете новый сокет для отправки? Это не обязательно. Если вы просто повторно используете существующий сокет, все будет хорошо. В качестве альтернативы мы можем использовать wireshark и посмотреть.

Использование нового сокета для ответа (код, указанный в вопросе)

Возвращаемое значение sendTo предполагает правильное число байты отправлены. Посмотрев на проводную акулу, мы видим ответ на проводе, но затем ответ отклоняется с недоступным портом назначения ICMP (n c не принимает сообщения от других портов).

Использование оригинального сокет *

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

1 голос
/ 05 апреля 2020

Хорошо, может быть, я должен сделать свой комментарий ответом, если кто-то может доказать, что Network.Socket не потоко-безопасен и может прокомментировать некоторые убедительные комментарии.

Модуль Network.Socket представляет собой тонкий слой вокруг обычных сокетов Беркли API. Для сокетов Беркли нет проблем с использованием одного UDP-сокета с несколькими потоками, выполняющими одновременные вызовы recvFrom или sendTo или оба. Для соединений UDP сам сокет в основном не имеет состояния, за исключением локального IP-адреса и порта, к которому он был привязан. В частности, вызовы recvFrom и sendTo по сути являются «атомами c» для UDP - нет никакого способа, чтобы две одновременные исходящие дейтаграммы были бы «чередованы», и нет никакой возможности, чтобы входящая дейтаграмма была бы разделена на маленькие куски между потоками.

Протокол UDP не гарантирует, что все пакеты будут доставлены, и не гарантирует, что они не будут дублироваться, поэтому ваше приложение должно быть подготовлено для (целого, полного) датаграмма должна быть доставлена ​​более чем одному потоку или нулевым потокам, но это не имеет никакого отношения к безопасности потока. Это просто UDP.

Если Network.Socket добавил слой буферизации или какую-либо другую сложную обработку, то, возможно, он не может быть потокобезопасным, даже для UDP, но, глядя на код, я вижу recvFrom и sendTo не делает ничего, кроме выделения памяти и эквивалентных сокетов C вызовов.

Учитывая это, наиболее разумной архитектурой для многопоточного эхо-сервера UDP является использование одного принимающего потока, который безоговорочно отправляет новый поток за каждый запрос. Вы, вероятно, не использовали бы эту архитектуру в программе C с pthreads, потому что эти потоки довольно дороги, если вы обрабатываете много запросов, но потоки GH C forkIO легки, поэтому, скажем, несколько тысячи из них не должны быть проблемой.

module Main where

import Network.Socket hiding (recvFrom, sendTo)
import Network.Socket.ByteString
import Control.Concurrent
import Control.Monad
import Data.ByteString (ByteString)

main :: IO ()
main = do
  sock <- socket AF_INET6 Datagram defaultProtocol
  addr:_ <- getAddrInfo (Just defaultHints
                          { addrFamily = AF_INET6, addrSocketType = Datagram })
                        (Just "::1") (Just "7331")
  bind sock (addrAddress addr)
  forever $ do
    result <- recvFrom sock 4096
    forkIO $ worker sock result

worker :: Socket -> (ByteString, SockAddr) -> IO ()
worker sock (msg, client) = do
  threadDelay 1000000   -- simulate some processing
  void $ sendTo sock msg client

В своем первоначальном ответе, в дополнение к описанному выше подходу, я предложил альтернативную архитектуру, использующую фиксированное число рабочих потоков в recv-send l oop, вот так:

import Network.Socket hiding (recvFrom, sendTo)
import Network.Socket.ByteString
import Control.Concurrent
import Control.Monad

main :: IO ()
main = do
  sock <- socket AF_INET6 Datagram defaultProtocol
  addr:_ <- getAddrInfo (Just defaultHints
                          { addrFamily = AF_INET6, addrSocketType = Datagram })
                        (Just "::1") (Just "7331")
  bind sock (addrAddress addr)
  replicateM_ 16 $ forkIO $ worker sock
  forever $ threadDelay longtime
  where longtime = 10^12

worker :: Socket -> IO ()
worker sock = forever $ do
  (msg, client) <- recvFrom sock 4096
  threadDelay 1000000   -- simulate some processing
  sendTo sock msg client

Преимущество этого состоит в том, что, даже если одновременно поступает много запросов, существует заранее заданная верхняя граница для числа одновременных рабочих, которые будут работать , (Фактический верхний предел для «всплеска» запросов до того, как они начнут отбрасываться, будет выше, чем число работников, так как O / S будет буферизовать пакеты, даже если все работники заняты.) Один недостаток, указанный восходящим потоком, заключается в том, что все потоки Haskell (в приведенном выше примере 16 из них) просыпаются, что приводит к связке вызовов recvFrom, только один из которых получает ответ. Однако весь смысл использования этого подхода состоит в том, чтобы ограничить число одновременных запросов, поэтому мы не находимся в контексте, где несколько десятков дополнительных системных вызовов имеют значение.

Факт остается фактом: при любом подходе при использовании одного разъема проблем с безопасностью потоков не существует.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...