Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go net/textproto 包详解

概述

net/textproto 包实现了基于文本的请求/响应协议的通用支持,类似于 HTTP、NNTP 和 SMTP 的风格。该包为文本协议的网络连接提供了读写工具,强制实施 RFC 9112 定义的 HTTP/1.1 字符集用于头部键值。

重要说明

  • ✓ 实现基于文本的请求/响应协议支持
  • ✓ 适用于 HTTP、NNTP、SMTP 等协议
  • ✓ 强制实施 RFC 9112 HTTP/1.1 字符集
  • ✓ 提供点编码(dot-encoding)支持
  • ✓ 支持管道化请求/响应管理
  • ✓ Go 1.0+ 引入
  • ✓ 低级别协议工具包

包提供的功能

  • Error - 表示服务器的数字错误响应
  • Pipeline - 管理客户端中的管道化请求/响应序列
  • Reader - 读取数字响应码行、键值对头部、点编码块等
  • Writer - 写入点编码文本块
  • Conn - Reader、Writer 和 Pipeline 的便捷包装

包导入

import (
    "net/textproto"
)

基本使用

1. 简单的客户端 - 服务器示例

// 服务器端
package main

import (
    "fmt"
    "net"
    "net/textproto"
    "log"
)

func main() {
    listener, err := net.Listen("tcp", "localhost:9000")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()
    
    fmt.Println("服务器监听在 :9000")
    
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Println("accept error:", err)
            continue
        }
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()
    
    // 创建 textproto 连接
    tp := textproto.NewConn(conn)
    
    // 读取一行
    line, err := tp.Reader.ReadLine()
    if err != nil {
        log.Println("read error:", err)
        return
    }
    fmt.Printf("收到:%s\n", line)
    
    // 写入响应
    err = tp.Writer.PrintfLine("收到你的消息:%s", line)
    if err != nil {
        log.Println("write error:", err)
    }
}

// 客户端
package main

import (
    "bufio"
    "fmt"
    "net"
    "net/textproto"
    "os"
)

func main() {
    conn, err := net.Dial("tcp", "localhost:9000")
    if err != nil {
        fmt.Println("连接失败:", err)
        return
    }
    defer conn.Close()
    
    // 创建 textproto 连接
    tp := textproto.NewConn(conn)
    
    // 读取用户输入
    fmt.Print("请输入消息:")
    reader := bufio.NewReader(os.Stdin)
    msg, _ := reader.ReadString('\n')
    
    // 发送消息
    err = tp.Writer.PrintfLine(msg)
    if err != nil {
        fmt.Println("发送失败:", err)
        return
    }
    
    // 读取响应
    response, err := tp.Reader.ReadLine()
    if err != nil {
        fmt.Println("读取响应失败:", err)
        return
    }
    fmt.Println("服务器响应:", response)
}

2. 使用 Dot 编码

package main

import (
    "fmt"
    "net"
    "net/textproto"
    "log"
)

func main() {
    // 服务器
    listener, err := net.Listen("tcp", "localhost:9001")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()
    
    conn, err := listener.Accept()
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    tp := textproto.NewConn(conn)
    
    // 读取点编码数据
    data, err := tp.Reader.ReadDotBytes()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("收到数据:%s\n", string(data))
    
    // 写入点编码数据
    w := tp.Writer.DotWriter()
    fmt.Fprint(w, "第一行\n第二行\n第三行")
    w.Close()
}

一、函数(按 a-z 排序)

CanonicalMIMEHeaderKey

func CanonicalMIMEHeaderKey(s string) string

CanonicalMIMEHeaderKey 返回 MIME 头部键 s 的规范格式。规范化将第一个字母和任何连字符后的字母转换为大写,其余转换为小写。例如,“accept-encoding” 的规范键是“Accept-Encoding“。

参数:

  • s - 头部键字符串

返回值:

  • string - 规范化的头部键

示例:

key := textproto.CanonicalMIMEHeaderKey("content-type")
fmt.Println(key) // Content-Type

key = textproto.CanonicalMIMEHeaderKey("ACCEPT-ENCODING")
fmt.Println(key) // Accept-Encoding

key = textproto.CanonicalMIMEHeaderKey("my-custom-header")
fmt.Println(key) // My-Custom-Header

