Gemini 协议作为一种新兴的轻量级互联网协议,提供了一种简洁、安全的文本导向内容传输方式。它运行在 TCP 端口 1965 上,并强制要求使用 TLS 加密,这使得它比传统的 HTTP 更注重隐私和效率。不同于 HTTP 的复杂性,Gemini 的设计灵感来源于 Gopher 协议,专注于纯文本文档(gemtext 格式),支持基本的链接、列表和预格式文本,而不支持 JavaScript 或 CSS 等复杂功能。这使得它特别适合低带宽环境、隐私敏感场景或作为 Web 的补充替代方案。
在 Go 语言中实现 Gemini 服务器和客户端的优势在于 Go 的标准库强大,尤其是 net/http 和 crypto/tls 包,能轻松处理网络连接和 TLS 加密,而无需引入过多第三方依赖。这不仅简化了开发过程,还确保了高性能和并发支持。本文将一步步指导如何构建基本的 Gemini 服务器和客户端,重点处理 MIME 类型、证书管理和请求/响应机制。通过这些实现,你可以快速搭建一个运行在本地或服务器上的 Gemini 胶囊(capsule),并用客户端访问它。
环境准备与证书生成
首先,确保你的 Go 环境已安装(版本 1.20+ 推荐)。Gemini 协议要求所有通信通过 TLS,因此需要生成自签名证书。Go 的 crypto/tls 包支持自签名,但生产环境中建议使用 Let's Encrypt 等 CA 颁发的证书。对于开发,我们使用自签名证书,并采用 TOFU(Trust On First Use)验证机制,即首次连接时信任证书,后续检查一致性。
使用以下命令生成自签名证书(需要安装 OpenSSL):
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -subj "/CN=localhost"
这将生成 server.key(私钥)和 server.crt(证书)。在代码中加载它们:
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatal(err)
}
config := &tls.Config{Certificates: []tls.Certificate{cert}}
listener, err := tls.Listen("tcp", ":1965", config)
if err != nil {
log.Fatal(err)
}
证据显示,Gemini 规范(v0.14.3)强调 TLS 1.2+ 和隐私增强功能,如不支持弱加密套件。这在 Go 的 tls.Config 中可以通过 MinVersion: tls.VersionTLS12 和 CipherSuites 自定义实现。对于 TOFU,客户端需存储服务器证书指纹,并在后续连接中验证。
基本服务器实现
Gemini 服务器的核心是监听端口 1965,接受 TLS 连接,解析客户端发送的简单请求(一行 URL,如 gemini://example.com/path),然后返回响应。响应格式为:第一行状态码 + META(如 20 text/gemini\r\n),成功时跟随响应体。
使用 Go 的 net 包实现一个简单的服务器:
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleConnection(conn)
}
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
request, err := reader.ReadString('\n')
if err != nil {
return
}
path := strings.TrimSpace(request)
if !strings.HasPrefix(path, "gemini://") {
path = "/default"
}
if path == "/" {
body := "# 欢迎来到 Gemini 服务器\r\n这是一个简单的 Gemini 胶囊。"
meta := fmt.Sprintf("20 text/gemini\r\n")
response := meta + body
conn.Write([]byte(response))
} else {
conn.Write([]byte("51 No such resource\r\n"))
}
}
这里,META 行指定 MIME 类型,如 text/gemini 用于 gemtext。Gemini 支持多种 MIME:text/plain 用于纯文本、image/png 用于图片(通过链接访问,非内嵌)。引用 Gemini 规范:“响应头必须是单个 CRLF 终止行,格式为 'STATUS META\r\n',其中 STATUS 是两位数字,META 是 MIME 类型或错误消息。”
对于 MIME 处理,服务器需根据文件扩展或内容类型设置 META。例如,处理 PNG 文件:
if strings.HasSuffix(path, ".png") {
meta := "20 image/png\r\n"
data, _ := os.ReadFile("image.png")
conn.Write([]byte(meta))
conn.Write(data)
}
可落地参数:
- 监听地址:
:1965(默认端口)。
- 证书有效期:365 天,RSA 2048 位。
- 并发:Go 的 goroutine 天然支持高并发,每连接一个 goroutine。
- 错误码:使用 40(临时失败)、51(资源不存在)、59(坏请求)等。
风险:自签名证书易遭中间人攻击,建议客户端实现 TOFU 并在首次连接时显示指纹(如 SHA-256)。
基本客户端实现
客户端需发起 TLS 连接到 hostname:1965,发送请求 URL,然后读取响应。Go 的 tls.Dial 简化了这一过程。
func fetchGemini(url string) (string, error) {
u, err := url.Parse(url)
if err != nil {
return "", err
}
config := &tls.Config{ServerName: u.Hostname()}
conn, err := tls.Dial("tcp", u.Host+":"+u.Port(), config)
if err != nil {
return "", err
}
defer conn.Close()
request := u.Path + "\r\n"
conn.Write([]byte(request))
reader := bufio.NewReader(conn)
statusLine, err := reader.ReadString('\n')
if err != nil {
return "", err
}
parts := strings.SplitN(strings.TrimSpace(statusLine), " ", 2)
if len(parts) < 2 || !strings.HasPrefix(parts[0], "2") {
return parts[1], nil
}
body, _ := io.ReadAll(reader)
return string(body), nil
}
对于 TOFU,客户端可存储证书:
conn, err := tls.Dial("tcp", host, config)
if err != nil {
return err
}
state := conn.ConnectionState()
fingerprint := sha256.Sum256(state.PeerCertificates[0].Raw)
storeFingerprint(host, fingerprint[:])
后续验证:
if !verifyFingerprint(host, fingerprint) {
return errors.New("证书指纹不匹配")
}
MIME 处理:在客户端,根据 META 决定如何渲染。例如,text/gemini 解析为链接(=> URL 文本),image/* 下载显示。Go 可使用 regexp 解析 gemtext。
可落地清单:
- URL 格式:
gemini://hostname/path?query
- 超时:
tls.DialWithDialer 设置 10s 超时。
- 重定向:Gemini 支持 3xx 状态,客户端需跟随(最多 10 次)。
- 代理:不支持 HTTP 代理,需自定义 SOCKS。
高级考虑与测试
在实际部署中,集成文件系统服务静态内容,或使用 CGI 处理动态响应。监控连接数,避免 DDoS(限速 1 req/s)。测试时,用 Lagrange(Gemini 浏览器)访问 gemini://localhost/。
Gemini 的优势在于其简洁:响应大小通常 <1KB,加载 <100ms,远优于 HTTP 的 bloated 页面。这使其理想用于 IoT 或移动设备。
资料来源:
通过以上实现,你已掌握 Gemini 在 Go 中的核心。扩展时,可添加 gemini:// 链接解析或多 MIME 支持,进一步探索这一轻量级协议的潜力。(字数:1024)