mariadb-haskell-0.1.0.0: src/System/IO/Streams/TCP.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
-- | This module provides convenience functions for interfacing raw tcp.
--
-- Please use 'E.bracket' or its friends to ensure exception safety.
--
-- This module is intended to be imported @qualified@, e.g.:
--
-- @
-- import "Data.Connection"
-- import qualified "System.IO.Streams.TCP" as TCP
-- @
--
module System.IO.Streams.TCP
( TCPConnection
-- * client
, connect
, connectSocket
, connectSocketTimeout
, socketToConnection
, socketToConnectionAdaptive
, defaultChunkSize
-- * server
, bindAndListen
, bindAndListenWith
, accept
, acceptWith
) where
import qualified Control.Exception as E
import System.Timeout (timeout)
import Control.Monad
import Data.Connection
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy.Internal as L
import qualified Network.Socket as N
import qualified Network.Socket.ByteString as NB
import qualified Network.Socket.ByteString.Lazy as NL
import qualified System.IO.Streams as S
import Foreign.Storable (sizeOf)
import Data.IORef (newIORef,readIORef,writeIORef)
addrAny :: N.HostAddress
#if MIN_VERSION_network(2,7,0)
addrAny = N.tupleToHostAddress (0,0,0,0)
#else
addrAny = N.iNADDR_ANY
#endif
-- | Type alias for tcp connection.
--
-- Normally you shouldn't use 'N.Socket' in 'connExtraInfo' directly, this field is
-- intend for used with 'N.setSocketOption' if you need to.
--
type TCPConnection = Connection (N.Socket, N.SockAddr)
-- | The chunk size used for I\/O, less the memory management overhead.
--
-- Currently set to 32k.
--
defaultChunkSize :: Int
defaultChunkSize = 32 * k - chunkOverhead
where
k = 1024
chunkOverhead = 2 * sizeOf (undefined :: Int)
-- | Open a raw TCP socket for the address and port. Name resolution uses
-- @AI_ADDRCONFIG@ and @AI_NUMERICSERV@; usable IPv4/IPv6 addresses are tried
-- within one ten-second default deadline. @TCP_NODELAY@ is enabled.
-- The caller owns the returned socket. This does not authenticate MariaDB.
--
connectSocket :: N.HostName -- ^ hostname to connect to
-> N.PortNumber -- ^ port number to connect to
-> IO (N.Socket, N.SockAddr)
connectSocket = connectSocketTimeout 10000000
-- | Resolve and try all addresses within one deadline, including DNS time.
-- Each failed attempt closes its socket. Only IO errors permit fallback;
-- asynchronous cancellation propagates immediately. No authentication or TLS
-- failure is retried against another address after this function returns.
connectSocketTimeout :: Int -> N.HostName -> N.PortNumber -> IO (N.Socket, N.SockAddr)
connectSocketTimeout deadline host port
| deadline <= 0 = ioError (userError "connection timeout must be positive")
| otherwise = timeout deadline establish >>= maybe
(ioError (userError "TCP connection establishment timed out")) pure
where
establish = N.getAddrInfo (Just hints) (Just host) (Just (show port)) >>= attempts
hints = N.defaultHints
{ N.addrFlags = [N.AI_ADDRCONFIG, N.AI_NUMERICSERV]
, N.addrSocketType = N.Stream
}
attempts [] = ioError (userError "hostname resolved to no usable addresses")
attempts (address:rest) = do
result <- E.try $ E.bracketOnError
(N.socket (N.addrFamily address) (N.addrSocketType address) (N.addrProtocol address))
N.close $ \sock -> do
N.connect sock (N.addrAddress address)
N.setSocketOption sock N.NoDelay 1
pure (sock, N.addrAddress address)
case result of
Right connected -> pure connected
Left (err :: E.IOException) -> if null rest then E.throwIO err else attempts rest
-- | Make a t'Connection' from a 'Network.Socket.Socket' with given buffer size.
--
socketToConnection
:: Int -- ^ receive buffer size
-> (N.Socket, N.SockAddr) -- ^ socket address pair
-> IO TCPConnection
socketToConnection bufsiz (sock, addr) = do
is <- S.makeInputStream $ do
s <- NB.recv sock bufsiz
return $! if B.null s then Nothing else Just s
return (Connection is (sendSocket sock) (N.close sock) (sock, addr))
-- | Start with a small receive allocation and grow on full reads, up to the
-- supplied ceiling. Each recv still returns independent immutable bytes; no
-- mutable buffer is reused underneath a returned row or metadata slice.
socketToConnectionAdaptive :: Int -> (N.Socket,N.SockAddr) -> IO TCPConnection
socketToConnectionAdaptive ceilingBytes (sock,addr) = do
when (ceilingBytes<=0) (E.throwIO (userError "receive buffer ceiling must be positive"))
capacity <- newIORef (min 4096 ceilingBytes)
is <- S.makeInputStream $ do
size <- readIORef capacity
bytes <- NB.recv sock size
when (B.length bytes==size && size<ceilingBytes) $
writeIORef capacity (size+min size (ceilingBytes-size))
pure $! if B.null bytes then Nothing else Just bytes
pure (Connection is (sendSocket sock) (N.close sock) (sock,addr))
sendSocket :: N.Socket -> L.ByteString -> IO ()
sendSocket _ L.Empty = pure ()
sendSocket sock (L.Chunk bytes L.Empty) = unless (B.null bytes) (NB.sendAll sock bytes)
sendSocket sock bytes = NL.sendAll sock bytes
-- | Connect to server using 'defaultChunkSize'.
--
connect :: N.HostName -- ^ hostname to connect to
-> N.PortNumber -- ^ port number to connect to
-> IO TCPConnection
connect host port = connectSocket host port >>= socketToConnection defaultChunkSize
-- | Bind and listen on port with a limit on connection count.
--
-- This function will set @SO_REUSEADDR@, @TCP_NODELAY@ before binding.
--
bindAndListen :: Int -- ^ connection limit
-> N.PortNumber -- ^ port number
-> IO N.Socket
bindAndListen = bindAndListenWith $ \ sock -> do
N.setSocketOption sock N.ReuseAddr 1
N.setSocketOption sock N.NoDelay 1
-- | Bind and listen on port with a limit on connection count.
--
-- Note: The following socket options are inherited by a connected TCP socket from the listening socket:
--
-- @
-- SO_DEBUG
-- SO_DONTROUTE
-- SO_KEEPALIVE
-- SO_LINGER
-- SO_OOBINLINE
-- SO_RCVBUF
-- SO_RCVLOWAT
-- SO_SNDBUF
-- SO_SNDLOWAT
-- TCP_MAXSEG
-- TCP_NODELAY
-- @
--
bindAndListenWith :: (N.Socket -> IO ()) -- ^ set socket options before binding
-> Int -- ^ connection limit
-> N.PortNumber -- ^ port number
-> IO N.Socket
bindAndListenWith f maxc port =
E.bracketOnError (N.socket N.AF_INET N.Stream 0)
N.close
(\sock -> do f sock
N.bind sock (N.SockAddrInet port addrAny)
N.listen sock maxc
return sock
)
-- | Accept a connection with 'defaultChunkSize'.
--
accept :: N.Socket -> IO TCPConnection
accept = acceptWith (socketToConnection defaultChunkSize)
-- | Accept a connection with user customization.
--
acceptWith :: ((N.Socket, N.SockAddr) -> IO TCPConnection) -- ^ set socket options, adjust receive buffer, etc.
-> N.Socket
-> IO TCPConnection
acceptWith f = f <=< N.accept