注意:

  • MIME 头部键假定为 ASCII
  • 如果 s 包含空格或无效的头部字段字节(根据 RFC 9112),则原样返回

TrimBytes

func TrimBytes(b []byte) []byte

TrimBytes 返回去除前后 ASCII 空格的字节切片。

参数:

  • b - 原始字节切片

返回值:

  • []byte - 去除空格后的字节切片

示例:

b := []byte("  hello world  ")
trimmed := textproto.TrimBytes(b)
fmt.Printf("%q\n", string(trimmed)) // "hello world"

TrimString

func TrimString(s string) string

TrimString 返回去除前后 ASCII 空格的字符串。

参数:

  • s - 原始字符串

返回值:

  • string - 去除空格后的字符串

示例:

s := "  hello world  "
trimmed := textproto.TrimString(s)
fmt.Printf("%q\n", trimmed) // "hello world"

二、类型(按 a-z 排序)

Conn

Conn 表示文本网络协议连接。它由 Reader 和 Writer 组成用于管理 I/O,以及 Pipeline 用于对连接上的并发请求进行排序。

type Conn struct {
    Reader   Reader
    Writer   Writer
    Pipeline Pipeline
}

嵌入类型:

  • Reader - 读取请求/响应
  • Writer - 写入请求/响应
  • Pipeline - 管道化管理

Conn.Close

func (c *Conn) Close() error

Close 关闭连接。

示例:

conn, err := textproto.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

Conn.Cmd

func (c *Conn) Cmd(format string, args ...any) (id uint, err error)

Cmd 是便捷方法,在管道中等待其轮次后发送命令。命令文本是将 format 与 args 格式化并追加 \r\n 的结果。Cmd 返回命令的 id,用于 StartResponse 和 EndResponse。

参数:

  • format - 格式化字符串
  • args - 格式化参数

返回值:

  • id - 命令 ID
  • err - 错误

示例:

// 发送 HELP 命令并读取点编码响应
id, err := c.Cmd("HELP")
if err != nil {
    return nil, err
}

c.StartResponse(id)
defer c.EndResponse(id)

if _, _, err = c.ReadCodeLine(110); err != nil {
    return nil, err
}

text, err := c.ReadDotBytes()
if err != nil {
    return nil, err
}

return c.ReadCodeLine(250)

Dial

func Dial(network, addr string) (*Conn, error)

Dial 使用 net.Dial 连接到给定网络上的给定地址,然后返回一个新的 Conn 用于该连接。

参数:

  • network - 网络类型(如“tcp“)
  • addr - 地址(如“localhost:9000“)

返回值:

  • *Conn - 文本协议连接
  • error - 连接错误

示例:

c, err := textproto.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}
defer c.Close()

NewConn

func NewConn(conn io.ReadWriteCloser) *Conn

NewConn 使用 conn 进行 I/O 返回一个新的 Conn。

参数:

  • conn - 网络连接

返回值:

  • *Conn - 文本协议连接

示例:

netConn, err := net.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}
defer netConn.Close()

c := textproto.NewConn(netConn)

Error

Error 表示来自服务器的数字错误响应。

type Error struct {
    Code int
    Msg  string
}

字段说明:

  • Code - 错误码
  • Msg - 错误消息

Error.Error

func (e *Error) Error() string

Error 返回错误的字符串表示。

示例:

err := &textproto.Error{Code: 500, Msg: "Internal Server Error"}
fmt.Println(err.Error()) // "500 Internal Server Error"

MIMEHeader

MIMEHeader 表示 MIME 风格的头部,将键映射到值集合。

type MIMEHeader map[string][]string

MIMEHeader.Add

func (h MIMEHeader) Add(key, value string)

Add 将键值对添加到头部。它追加到与键关联的任何现有值。

参数:

  • key - 头部键
  • value - 头部值

示例:

h := make(textproto.MIMEHeader)
h.Add("Content-Type", "text/html")
h.Add("Content-Type", "charset=utf-8")
h.Add("Accept", "application/json")

fmt.Println(h) // map[Accept:[application/json] Content-Type:[text/html charset=utf-8]]

MIMEHeader.Del

func (h MIMEHeader) Del(key string)

Del 删除与键关联的值。

参数:

  • key - 头部键

示例:

h := make(textproto.MIMEHeader)
h.Add("X-Custom", "value1")
h.Add("X-Custom", "value2")

