hGetContents
gets all remaining input, using lazy IO. The second call to hGetContents
throws an error because the first call has already claimed the data, in a sense.
What I would do is have a separate thread tunnel data from the handle to standard output, and have the main thread tunnel data from standard input to the handle:
_ <- forkIO $ hGetContents h >>= putStr
getContents >>= hPutStr h
I would also use LineBuffering
rather than NoBuffering
, so it doesn't have to transmit a TCP packet for every character (*).
Thus, we have:
import Control.Concurrent
import Network
import System.Environment
import System.IO
main :: IO ()
main = do
[host, port] <- getArgs
h <- connectTo host $ PortNumber $ toEnum $ read port
hSetBuffering stdout LineBuffering
hSetBuffering h LineBuffering
_ <- forkIO $ hGetContents h >>= putStr
getContents >>= hPutStr h
A couple notes:
We can do better, though. Currently, if the server disconnects, the user does not see that the server disconnected until hitting enter a couple times, which produces an ugly error message:
tcp-client: <socket: 3>: commitBuffer: resource vanished (Broken pipe)
Let's see if we can get the program to terminate when either the server or the client closes the connection. Bear in mind that:
getContents
and hGetContents
terminate the list when EOF is reached. Thus:
The program terminates when the main thread terminates, regardless if child threads still have work to do.
A good way to do this, I think, is to perform the receiving and sending in two separate threads, and have the main thread wait on an MVar:
done <- newEmptyMVar
_ <- forkIO $ (hGetContents h >>= putStr)
`finally` tryPutMVar done ()
_ <- forkIO $ (getContents >>= hPutStr h)
`finally` tryPutMVar done ()
-- Wait for at least one of the above threads to complete
takeMVar done
* Actually, sending individual characters at a time will probably trigger Nagle's algorithm. Still, sending characters one at a time creates a lot of unnecessary CPU overhead.