Simple socket tutorial
Client send messages, server receive and print them.
I use an old comm. style convention of sending length before the message. If you know the types involved, the length prefix should not be needed.
Encoding / decoding msg length
ByteString lengths are returned as Int64. Since message lengths are not going to be that big, I constraint length encoding to 2 bytes, validating lengths < 2^16.
{-| Validate and encode/decode msg length
-}
module XmitLenCodec (
encodeXmitLen,
decodeXmitLen,
MsgLen,
validateMsgLen,
msgLenEOT,
isLenEOT,
lenLen,
) where
import Control.Exception (assert)
import Data.ByteString.Lazy (ByteString, pack, unpack)
import qualified Data.ByteString.Lazy as BS
import Data.Char (chr, ord)
import Data.Int (Int64)
import Data.Word (Word16, Word8)
import Data.Binary (Binary, encode, decode)
-- | validated msg length
newtype MsgLen = MsgLen Word16
maxlen = 0x0FFFF :: Int64
lenLen = 2 :: Int64
msgLenEOT = MsgLen 0
isLenEOT :: Int64 -> Bool
isLenEOT = (== 0)
validateMsgLen :: Int64 -> Maybe MsgLen
validateMsgLen len = if len <= maxlen && len >= 0
then Just $ MsgLen $ fromIntegral len
else Nothing
-- | encode with Binary
encodeXmitLen :: MsgLen -> ByteString
encodeXmitLen (MsgLen w) = encode w
-- | decode with Binary
decodeXmitLen :: ByteString -> Int64
decodeXmitLen bs =
assert (BS.length bs == lenLen) $
let w = decode bs :: Word16
in fromIntegral w
Sending / recieving a sequence of messages
To support a sequence of messages of unknown length, I give the receive function a type suitable to be used with a monadic unfoldrM
.
{-| send / receive msgs / EOT -}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE Rank2Types #-}
module SendReceive (sendAsBS, sendEOT, receiveMay) where
import Control.Monad
import Data.Binary (Binary, decode, encode)
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BS
import Debug.Trace (traceIO)
import "network" Network.Socket
import qualified "network" Network.Socket.ByteString.Lazy as NSBS
import Text.Printf
-- app modules
import XmitLenCodec (decodeXmitLen, encodeXmitLen,
isLenEOT, lenLen, msgLenEOT,
validateMsgLen)
-- | send msg
sendAsBS :: Binary a => Socket -> a -> IO ()
sendAsBS skt myData =
case validateMsgLen len of
Nothing -> traceIO $ -- error as trace msg
printf "sendAsBS: msg length %d out of range" len
Just msgLen -> do
NSBS.send skt $ encodeXmitLen msgLen
bytes <- NSBS.send skt bsMsg
when (bytes /= len) $ traceIO "sendAsBS: bytes sent disagreement"
-- return ()
where
bsMsg = encode myData
len = BS.length bsMsg
-- | send EOT
sendEOT :: Socket -> IO ()
sendEOT skt = do
NSBS.send skt $ encodeXmitLen msgLenEOT
return ()
-- | type alias for a monadic unfoldrM
-- adding a context here requires the language extensions
-- Rank2Types, FlexibleContexts
type UnfoldableM m b a = Monad m => b -> m (Maybe (a, b))
-- | receive EOT or msg
receiveMay :: Binary a => Socket -> UnfoldableM IO b a
receiveMay skt st = do
len <- fmap decodeXmitLen $ NSBS.recv skt lenLen
if isLenEOT len
then return Nothing
else do
bs <- NSBS.recv skt len
return $ Just (decode bs, st)
Setting up client, server and connections
Wrapping receiveMay
with unfoldrM for an unknown number of messages to receive, gathering its results in a list.
It would have been easier to send the list as a whole in a unique message, since there is a Binary instance for lists, but I wanted to test sending a sequence of messages.
{-| file Sockets.hs -}
{-# LANGUAGE PackageImports #-}
module Sockets (client, server) where
-- import Prelude as P
import Text.Printf (printf)
import System.IO (stdout, hFlush)
import Control.Exception (bracket)
import Control.Monad
import Debug.Trace (traceIO)
import Numeric (showHex)
import "monad-loops" Control.Monad.Loops (unfoldrM)
import qualified "network" Network.BSD as BSD
import "network" Network.Socket
import qualified "network" Network.Socket.ByteString.Lazy as NSBS
import Data.Int
-- | app modules
import SendReceive (receiveMay, sendAsBS, sendEOT)
type XmitType = Int32
withSocket :: Family -> SocketType -> ProtocolNumber ->
(Socket -> IO a) -> IO a
withSocket family sktType protocol =
bracket (socket family sktType protocol) -- acquire resource
(\skt -> shutdown skt ShutdownBoth >> close skt) -- release
withAccept :: Socket -> ((Socket, SockAddr) -> IO a) -> IO a
withAccept skt = bracket (accept skt) -- acquire
(\(connSkt, _) -> do -- release
shutdown connSkt ShutdownBoth
close connSkt)
-- | server accept connection, receive some msgs, gather results and print
acceptConn :: Socket -> HostAddress -> IO ()
acceptConn skt expectedHost = do
res <- withAccept skt $ \(connSkt, connSktAddr) ->
case connSktAddr of
SockAddrInet _ originHost
| originHost == expectedHost ->
-- read msgs until EOT
-- wrap receiveMay with unfoldrM
-- for an unknown number of msgs to receive
unfoldrM (receiveMay connSkt) 0 :: IO [XmitType]
_ -> return []
------------------------
-- print received data
putStrLn "received:"
forM_ res print
-- | client start / send a few data msgs, then EOT
-- withSocketsDo required in Windows platform for initialisation,
-- no-op otherwise
client :: PortNumber -> IO ()
client port = withSocketsDo $ do
protocol <- fmap BSD.protoNumber $ BSD.getProtocolByName "TCP"
withSocket AF_INET Stream protocol $ \skt -> do
localhost <- inet_addr "127.0.0.1"
-- traceIO $ "localhost: " ++ showHex localhost ""
let sktAddr = SockAddrInet port localhost
connect skt sktAddr
-- send msgs, EOT
let testList = [1..4] :: [XmitType]
putStrLn "to send:"
forM_ testList print -- print test msgs
hFlush stdout
forM_ testList $ \n -> sendAsBS skt n
sendEOT skt
-- | server side params
maxConnToListenTo = 5
-- | server start / shutdown
-- withSocketsDo required in Windows platform for initialisation,
-- no-op otherwise
server :: PortNumber -> IO ()
server port = withSocketsDo $ do
protocol <- fmap BSD.protoNumber $ BSD.getProtocolByName "TCP"
withSocket AF_INET Stream protocol $ \skt -> do
localhost <- inet_addr "127.0.0.1"
let sktAddr = SockAddrInet port localhost
bind skt sktAddr
listen skt maxConnToListenTo
---------------------
acceptConn skt localhost -- accept once from LocalHost
Starting client and server
Using port 3000 for tests in FPComplete server.
Server and Client threads controlled with async library.
{-| file Main.hs -}
{-# LANGUAGE PackageImports #-}
module Main where
import "async" Control.Concurrent.Async (wait, withAsync)
import Control.Monad
import "network" Network.Socket (PortNumber (PortNum))
import "safe" Safe (readMay)
import System.Environment (getArgs, getProgName)
import System.Exit (ExitCode (..), exitWith)
import Text.Printf (printf)
-- | app modules
import Sockets (client, server)
-- | thread control
process :: PortNumber -> IO ()
process nPort =
withAsync (server nPort) $ \asyncServer ->
withAsync (client nPort) $ \asyncClient -> do
wait asyncClient
wait asyncServer
putStrLn "end of program"
-- | usage info
usage :: IO ()
usage = do
prog <- getProgName
printf "%s port (e.g.: 8007)\n" prog
-- | The main entry point.
main :: IO ()
main = do
args <- getArgs
case args of
[sPort] ->
case readMay sPort of
Just nPort -> process $ PortNum nPort
Nothing -> do
printf "unreadable port number %s\n" sPort
usage
exitWith (ExitFailure 1)
_ -> -- using fixed port number 3000,
-- accepted for test in FPComplete projects
process $ PortNum 3000