Adds ping-pong for detecting client timeout.

This commit is contained in:
Abhinav Sarkar 2015-09-10 02:26:51 +05:30
parent 5e5baeab06
commit 01454a644f
6 changed files with 64 additions and 25 deletions

View File

@ -17,12 +17,15 @@ library
hs-source-dirs: src hs-source-dirs: src
exposed-modules: Link.Server, exposed-modules: Link.Server,
Link.Types, Link.Types,
Link.Protocol Link.Protocol,
Link.Client
other-modules: Link.Util
default-extensions: OverloadedStrings, RecordWildCards, ScopedTypeVariables, default-extensions: OverloadedStrings, RecordWildCards, ScopedTypeVariables,
BangPatterns, TupleSections, NamedFieldPuns BangPatterns, TupleSections, NamedFieldPuns
build-depends: base >= 4.7 && < 5, build-depends: base >= 4.7 && < 5,
network >= 2.6 && < 2.7, network >= 2.6 && < 2.7,
containers >= 0.5 && < 0.6 containers >= 0.5 && < 0.6,
time >= 1.4 && < 1.6
default-language: Haskell2010 default-language: Haskell2010
executable link-exe executable link-exe

View File

@ -1,9 +1,11 @@
module Link.Client where module Link.Client where
import Control.Exception hiding (handle)
import Control.Concurrent hiding (forkFinally) import Control.Concurrent hiding (forkFinally)
import Control.Monad (forever) import Control.Exception hiding (handle)
import Control.Monad (void)
import Data.Time (getCurrentTime, diffUTCTime)
import System.IO (hGetLine) import System.IO (hGetLine)
import System.Timeout (timeout)
import Text.Printf (printf) import Text.Printf (printf)
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
@ -20,29 +22,52 @@ forkFinally action fun =
race :: IO a -> IO b -> IO (Either a b) race :: IO a -> IO b -> IO (Either a b)
race ioa iob = do race ioa iob = do
m <- newEmptyMVar m <- newEmptyMVar
bracket (forkFinally (fmap Left ioa) (putMVar m)) cancel $ \_ -> bracket (forkFinally (fmap Left ioa) (putMVar m)) killThread $ \_ ->
bracket (forkFinally (fmap Right iob) (putMVar m)) cancel $ \_ -> do bracket (forkFinally (fmap Right iob) (putMVar m)) killThread $ \_ -> do
r <- readMVar m r <- readMVar m
case r of case r of
Left e -> throwIO e Left e -> throwIO e
Right a -> return a Right a -> return a
where
cancel t = throwTo t ThreadKilled
sendMessage :: Message -> Client -> IO () sendMessage :: Message -> Client -> IO ()
sendMessage message Client {..} = writeChan clientChan message sendMessage message Client {..} = writeChan clientChan message
runClient :: Server -> Client -> IO () runClient :: Server -> Client -> IO ()
runClient Server {..} Client {..} = forever $ do runClient Server {..} client@Client {..} = do
r <- try $ race readCommand readMessage clientAlive <- newMVar True
case r of pingThread <- forkIO $ ping clientAlive
Left (e :: SomeException) -> printf "Exception: %s\n" (show e) run clientAlive `finally` killThread pingThread
Right cm -> case cm of
Left mcommand -> case mcommand of
Nothing -> printf "Could not parse command\n"
Just command -> handleCommand command
Right message -> handleMessage message
where where
pingDelay = 5
pingDelayMicros = pingDelay * 1000 * 1000
ping clientAlive = do
sendMessage Ping client
threadDelay pingDelayMicros
now <- getCurrentTime
pongTime <- readMVar clientPongTime
if diffUTCTime now pongTime > fromIntegral pingDelay
then void $ swapMVar clientAlive False
else ping clientAlive
run clientAlive = do
alive <- readMVar clientAlive
if not alive
then printf "Client timed out: %s\n" (userName clientUser)
else do
r <- try . timeout pingDelayMicros $ race readCommand readMessage
case r of
Left (e :: SomeException) -> printf "Exception: %s\n" (show e)
Right g -> case g of
Nothing -> run clientAlive
Just cm -> do
case cm of
Left mcommand -> case mcommand of
Nothing -> printf "Could not parse command\n"
Just command -> handleCommand command
Right message -> handleMessage message
run clientAlive
readCommand = do readCommand = do
command <- hGetLine clientHandle command <- hGetLine clientHandle
printf "<%s>: %s\n" (userName clientUser) command printf "<%s>: %s\n" (userName clientUser) command
@ -54,6 +79,9 @@ runClient Server {..} Client {..} = forever $ do
withMVar serverUsers $ \clientMap -> withMVar serverUsers $ \clientMap ->
case Map.lookup user clientMap of case Map.lookup user clientMap of
Nothing -> printf "No such user: %s\n" (userName user) Nothing -> printf "No such user: %s\n" (userName user)
Just client -> sendMessage (PrivMsg clientUser msg) client Just client' -> sendMessage (PrivMsg clientUser msg) client'
handleCommand Pong = do
now <- getCurrentTime
void $ swapMVar clientPongTime now
handleMessage = printToHandle clientHandle . formatMessage handleMessage = printToHandle clientHandle . formatMessage

View File

@ -6,10 +6,12 @@ import Link.Types
parseCommand :: String -> Maybe Message parseCommand :: String -> Maybe Message
parseCommand command = case words command of parseCommand command = case words command of
["PONG"] -> Just Pong
"PRIVMSG" : userName : msg -> Just $ PrivMsg (User userName) (unwords msg) "PRIVMSG" : userName : msg -> Just $ PrivMsg (User userName) (unwords msg)
_ -> Nothing _ -> Nothing
formatMessage :: Message -> String formatMessage :: Message -> String
formatMessage (PrivMsg user msg) = printf "PRIVMSG %s %s" (userName user) msg formatMessage (PrivMsg user msg) = printf "PRIVMSG %s %s" (userName user) msg
formatMessage (NameInUse name) = printf "NAMEINUSE %s" name formatMessage (NameInUse name) = printf "NAMEINUSE %s" name
formatMessage (Connected name) = printf "CONNECTED %s" name formatMessage (Connected name) = printf "CONNECTED %s" name
formatMessage Ping = "PING"

View File

@ -3,6 +3,7 @@ module Link.Server where
import Control.Exception hiding (handle) import Control.Exception hiding (handle)
import Control.Concurrent hiding (forkFinally) import Control.Concurrent hiding (forkFinally)
import Control.Monad (forever) import Control.Monad (forever)
import Data.Time (getCurrentTime)
import Network (withSocketsDo, listenOn, accept, PortID(..)) import Network (withSocketsDo, listenOn, accept, PortID(..))
import System.IO (hClose, hSetNewlineMode, hSetBuffering, BufferMode(..), import System.IO (hClose, hSetNewlineMode, hSetBuffering, BufferMode(..),
universalNewlineMode, hGetLine, Handle, stdout) universalNewlineMode, hGetLine, Handle, stdout)
@ -25,7 +26,7 @@ runServer port = withSocketsDo $ do
forever $ do forever $ do
(handle, host, port') <- accept sock (handle, host, port') <- accept sock
printf "Accepted connection from %s: %s\n" host (show port') printf "Accepted connection from %s: %s\n" host (show port')
forkIO (connectClient server handle) `finally` (hClose handle) forkIO $ connectClient server handle `finally` hClose handle
connectClient :: Server -> Handle -> IO () connectClient :: Server -> Handle -> IO ()
connectClient server handle = do connectClient server handle = do
@ -55,7 +56,8 @@ checkAddClient Server {..} user@User {..} handle =
then return (clientMap, Nothing) then return (clientMap, Nothing)
else do else do
clientChan <- newChan clientChan <- newChan
let client = Client user handle clientChan now <- newMVar =<< getCurrentTime
let client = Client user handle clientChan now
printf "New user connected: %s\n" userName printf "New user connected: %s\n" userName
return (Map.insert user client clientMap, Just client) return (Map.insert user client clientMap, Just client)

View File

@ -1,5 +1,6 @@
module Link.Types where module Link.Types where
import Data.Time (UTCTime)
import System.IO (Handle) import System.IO (Handle)
import Control.Concurrent (MVar, Chan) import Control.Concurrent (MVar, Chan)
import qualified Data.Map as Map import qualified Data.Map as Map
@ -10,9 +11,10 @@ data User = User { userName :: !UserName }
deriving (Show, Eq, Ord) deriving (Show, Eq, Ord)
data Client = Client { data Client = Client {
clientUser :: !User clientUser :: !User
, clientHandle :: !Handle , clientHandle :: !Handle
, clientChan :: !(Chan Message) , clientChan :: !(Chan Message)
, clientPongTime :: MVar UTCTime
} }
data Server = Server { data Server = Server {
@ -21,5 +23,7 @@ data Server = Server {
data Message = NameInUse UserName data Message = NameInUse UserName
| Connected UserName | Connected UserName
| Ping
| Pong
| PrivMsg User String | PrivMsg User String
deriving (Show, Eq) deriving (Show, Eq)

View File

@ -4,4 +4,4 @@ import System.IO (Handle)
import Text.Printf (hPrintf) import Text.Printf (hPrintf)
printToHandle :: Handle -> String -> IO () printToHandle :: Handle -> String -> IO ()
printToHandle handle str = hPrintf handle "%s\n" str printToHandle handle = hPrintf handle "%s\n"