h.Del("X-Custom")
fmt.Println(h.Get("X-Custom")) // ""

MIMEHeader.Get

func (h MIMEHeader) Get(key string) string

Get 获取与给定键关联的第一个值。它不区分大小写;使用 CanonicalMIMEHeaderKey 规范化提供的键。如果没有与键关联的值,Get 返回““。

参数:

  • key - 头部键(不区分大小写)

返回值:

  • string - 第一个值

示例:

h := make(textproto.MIMEHeader)
h.Add("Content-Type", "text/html")
h.Add("Content-Type", "charset=utf-8")

fmt.Println(h.Get("content-type")) // "text/html"
fmt.Println(h.Get("Content-Type")) // "text/html"

MIMEHeader.Set

func (h MIMEHeader) Set(key, value string)

Set 将与键关联的头部条目设置为单个元素值。它替换与键关联的任何现有值。

参数:

  • key - 头部键
  • value - 头部值

示例:

h := make(textproto.MIMEHeader)
h.Add("Content-Type", "text/plain")
h.Add("Content-Type", "charset=utf-8")

h.Set("Content-Type", "text/html")
fmt.Println(h.Values("Content-Type")) // ["text/html"]

MIMEHeader.Values

func (h MIMEHeader) Values(key string) []string

Values 返回与给定键关联的所有值。它不区分大小写;使用 CanonicalMIMEHeaderKey 规范化提供的键。要使用非规范键,直接访问 map。返回的切片不是副本。

参数:

  • key - 头部键(不区分大小写)

返回值:

  • []string - 所有值的切片

示例:

h := make(textproto.MIMEHeader)
h.Add("Accept", "text/html")
h.Add("Accept", "application/json")
h.Add("Accept", "*/*")

values := h.Values("accept")
fmt.Println(values) // ["text/html" "application/json" "*/*"]

Pipeline

Pipeline 管理管道化的有序请求/响应序列。

type Pipeline struct {
    // 包含隐藏或未导出的字段
}

使用方法:

id := p.Next()           // 获取号码

p.StartRequest(id)       // 等待发送请求的轮次
«发送请求»
p.EndRequest(id)         // 通知 Pipeline 请求已发送

p.StartResponse(id)      // 等待读取响应的轮次
«读取响应»
p.EndResponse(id)        // 通知 Pipeline 响应已读取

Pipeline.EndRequest

func (p *Pipeline) EndRequest(id uint)

EndRequest 通知 p 具有给定 id 的请求已发送(或者,如果这是服务器,已接收)。

Pipeline.EndResponse

func (p *Pipeline) EndResponse(id uint)

EndResponse 通知 p 具有给定 id 的响应已接收(或者,如果这是服务器,已发送)。

Pipeline.Next

func (p *Pipeline) Next() uint

Next 返回请求/响应对的下一个 id。

示例:

p := &textproto.Pipeline{}
id := p.Next() // 1
id = p.Next()  // 2

Pipeline.StartRequest

func (p *Pipeline) StartRequest(id uint)

StartRequest 阻塞直到轮到发送(或者,如果这是服务器,接收)具有给定 id 的请求。

Pipeline.StartResponse

func (p *Pipeline) StartResponse(id uint)

StartResponse 阻塞直到轮到接收(或者,如果这是服务器,发送)具有给定 id 的请求。

ProtocolError

ProtocolError 描述协议违规,如无效响应或挂起的连接。

type ProtocolError string

ProtocolError.Error

func (p ProtocolError) Error() string

Error 返回错误的字符串表示。

示例:

err := textproto.ProtocolError("unexpected EOF")
fmt.Println(err.Error()) // "unexpected EOF"

Reader

Reader 为实现从文本协议网络连接读取请求或响应的便捷方法。

type Reader struct {
    R *bufio.Reader
    // 包含隐藏或未导出的字段
}

NewReader

func NewReader(r *bufio.Reader) *Reader

NewReader 返回一个新的 Reader,从 r 读取。为避免拒绝服务攻击,提供的 bufio.Reader 应该从 io.LimitReader 或类似的 Reader 读取以限制响应大小。

参数:

  • r - bufio.Reader

返回值:

  • *Reader - 文本协议 Reader

示例:

conn, err := net.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

