Source File
http.go
Belonging Package
github.com/gotd/td/telegram/dcs
package dcsimport ()const (// defaultHTTPMaxWait is the default http_wait max_wait (ms): the server holds// the long-poll response open up to this long. Matches the MTProto default.defaultHTTPMaxWait = 25000// httpClientTimeoutMargin is added on top of max_wait for the HTTP client// timeout so a full-length long-poll response is not cut off.httpClientTimeoutMargin = 5 * time.Second// httpWaitRetryInterval throttles the poll loop when a wait frame cannot be// built (e.g. during key exchange) or a POST fails, avoiding a busy loop.httpWaitRetryInterval = 500 * time.Millisecond// httpInboxSize buffers response frames until Recv consumes them.httpInboxSize = 32// httpMaxConnsPerHost bounds concurrent connections of the default client to// a single DC, so a burst of sends cannot exhaust file descriptors.httpMaxConnsPerHost = 16)// recvResult is a decoded response delivered to Recv: either a frame or a// transport-level protocol error (e.g. auth_key_not_found).type recvResult struct {data []byteerr error}// httpConn implements transport.Conn over the MTProto HTTP transport.//// See https://core.telegram.org/mtproto/transports#http-transport.//// MTProto over HTTP is request/response: the server can only deliver messages// as the body of a response to a client POST. Send posts one raw MTProto frame// (framing is done by HTTP Content-Length, there is no codec tag) and the// response body — the messages the server had queued for the session — is// delivered to Recv through inbox. Send is non-blocking: the POST and its// (possibly long-polling) response are handled on a separate goroutine so a 25s// http_wait long-poll never stalls the mtproto write path.type httpConn struct {client *http.Client// urls holds one /api endpoint per candidate DC address. Requests use the// current index; a failed POST rotates to the next candidate.urls []stringurlIdx atomic.Uint32// inbox buffers responses (frames or protocol errors) until Recv consumes// them.inbox chan recvResult// http_wait parameters in milliseconds, reported via HTTPWaitParams and used// by the mtproto layer to build http_wait messages.maxDelay intwaitAfter intmaxWait intctx context.Contextcancel context.CancelFuncstartOnce sync.OncecloseOnce sync.Once}func ( *http.Client, []string, , , int) *httpConn {, := context.WithCancel(context.Background())return &httpConn{client: ,urls: ,inbox: make(chan recvResult, httpInboxSize),maxDelay: ,waitAfter: ,maxWait: ,ctx: ,cancel: ,}}var _ transport.Conn = (*httpConn)(nil)// Send posts a single MTProto frame. It is non-blocking: the POST and response// are handled on a separate goroutine, so a long-poll response never stalls the// caller. Delivery reliability is provided by the mtproto rpc engine// (acks/retransmits) and liveness by the ping loop; a failed POST is therefore// dropped here and recovered upstream.func ( *httpConn) ( context.Context, *bin.Buffer) error {select {case <-.ctx.Done():return errors.Wrap(net.ErrClosed, "send")default:}// b.Buf is reused by the caller once Send returns; copy before handing it to// the POST goroutine.:= make([]byte, len(.Buf))copy(, .Buf)// One goroutine per send: bounded in practice by the mtproto rpc engine's// in-flight window and by the client's MaxConnsPerHost (which the goroutines// block on). All are cancelled on Close via c.ctx.go func() { _, _ = .roundtrip() }()return nil}// Recv blocks until an incoming frame or a transport-level protocol error is// available.func ( *httpConn) ( context.Context, *bin.Buffer) error {select {case := <-.inbox:if .err != nil {return .err}.Buf = append(.Buf[:0], .data...)return nilcase <-.Done():return .Err()case <-.ctx.Done():return errors.Wrap(net.ErrClosed, "recv")}}// Close releases the connection and cancels in-flight requests.func ( *httpConn) () error {.closeOnce.Do(.cancel)return nil}// roundtrip posts one frame and delivers the response to inbox. It reports// whether anything was delivered (a frame or a protocol error) and returns an// error only for transport-level failures (POST error, non-200), which callers// use to back off.func ( *httpConn) ( []byte) ( bool, error) {:= .urls[.urlIndex()], := http.NewRequestWithContext(.ctx, http.MethodPost, , bytes.NewReader())if != nil {return false, errors.Wrap(, "build request")}.Header.Set("Content-Type", "application/octet-stream"), := .client.Do()if != nil {.rotateURL() // fail over to the next candidate addressreturn false, errors.Wrap(, "do request")}defer func() { _ = .Body.Close() }()if .StatusCode != http.StatusOK {_, _ = io.Copy(io.Discard, .Body)return false, errors.Errorf("unexpected status %d", .StatusCode)}, := io.ReadAll(.Body)if != nil {return false, errors.Wrap(, "read body")}switch {case len() == 0:// Empty body is a valid "dummy" long-poll response (max_wait elapsed with// no messages); nothing to deliver.return false, nilcase len() == bin.Word:// A 4-byte body is a transport-level protocol error (e.g. -404 auth key// not found), not an encrypted message. The TCP codecs surface it via// checkProtocolError; do the same so the read loop can react (e.g.// regenerate the auth key) instead of feeding garbage to the decrypter.:= int32(binary.LittleEndian.Uint32()).deliver(recvResult{err: &codec.ProtocolErr{Code: -}})return true, nildefault:.deliver(recvResult{data: })return true, nil}}// urlIndex returns the current candidate index. The modulo is taken on the// unsigned counter so the index stays in range even if the counter wraps.func ( *httpConn) () int {return int(.urlIdx.Load() % uint32(len(.urls)))}func ( *httpConn) ( recvResult) {select {case .inbox <- :case <-.ctx.Done():}}// rotateURL advances to the next candidate address after a failed POST. Failover// is best-effort: a burst of concurrent failures may advance the index by more// than one, but the poll loop's round-robin still reaches every candidate.func ( *httpConn) () {if len(.urls) > 1 {.urlIdx.Add(1)}}// HTTPWaitParams reports the http_wait fields (milliseconds) the transport wants// the mtproto layer to use. It is part of the mtproto HTTP long-poll capability.func ( *httpConn) () (, , int) {return .maxDelay, .waitAfter, .maxWait}// StartHTTPWait starts the long-poll loop. frame yields a freshly-encrypted// http_wait service message — only the mtproto layer can encrypt it. The loop// keeps exactly one long-poll POST outstanding, re-issuing it as soon as the// previous response arrives, to minimize update latency. It is safe to call at// most once; further calls are no-ops.//// The loop runs in a goroutine whose lifetime is bound solely to Close: it is// not part of the mtproto Run errgroup, so the connection must be closed to stop// it (Conn.handleClose does this on teardown).func ( *httpConn) ( func( context.Context) (*bin.Buffer, error)) {.startOnce.Do(func() {go .pollLoop()})}func ( *httpConn) ( func( context.Context) (*bin.Buffer, error)) {// A conforming server holds an empty response until max_wait; a response that// comes back much sooner than this signals a middlebox not honoring the// long-poll, and must be throttled to avoid a request storm.:= time.Duration(.maxWait) * time.Millisecond / 2for {if .ctx.Err() != nil {return}, := (.ctx)if != nil {// Wait frame cannot be built (e.g. key exchange in progress) or the// connection is closing. Back off to avoid a hot loop.if !.sleep(httpWaitRetryInterval) {return}continue}// Synchronous: blocks up to max_wait, then immediately loops to re-issue// the next http_wait (low update latency, one poll outstanding).:= time.Now(), := .roundtrip(.Buf)switch {case != nil:// POST failed; back off instead of hammering a dead connection. The// ping loop ultimately detects and tears down a dead connection.if !.sleep(httpWaitRetryInterval) {return}case ! && time.Since() < :// Instant empty 200 from a non-conforming middlebox: throttle.if !.sleep(httpWaitRetryInterval) {return}}}}// sleep waits for d, returning false if the connection was closed while waiting.func ( *httpConn) ( time.Duration) bool {select {case <-.ctx.Done():return falsecase <-time.After():return true}}var _ Resolver = httpResolver{}type httpResolver struct {client *http.Clientscheme stringport intpreferIPv6 boolmaxDelay intwaitAfter intmaxWait int// startIdx rotates the preferred candidate across successive Primary calls// (i.e. across reconnects), so a dead first address does not trap the client.startIdx *atomic.Uint32}func ( httpResolver) ( []tg.DCOption) []string {// Rotate the starting candidate per call for cross-reconnect failover. Add(1)-1// makes the first call start at index 0 (the highest-priority DC); the modulo// is taken on the unsigned counter to stay in range on wrap.:= int((.startIdx.Add(1) - 1) % uint32(len())):= make([]string, len())for := range {:= [(+)%len()]:= net.JoinHostPort(.IPAddress, strconv.Itoa(.port))[] = .scheme + "://" + + "/api"}return}func ( httpResolver) ( context.Context, int, List) (transport.Conn, error) {:= FindPrimaryDCs(.Options, , .preferIPv6)// The HTTP transport cannot speak to TCP-obfuscated-only DCs.:= 0for , := range {if !.TCPObfuscatedOnly {[] =++}}= [:]if len() == 0 {return nil, errors.Errorf("no addresses for DC %d", )}return newHTTPConn(.client, .urls(), .maxDelay, .waitAfter, .maxWait), nil}func ( httpResolver) ( context.Context, int, List) (transport.Conn, error) {return nil, errors.Errorf("can't resolve %d: MediaOnly is unsupported", )}func ( httpResolver) ( context.Context, int, List) (transport.Conn, error) {return nil, errors.Errorf("can't resolve %d: CDN is unsupported", )}// HTTPOptions is HTTP resolver creation options.type HTTPOptions struct {// Client is the HTTP client used for POST requests. If nil, a client with a// bounded transport and a timeout derived from MaxWait is used. A custom// client's timeout must exceed MaxWait, otherwise long-poll responses are cut// off. For Scheme "https" a custom client with an appropriate tls.Config// (SNI/certificates for the DC) is required.Client *http.Client// Scheme is "http" (default) or "https".//// Note: https support is experimental. Connecting over https to a bare DC IP// needs a custom Client whose tls.Config matches the DC certificate; the// default client will fail TLS verification.Scheme string// Port overrides the transport port. Defaults to 80 for http and 443 for// https.//// The HTTP transport uses this fixed port for every DC and intentionally// ignores tg.DCOption.Port (which is the TCP MTProto port), per the MTProto// HTTP transport specification.Port int// PreferIPv6 gives IPv6 DCs higher precedence.// Default is to prefer IPv4 DCs over IPv6.PreferIPv6 bool// MaxDelay, WaitAfter and MaxWait are the http_wait fields in milliseconds.// Defaults: 0, 0, 25000.//// See https://core.telegram.org/mtproto/service_messages.MaxDelay intWaitAfter intMaxWait int}func ( *HTTPOptions) () {if .Scheme == "" {.Scheme = "http"}if .Port == 0 {if .Scheme == "https" {.Port = 443} else {.Port = 80}}if .MaxWait == 0 {.MaxWait = defaultHTTPMaxWait}if .Client == nil {.Client = &http.Client{Timeout: time.Duration(.MaxWait)*time.Millisecond + httpClientTimeoutMargin,Transport: &http.Transport{Proxy: http.ProxyFromEnvironment,MaxConnsPerHost: httpMaxConnsPerHost,MaxIdleConnsPerHost: httpMaxConnsPerHost,},}}}// HTTP creates an MTProto-over-HTTP DC resolver with http_wait long-polling.//// It suits environments where a raw persistent MTProto TCP socket cannot be// held but HTTP POST to the DC is possible. Updates are delivered via http_wait// long-polling, so their latency is bounded by the round-trip rather than being// instantaneous. MediaOnly and CDN DCs are not supported.//// See https://core.telegram.org/mtproto/transports#http-transport.func ( HTTPOptions) Resolver {.setDefaults()return httpResolver{client: .Client,scheme: .Scheme,port: .Port,preferIPv6: .PreferIPv6,maxDelay: .MaxDelay,waitAfter: .WaitAfter,maxWait: .MaxWait,startIdx: new(atomic.Uint32),}}
![]() |
The pages are generated with Golds v0.8.4. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |