TCP network programming
Problems:
- Unpacking:
- For the sender, the data written by the application is far larger than the socket buffer size, and the packet can't be unpacked when the data is sent to the server at one time.
- The maximum data packet transmitted through the network is 1500 bytes. When the length of TCP message - the length of TCP header > MSS (maximum message length), packet splitting will occur, and MSS is generally long (1460-1480) bytes.
- Gluing:
- For the sender: the data sent by the application is very small, far smaller than the size of the socket buffer, which results in a lot of data in a packet that is not requested.
- For the receiving end, the method of receiving data can not read the data in the socket buffer in time, resulting in the backlog of different request data in the buffer.
resolvent:
- Record the length of data in the message header using the protocol with the message header.
- Use fixed length protocol, read fixed length content every time, and use space to supplement if not enough.
- Use message boundaries, such as using \ n to separate different messages.
- Use complex protocols such as xml json protobuf.
Experiment: using custom protocol
Overall process:
Client: the sender connects to the server and encodes the data to be sent through the encoder.
Server end: start, monitor port, receive connection, process connection in cooperation process, decode data through decoder.
//########################### //######Server code###### //########################### func main() { // 1. Monitor port 2.accept connection 3. Enable goroutine to process connection listen, err := net.Listen("tcp", "0.0.0.0:9090") if err != nil { fmt.Printf("error : %v", err) return } for{ conn, err := listen.Accept() if err != nil { fmt.Printf("Fail listen.Accept : %v", err) continue } go ProcessConn(conn) } } // Processing network requests func ProcessConn(conn net.Conn) { defer conn.Close() for { bt,err:=coder.Decode(conn) if err != nil { fmt.Printf("Fail to decode error [%v]", err) return } s := string(bt) fmt.Printf("Read from conn:[%v]\n",s) } } //########################### //######Clinet end code###### //########################### func main() { conn, err := net.Dial("tcp", ":9090") defer conn.Close() if err != nil { fmt.Printf("error : %v", err) return } // Encode and send data coder.Encode(conn,"hi server i am here"); } //########################### //######Codec code###### //########################### /** * decode: */ func Decode(reader io.Reader) (bytes []byte, err error) { // Read out the header first headerBuf := make([]byte, len(msgHeader)) if _, err = io.ReadFull(reader, headerBuf); err != nil { fmt.Printf("Fail to read header from conn error:[%v]", err) return nil, err } // Verify message header if string(headerBuf) != msgHeader { err = errors.New("msgHeader error") return nil, err } // Read the length of the actual content lengthBuf := make([]byte, 4) if _, err = io.ReadFull(reader, lengthBuf); err != nil { return nil, err } contentLength := binary.BigEndian.Uint32(lengthBuf) contentBuf := make([]byte, contentLength) // Read message body if _, err := io.ReadFull(reader, contentBuf); err != nil { return nil, err } return contentBuf, err } /** * code * Define the format of the message: msgHeader + contentLength + content * conn In itself io.Writer Interface */ func Encode(conn io.Writer, content string) (err error) { // Write header if err = binary.Write(conn, binary.BigEndian, []byte(msgHeader)); err != nil { fmt.Printf("Fail to write msgHeader to conn,err:[%v]", err) } // Write message body length contentLength := int32(len([]byte(content))) if err = binary.Write(conn, binary.BigEndian, contentLength); err != nil { fmt.Printf("Fail to write contentLength to conn,err:[%v]", err) } // Write message if err = binary.Write(conn, binary.BigEndian, []byte(content)); err != nil { fmt.Printf("Fail to write content to conn,err:[%v]", err) } return err
What is the performance of the client's conn not being closed?
The four wave states are as follows:
Master slave Closing Party Passive Closing Party established established Fin-wait1 closeWait Fin-wait2 Tiem-wait lastAck Closed Closed
If the client's connection is closed manually, the state of the client and the server will remain in the state of established connection.
MacBook-Pro% netstat -aln | grep 9090 tcp4 0 0 127.0.0.1.9090 127.0.0.1.62348 ESTABLISHED tcp4 0 0 127.0.0.1.62348 127.0.0.1.9090 ESTABLISHED tcp46 0 0 *.9090 *.* LISTEN
What's the performance of the server's conn not being closed all the time?
After the process of the client ends, it will send the fin packet to the server to request disconnection from the server.
If the conn of the server is not closed, the server will stay at the Close of four waves_ Wait stage (we don't Close manually, the server has data / tasks that haven't been processed, so it doesn't Close).
Client stays in fin_wait2 stage (in this stage, wait for the server to tell itself that it can actually disconnect the message).
DXMdeMacBook-Pro% netstat -aln | grep 9090 tcp4 0 0 127.0.0.1.9090 127.0.0.1.62888 CLOSE_WAIT tcp4 0 0 127.0.0.1.62888 127.0.0.1.9090 FIN_WAIT_2 tcp46 0 0 *.9090 *.* LISTEN
What is? binary.BigEndian ? What is? binary.LittleEndian?
For a computer, everything is binary data. BigEndian and LittleEndian describe the byte order of binary data. In the computer, small end sequence is widely used to store data in the modern CPU; large end sequence is often used for network transmission and file storage.
For example:
The binary representation of a number is 0x12345678 BigEndian is represented as: 0x12 0x34 0x56 0x78 LittleEndian: 0x78 0x56 0x34 0x12
UDP network programming
Ideas:
UDP server: 1. Listen to 2. Read message circularly 3. Reply data.
UDP client: 1. Connect to server 2. Send message 3. Receive message.
// ################################ // ######## UDPServer ######### // ################################ func main() { // 1. Monitor port 2.accept connection 3. Enable goroutine to process connection listen, err := net.Listen("tcp", "0.0.0.0:9090") if err != nil { fmt.Printf("error : %v", err) return } for{ conn, err := listen.Accept() if err != nil { fmt.Printf("Fail listen.Accept : %v", err) continue } go ProcessConn(conn) } } // Processing network requests func ProcessConn(conn net.Conn) { defer conn.Close() for { bt,err:= coder.Decode(conn) if err != nil { fmt.Printf("Fail to decode error [%v]", err) return } s := string(bt) fmt.Printf("Read from conn:[%v]\n",s) } } // ################################ // ######## UDPClient ######### // ################################ func main() { udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 9091, }) if err != nil { fmt.Printf("error : %v", err) return } _, err = udpConn.Write([]byte("i am udp client")) if err != nil { fmt.Printf("error : %v", err) return } bytes:=make([]byte,1024) num, addr, err := udpConn.ReadFromUDP(bytes) if err != nil { fmt.Printf("Fail to read from udp error: [%v]", err) return } fmt.Printf("Recieve from udp address:[%v], bytes:[%v], content:[%v]",addr,num,string(bytes)) }
Http network programming
Train of thought:
HttpServer: 1. Create a router. 2. Bind routing rules for routers. 3. Create server and listen port. 4 start the read service.
HttpClient: 1. Create a connection pool. 2. Create a client and bind the connection pool. 3. Send the request. 4. Read the response.
func main() { mux := http.NewServeMux() mux.HandleFunc("/login", doLogin) server := &http.Server{ Addr: ":8081", WriteTimeout: time.Second * 2, Handler: mux, } log.Fatal(server.ListenAndServe()) } func doLogin(writer http.ResponseWriter,req *http.Request){ _, err := writer.Write([]byte("do login")) if err != nil { fmt.Printf("error : %v", err) return } }
HttpClient
func main() { transport := &http.Transport{ // Dialing context DialContext: (&net.Dialer{ Timeout: 30 * time.Second, // Timeout when dialing to establish a connection KeepAlive: 30 * time.Second, // Long connection lifetime }).DialContext, // Maximum number of idle connections MaxIdleConns: 100, // Connections exceeding the maximum number of idle connections will fail after idlecontimeout IdleConnTimeout: 10 * time.Second, // https uses SSL security certificate, TSL is the upgraded version of SSL // When we use https, this line of configuration takes effect TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, // 100 continue status code timeout } // Create client client := &http.Client{ Timeout: time.Second * 10, //Request timeout Transport: transport, } // Request data res, err := client.Get("http://localhost:8081/login") if err != nil { fmt.Printf("error : %v", err) return } defer res.Body.Close() bytes, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Printf("error : %v", err) return } fmt.Printf("Read from http server res:[%v]", string(bytes)) }
Understand that functions are first class citizens
Click to view notes related to functions in github
In golang, a function is a first-class citizen. We can use a function as a common variable.
For example, we have a function HelloHandle, which we can use directly.
func HelloHandle(name string, age int) { fmt.Printf("name:[%v] age:[%v]", name, age) } func main() { HelloHandle("tom",12) }
closure
How to understand closure: closure is essentially a function, and this function will reference its external variables. In the following example, the anonymous function in f3 is itself a closure. Usually we use closures to play an adaptive role.
Example 1:
// f2 is a normal function with two input parameters func f2() { fmt.Printf("f2222") } // The input parameter of function f1 is a function of type f2 func f1(f2 func()) { f2() } func main() { // Because functions in golang are first-class citizens, we can pass f2 and common variables to f1 f1(f2) }
Example 2: further in the above example. f2 has its own parameters, so it can't be passed to f1 directly.
Can't be silly like this, f1(f2(1,2))???
Closure can solve this problem.
// f2 is a normal function with two input parameters func f2(x int, y int) { fmt.Println("this is f2 start") fmt.Printf("x: %d y: %d \n", x, y) fmt.Println("this is f2 end") } // The input parameter of function f1 is a function of type f2 func f1(f2 func()) { fmt.Println("this is f1 will call f2") f2() fmt.Println("this is f1 finished call f2") } // A function that takes two arguments and returns a wrapper function func f3(f func(int,int) ,x,y int) func() { fun := func() { f(x,y) } return fun } func main() { // The goal is to achieve the following transfers and calls f1(f3(f2,6,6)) }
Callback of implementation method:
In the following example, the function is implemented as follows: it's like I designed a framework, defined the flow of the whole framework operation (or provided a programming template). You can implement the specific functions of the framework according to your own needs, and my framework is only responsible for callback your specific methods.
// User defined type, handler is essentially a function type HandlerFunc func(string, int) // closure func (f HandlerFunc) Serve(name string, age int) { f(name, age) } // Specific processing functions func HelloHandle(name string, age int) { fmt.Printf("name:[%v] age:[%v]", name, age) } func main() { // Convert HelloHandle into a custom func handlerFunc := HandlerFunc(HelloHandle) // In essence, it will call back the HelloHandle method handlerFunc.Serve("tom", 12) // Top two lines effect = = bottom line // But the code above is that I am calling back for you, and the code below is that you call it on your own initiative HelloHandle("tom",12) }
HttpServer source reading
Register route
Intuitively, this step is to associate the router url pattern with the func provided by the developer. It's easy to think that it's probably implemented through map.
func main() { // Create router // Binding routing rules for routers mux := http.NewServeMux() mux.HandleFunc("/login", doLogin) ... } func doLogin(writer http.ResponseWriter,req *http.Request){ _, err := writer.Write([]byte("do login")) if err != nil { fmt.Printf("error : %v", err) return } }
Consider ServeMux as a router. We use the NewServerMux function under the http package to create a new router object, and then use its HandleFunc(pattern, func) function to complete the route registration.
Following up on the NewServerMux function, you can see that it returns a ServeMux structure to us through the new function.
func NewServeMux() *ServeMux { return new(ServeMux) }
The ServeMux structure is as follows: in this ServeMux structure, we can see the map of maintenance pattern and func
type ServeMux struct { mu sync.RWMutex m map[string]muxEntry hosts bool // whether any patterns contain hostnames }
This muxEntry is as follows:
type muxEntry struct { h Handler pattern string } type Handler interface { ServeHTTP(ResponseWriter, *Request) }
Here comes the problem. The above method we manually registered into the router is only a method with specified parameters. How can it become a Handle? We haven't said to implement the Handler interface manually or rewrite the serverhttp function. Can't we implement an interface in golang like this? * *
type Handle interface { Serve(string, int, string) } type HandleImpl struct { } func (h HandleImpl)Serve(string, int, string){ }
Take a look at the following methods with this question:
// Because the function is a first-class citizen, we pass in the doLogin function as an input parameter like a normal variable. mux.HandleFunc("/login", doLogin) func doLogin(writer http.ResponseWriter,req *http.Request){ ... }
Follow in to see the implementation of HandleFunc function:
First, the second parameter of the HandleFunc function is that the type of the received function is the same as that of the doLogin function, so doLogin can be passed into HandleFunc normally.
Second: our focus should be on the following handler func (handler)
// HandleFunc registers the handler function for the given pattern. func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) }
Follow up the handler func (handler) and see the figure below, and the truth will come out. In an elegant way, golang quietly completes an adaptation for us. It seems that the above HandlerFunc(handler) is not a function call, but a doLogin conversion to a custom type. This custom type implements the Handle interface (because it overrides the serverhttp function) and perfectly adapts our doLogin to the Handle type in the form of a closure.
Look down at the Handle method:
First: register pattern and handler in map
Second: in order to ensure the concurrent security of the whole process, lock is used to protect the whole process.
// Handle registers the handler for the given pattern. // If a handler already exists for pattern, Handle panics. func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() if pattern == "" { panic("http: invalid pattern") } if handler == nil { panic("http: nil handler") } if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } if mux.m == nil { mux.m = make(map[string]muxEntry) } mux.m[pattern] = muxEntry{h: handler, pattern: pattern} if pattern[0] != '/' { mux.hosts = true }
Start service
Overview:
Compared with java, a complex set of logic in java will be encapsulated into a class. In golang, a complex set of logic will be encapsulated into a structure.
Corresponding to HttpServer, http Server has its own structure in the implementation of golang. It is the Server under the http package.
It has a series of descriptive properties. Such as listening address, write timeout, router.
server := &http.Server{ Addr: ":8081", WriteTimeout: time.Second * 2, Handler: mux, } log.Fatal(server.ListenAndServe())
Let's see the function to start the service: server.ListenAndServe()
The logic of the implementation is to use the Listen function under the net package to obtain the tcp connection on the given address.
Then encapsulate the tcp connection into the tcpKeepAliveListenner structure.
This tcpKeepAliveListenner is handled in the Server's Serve function
// ListenAndServe will listen to the tcp connection on the given network address of the developer. When a request arrives, it will call the Serve function to handle the connection. // It receives all connections using TCP keep alive related configuration // // If Addr is not specified when constructing the Server, it will use the default value: "http" // // When the Server ShutDown or Close, ListenAndServe will always return a non nil error. // The Error returned is ErrServerClosed func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } // The bottom layer is realized by tcp ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) } // tcpKeepAliveListener will set a keep alive timeout for TCP. // It is commonly used by ListenAndServe and ListenAndServeTLS. // It ensures that the dead TCP will eventually disappear. type tcpKeepAliveListener struct { *net.TCPListener }
Then go to the Serve method. In the previous function, we got a Listener based on tcp. From this Listener, we can get new connections continuously. In the following method, we use infinite for loop to complete this task. After conn is obtained, it encapsulates the connection into httpConn. To ensure that the next connection does not block the arrival, a new goroutine is opened to process the http connection.
func (srv *Server) Serve(l net.Listener) error { // If you have a hook function wrapped with srv and listener, execute it if fn := testHookServerServe; fn != nil { fn(srv, l) // call hook with unwrapped listener } // Encapsulate the Listener of tcp into onceCloseListener to ensure that the connection will not be closed multiple times. l = &onceCloseListener{Listener: l} defer l.Close() // http2 related configuration if err := srv.setupHTTP2_Serve(); err != nil { return err } if !srv.trackListener(&l, true) { return ErrServerClosed } defer srv.trackListener(&l, false) // If you don't receive a request to sleep for how long var tempDelay time.Duration // how long to sleep on accept failure baseCtx := context.Background() // base is always background, per Issue 16220 ctx := context.WithValue(baseCtx, ServerContextKey, srv) // Open infinite loop and try to get connection from listener. for { rw, e := l.Accept() // Wrong house in the process of accpet if e != nil { select { // If the content can be obtained from the doneChan of the server, the server is shut down case <-srv.getDoneChan(): return ErrServerClosed default: } // If it happens net.Error And if it's a temporary error, sleep for 5ms. If it happens again, sleep time * 2. The online time is 1s if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } // If no error occurs, clear the sleep time tempDelay = 0 // Encapsulate received connections into httpConn c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return // Open a new process to handle this connection go c.serve(ctx) } }
Process request
In c.serve(ctx), the http related message information will be parsed, and the http message will be parsed into the Request structure.
Some codes are as follows:
// Wrap the server as an instance of serverHandler, execute its serverhttp method, process the request and return the response. // The Handler or DefaultServeMux (default router) that the serverHandler delegates to the server // To process "OPTIONS *" requests. serverHandler{c.server}.ServeHTTP(w, w.req)
// serverHandler delegates to either the server's Handler or // DefaultServeMux and also handles "OPTIONS *" requests. type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { // If no Handler is defined, use the default handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } // Process the request and return the response. handler.ServeHTTP(rw, req) }
As you can see, req contains the pattern we mentioned earlier, called RequestUri. With it, you will know which function in ServeMux is the callback.
HttpClient source reading
DemoCode
func main() { // Create connection pool // Create client, bind connection pool // Send request // Read response transport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, // connection timed out KeepAlive: 30 * time.Second, // Long connection lifetime }).DialContext, // Maximum number of idle connections MaxIdleConns: 100, // Connections exceeding the maximum number of idle connections will be destroyed after IdleConnTimeout IdleConnTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second, // tls handshake timeout ExpectContinueTimeout: 1 * time.Second, // 100 continue status code timeout } // Create client client := &http.Client{ Timeout: time.Second * 10, //Request timeout Transport: transport, } // Request data, get response res, err := client.Get("http://localhost:8081/login") if err != nil { fmt.Printf("error : %v", err) return } defer res.Body.Close() // Processing data bytes, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Printf("error : %v", err) return } fmt.Printf("Read from http server res:[%v]", string(bytes)) }
Organizing ideas
http.Client In fact, there are a lot of codes. It will be difficult to go through all the codes in a very detailed way. I can only mention some of them below.
First of all, understand one thing, what is HttpClient we are writing? (it's a silly question, but I have to ask) it's sending Http requests.
In general, when we are developing, we write more HttpServer code. It is to process Http requests, not to send Http requests. Http requests are sent from the front end to the back end through ajax through the browser.
Second, Http requests are actually based on tcp connections, so if we look at http.Client I'm sure we can find it net.Dial ("tcp", add) related code.
That is to say, we need to see, http.Client How to establish connection with the server, send data and receive data.
Important struct
http.Client Some important struct s are as follows
http.Client The structure encapsulates properties related to HTTP requests, such as cookie s, timeout, redirect, and Transport.
type Client struct { Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration }
Tranport implements the RoundTrpper interface:
type RoundTripper interface { // 1. RoundTrip performs a simple HTTP transaction and returns a response for the request // 2. RoundTrip will not attempt to resolve the response // 3. Note: as long as the response is returned, the result returned by RoundTrip is err == nil regardless of the response status code // 4. After RoundTrip sends the request, if it does not get the response, it will return a non empty err. // 5. Similarly, RoundTrip does not attempt to resolve more advanced protocols such as redirection, authentication, and cookie s. // 6. RoundTrip does not modify other fields of the request except for consuming and closing the request body // 7. RoundTrip can read part of the request field in a separate gorountine. Until the ResponseBody is closed, the caller cannot cancel or reuse the request // 8. RoundTrip always ensures that the Body is shut down (including when err occurs). Depending on the implementation, closing the Body may be done in a separate goroutine before the RoundTrip is closed. This means that if the caller wants to use the Body of the request for subsequent requests, he must wait until Close occurs // 9. The requested URL and Header fields must be initialized. RoundTrip(*Request) (*Response, error) }
Look at the RoundTrpper interface above. There is only one method RoundTrip in it. The function of the method is to execute an Http Request, send the Request and get the Response.
RoundTrpper is designed to support concurrency.
The Transport structure is as follows:
type Transport struct { idleMu sync.Mutex // user has requested to close all idle conns wantIdle bool // Transport is used to establish a connection. This idleConn is the free connection pool maintained by transport. idleConn map[connectMethodKey][]*persistConn // most recently used at end idleConnCh map[connectMethodKey]chan *persistConn }
The connectMethodKey is also a structure:
type connectMethodKey struct { // The URL of the proxy proxy. When it is not empty, the key will be used all the time // scheme protocol type, http https // The url of the addr agent, that is, the downstream url proxy, scheme, addr string }
persistConn is a concrete connection instance that contains the context of the connection.
type persistConn struct { // alt optionally specifies TLS NextProto RoundTripper. // This is for today's HTTP / 2 and future protocols. If non-zero, the remaining fields are not used. alt RoundTripper t *Transport cacheKey connectMethodKey conn net.Conn tlsState *tls.ConnectionState // Used to read content from conn br *bufio.Reader // from conn // Used to write content to conn bw *bufio.Writer // to conn nwrite int64 // bytes written // He is a chan. roundTrip will write the contents of readLoop to reqch reqch chan requestAndChan // He is a chan. roundTrip will write the contents of writeLoop to write writech chan writeRequest closech chan struct{} // closed when conn closed
Add another structure: Request, which is used to describe an instance of an HTTP Request. It is defined in the HTTP package request.go , which encapsulates the attributes related to HTTP requests
type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header Body io.ReadCloser GetBody func() (io.ReadCloser, error) ContentLength int64 TransferEncoding []string Close bool Host string Form url.Values PostForm url.Values MultipartForm *multipart.Form Trailer Header RemoteAddr string RequestURI string TLS *tls.ConnectionState Cancel <-chan struct{} Response *Response ctx context.Context }
These structures are completed together as shown in the figure below http.Client Workflow of
technological process
We want to send an Http Request. First of all, we need to construct a Request, which is essentially a description of Http protocol (since everyone uses Http protocol, after sending this Request to HttpServer, HttpServer can recognize and parse it).
// Look down from this line of code res, err := client.Get("http://localhost:8081/login") // Follow up Get req, err := NewRequest("GET", url, nil) if err != nil { return nil, err } return c.Do(req) // Follow up Do func (c *Client) Do(req *Request) (*Response, error) { return c.do(req) } // Follow up do. The do function has the following logic. You can see that the return value has been obtained after the send. So we have to keep following the send method if resp, didTimeout, err = c.send(req, deadline); err != nil // Follow up the send method. You can see that there is another send method in send. The input parameters are: request, tranpost, deadline // So far, we haven't seen any actions to establish a connection with the server, but the constructed req and the tranport with the connection pool have already met resp, didTimeout, err = send(req, c.transport(), deadline) // Continue to follow up the send method and see that the RoundTrip method of rt is called. // This rt is created when we write HttpClient code. It is bound to http.Client tranport instance on. // The function of this RoundTrip method has been mentioned above. The most direct function is to send a request and get a response. resp, err = rt.RoundTrip(req)
But RoundTrip is an abstract method defined in the RoundTrip interface. We must see the specific implementation of the code
Here you can use breakpoint debugging: you can put a breakpoint on the last line above and enter into his specific implementation. You can see from the figure that the specific implementation is in roundtrip.
The function invoked in RoundTrip is the roundTrip function of our custom transport.
Then we need a conn, which we can get through Transport. The type of conn is persistConn.
// Another infinite for loop in roundTrip function for { // Check if the requested context is closed select { case <-ctx.Done(): req.closeBody() return nil, ctx.Err() default: } // There is a layer of encapsulation for the passed req. After encapsulation, the treq can be modified by roundTrip, so every retry will create a new one treq := &transportRequest{Request: req, trace: trace} cm, err := t.connectMethodForRequest(treq) if err != nil { req.closeBody() return nil, err } // To get the connection with the corresponding host from the tranport, this connection may be the cache of http, https, http proxy and http proxy, but we are ready to send treq to this connection anyway // The connection obtained here is the persistConn we mentioned above pconn, err := t.getConn(treq, cm) if err != nil { t.setReqCanceler(req, nil) req.closeBody() return nil, err } var resp *Response if pconn.alt != nil { // HTTP/2 path. t.decHostConnCount(cm.key()) // don't count cached http2 conns toward conns per host t.setReqCanceler(req, nil) // not cancelable with CancelRequest resp, err = pconn.alt.RoundTrip(req) } else { // Call the roundTrip method of persistConn, send treq and get the response. resp, err = pconn.roundTrip(treq) } if err == nil { return resp, nil } if !pconn.shouldRetryRequest(req, err) { // Issue 16465: return underlying net.Conn.Read error from peek, // as we've historically done. if e, ok := err.(transportReadFromServerError); ok { err = e.err } return nil, err } testHookRoundTripRetried() // Rewind the body if we're able to. (HTTP/2 does this itself so we only // need to do it for HTTP/1.1 connections.) if req.GetBody != nil && pconn.alt == nil { newReq := *req var err error newReq.Body, err = req.GetBody() if err != nil { return nil, err } req = &newReq } }
Organize ideas: then look at the above code to get the implementation details of conn and roundTrip.
We need a conn, which can be obtained through Transport. The type of conn is persistConn. However, no matter what, you need to get the persistConn first, then you can send the request further and get the server-side response.
Then I have already mentioned about the persistConn structure. Reapply below
type persistConn struct { // alt optionally specifies TLS NextProto RoundTripper. // This is for today's HTTP / 2 and future protocols. If non-zero, the remaining fields are not used. alt RoundTripper conn net.Conn t *Transport br *bufio.Reader // Used to read content from conn bw *bufio.Writer // Used to write content to conn // He is a chan. roundTrip will write the contents of readLoop to reqch reqch chan requestAndChan // He is a chan. roundTrip will write the contents of writeLoop to write nwrite int64 // bytes written cacheKey connectMethodKey tlsState *tls.ConnectionState writech chan writeRequest closech chan struct{} // closed when conn closed
Follow up t.getConn(treq, cm) code is as follows:
// Try to get a connection from the free buffer pool first // The so-called free buffer pool is in the Tranport structure: idleConn map[connectMethodKey][]*persistConn // The cm of the reference position is as follows: /* type connectMethod struct { // The url of the agent. If there is no agent, the value is nil proxyURL *url.URL // Protocol http, https used for connection targetScheme string // If the proxyURL specifies http proxy or https proxy, and the protocol used is http instead of https. // Then the following targetAddr will not be included in the connect method key. // Because socket can reuse different targetAddr values targetAddr string }*/ t.getIdleConn(cm); // If there are idle connections in the free buffer pool, it returns conn. otherwise, select as follows select { // todo, I'm not sure what I'm doing here. At present, I guess it's like this: each server can open a limited socket handle // Every time we come to get a link, we count + 1. When the overall handle is within the range allowed by the Host, we do not interfere case <-t.incHostConnCount(cmKey): // count below conn per host limit; proceed // Try to get the connection from the free connection pool again, because some connections may be put back into the connection pool after use case pc := <-t.getIdleConnCh(cm): if trace != nil && trace.GotConn != nil { trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()}) } return pc, nil // Has the request been cancelled case <-req.Cancel: return nil, errRequestCanceledConn // Does the requested context drop case <-req.Context().Done(): return nil, req.Context().Err() case err := <-cancelc: if err == errRequestCanceled { err = errRequestCanceledConn } return nil, err } // Open a new gorountine new connection a connection go func() { /** * Create a new connection. The bottom layer of the method encapsulates the logic related to tcp client dial * conn, err := t.dial(ctx, "tcp", cm.addr()) * And the logic of building different request s according to different targetschemes. */ // Get persistConn pc, err := t.dialConn(ctx, cm) // Write persistConn to chan dialc <- dialRes{pc, err} }() // Try again to get from the free connection pool idleConnCh := t.getIdleConnCh(cm) select { // If the go coordination dial above is successful, the value can be taken out here case v := <-dialc: // Our dial finished. if v.pc != nil { if trace != nil && trace.GotConn != nil && v.pc.alt == nil { trace.GotConn(httptrace.GotConnInfo{Conn: v.pc.conn}) } return v.pc, nil } // Our dial failed. See why to return a nicer error // value. // Connect the Host to - 1 t.decHostConnCount(cmKey) select { ...
transport.dialConn
The cm in the following code is as long as this
// dialConn is transpprot's method // Input parameter: context context, connectMethod // Parameter output: persesnconn func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) { // Build persistConn to return pconn := &persistConn{ t: t, cacheKey: cm.key(), reqch: make(chan requestAndChan, 1), writech: make(chan writeRequest, 1), closech: make(chan struct{}), writeErrCh: make(chan error, 1), writeLoopDone: make(chan struct{}), } trace := httptrace.ContextClientTrace(ctx) wrapErr := func(err error) error { if cm.proxyURL != nil { // Return a typed error, per Issue 16997 return &net.OpError{Op: "proxyconnect", Net: "tcp", Err: err} } return err } // Determine whether the protocol used in cm is https if cm.scheme() == "https" && t.DialTLS != nil { var err error pconn.conn, err = t.DialTLS("tcp", cm.addr()) if err != nil { return nil, wrapErr(err) } if pconn.conn == nil { return nil, wrapErr(errors.New("net/http: Transport.DialTLS returned (nil, nil)")) } if tc, ok := pconn.conn.(*tls.Conn); ok { // Handshake here, in case DialTLS didn't. TLSNextProto below // depends on it for knowing the connection state. if trace != nil && trace.TLSHandshakeStart != nil { trace.TLSHandshakeStart() } if err := tc.Handshake(); err != nil { go pconn.conn.Close() if trace != nil && trace.TLSHandshakeDone != nil { trace.TLSHandshakeDone(tls.ConnectionState{}, err) } return nil, err } cs := tc.ConnectionState() if trace != nil && trace.TLSHandshakeDone != nil { trace.TLSHandshakeDone(cs, nil) } pconn.tlsState = &cs } } else { // If it's not https protocol, come here and use tcp to dial httpserver to get a tcp connection. conn, err := t.dial(ctx, "tcp", cm.addr()) if err != nil { return nil, wrapErr(err) } // Give our persistConn maintenance the access to tcp pconn.conn = conn // Deal with https related logic if cm.scheme() == "https" { var firstTLSHost string if firstTLSHost, _, err = net.SplitHostPort(cm.addr()); err != nil { return nil, wrapErr(err) } if err = pconn.addTLS(firstTLSHost, trace); err != nil { return nil, wrapErr(err) } } } // Proxy setup. switch { // If the proxy URL is empty, do nothing case cm.proxyURL == nil: // Do nothing. Not using a proxy. // case cm.proxyURL.Scheme == "socks5": conn := pconn.conn d := socksNewDialer("tcp", conn.RemoteAddr().String()) if u := cm.proxyURL.User; u != nil { auth := &socksUsernamePassword{ Username: u.Username(), } auth.Password, _ = u.Password() d.AuthMethods = []socksAuthMethod{ socksAuthMethodNotRequired, socksAuthMethodUsernamePassword, } d.Authenticate = auth.Authenticate } if _, err := d.DialWithConn(ctx, conn, "tcp", cm.targetAddr); err != nil { conn.Close() return nil, err } case cm.targetScheme == "http": pconn.isProxy = true if pa := cm.proxyAuth(); pa != "" { pconn.mutateHeaderFunc = func(h Header) { h.Set("Proxy-Authorization", pa) } } case cm.targetScheme == "https": conn := pconn.conn hdr := t.ProxyConnectHeader if hdr == nil { hdr = make(Header) } connectReq := &Request{ Method: "CONNECT", URL: &url.URL{Opaque: cm.targetAddr}, Host: cm.targetAddr, Header: hdr, } if pa := cm.proxyAuth(); pa != "" { connectReq.Header.Set("Proxy-Authorization", pa) } connectReq.Write(conn) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(conn) resp, err := ReadResponse(br, connectReq) if err != nil { conn.Close() return nil, err } if resp.StatusCode != 200 { f := strings.SplitN(resp.Status, " ", 2) conn.Close() if len(f) < 2 { return nil, errors.New("unknown status code") } return nil, errors.New(f[1]) } } if cm.proxyURL != nil && cm.targetScheme == "https" { if err := pconn.addTLS(cm.tlsHost(), trace); err != nil { return nil, err } } 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 } } if t.MaxConnsPerHost > 0 { pconn.conn = &connCloseListener{Conn: pconn.conn, t: t, cmKey: pconn.cacheKey} } // Initializing the bufferReader and bufferWriter of persistConn pconn.br = bufio.NewReader(pconn) // Data can be read from the tcpConn maintained for pconn above pconn.bw = bufio.NewWriter(persistConnWriter{pconn})// You can write data to tcpConn maintained by pconn // Two new go processes related to persistConn have been opened. go pconn.readLoop() go pconn.writeLoop() return pconn, nil }
The above two goroutine and br bw work together to complete the process as shown in the figure below
Send request
The logic of sending req is in func (t *Transport) roundTrip(req *Request) (*Response, error) {} function in tranport package under http package.
As follows:
// Send treq resp, err = pconn.roundTrip(treq) // Follow up roundTrip // You can see that he wrote an instance of the writeRequest structure type into the write // The writech will be consumed by the writeLoop in the figure above, and written into the tcp connection with the help of the buffer writer to complete the sending of data to the server. pc.writech <- writeRequest{req, writeErrCh, continueCh}