reader := textproto.NewReader(bufio.NewReader(conn))

Reader.DotReader

func (r *Reader) DotReader() io.Reader

DotReader 返回一个新的 Reader,使用从 r 读取的点编码块的解码文本满足 Reads。返回的 Reader 仅在下次调用 r 的方法之前有效。

点编码说明:

  • 数据由一系列行组成,每行以“\r\n“结尾
  • 序列本身在以点单独成行结束:“.\r\n”
  • 以点开头的行用额外的点转义
  • 解码形式将“\r\n“重写为“\n“
  • 移除前导点转义
  • 在消耗(并丢弃)序列结束行后以 io.EOF 停止

示例:

// 读取点编码数据
dotReader := r.DotReader()
data, err := io.ReadAll(dotReader)
if err != nil {
    log.Fatal(err)
}

Reader.ReadCodeLine

func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err error)

ReadCodeLine 读取形式为 code message 的响应码行,其中 code 是三位状态码,message 扩展到行的其余部分。例如:220 plan9.bell-labs.com ESMTP

参数:

  • expectCode - 期望的状态码前缀(如 31 表示期望 310-319)

返回值:

  • code - 实际状态码
  • message - 消息文本
  • err - 错误(如果状态码不匹配,返回 &Error{code, message})

示例:

code, msg, err := r.ReadCodeLine(220)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("服务器:%d %s\n", code, msg)

Reader.ReadContinuedLine

func (r *Reader) ReadContinuedLine() (string, error)

ReadContinuedLine 从 r 读取可能延续的行,省略最终的尾部 ASCII 空白。第一行之后的行如果以空格或制表符开头则被视为延续行。在返回的数据中,延续行与前一行仅用单个空格分隔:换行符和前导空白被移除。

返回值:

  • string - 合并后的行
  • error - 错误

示例:

// 输入:
// Line 1
//   continued...
// Line 2

line1, _ := r.ReadContinuedLine() // "Line 1 continued..."
line2, _ := r.ReadContinuedLine() // "Line 2"

Reader.ReadContinuedLineBytes

func (r *Reader) ReadContinuedLineBytes() ([]byte, error)

ReadContinuedLineBytes 类似于 Reader.ReadContinuedLine,但返回 []byte 而不是字符串。

Reader.ReadDotBytes

func (r *Reader) ReadDotBytes() ([]byte, error)

ReadDotBytes 读取点编码并返回解码的数据。

返回值:

  • []byte - 解码的数据
  • error - 错误

示例:

data, err := r.ReadDotBytes()
if err != nil {
    log.Fatal(err)
}
fmt.Printf("收到:%s\n", string(data))

Reader.ReadDotLines

func (r *Reader) ReadDotLines() ([]string, error)

ReadDotLines 读取点编码并返回一个切片,包含解码的行,每行省略最终的 \r\n 或 \n。

返回值:

  • []string - 解码的行切片
  • error - 错误

示例:

lines, err := r.ReadDotLines()
if err != nil {
    log.Fatal(err)
}
for i, line := range lines {
    fmt.Printf("行 %d: %s\n", i, line)
}

Reader.ReadLine

func (r *Reader) ReadLine() (string, error)

ReadLine 从 r 读取单行,省略返回字符串中的最终 \n 或 \r\n。

返回值:

  • string - 读取的行
  • error - 错误

示例:

line, err := r.ReadLine()
if err != nil {
    log.Fatal(err)
}
fmt.Printf("收到:%s\n", line)

Reader.ReadLineBytes

func (r *Reader) ReadLineBytes() ([]byte, error)

ReadLineBytes 类似于 Reader.ReadLine,但返回 []byte 而不是字符串。

Reader.ReadMIMEHeader

func (r *Reader) ReadMIMEHeader() (MIMEHeader, error)

ReadMIMEHeader 从 r 读取 MIME 风格的头部。头部是可能延续的 Key: Value 行序列,以空行结束。返回的 map m 将 CanonicalMIMEHeaderKey(key) 映射到输入中遇到的值序列。

返回值:

  • MIMEHeader - 头部 map
  • error - 错误

示例:

// 输入:
// My-Key: Value 1
// Long-Key: Even
//        Longer Value
// My-Key: Value 2
//
// (空行)

h, err := r.ReadMIMEHeader()
if err != nil {
    log.Fatal(err)
}

