Link Search Menu Expand Document

目录

  1. 客户机(Client)与服务器(Server)
  2. 发送和接收数据包
  3. UDP

客户机(Client)与服务器(Server)

网络(Network)模块围绕发送与接收数据构建。下面的例子展示了一些使得 Z-IO 简明易用的特性:

{-# LANGUAGE OverloadedStrings #-}
import Z.IO
import Z.IO.Network
import Z.Data.Text as T

main :: IO ()
main = do
    -- 使用 getAddrInfo 函数解析 DNS。
    addr:_ <- getAddrInfo Nothing "www.bing.com" "http"
    -- 使用 initTCPClient 函数初始化一个 TCP 客户机。
    withResource (initTCPClient defaultTCPClientConfig{
            tcpRemoteAddr = addrAddress addr}) $ \ tcp -> do
        -- 使用 BufferedInput / BufferedOutput 及其相关函数以从 TCP 套接字(Socket)中读取或写入内容。
        i <- newBufferedInput tcp
        o <- newBufferedOutput tcp
        writeBuffer o "GET http://www.bing.com HTTP/1.1\r\nHost: www.bing.com\r\n\r\n"
        flushBuffer o
        readBuffer i >>= pure . T.validate

    -- 使用 startTCPServer 函数启动 TCP 服务。
    startTCPServer defaultTCPServerConfig{
        tcpListenAddr = SocketAddrIPv4 ipv4Loopback 8080} $ \ tcp -> do
            o <- newBufferedOutput tcp
            writeBuffer o "hello world" >> flushBuffer o

Z.Haskell 提供了一些网络工具:

  • Z.IO.Network.IPC 为基于域套接字(Unix)或命名管道(Windows)的进程间通信提供流通道(Stream Channel)。
  • Z.IO.Network.TCP 提供基于 TCP 套接字的远程通信流通道。
  • Z.IO.Network.UDP 在 UDP 套接字之上提供消息通道。
  • 一个基于 botan 的 TLS 实现正处于开发阶段。

以 TCP 模块为例。不少于套接字相关的底层细节都被隐藏起来,如 bindlistenaccept 等,而是提供两个高阶操作:

-- | 连接至 TCP 目标。
initTCPClient :: HasCallStack => TCPClientConfig -> Resource UVStream
-- | 启动 TCP 服务器。
startTCPServer :: HasCallStack
               => TCPServerConfig
               -> (UVStream -> IO ())
               -- ^ 工作函数将接受一个 TCP 流,
               -- 并运行于一个独立的 Haskell 线程。
               -> IO

发送和接收数据包

由于 UVStream 类型是 Z.IO.Buffered 模块中 InputOutput 类的实例,因此可以复用所有与带缓冲读写(Buffered read / write)相关的 API。假设已经设计了一个简单的帧消息协议(Framed Message Protocol):

import Data.Word
import qualified Z.Data.Vector as V

--   uint8 消息类型     uint16 有效载荷长度    信息有效载荷
--  +------------------+----------------------+------------------
--  |       0xXX       |   0xXXXX(大端)     |       ...
--  +------------------+----------------------+------------------

data Message = Message { msgTyp :: Word8, msgPayload :: V.Bytes }

既可以以如下方式手动解码消息帧:

-- 导入位运算相关操作。
import Data.Bits    (unsafeShiftL, (.|.))
import Z.IO

readMessage :: HasCallStack => BufferedInput -> IO Message
readMessage bi = do
    msg_typ <- readExactly buffered_i 1
    payload_len_h <- readExactly buffered_i 1
    payload_len_l <- readExactly buffered_i 1
    let payload_len =
        (fromIntegral payload_len_h) `unsafeShiftL` 8
            .|. (fromIntegral payload_len_l)
    payload <- readExactly payload_len
    return (Message msg_typ payload)

也可以使用 Z.Data.Parser 模块中的 Parser 相关机能:

{-# LANGUAGE TypeApplications #-}
import qualified Z.Data.Parser as P
import Data.Word
import Z.IO

parseMessage :: P.Parser Message
parseMessage = do
    msg_type <- P.decodePrim @Word8
    payload_len <- P.decodePrimBE @Word16
    payload <- P.take (fromIntegral payload_len)
    return (Message msg_typ payload)

readMessage :: HasCallStack => BufferedInput -> IO Message
readMessage = readParser parseMessage

函数 readParser 将恰好运行一次给定的 Parser,自动解析流出缓冲区的消息 Message 并等待输入。将消息 Message 写入到给定的 TCP 套接字做法类似:

import qualified Z.Data.Builder as B
import qualified Z.Data.Vector as V
import Z.IO

writeMessage :: HasCallStack => BufferedOutput -> Message -> IO ()
writeMessage bo (Message msg_typ payload) = do
    -- 使用 Builder 单子组合写入函数。
    writeBuilder bo $ do
        B.encodePrim msg_typ
        B.encodePrimBE (V.length payload)
        B.bytes payload
    -- 可以在每次写入消息后刷新缓冲区,或留给调用者来做。
    -- 这需要使用 flushBuffer bo。

Z.Haskell 提供了许多工具以处理 TCP 的流性质,连同许多其它流设备例如 IPC 和文件等。在下一节中我们将介绍一个更高阶的流 API,BIO

UDP

与 IPC 或 TCP 不同的是,UDP 是一个消息协议,而不是一个流协议。类型 UDP 不是 InputOutput 类的实例,因此 Z-IO 为读写 UDP 消息提供了一些函数:

-- | 初始化一个 UDP 套接字。
initUDP :: UDPConfig -> Resource UDP
-- | 向目标地址发送一条 UDP 消息。
sendUDP :: HasCallStack => UDP -> SocketAddr -> V.Bytes -> IO ()
-- | 从 UDP 套接字中获取消息,在可用时返回消息源地址。
-- 返回值中的 `Bool` 用于指明接收的消息是否是不完全的,即消息大小大于接收缓冲区的大小。
recvUDP :: HasCallStack => UDPRecvConfig -> UDP -> IO [(Maybe SocketAddr, Bool, V.Bytes)]
-- | 循环接收 UDP 消息。
recvUDPLoop :: HasCallStack
            => UDPRecvConfig
            -> UDP
            -> ((Maybe SocketAddr, Bool, V.Bytes) -> IO a)
            -> IO ()

由于 recvUDPLoop 函数在其内部复用接收缓冲区,使用它循环接收消息比手动写循环快。与上述的 TCP 服务器不同的是,UDP 的工作函数在当前的 Haskell 线程被调用,而不是分支出的一个。因此如果在一个工作函数中有大量的计算任务,可以使用 Z.IO.UV.Manager 模块中的 forkBa 函数。它类似于 forkIO,但有着积极的线程平衡策略。