نقل HTTPUpgrade
مقدمة
HTTPUpgrade هو نقل خفيف الوزن يحاكي مصافحة WebSocket (HTTP/1.1 Upgrade: websocket) لكنه لا يستخدم تأطير WebSocket. بعد اكتمال مصافحة الترقية، يصبح الاتصال تدفق TCP خام. هذا يجعله أبسط وأكفأ من نقل WebSocket الكامل، مع الاستمرار في المرور عبر الأجهزة الوسيطة التي تتوقع تدفقات HTTP Upgrade. يدعم TLS وبصمة uTLS وبروتوكول PROXY والبيانات المبكرة.
تسجيل البروتوكول
مسجل باسم "httpupgrade" (transport/internet/httpupgrade/httpupgrade.go:3):
const protocolName = "httpupgrade"- المتصل:
httpupgrade/dialer.go:134-136 - المستمع:
httpupgrade/hub.go:165-167 - الإعدادات:
httpupgrade/config.go:19-23
تدفق الاتصال
المصافحة
dialhttpUpgrade (httpupgrade/dialer.go:46-115) ينفذ ترقية HTTP يدوية:
func dialhttpUpgrade(ctx context.Context, dest net.Destination,
streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
// 1. Dial raw TCP via internet.DialSystem
pconn, _ := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
// 2. Optionally wrap with TLS
if tConfig != nil {
// Apply TLS with uTLS fingerprinting or standard Go TLS
conn = tls.UClient(pconn, tlsConfig, fingerprint) // or tls.Client
}
// 3. Build HTTP request
req := &http.Request{
Method: http.MethodGet,
URL: &requestURL,
Header: make(http.Header),
}
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
// 4. Write request directly to connection
req.Write(conn)
// 5. Wrap with ConnRF for response reading
connRF := &ConnRF{Conn: conn, Req: req, First: true}
return connRF, nil
}على عكس نقل WebSocket، هذا لا يستخدم gorilla/websocket. يتم كتابة طلب HTTP مباشرة إلى تدفق TCP ويتم تحليل الاستجابة يدويًا.
قراءة الاستجابة (ConnRF)
بنية ConnRF (httpupgrade/dialer.go:19-44) تعترض أول استدعاء Read لتحليل استجابة HTTP:
type ConnRF struct {
net.Conn
Req *http.Request
First bool
}
func (c *ConnRF) Read(b []byte) (int, error) {
if c.First {
c.First = false
reader := bufio.NewReaderSize(c.Conn, len(b))
resp, err := http.ReadResponse(reader, c.Req)
if resp.Status != "101 Switching Protocols" ||
strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return 0, errors.New("unrecognized reply")
}
// Drain buffered bytes from bufreader
return reader.Read(b[:reader.Buffered()])
}
return c.Conn.Read(b)
}تصميم أساسي: حجم bufio.Reader يساوي بالضبط len(b) لضمان التقاط أي بيانات بعد رؤوس استجابة HTTP (التي قد تصل في نفس مقطع TCP) وإعادتها في هذه القراءة الأولى.
البيانات المبكرة
عندما Ed == 0 (الافتراضي)، تُقرأ الاستجابة فورًا أثناء الاتصال لتأكيد نجاح الترقية (dialer.go:107-112). عندما Ed > 0، يتم تأجيل قراءة الاستجابة إلى أول استدعاء Read من التطبيق، مما يسمح للاتصال بالاكتمال أسرع.
معالجة الرؤوس
يتم إضافة الرؤوس المخصصة عبر دالة AddHeader (dialer.go:120-122) التي تتجاوز تطبيع رؤوس MIME في Go:
func AddHeader(header http.Header, key, value string) {
header[key] = append(header[key], value)
}هذا يحافظ على حالة أحرف أسماء الرؤوس بالضبط (مثل "Web*S*ocket" بدلاً من "Websocket").
تدفق الاستماع
بنية الخادم
ListenHTTPUpgrade (httpupgrade/hub.go:115-163) ينشئ مستمع TCP خام (ليس خادم HTTP):
func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
// Create TCP/Unix listener via internet.ListenSystem
// Optionally wrap with TLS
serverInstance := &server{
config: transportConfiguration,
addConn: addConn,
innnerListener: listener,
}
go serverInstance.keepAccepting()
return serverInstance, nil
}على عكس نقل WebSocket (الذي يستخدم http.Server)، يقبل HTTPUpgrade اتصالات خام ويحلل طلبات HTTP يدويًا.
معالجة الاتصال
server.Handle (httpupgrade/hub.go:34-42) و server.upgrade (hub.go:45-103):
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
connReader := bufio.NewReader(conn)
req, _ := http.ReadRequest(connReader)
// Validate host and path
if len(s.config.Host) > 0 && !internet.IsValidHTTPHost(host, s.config.Host) {
return nil, errors.New("bad host")
}
if req.URL.Path != path {
return nil, errors.New("bad path")
}
// Validate upgrade headers
if connection != "upgrade" || upgrade != "websocket" {
return nil, errors.New("unrecognized request")
}
// Send 101 response
resp := &http.Response{
Status: "101 Switching Protocols",
StatusCode: 101,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
resp.Header.Set("Connection", "Upgrade")
resp.Header.Set("Upgrade", "websocket")
resp.Write(conn)
return stat.Connection(newConnection(conn, remoteAddr)), nil
}X-Forwarded-For
يتم استخراج عنوان IP الحقيقي للعميل من رؤوس إعادة التوجيه (hub.go:83-100)، مع احترام إعداد TrustedXForwardedFor في المقبس.
تنسيق السلك
Client -> Server:
GET /path HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
User-Agent: Mozilla/5.0 ...
[custom headers]
Server -> Client:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
[raw bidirectional TCP stream]بعد المصافحة، تتدفق البيانات كبايتات خام -- بدون تأطير WebSocket، بدون بادئات طول، بدون تقنيع. هذا هو الفرق الجوهري عن نقل WebSocket الكامل.
sequenceDiagram
participant Client
participant Server
Client->>Server: GET /path HTTP/1.1\r\nUpgrade: websocket\r\n...
Server->>Client: HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n
Note over Client,Server: Raw TCP stream begins
Client->>Server: [proxy data bytes]
Server->>Client: [proxy data bytes]مغلف الاتصال
بنية connection (httpupgrade/connection.go:5-19) بسيطة:
type connection struct {
net.Conn
remoteAddr net.Addr
}
func (c *connection) RemoteAddr() net.Addr {
return c.remoteAddr
}تتجاوز فقط RemoteAddr() لدعم X-Forwarded-For. جميع الطرق الأخرى تُفوض إلى net.Conn الأساسي.
دعم بروتوكول PROXY
يدعم كل من العميل والخادم بروتوكول PROXY:
- الخادم: موروث من إعدادات المقبس. عندما يكون
AcceptProxyProtocolمفعلاً، يغلفinternet.ListenSystemالأساسي المستمع (hub.go:117-122،hub.go:145-147). - دمج الإعدادات:
AcceptProxyProtocolمن إعدادات النقل أو إعدادات المقبس يُفعّل بروتوكول PROXY (hub.go:121).
خيارات الإعدادات
من httpupgrade/config.go:
- Path: مسار URL، مُطبّع بـ
/في البداية (config.go:8-17) - Host: رأس Host المتوقع للتحقق من جانب الخادم
- Header: رؤوس HTTP مخصصة (قاموس)
- AcceptProxyProtocol: تفعيل بروتوكول PROXY على المستمع
- Ed: حجم البيانات المبكرة. عند قيمة غير صفرية، يتم تأجيل تحليل الاستجابة.
ملاحظات التنفيذ
- بدون تأطير WebSocket: بعد استجابة
101 Switching Protocols، تُرسل البيانات كبايتات خام. هذا يزيل حمل WebSocket (2-14 بايت لكل إطار) ومتطلب التقنيع. - بدون اعتماد على gorilla: على عكس نقل WebSocket، لا يستخدم HTTPUpgrade مكتبة
gorilla/websocket. يبني ويحلل رسائل HTTP مباشرة. - خادم بدون حالة: كل اتصال يُعالج بشكل مستقل. الخادم لا يحتفظ بحالة جلسة.
- الحفاظ على حالة أحرف الرؤوس: دالة
AddHeaderتتجاوزtextproto.CanonicalMIMEHeaderKeyفي Go، مما يسمح بأسماء رؤوس بحالة أحرف محددة. هذا يمكن أن يساعد في مطابقة توقعات CDN أو أجهزة وسيطة محددة. - خدعة حجم bufio:
ConnRF.Readتنشئbufio.ReaderSize(conn, len(b))محدودًا بحجم مخزن المستدعي. هذا يضمن أن القارئ المخزن لا يقرأ أكثر مما يمكن إعادته، مما يمنع فقدان البيانات من التخزين المؤقت المزدوج. - TLS على المستمع: على عكس gRPC (الذي يستخدم نظام بيانات اعتماد gRPC)، يغلف HTTPUpgrade المستمع
net.Listenerبـtls.NewListenerمباشرة (hub.go:149-153). - المقارنة مع WebSocket: HTTPUpgrade أبسط وأكفأ بشكل صارم لاستخدام البروكسي. يُفضل نقل WebSocket فقط عند الحاجة لميزات خاصة بـ WebSocket (نبضات Ping، ضغط لكل رسالة، متصل المتصفح).