packages feed

sockets-and-pipes-0.1: src/SocketsAndPipes/Serve/Setup.hs

module SocketsAndPipes.Serve.Setup ( withSocketOnPort ) where

import SocketsAndPipes.Serve.Sockets
    ( PortNumber, Socket, PassiveSocket (..), closePassiveSocket )

import SocketsAndPipes.Serve.Exceptions
    ( BindFailed (..), AddrTried (..),
      overException, firstSuccessOrAllExceptions )

import Control.Monad ( (>=>), when )
import Data.Function ( on )

import qualified Control.Exception.Safe as Exception
import qualified Data.List              as List
import qualified Network.Socket         as Socket

withSocketOnPort :: PortNumber -> (PassiveSocket -> IO a) -> IO a
withSocketOnPort port = Exception.bracket (bindToPort port) closePassiveSocket

bindToPort :: PortNumber -> IO PassiveSocket
bindToPort = addrsForPort >=> chooseAddrAndBind

addrsForPort :: PortNumber -> IO [Socket.AddrInfo]
addrsForPort port = Socket.getAddrInfo hints hostName serviceName
  where
    hints       = Just serverAddrHints   :: Maybe Socket.AddrInfo
    hostName    = Nothing                :: Maybe Socket.HostName
    serviceName = Just (show port)       :: Maybe Socket.ServiceName
{- ^
    The first thing we have to do when starting a server is figure
    out exactly what network address to listen on.

    We've been given a port number, but that's only half the story;
    a network address actually include a lot more obnoxious details
    in addition to the port number.

    'addrsForPort' uses the 'S.getAddrInfo' function from the network
    library to find a list of possible addresses for us to choose from.
-}

serverAddrHints :: Socket.AddrInfo
serverAddrHints =
    Socket.defaultHints{ Socket.addrSocketType, Socket.addrFlags }
  where
    addrSocketType = Socket.Stream :: Socket.SocketType
        {- A "stream" socket uses TCP to make sure all the
           packets arrive in the right order. -}
    addrFlags = [Socket.AI_PASSIVE] :: [Socket.AddrInfoFlag]
        {- A "passive" socket is a socket that will be
           used to listen for incoming connections. -}

chooseAddrAndBind :: [Socket.AddrInfo] -> IO PassiveSocket
chooseAddrAndBind =
    firstSuccessOrAllExceptions BindFailed . map bindToAddr
    . List.sortBy (compare `on` addrPreference)

addrPreference :: Socket.AddrInfo -> Int
addrPreference addr =
    case Socket.addrFamily addr of
        Socket.AF_INET6 -> 1 {- IPv6 is best, because these addresses can
                                accept both IPv4 and IPv6 connections. -}
        Socket.AF_INET  -> 2 {- IPv4 is next best, if IPv6 is not supported. -}
        _               -> 3 {- Other addressing schemes are unfamiliar. -}
{- ^
    Assigns a ranking to each address, indicating our relative preference.
    A lesser number indicates a more preferable address.
-}

bindToAddr :: Socket.AddrInfo -> IO PassiveSocket
bindToAddr addr =
    overException (AddrTried addr) $
        Exception.bracketOnError (Socket.openSocket addr) Socket.close $ \s ->
            initServerSocket addr s *> return (PassiveSocket s)

initServerSocket :: Socket.AddrInfo -> Socket -> IO ()
initServerSocket addr s =
  do
    setReuseAddr s       -- Disable some safety to permit fast restarts.
    setKeepAlive s       -- Send empty packets to keep connections alive.
    setNoDelay s         -- Send bytes immediately without buffering.
    allowIPv4and6 addr s -- If it's an IPv6 address, enable IPv4 also.
    bind addr s          -- Assign the address to the socket.
    listen s             -- Announce willingness to receive connections.

bind :: Socket.AddrInfo -> Socket -> IO ()
bind addr s = Socket.bind s (Socket.addrAddress addr)

allowIPv4and6 :: Socket.AddrInfo -> Socket -> IO ()
allowIPv4and6 addr s =
    when (Socket.addrFamily addr == Socket.AF_INET6) $
        Socket.setSocketOption s Socket.IPv6Only 0

setReuseAddr :: Socket -> IO ()
setReuseAddr s = Socket.setSocketOption s Socket.ReuseAddr 1
{- ^
    By default, the operating system will not let us restart our server and
    bind to the same address immediately, because the new process will
    receive any TCP packets that were in flight during the restart, which
    is typically undesirable.

    Overriding the default behavior like this is not really safe!
    But it lets us restart our server quickly :)
-}

setKeepAlive :: Socket -> IO ()
setKeepAlive s = Socket.setSocketOption s Socket.KeepAlive 1
{- ^
    This enables a nice TCP feature: if there is a long period of time
    with no activity on the socket, the OS will occasionally send an
    empty packet. This has two benefits:

      1. It lets the peer know that we're still here; otherwise the peer will
         close the connection, assuming that we've abandoned the conversation.

      2. If lets us know whether the peer is still there. If we don't receive
         an acknowledgement of the empty packet, we can close the connection.
-}

setNoDelay :: Socket -> IO ()
setNoDelay s = Socket.setSocketOption s Socket.NoDelay 1
{- ^

    Since it's more efficient to transmit a few large packets than many
    small packets, the OS doesn't always send your bytes right away when
    you write to a socket; By default, it make some effort to group
    together small writes into larger packets.

    The downside of this optimization is that it means sometimes we don't
    immediately see the effect of writing to a socket. For experimental
    and demonstration purposes, this can be quite undesirable, so we use
    this setting to disable the feature.
-}

listen :: Socket -> IO ()
listen s = Socket.listen s listenBacklog
{- ^
    Informs the operating system that this socket will be used to
    accept incoming connection requests.

    Such as socket is called a "passive socket".
-}

listenBacklog :: Int
listenBacklog = 1_024
{- ^
    The 'S.accept' function pulls sockets from a queue maintained by
    the operating system. This is the size we are requesting for that queue.
    (The OS might not actually give us a queue as big as we ask for.)
-}