fmt.Println(h)
// map[string][]string{
//     "My-Key": {"Value 1", "Value 2"},
//     "Long-Key": {"Even Longer Value"},
// }

Reader.ReadResponse

func (r *Reader) ReadResponse(expectCode int) (code int, message string, err error)

ReadResponse 读取多行响应,形式为:

code-message line 1
code-message line 2
...
code message line n

其中 code 是三位状态码。第一行以 code 和连字符开头。响应以以相同 code 后跟空格开头的行结束。message 中的每行由换行符(\n)分隔。

参数:

  • expectCode - 期望的状态码前缀

返回值:

  • code - 实际状态码
  • message - 完整消息(行由 \n 分隔)
  • err - 错误(如果状态码不匹配)

示例:

// 服务器响应:
// 250-Hello
// 250-PIPELINING
// 250 OK

code, msg, err := r.ReadResponse(250)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("响应:%d %s\n", code, msg)
// 输出:250 Hello\nPIPELINING\nOK

Writer

Writer 为实现向文本协议网络连接写入请求或响应的便捷方法。

type Writer struct {
    W *bufio.Writer
    // 包含隐藏或未导出的字段
}

NewWriter

func NewWriter(w *bufio.Writer) *Writer

NewWriter 返回一个新的 Writer,写入到 w。

参数:

  • w - bufio.Writer

返回值:

  • *Writer - 文本协议 Writer

示例:

conn, err := net.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

writer := textproto.NewWriter(bufio.NewWriter(conn))

Writer.DotWriter

func (w *Writer) DotWriter() io.WriteCloser

DotWriter 返回一个 writer,可用于向 w 写入点编码。它负责在必要时插入前导点,将行结束符 \n 转换为 \r\n,并在 DotWriter 关闭时添加最终的 .\r\n 行。调用者应在下次调用 w 的方法之前关闭 DotWriter。

返回值:

  • io.WriteCloser - 点编码写入器

示例:

dotWriter := w.DotWriter()
fmt.Fprint(dotWriter, "第一行\n第二行\n第三行")
dotWriter.Close()

Writer.PrintfLine

func (w *Writer) PrintfLine(format string, args ...any) error

PrintfLine 写入格式化输出,后跟 \r\n。

参数:

  • format - 格式化字符串
  • args - 格式化参数

返回值:

  • error - 写入错误

示例:

err := w.PrintfLine("HELO example.com")
if err != nil {
    log.Fatal(err)
}

err = w.PrintfLine("MAIL FROM:<%s>", "sender@example.com")
if err != nil {
    log.Fatal(err)
}

三、典型示例

示例 1:简单的 SMTP 客户端

package main

import (
    "fmt"
    "net"
    "net/textproto"
    "log"
    "strings"
)

func main() {
    // 连接到 SMTP 服务器
    conn, err := net.Dial("tcp", "smtp.example.com:25")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    c := textproto.NewConn(conn)
    defer c.Close()
    
    // 读取服务器欢迎消息
    code, msg, err := c.ReadCodeLine(220)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("服务器:%d %s\n", code, msg)
    
    // 发送 HELO
    err = c.PrintfLine("HELO example.com")
    if err != nil {
        log.Fatal(err)
    }
    
    code, msg, err = c.ReadCodeLine(250)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("HELO 响应:%d %s\n", code, msg)
    
    // 发送邮件
    err = c.PrintfLine("MAIL FROM:<sender@example.com>")
    if err != nil {
        log.Fatal(err)
    }
    c.ReadCodeLine(250)
    
    err = c.PrintfLine("RCPT TO:<recipient@example.com>")
    if err != nil {
        log.Fatal(err)
    }
    c.ReadCodeLine(250)
    
    // 写入邮件内容
    err = c.PrintfLine("DATA")
    if err != nil {
        log.Fatal(err)
    }
    c.ReadCodeLine(354)
    
    // 使用 DotWriter 写入邮件
    dotWriter := c.DotWriter()
    fmt.Fprint(dotWriter, strings.Join([]string{
        "From: sender@example.com",
        "To: recipient@example.com",
        "Subject: Test",
        "",
        "This is a test email.",
    }, "\r\n"))
    dotWriter.Close()
    
    c.ReadCodeLine(250)
    
    // 退出
    c.PrintfLine("QUIT")
    c.ReadCodeLine(221)
}

