Commit d4d16688 authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: add Transport.TLSNextProto, ErrSkipAltProtocol

This is the start of wiring up the HTTP/2 Transport. It is still
disabled in this commit.

This change does two main things:

1) Transport.RegisterProtocol now permits registering "http" or
   "https" (they previously paniced), and the semantics of the
   registered RoundTripper have been extended to say that the new
   sentinel error value (ErrSkipAltProtocol, added in this CL) means
   that the Transport's RoundTrip method proceeds as if the alternate
   protocol had not been registered. This gives us a place to register
   an alternate "https" RoundTripper which gets first dibs on using
   HTTP/2 if there's already a cached connection.

2) adds Transport.TLSNextProto, a map keyed by TLS NPN/ALPN protocol
   strings, similar in feel to the existing Server.TLSNextProto map.
   This map is the glue between the HTTP/1 and HTTP/2 clients, since
   we don't know which protocol we're going to speak (and thus which
   Transport type to use) until we've already made the TCP connection.

Updates #6891

Change-Id: I7328c7ff24f52d9fe4899facabf7ecc5dcb989f3
Reviewed-on: https://go-review.googlesource.com/16090Reviewed-by: 's avatarAndrew Gerrand <adg@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent a3156aaa
......@@ -25,6 +25,10 @@ import (
"time"
)
// h2DefaultTransport is the HTTP/2 version of DefaultTransport.
// DefaultTransport and h2DefaultTransport are wired up together.
var h2DefaultTransport = &http2Transport{}
// DefaultTransport is the default implementation of Transport and is
// used by DefaultClient. It establishes network connections as needed
// and caches them for reuse by subsequent calls. It uses HTTP proxies
......@@ -40,6 +44,41 @@ var DefaultTransport RoundTripper = &Transport{
ExpectContinueTimeout: 1 * time.Second,
}
func init() {
// TODO(bradfitz,adg): remove the following line before Go 1.6
// ships. This just gives us a mechanism to temporarily
// enable the http2 client during development.
if !strings.Contains(os.Getenv("GODEBUG"), "h2client=1") {
return
}
t := DefaultTransport.(*Transport)
// TODO(bradfitz,adg): move all this up to DefaultTransport before Go 1.6:
t.RegisterProtocol("https", noDialH2Transport{h2DefaultTransport})
t.TLSClientConfig = &tls.Config{
NextProtos: []string{"h2"},
}
t.TLSNextProto = map[string]func(string, *tls.Conn) RoundTripper{
"h2": http2TransportForConn,
}
}
// noDialH2Transport is a RoundTripper which only tries to complete the request if
// the wrapped *http2Transport already has a cached connection to the host.
type noDialH2Transport struct{ rt *http2Transport }
func (t noDialH2Transport) RoundTrip(req *Request) (*Response, error) {
// TODO(bradfitz): wire up http2.Transport
return nil, ErrSkipAltProtocol
}
func http2TransportForConn(authority string, c *tls.Conn) RoundTripper {
// TODO(bradfitz): donate c to h2DefaultTransport:
// h2DefaultTransport.AddIdleConn(authority, c)
return h2DefaultTransport
}
// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2
......@@ -121,6 +160,16 @@ type Transport struct {
// This time does not include the time to send the request header.
ExpectContinueTimeout time.Duration
// TLSNextProto specifies how the Transport switches to an
// alternate protocol (such as HTTP/2) after a TLS NPN/ALPN
// protocol negotiation. If Transport dials an TLS connection
// with a non-empty protocol name and TLSNextProto contains a
// map entry for that key (such as "h2"), then the func is
// called with the request's authority (such as "example.com"
// or "example.com:1234") and the TLS connection. The function
// must return a RoundTripper that then handles the request.
TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper
// TODO: tunable on global max cached connections
// TODO: tunable on timeout on cached connections
}
......@@ -196,7 +245,7 @@ func (tr *transportRequest) extraHeaders() Header {
//
// For higher-level HTTP client support (such as handling of cookies
// and redirects), see Get, Post, and the Client type.
func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
if req.URL == nil {
req.closeBody()
return nil, errors.New("http: nil Request.URL")
......@@ -205,18 +254,18 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
req.closeBody()
return nil, errors.New("http: nil Request.Header")
}
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
t.altMu.RLock()
var rt RoundTripper
if t.altProto != nil {
rt = t.altProto[req.URL.Scheme]
// TODO(bradfitz): switch to atomic.Value for this map instead of RWMutex
t.altMu.RLock()
altRT := t.altProto[req.URL.Scheme]
t.altMu.RUnlock()
if altRT != nil {
if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol {
return resp, err
}
t.altMu.RUnlock()
if rt == nil {
req.closeBody()
return nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme}
}
return rt.RoundTrip(req)
}
if s := req.URL.Scheme; s != "http" && s != "https" {
req.closeBody()
return nil, &badStringError{"unsupported protocol scheme", s}
}
if req.URL.Host == "" {
req.closeBody()
......@@ -239,20 +288,27 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
req.closeBody()
return nil, err
}
if pconn.alt != nil {
// HTTP/2 path.
return pconn.alt.RoundTrip(req)
}
return pconn.roundTrip(treq)
}
// ErrSkipAltProtocol is a sentinel error value defined by Transport.RegisterProtocol.
var ErrSkipAltProtocol = errors.New("net/http: skip alternate protocol")
// RegisterProtocol registers a new protocol with scheme.
// The Transport will pass requests using the given scheme to rt.
// It is rt's responsibility to simulate HTTP request semantics.
//
// RegisterProtocol can be used by other packages to provide
// implementations of protocol schemes like "ftp" or "file".
//
// If rt.RoundTrip returns ErrSkipAltProtocol, the Transport will
// handle the RoundTrip itself for that one request, as if the
// protocol were not registered.
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {
if scheme == "http" || scheme == "https" {
panic("protocol " + scheme + " already registered")
}
t.altMu.Lock()
defer t.altMu.Unlock()
if t.altProto == nil {
......@@ -688,6 +744,12 @@ func (t *Transport) dialConn(cm connectMethod) (*persistConn, error) {
pconn.conn = tlsConn
}
if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
return &persistConn{alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
}
}
pconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF})
pconn.bw = bufio.NewWriter(pconn.conn)
go pconn.readLoop()
......@@ -817,6 +879,11 @@ func (k connectMethodKey) String() string {
// persistConn wraps a connection, usually a persistent one
// (but may be used for non-keep-alive requests as well)
type persistConn struct {
// alt optionally specifies the TLS NextProto RoundTripper.
// This is used for HTTP/2 today and future protocol laters.
// If it's non-nil, the rest of the fields are unused.
alt RoundTripper
t *Transport
cacheKey connectMethodKey
conn net.Conn
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment