How HTTP1.1 protocol is implemented in Golang net/http package: part one - request workflow
Background
In this article, I’ll write about one topic: how to implement the HTTP protocol. I keep planning to write about this topic for a long time. In my previous articles, I already wrote several articles about HTTP protocol:
- How to write a Golang HTTP server with Linux system calls
- Understand how HTTP/1.1 persistent connection works based on Golang: part one - sequential requests
- Understand how HTTP/1.1 persistent connection works based on Golang: part two - concurrent requests
I recommend you to read these articles above before this one.
As you know, HTTP protocol is in the application layer, which is the closest one to the end-user in the protocol stack.
So relatively speaking, HTTP protocol is not as mysterious as other protocols in the lower layers of this stack. Software engineers use HTTP every day and take it for granted. Have you ever thought about how we can implement a fully functional HTTP protocol library?
It turns out to be a very complex and big work in terms of software engineering. Frankly speaking, I can’t work it out by myself in a short period. So in this article, we’ll try to understand how to do it by investigating Golang net/http package as an example. We’ll read a lot of source code and draw diagrams to help your understanding of the source code.
Note HTTP protocol itself has evolved a lot from HTTP1.1 to HTTP2 and HTTP3, not to mention HTTPS. In this article, we’ll focus on the mechanism of HTTP1.1, but what you learned here can help you understand other new versions of HTTP protocol.
Note HTTP protocol is on the basis of client-server model. This article will focus on the client-side. For the HTTP server part, I’ll write another article next.
Main workflow of http.Client
HTTP client’s request starts from the application’s call to Get method of net/http package, and ends by writing the HTTP message to the TCP socket. The whole workflow can be simplified to the following diagram:
First, the public Get method calls Get method of DefaultClient, which is a global variable of type Client,
1 | // Get method |
Then, NewRequest method is used to construct a new request of type Request:
1 | func (c *Client) Get(url string) (resp *Response, err error) { |
I’ll not show the function body of NewRequestWithContext, since it’s very long. But only paste the block of code for actually building the Request object as follows:
1 | req := &Request{ |
Note that by default the HTTP protocol version is set to 1.1. If you want to send HTTP2 request, then you need other solutions, and I’ll write about it in other articles.
Next, Do method is called, which delegates the work to the private do method.
1 | func (c *Client) Do(req *Request) (*Response, error) { |
do method handles the HTTP redirect behavior, which is very interesting. But since the code block is too long, I’ll not show its function body here. You can refer to the source code of it here.
Next, send method of Client is called which goes as follows:
1 | func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { |
It handles cookies for the request, then calls the private method send with three parameters.
We already talked about the first parameter above. Let’s take a look at the second parameter c.transport() as follows:
1 | func (c *Client) transport() RoundTripper { |
Transport is extremely important for HTTP client workflow. Let’s examine how it works bit by bit. First of all, it’s type of RoundTripper interface.
1 | // this interface is defined inside client.go file |
RoundTripper interface only defines one method RoundTrip, all right.
If you don’t have any special settings, the DefaultTransport will be used for c.Transport above.
The DefaultTransport is going as follows:
1 | // defined in transport.go |
Note that its actual type is Transport as below:
1 | type Transport struct { |
I list the full content of Transport struct here, although it contains many fields, and many of them will not be discussed in this article.
As we just mentioned, Transport is type of RoundTripper interface, it must implement the method RoundTrip, right?
You can find the RoundTrip method implementation of Transport struct type in roundtrip.go file as follows:
1 | // RoundTrip method in roundtrip.go |
In the beginning, I thought this method should be included inside transport.go file, but it is defined inside another file.
Let’s back to the send method which takes c.Transport as the second argument:
1 | // send method in client.go |
At line 50 of send method above:
1 | resp, err = rt.RoundTrip(req) |
RoundTrip method is called to send the request. Based on the comments in the source code, you can understand it in the following way:
- RoundTripper is an interface representing the ability to execute a single HTTP transaction, obtaining the Response for a given Request.
Next, let’s go to roundTrip method of Transport:
1 | // roundTrip method in transport.go, which is called by RoundTrip method internally |
There are three key points:
- at line 70, a new variable of type
transportRequest, which embedsRequest, is created. - at line 81,
getConnmethod is called, which implements the cachedconnection poolto support thepersistent connectionmode. Of course, if no cached connection is available, a new connection will be created and added to the connection pool. I will explain this behavior in detail next section. - from line 89 to line 95,
pconn.roundTripis called. The name of variablepconnis self-explaining which means it is type ofpersistConn.
transportRequest is passed as parameter to getConn method, which returns pconn. pconn.roundTrip is called to execute the HTTP request. we have covered all the steps in the above workflow diagram.
Summary
In this first article of this series, we talked about the workflow of sending an HTTP request step by step. And I’ll discuss how to send the HTTP message to the TCP stack in the second article.