示例 2:读取 MIME 头部

package main

import (
    "bufio"
    "fmt"
    "net/textproto"
    "strings"
)

func main() {
    // 模拟 HTTP 响应头部
    response := "HTTP/1.1 200 OK\r\n" +
        "Content-Type: text/html; charset=utf-8\r\n" +
        "Content-Length: 1234\r\n" +
        "Set-Cookie: session=abc123\r\n" +
        "Set-Cookie: user=john\r\n" +
        "X-Custom-Header: value\r\n" +
        "\r\n"
    
    reader := textproto.NewReader(bufio.NewReader(strings.NewReader(response)))
    
    // 读取状态行
    line, err := reader.ReadLine()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("状态行:", line)
    
    // 读取 MIME 头部
    header, err := reader.ReadMIMEHeader()
    if err != nil {
        fmt.Println(err)
        return
    }
    
    // 访问头部
    fmt.Println("Content-Type:", header.Get("content-type"))
    fmt.Println("Content-Length:", header.Get("Content-Length"))
    fmt.Println("Set-Cookie:", header.Values("set-cookie"))
    fmt.Println("X-Custom-Header:", header.Get("x-custom-header"))
}

运行结果:

状态行:HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Set-Cookie: [session=abc123 user=john]
X-Custom-Header: value

示例 3:点编码数据传输

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "net/textproto"
)

func main() {
    // 模拟点编码数据
    dotEncoded := "First line\r\n" +
        "..Second line starts with dot\r\n" +
        "Third line\r\n" +
        ".\r\n"
    
    reader := textproto.NewReader(bufio.NewReader(strings.NewReader(dotEncoded)))
    
    // 读取点编码数据
    data, err := reader.ReadDotBytes()
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Printf("解码数据:%q\n", string(data))
    // 输出:"First line\n.Second line starts with dot\nThird line\n"
    
    // 写入点编码数据
    var buf bytes.Buffer
    writer := textproto.NewWriter(bufio.NewWriter(&buf))
    
    dotWriter := writer.DotWriter()
    fmt.Fprint(dotWriter, "Line 1\nLine 2\n.Line 3 starts with dot\n")
    dotWriter.Close()
    
    fmt.Printf("编码数据:%q\n", buf.String())
    // 输出:"Line 1\r\nLine 2\r\n..Line 3 starts with dot\r\n.\r\n"
}

示例 4:管道化请求

package main

import (
    "fmt"
    "net"
    "net/textproto"
    "sync"
)

func client(id uint, c *textproto.Conn, wg *sync.WaitGroup) {
    defer wg.Done()
    
    // 等待发送请求的轮次
    c.Pipeline.StartRequest(id)
    
    // 发送请求
    err := c.PrintfLine("REQUEST %d", id)
    if err != nil {
        fmt.Println("发送错误:", err)
        return
    }
    
    // 通知请求已发送
    c.Pipeline.EndRequest(id)
    
    // 等待读取响应的轮次
    c.Pipeline.StartResponse(id)
    
    // 读取响应
    line, err := c.ReadLine()
    if err != nil {
        fmt.Println("读取错误:", err)
        return
    }
    
    fmt.Printf("客户端 %d 收到:%s\n", id, line)
    
    // 通知响应已读取
    c.Pipeline.EndResponse(id)
}

