有没有办法让管道从多个来源获取数据而不阻塞任何来源?

时间:2015-08-11 03:54:47

标签: multithreading sockets haskell conduit network-conduit

我正在编写服务器,其中一个要求是它需要能够将数据推送到客户端而无需客户端直接请求数据。我使用导管但感觉这超出了导管的能力。我遇到的问题是,似乎没有办法判断套接字是否有数据可用,并且await将阻止执行直到有可用数据。我们说我有以下功能

getPacket :: Conduit ByteString IO ClientPacket --take a bytestring and yield a ClientPacket i.e. the ByteString deserialized into a sensible form
processPacket :: Conduit ClientPacket IO ServerPacket --take a ClientPacket and yield a ServerPacket i.e. a response to the client's request
putPacket :: Conduit ServerPacket IO ByteString --serialize the ServerPacket

然后我将导管与Conduit.Network库中的源和接收器连接在一起

appSource appData $$ getPacket =$= processPacket =$= putPacket $= appSink appData

现在,我从管道外部引入了一个数据源,我希望将这些数据合并到管道中。例如,如果这是聊天服务器,则外部数据将是其他客户端发送的消息。问题在于,无论我在哪里尝试引入这些外部数据,它都会被等待的调用阻止。从本质上讲,我最终会得到看起来像这样的代码。

yield processOutsideData --deal with the outside data
data <- await            --await data from upstream

处理更多外部数据的唯一方法是上游组件产生某些东西,但上游只有在从客户端获取数据时才会产生,这正是我试图避免的。我尝试使用多个线程和TChan来解决这个问题,但似乎必须在同一个线程中使用appSource和appSink,否则我会从recv中获得无效的文件描述符异常(这是有意义的)。

但是,如果套接字源和接收器在同一个线程中运行,我再次遇到await阻塞的问题,我无法检查套接字中是否有数据可用。在这一点上,似乎我已经用管道撞墙了。

但我确实喜欢使用导管,并希望继续使用它们。所以我的问题是:有没有办法做我想用管道实现的目标?

1 个答案:

答案 0 :(得分:1)

迈克尔斯诺曼conduit network examples use concurrency。 telnet客户端示例运行一个线程用于发送输入,另一个线程用于显示收到的内容。我已经对它进行了调整以发送和接收整行

{-# LANGUAGE OverloadedStrings #-}
import Conduit
import Control.Concurrent.Async (concurrently)
import Control.Monad            (liftM, void)
import Data.ByteString          (ByteString)
import Data.ByteString.Char8    (unpack)
import Data.Conduit.Network
import Data.String              (IsString, fromString)
import Network                  (withSocketsDo)

getLines :: (IsString a, MonadIO m) => Producer m a
getLines = repeatMC . liftM fromString $ liftIO getLine

putLines :: (MonadIO m) => Consumer ByteString m ()
putLines = mapM_C $ liftIO . putStrLn . unpack

main :: IO ()
main = withSocketsDo $
    runTCPClient (clientSettings 4000 "localhost") $ \server ->
        void $ concurrently
            (getLines $$ appSink server)
            (appSource server $$ putLines)

我们可以在服务器上做同样的事情。创建STM通道,将接收的数据写入通道,并将数据从通道发送到客户端。这使用了stm-conduit包的STM频道sourceTBMChansinkTBMChan周围的简单包装。

{-# LANGUAGE OverloadedStrings #-}
import Conduit
import Control.Concurrent.Async       (concurrently)
import Control.Concurrent.STM.TBMChan (newTBMChan)
import Control.Monad                  (void)
import Control.Monad.STM              (atomically)
import Data.Conduit.Network
import Data.Conduit.TMChan            (sourceTBMChan, sinkTBMChan)
import Network                        (withSocketsDo)

main :: IO ()
main = withSocketsDo $ do
    channel <- atomically $ newTBMChan 10
    runTCPServer (serverSettings 4000 "*") $ \server ->
        void $ concurrently
            (appSource server $$ sinkTBMChan channel False)
            (sourceTBMChan channel $$ appSink server)

如果我们只连接一个客户端来运行服务器,它会回复客户端发送的内容。

----------
| a      | (sent)
| a      | (received)
| b      | (sent)
| b      | (received)
| c      | (sent)
| c      | (received) 
----------

如果我们运行连接了多个客户端的服务器,则消息将在客户端之间分配,其中一个客户端会收到每条消息。

----------             ----------
| 1      | (sent)      | 1      | (received)
| 2      | (sent)      | 3      | (received)
| 2      | (received)  |        |
| 3      | (sent)      |        |
|        |             |        |
|        |             |        |
----------             ----------

此示例不处理客户端关闭连接时要执行的操作。