Gorang network programming

Posted by nakago on Sun, 28 Jun 2020 08:54:44 +0200

catalog

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}

Topics: network Programming socket SSL