func main() {
    conn, err := net.Dial("tcp", "localhost:9000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    c := textproto.NewConn(conn)
    
    var wg sync.WaitGroup
    
    // 启动多个客户端
    for i := uint(1); i <= 5; i++ {
        wg.Add(1)
        go client(i, c, &wg)
    }
    
    wg.Wait()
}

示例 5:多行响应处理

package main

import (
    "bufio"
    "fmt"
    "net/textproto"
    "strings"
)

func main() {
    // 模拟 FTP 多行响应
    response := "211-Features:\r\n" +
        "211-AUTH TLS\r\n" +
        "211-PBSZ\r\n" +
        "211 PROT\r\n" +
        "211 End\r\n"
    
    reader := textproto.NewReader(bufio.NewReader(strings.NewReader(response)))
    
    // 读取多行响应
    code, msg, err := reader.ReadResponse(211)
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Printf("响应码:%d\n", code)
    fmt.Printf("消息:\n%s\n", msg)
    // 输出:
    // 响应码:211
    // 消息:
    // Features:
    // AUTH TLS
    // PBSZ
    // PROT
    // End
}

示例 6:延续行处理

package main

import (
    "bufio"
    "fmt"
    "net/textproto"
    "strings"
)

func main() {
    // 模拟延续行
    input := "Line 1\r\n" +
        "  continued with more text\r\n" +
        "  and even more\r\n" +
        "Line 2\r\n" +
        "Line 3\r\n"
    
    reader := textproto.NewReader(bufio.NewReader(strings.NewReader(input)))
    
    for {
        line, err := reader.ReadContinuedLine()
        if err != nil {
            break
        }
        fmt.Printf("行:%q\n", line)
    }
}

运行结果:

行:"Line 1 continued with more text and even more"
行:"Line 2"
行:"Line 3"

示例 7:自定义协议服务器

package main

import (
    "fmt"
    "log"
    "net"
    "net/textproto"
    "strings"
)

func handleClient(conn net.Conn) {
    defer conn.Close()
    
    c := textproto.NewConn(conn)
    
    // 发送欢迎消息
    c.PrintfLine("220 Welcome to Custom Server")
    
    for {
        // 读取命令
        line, err := c.ReadLine()
        if err != nil {
            log.Println("读取错误:", err)
            return
        }
        
        // 解析命令
        parts := strings.SplitN(line, " ", 2)
        cmd := strings.ToUpper(parts[0])
        
        switch cmd {
        case "ECHO":
            if len(parts) > 1 {
                c.PrintfLine("200 %s", parts[1])
            } else {
                c.PrintfLine("400 Missing argument")
            }
        
        case "HELP":
            c.PrintfLine("214-ECHO <text> - Echo the text")
            c.PrintfLine("214 QUIT - Close connection")
        
        case "QUIT":
            c.PrintfLine("221 Goodbye")
            return
        
        default:
            c.PrintfLine("500 Unknown command")
        }
    }
}

func main() {
    listener, err := net.Listen("tcp", ":9000")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()
    
    fmt.Println("服务器启动在 :9000")
    
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Println("accept error:", err)
            continue
        }
        go handleClient(conn)
    }
}

四、最佳实践

1. 使用 LimitReader 防止 DoS 攻击

import (
    "io"
    "net"
    "net/textproto"
)

conn, err := net.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}

// 限制响应大小为 1MB
limitedReader := io.LimitReader(conn, 1024*1024)
reader := textproto.NewReader(bufio.NewReader(limitedReader))

2. 正确使用 DotWriter

// ✓ 正确 - 记得关闭
dotWriter := w.DotWriter()
fmt.Fprint(dotWriter, "data")
dotWriter.Close()

// ✗ 错误 - 不关闭会导致数据不完整
dotWriter := w.DotWriter()
fmt.Fprint(dotWriter, "data")
// 忘记 Close()

3. 管道化请求顺序

// ✓ 正确 - 遵循正确的顺序
id := p.Next()
p.StartRequest(id)
sendRequest()
p.EndRequest(id)

p.StartResponse(id)
readResponse()
p.EndResponse(id)

// ✗ 错误 - 顺序混乱
p.StartRequest(id)
p.StartResponse(id) // 错误:请求还没结束

4. 错误处理

code, msg, err := r.ReadCodeLine(220)
if err != nil {
    if protoErr, ok := err.(*textproto.Error); ok {
        // 处理协议错误
        log.Printf("协议错误:%d %s", protoErr.Code, protoErr.Msg)
    } else {
        // 处理其他错误
        log.Fatal(err)
    }
}

5. MIME 头部大小写

h := make(textproto.MIMEHeader)
h.Add("Content-Type", "text/html")

// ✓ 正确 - 使用 Get(不区分大小写)
value := h.Get("content-type")

// ✓ 正确 - 直接访问 map(区分大小写)
values := h["Content-Type"]

五、与其他包配合

1. 与 net 包配合

import (
    "net"
    "net/textproto"
)

conn, err := net.Dial("tcp", "localhost:9000")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

c := textproto.NewConn(conn)

2. 与 bufio 包配合

import (
    "bufio"
    "net/textproto"
)

reader := textproto.NewReader(bufio.NewReader(conn))
writer := textproto.NewWriter(bufio.NewWriter(conn))

