【问题标题】:Why can't I use hPutStr after printing the result of hGetContents?为什么打印 hGetContents 的结果后不能使用 hPutStr?
【发布时间】:2015-04-17 02:13:33
【问题描述】:

我是 stackoverflow 的新手,如果我做错了什么,请原谅我。我试图了解一个简单的服务器如何在 Haskell 中工作。我想我错过了一些关于 hGetContents 如何工作的非常简单或基本的东西。

import Network 
import System.IO

main = withSocketsDo $ do
     socket <- listenOn $ PortNumber 5002
     (h, _, _) <- accept socket
     c <- hGetContents h
--   putStrLn c  -- doesn't work
--   putStrLn $ head $ lines c -- works!
--   putStrLn $ unlines $ take 2 $ lines c -- works!
--   putStrLn $ unlines $ take 3 $ lines c -- works!
--   putStrLn $ unlines $ take 6 $ lines c -- works!
     putStrLn $ unlines $ take 10 $ lines c -- doesn't work
     hPutStr h $ "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nHello!\r\n"
     hClose h

运行程序后,我通过网络浏览器导航到http://localhost:5002。问题似乎是,根据我解析句柄内容的程度,我最终无法发送响应。我希望能够在发送响应之前解析请求。我已经在代码中注释了可行的情况和不可行的情况。 Hoogle 说,对于 hGetContents (lazy),句柄在读取时是“半封闭的”。我是否误解了懒惰,或者我应该在开始解析其内容后考虑关闭句柄?

我得到的错误是“hPutChar:资源消失(断管)”。感谢您的帮助。

【问题讨论】:

    标签: http haskell network-programming


    【解决方案1】:

    我试图重现您的问题。为此,我执行了您的代码并使用 nc 向其发送请求:

    printf "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11" | nc localhost 5002
    

    正如预期的那样,服务器(来自您问题的代码)打印出前 10 行并退出,没有任何错误。客户端 (nc) 打印:

    HTTP/1.0 200 OK
    Content-Length: 5
    Hello!
    

    并且也没有错误地退出。

    所以,一开始我不明白你的问题是什么,但后来我尝试发送一个较小的请求:

    printf "1\n2\n3\n4\n5\n6\n" | nc localhost 5002
    

    服务器打印了前 6 行并且没有退出。客户端也没有退出,所以我用 Ctrl-C 中断了它,然后服务器退出了“资源消失”错误。

    我进行了一些思考,这对我来说开始有意义。我不太了解惰性 IO,所以如果我的解释不清楚或不正确,如果有更好理解的人可以改进它会有所帮助。

    让我们按照您的代码进行操作。第一:

    (h, _, _) <- accept socket
    c <- hGetContents h
    

    您打开一个句柄并阅读它的内容。请注意,句柄是惰性的,您获得的内容也是惰性的。当我们说某些东西是惰性的时,我们的意思是它可以在不经过评估的情况下被传递(它通常被称为“按名称调用”与“按值调用”)。

    现在:

    putStrLn $ unlines $ take 10 $ lines c
    

    在这里,您将惰性、未评估的内容传递给另一个函数 take 10take 10 将尝试评估列表的前 10 个元素并返回它们,如果列表中的元素少于 10 个,它将简单地返回所有元素。在take 10 之后,我们有putStrLnunlines,它们都与懒惰完美兼容。

    现在假设客户端发送了一个只有 6 行长的输入,然后开始等待响应。我们的服务器延迟接收内容并尝试打印前 10 行。首先,take 10 函数愉快地使用前 6 行并将它们传递给 putStrLn . unlines,然后会发生什么? take 10 不能只是完成它的输出,因为绝对没有迹象表明它已经结束。句柄仍然打开,字节仍然可以从客户端浮动到服务器,所以它只是等待更多的输入。

    可以通过运行观察到这种行为:

    nc localhost 5002
    

    并在那里手动输入 10 行。当您键入时,输入将逐行显示在服务器上。在您输入第 10 行后,服务器将响应“Hello”消息。

    P.S:我猜你描述的行为发生是因为你的网络浏览器在请求中发送了 6 到 9 行的内容。

    要测试、调试和分析这种低级服务器,您应该使用简单的工具,例如 nccurl,而不是您的网络浏览器 :)

    【讨论】:

    • 这个解释(尤其是从“现在假设客户......”开始)是正确的。 TCP 连接的每个方向都可以独立关闭,Web 浏览器在至少收到 HTTP 响应标头之前不会关闭 client->server 方向,以防服务器支持 HTTP keep-alive。所以你最终陷入了一个典型的死锁情况:你的服务器在收到 10 行请求或它的接收通道关闭之前无法发送响应,并且浏览器在收到来自的响应之前不会关闭它服务器。 (续)
    • 然而,这与惰性 IO 并没有太大的关系。你会从像int h = accept(socket, ...); for (int i = 0; i &lt; 10; i++) { if (fgets(buf, size, h)) { printf("%s", buf); } } fprintf(h, "..."); 这样的C 程序中得到相同的行为如果你对你强制请求的数量很小心,你可以用hGetContents 来实现你的服务器。然而,由于 HTTP 是一个简单的基于行的协议,使用hGetLine 和显式循环可能更容易。
    • 那么,单独hPutStrLn c 是否因类似原因不起作用,这意味着hGetContents 永远不会发出结束信号?
    【解决方案2】:

    当您在句柄上启动惰性读取时,您放弃对句柄执行任何其他操作的权利,直到内容字符串被完全强制,或者您手动关闭句柄(此时尝试强制内容字符串将导致不良行为或错误)。

    TL;DR

    这不是适合懒惰 I/O 的情况。适合在套接字上进行惰性读取的情况可能可以用零指来计算。如果你愿意,你可以使用常规的严格 I/O,或者conduit,或者pipes,或者一些 Haskell Web 框架,比如 Yesod 或 Scotty 或者其他各种竞争对手。

    【讨论】:

      【解决方案3】:

      调用hGetContents 会将句柄置于“半关闭”状态。在此之后,您不应在句柄上执行 任何 操作。您应该只使用从hGetContents 返回的字符串。

      简单地说,这里不要使用惰性 I/O。您需要一次手动读取和写入单个字符串,因为时间很重要。

      一般来说,惰性 I/O 有点简洁,但它不适用于超出玩具示例的任何内容。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-12-06
        • 2023-01-10
        • 1970-01-01
        • 2020-06-22
        • 1970-01-01
        • 1970-01-01
        • 2016-05-21
        • 2012-07-14
        相关资源
        最近更新 更多