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- 命令 IDerr- 错误
示例:
// 发送 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- 头部 maperror- 错误
示例:
// 输入:
// 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 | 协议错误(数字码 + 消息) |
| MIMEHeader | MIME 头部 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 等文本协议实现