3. 与 io 包配合

import (
    "io"
    "net/textproto"
)

// 使用 LimitReader 限制大小
limited := io.LimitReader(conn, 1024*1024)
reader := textproto.NewReader(bufio.NewReader(limited))

// 读取点编码数据
dotReader := reader.DotReader()
data, err := io.ReadAll(dotReader)

六、快速参考

函数总览

函数说明
CanonicalMIMEHeaderKey规范化 MIME 头部键
Dial连接到地址并返回 Conn
NewConn从现有连接创建 Conn
NewReader创建 Reader
NewWriter创建 Writer
TrimBytes去除字节切片前后空格
TrimString去除字符串前后空格

类型总览

类型说明
Conn文本协议连接(Reader+Writer+Pipeline)
Error协议错误(数字码 + 消息)
MIMEHeaderMIME 头部 map
Pipeline管道化管理
ProtocolError协议违规错误
Reader读取文本协议
Writer写入文本协议

Reader 方法

方法说明
DotReader返回点编码解码器
ReadCodeLine读取单行响应码
ReadContinuedLine读取延续行
ReadDotBytes读取点编码字节
ReadDotLines读取点编码行
ReadLine读取单行
ReadMIMEHeader读取 MIME 头部
ReadResponse读取多行响应

Writer 方法

方法说明
DotWriter返回点编码写入器
PrintfLine写入格式化行

Conn 方法

方法说明
Close关闭连接
Cmd发送命令(管道化)

点编码规则

规则说明
行结束\r\n
序列结束.\r\n(单独一行的点)
点转义行首的点转义为 ..
解码\r\n → \n,移除点转义

七、注意事项

1. 行结束符

// textproto 使用 \r\n 作为行结束符
// PrintfLine 自动添加 \r\n
w.PrintfLine("HELLO") // 实际发送 "HELLO\r\n"

// ReadLine 去除 \r\n 或 \n
line, _ := r.ReadLine() // 返回不带行结束符的行

2. DotWriter 必须关闭

// ✓ 正确
w := writer.DotWriter()
fmt.Fprint(w, "data")
w.Close() // 添加 .\r\n 结束标记

// ✗ 错误 - 不完整
w := writer.DotWriter()
fmt.Fprint(w, "data")
// 忘记 Close(),序列不完整

3. Pipeline 同步

// Pipeline 确保请求/响应按顺序
// 必须成对调用 Start/End
p.StartRequest(id)
sendRequest()
p.EndRequest(id)

p.StartResponse(id)
readResponse()
p.EndResponse(id)

4. MIME 头部键规范化

// Get 和 Values 自动规范化键
h.Add("Content-Type", "text/html")
h.Get("content-type")     // "text/html"
h.Get("CONTENT-TYPE")     // "text/html"

// 直接访问 map 不规范化
h["Content-Type"]         // ["text/html"]
h["content-type"]         // nil

5. ReadCodeLine vs ReadResponse

// ReadCodeLine - 单行响应
code, msg, err := r.ReadCodeLine(220)

// ReadResponse - 多行响应
// 211-First line
// 211-Second line
// 211 Final line
code, msg, err := r.ReadResponse(211)

6. 错误类型判断

err := r.ReadCodeLine(220)
if err != nil {
    if protoErr, ok := err.(*textproto.Error); ok {
        // 协议错误(服务器返回的错误码)
        fmt.Printf("%d: %s\n", protoErr.Code, protoErr.Msg)
    } else {
        // 其他错误(网络错误等)
        log.Fatal(err)
    }
}

7. 延续行规则

// 只有以空格或制表符开头的行才是延续行
// 空行永远不会延续

// 输入:
// Line 1
//   continued
// Line 2

// ReadContinuedLine 返回:
// "Line 1 continued"
// "Line 2"

8. 安全考虑

// 始终限制响应大小防止 DoS
limited := io.LimitReader(conn, maxBytes)
reader := textproto.NewReader(bufio.NewReader(limited))

最后更新: 2026-04-05
Go 版本: Go 1.0+
包文档: https://pkg.go.dev/net/textproto
相关 RFC: RFC 9112 (HTTP/1.1), RFC 959 (FTP)
应用场景: HTTP、NNTP、SMTP、FTP 等文本协议实现