如何在Go语言中根据主机名在同一端口监听HTTP和HTTPS?
同一端口同时处理HTTP和HTTPS请求的实现方案
可以在同一端口同时处理HTTP和HTTPS请求,但Go标准库的http.ListenAndServe与ListenAndServeTLS无法直接实现该需求,需要手动处理底层TCP连接的协议检测,根据连接初始数据判断请求类型后分发给对应逻辑处理。
你当前代码的问题
你的registerSite逻辑对同一端口仅启动单一类型服务器(HTTP或HTTPS),当同一端口下不同主机需要分别使用HTTP和HTTPS时,先注册的主机决定了端口的服务器类型,后注册的站点会被跳过,自然无法满足需求。
实现思路
- 手动监听TCP端口:替代标准库的自动监听逻辑,自行接收所有传入的TCP连接
- 协议检测:读取连接的第一个字节,判断是HTTP(起始为
GET/POST等ASCII字符)还是HTTPS(起始字节为0x16,对应TLS握手的ClientHello) - 连接分发:
- 若为HTTPS请求,用
autocert.Manager的TLS配置包装连接,再交给HTTP服务器处理 - 若为HTTP请求,直接将连接交给HTTP服务器处理
- 若为HTTPS请求,用
- 保留原有路由逻辑:原有的
handleRequest和sitesManager逻辑可以继续使用,仅需调整服务器启动方式
修改后的代码示例
import ( "errors" "log" "net" "net/http" "strings" "crypto/tls" "golang.org/x/crypto/acme/autocert" // 导入你的sites包 ) var sitesManager *sites.SiteManager = sites.CreateSiteManager() func handleRequest(res http.ResponseWriter, req *http.Request) { ctx := req.Context() svrAddr := ctx.Value(http.LocalAddrContextKey).(net.Addr) host := strings.Split(req.Host, ":")[0] port := strings.Split(svrAddr.String(), ":")[1] origin := host + ":" + port site, exists := sitesManager.GetSite(origin) if !exists { http.Error(res, "Site not found", http.StatusNotFound) return } site.HandleRequest(res, req) } func main() { manager := &autocert.Manager{ Cache: autocert.DirCache("certsCache"), Prompt: autocert.AcceptTOS, HostPolicy: sitesManager.HostPolicy(), } // 直接注册所有站点,不再通过registerSite启动服务器 sitesManager.RegisterSite("example1.com:8000", &sites.ReverseProxySite{ Destination: "localhost:3000", HTTPS: false, }) sitesManager.RegisterSite("example2.com:8000", &sites.ReverseProxySite{ Destination: "localhost:3001", HTTPS: true, }) sitesManager.RegisterSite("example.com:80", &sites.RedirectSite{ Destination: "https://example.com", HTTPS: true, }) sitesManager.RegisterSite("example.com:443", &sites.ReverseProxySite{ Destination: "http://localhost:8000", HTTPS: true, }) // 启动标准80端口HTTP服务 go func() { err := http.ListenAndServe(":80", manager.HTTPHandler(http.HandlerFunc(handleRequest))) if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("HTTP server on 80 failed: %v", err) } }() // 启动标准443端口HTTPS服务 go func() { tlsServer := &http.Server{ Addr: ":443", Handler: http.HandlerFunc(handleRequest), TLSConfig: manager.TLSConfig(), } err := tlsServer.ListenAndServeTLS("", "") if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("HTTPS server on 443 failed: %v", err) } }() // 处理自定义8000端口的混合协议请求 ln, err := net.Listen("tcp", ":8000") if err != nil { log.Fatalf("Failed to listen on 8000: %v", err) } defer ln.Close() handler := http.HandlerFunc(handleRequest) tlsConfig := manager.TLSConfig() for { conn, err := ln.Accept() if err != nil { log.Printf("Accept error: %v", err) continue } go func(c net.Conn) { defer c.Close() // 读取第一个字节判断协议类型 buf := make([]byte, 1) _, err := c.Read(buf) if err != nil { log.Printf("Read error: %v", err) return } // 将读取的字节放回连接,保证后续处理能拿到完整请求数据 if err := c.(*net.TCPConn).UnreadByte(); err != nil { log.Printf("Unread error: %v", err) return } if buf[0] == 0x16 { // TLS握手起始字节,交给TLS包装后的服务处理 tlsConn := tls.Server(c, tlsConfig) if err := http.Serve(tlsConn, handler); err != nil { log.Printf("TLS serve error: %v", err) } } else { // 普通HTTP请求,直接交给HTTP服务处理 if err := http.Serve(c, handler); err != nil { log.Printf("HTTP serve error: %v", err) } } }(conn) } }
关键细节说明
- 协议检测:通过读取第一个字节判断是否为TLS握手(
0x16是TLS Record Layer的Handshake类型标识),再用UnreadByte将字节放回连接,确保后续服务能读取完整请求 - autocert集成:
autocert.Manager的TLSConfig()会自动为请求的主机名获取并管理证书,无需手动为每个站点配置证书 - 并发处理:每个连接启动独立goroutine处理,保证服务器的并发能力
- 端口兼容:标准80/443端口仍可使用原有逻辑,自定义端口采用混合协议处理逻辑
内容的提问来源于stack exchange,提问作者PortalGamesMais
相关产品推荐
相关产品推荐

