gbnet-hs-0.2.2.0: src/GBNet/Socket.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : GBNet.Socket
-- Description : Non-blocking UDP socket wrapper with statistics
--
-- Platform-agnostic non-blocking UDP socket wrapper with per-socket
-- statistics tracking.
module GBNet.Socket
( -- * Constants
maxUdpPacketSize,
-- * Socket errors
SocketError (..),
-- * UDP socket
UdpSocket (..),
newUdpSocket,
closeSocket,
socketLocalAddr,
socketSendTo,
)
where
import Control.Exception (IOException, catch)
import qualified Data.ByteString as BS
import GBNet.Reliability (MonoTime)
import GBNet.Stats (SocketStats (..), defaultSocketStats)
import Network.Socket (SockAddr, Socket)
import qualified Network.Socket as NS
import qualified Network.Socket.ByteString as NSB
import Optics ((%), (%~), (&), (?~))
import Optics.TH (makeFieldLabelsNoPrefix)
-- | Maximum size of a single UDP datagram.
maxUdpPacketSize :: Int
maxUdpPacketSize = 65536
-- | Errors that can occur during socket operations.
data SocketError
= SocketIoError !String
| SocketInvalidAddress
| SocketClosed
| SocketOther !String
deriving (Eq, Show)
-- | Non-blocking UDP socket with per-socket statistics.
data UdpSocket = UdpSocket
{ usSocket :: !Socket,
usStats :: !SocketStats
}
deriving (Show)
makeFieldLabelsNoPrefix ''UdpSocket
-- | Create a new UDP socket bound to the specified address.
-- Uses bracket-style cleanup to avoid leaking the socket FD if bind fails.
newUdpSocket :: SockAddr -> IO (Either SocketError UdpSocket)
newUdpSocket addr = do
let family = case addr of
NS.SockAddrInet {} -> NS.AF_INET
NS.SockAddrInet6 {} -> NS.AF_INET6
_ -> NS.AF_INET
sockResult <- tryIO $ NS.socket family NS.Datagram NS.defaultProtocol
case sockResult of
Left err -> return $ Left (SocketIoError err)
Right sock -> do
bindResult <- tryIO $ do
NS.setSocketOption sock NS.ReuseAddr 1
NS.bind sock addr
NS.withFdSocket sock NS.setNonBlockIfNeeded
case bindResult of
Left err -> do
NS.close sock -- Clean up on failure
return $ Left (SocketIoError err)
Right () ->
return $
Right
UdpSocket
{ usSocket = sock,
usStats = defaultSocketStats
}
-- | Close the socket.
closeSocket :: UdpSocket -> IO ()
closeSocket sock = NS.close (usSocket sock)
-- | Get the local address this socket is bound to.
socketLocalAddr :: UdpSocket -> IO (Either SocketError SockAddr)
socketLocalAddr sock = do
result <- tryIO $ NS.getSocketName (usSocket sock)
return $ case result of
Left err -> Left (SocketIoError err)
Right addr -> Right addr
-- | Send data to a specific address.
socketSendTo ::
BS.ByteString ->
SockAddr ->
MonoTime ->
UdpSocket ->
IO (Either SocketError (Int, UdpSocket))
socketSendTo dat addr now sock = do
result <- tryIO $ NSB.sendTo (usSocket sock) dat addr
return $ case result of
Left err -> Left (SocketIoError err)
Right sent ->
Right
( sent,
sock
& #usStats
% #ssBytesSent
%~ (+ fromIntegral sent)
& #usStats
% #ssPacketsSent
%~ (+ 1)
& #usStats
% #ssLastSendTime
?~ now
)
-- | Try an IO action, catching IOExceptions.
tryIO :: IO a -> IO (Either String a)
tryIO action =
(Right <$> action) `catch` handler
where
handler :: IOException -> IO (Either String a)
handler e = return $ Left (show e)