In this paper, starting from vivo Internet technology WeChat public links: mp.weixin.qq.com/s/WlT8070Ll… Author: Wu Yue
The main reason for the failure of this series is that I encountered many problems related to Web protocol in the process of learning large front-end. My previous understanding of this area was limited to using Charles to catch a package, and I lacked basic skills. Obsessive-compulsive disorder attacks on this occasion, I have decided to thoroughly understand the web agreement to get through, if you met the same problem with me, for example
-
The only thing you know about HTTP is Charles grabbing a packet.
-
Knowledge of HTTPS is limited to general knowledge of TLS, SSL, symmetric encryption, and asymmetric encryption. Really a complete interaction process, do not know.
-
Do not know enough about h5 cache fields.
-
Mobile terminal various depth of weak network optimization of the article because of the basic skills are not solid reasons do not understand.
-
Sometimes as a mobile terminal development in the determination of technical solutions, because of the lack of a basic understanding of the front-end or server, can not argue to develop a more perfect solution.
-
If there is a problem with the loading of web pages in the mobile terminal, we can only ask the front-end engineer for help, but cannot locate the problem by ourselves in the first time.
-
Every time I want to deeply learn web protocol, I can only read it in general because I can’t write server programs. I just find a few blogs on the Internet and muddled along without really solving the doubts in my heart. Never actually did it.
-
There is no actual comparison between the browser and Android using OKHTTP when sending network requests.
-
To be honest, 99 percent of okHTTP articles ignore the actual implementation of THE HTTP protocol, and are basically a brief introduction to okHTTP design patterns and upper layer encapsulation, which is actually a debuff for mobile engineers to understand the Web protocol itself (I am also a victim of that).
I hope this series of articles will help engineers who are as confused about Web protocols as I am. All of the server applications in this series were developed using Go. Wireshark was used as the packet capture tool. Charles was not used because Charles could not see things at the transport layer, which was not conducive to my understanding of the essence of the protocol. This series of articles doesn’t cut and paste much conceptual stuff from the web, but focuses on code and wireshark capture. Conceptual things need to be searched by the reader. It helps to understand the agreement itself.
This article is mainly divided into four parts, if you feel that the article is too long, you can choose the module you are interested in to read:
-
Use of Chrome Network panel: Mainly from the perspective of a mobile engineer to look at Chrome network module, mainly listed in my opinion may be helpful to locate the H5 problem points of knowledge.
-
Connection-keeplive: Mainly describes the way that the client/front end interacts with the server, and briefly introduces the general appearance of the server.
-
HTTP header congestion: Several experiments are conducted to understand the nature of HTTP header congestion and to show how OKHTTP differs from browsers in terms of policy.
-
HTTP packet Transport: Use several experiments to understand the process of HTTP packet transport.
First, use chrome Network panel
Open the mall page and open the Chrome Console.
Disable cache indicates that the browser cache is disabled. After enabling this option, the page will be pulled again every time you visit the page instead of using the cache. The Online drop-down menu on the right allows you to access the page in a weak network environment. This method is usually very effective in simulating weak network environment. For example, our normal access takes only 2s.
Open weak Network (FAST 3G)
Time has ballooned to 26 seconds.
Said about the role of these two options here, Preserve the log is saved before a page request logs, for example, we in the current page a click on a hyperlink to access the page b, then page a request in the console can’t see, if you check this option so you can see the front of a page request. In addition to this Hide Data Urls, the option also notes that some h5 pages have some resources that are transcoded directly in Base64 and embedded in the code (such as data in the screenshot: The advantage of this is that you can omit some HTTP requests, but the disadvantage is that the browser has no cache for this resource, and the size of the base64 transcoding is 3/4 larger than the original size. We can check this option to filter out things we don’t want to see.
For example, if we only want to see the requests for a certain domain name under the same page (which is very helpful for analyzing the number of domain names used by competing products), we can also do the following:
Or let’s say we just want to see a POST request or a GET request for this page.
For example, we can use is:from-cache to see which resources on our current page are cached. Large-than :(in bytes) this is also useful, as it is often used to filter out out-of-size requests to see how many large image resources there are (a way of checking for slow h5 pages on mobile).
We can also click on one of the requests, hold down shift, look for the blue is our selected request, green represents the upstream of the request is blue, that is to say, only when the green request execution is completed the request of the blue, and red request is only blue after the request has been completed the request. This approach of looking upstream and downstream of requests is often a technique for H5 optimization. Moving forward the resource requests that users care about most can greatly improve the user experience, although in a way that doesn’t improve the data (e.g. animation for jumping between activities, special themes for application launch optimizations, etc.), it doesn’t reduce the time per se. But the perception is that the page and app are fast).
This timing can display the detailed segmentation time of a request, such as queuing time, the time in bytes between the request sending and the first request response, and the time when the entire response has been transmitted. You can search for relevant information if you are interested.
Connection: keep-alive
In the modern server architecture, the client’s long connection most of the time not deal directly with the source server (the so-called source server can be roughly interpreted as server-side development brother actual code deployment of the server), but passes through a lot of proxy server, some of these proxy server is responsible for the firewall, some responsible for load balancing, Others are responsible for routing messages (e.g. mapping requests to clients to different nodes based on the client version number, ios or Android, etc.) and so on.
The long connection on the client only means that the TCP connection initiated by the client remains connected to the layer 1 proxy server. There is no direct hit to the original server.
Here’s another picture:
Typically, our request client will pass through several proxy servers before reaching our source server. If our source server wanted to do something based on the CLIENT’s requested IP address, it would theoretically need additional HTTP header support. Because based on the above architecture diagram, our source server got the address of the proxy server that established TCP connection with the source server, and could not get the IP address of the client that we actually initiated the request.
In the HTTP RFC specification, X-Forwarded-For carries the real IP address. Of course, in practice some proxy servers do not follow this rule. For example, Nginx uses the x-real-IP header to pass the Real IP address (Nginx does not enable this configuration by default and needs to manually change the configuration item).
In a real production environment, we can return the above proxy server information to the client in HTTP Response.
If you look at the header information in the reponse return, there’s an X-VIA that contains information about the proxy server.
For example, we open the home page of Taobao and find a request.
There is more proxy server information, indicating that the request was forwarded by multiple proxy servers. In addition, we sometimes hear about forward proxies and reverse proxies in technical conferences. In fact, these two proxies refer to the proxy server, and their functions are similar, but the application scenarios are different.
Forward proxy: For example, when kexue is surfing the Internet, we clearly know that we want to visit external websites such as Facebook, Google and so on. We can actively forward the request to a proxy server, which will forward the request to Facebook, and then Facebook will return the request to the proxy server. The server forwards it to us. This is called a forward proxy.
Reverse proxy: This actually we use every day, we visit the server, 99% are reverse proxy, modern computer systems, servers are often refers to the server cluster, we in the use of a function, simply do not know whether to which a request to the server, usually this is done by Nginx, we visit a web site, The DNS returns us an address, usually an Nginx address, and Nginx itself is responsible for forwarding the request to the server it thinks should be forwarded.
The concept of Nginx server and proxy server has been mentioned a number of times here. Considering that many front-end developers may not know much about back-end development, I will briefly introduce it here. The average server developer spends most of his or her day developing on an application server, and an HTTP application server is an HTTP server that dynamically generates content. For example, Java engineers write code and then type out the package to Tomcat, which is itself an application server. Go compiles good executables, is also an HTTP application server, and Python’s simpleServer. However, Nginx or Apache is more like a simple HTTP server. This simple HTTP server has almost no ability to dynamically generate HTTP response. They can only return static content or do a forwarding, which is a simple HTTP server. Strictly speaking, whether it’s Executable files compiled by Tomcat or Go or Python etc., they are also HTTP servers in essence, and can be used as proxy servers, but usually no one does that, because the industry is specialized, and this kind of work is usually done by Nginx.
Nginx uses a Master process to manage n worker processes. Each worker process has only one thread running.
Before Nginx, most servers are open multiple threads or processes work mode, process or thread switching is, however, there are costs, if the traffic is too high, the CPU will consume large amounts of resources in the process of creating or create a thread, and threads and processes before switching on, but not for Nginx use a similar scheme, Instead, the Nginx server uses a “process pool single thread” mode, in which a fixed number of processes are created at startup and no additional processes are created during subsequent runs, and these processes can be tied to the CPU, perfectly utilizing the multi-core capabilities of modern cpus.
In addition, web servers are IO intensive, and most of the time is spent on network overhead rather than CPU computation, so Nginx uses IO multiplexing. Nginx breaks up incoming HTTP requests into fragments and places the fragments on a single thread. Once a fragment of the thread is found to be in the IO wait, it will immediately switch out to handle other requests, and then cut back after it is determined to be readable and writable. This maximizes the power of the CPU. Pay attention to the switch is not thread again here, you can understand him in this thread to execute the program there are a lot of go to anchor, once found an execution fragments into the IO wait, use go immediately to ability to jump to other debris (in this case, the fragmentation is the HTTP request) to continue.
In fact, Nginx works a little like Go’s coroutine mechanism, except that there is not only one thread under several coroutines in Go, but also multiple threads. But the idea is the same: reduce the overhead of thread switching and use as few threads as possible to fulfill the “high concurrency” requirements of the business.
However, Nginx, no matter how good it is, can not resist the erosion of time, speaking of 15 years from today. There are some inherent defects, such as Nginx, whenever you modify the configuration, you must manually restart the Nginx process (master process), if your business is very large, once you need to modify the configuration, hundreds or even thousands of Nginx manual configuration restart is not only prone to error and repetitive work is not meaningful. In addition, Nginx scalability is mediocre, because Nginx is written in C language, we all know that C language is actually quite difficult to master, especially to master more difficult. Not everyone is confident that they can write good maintainable code in C. Especially if your code runs on basic services like Nginx that you use every day.
As a result, Ali had a programmer named Zhang Yichun, nicknamed “Brother Chun”, who developed an even better OpenResty project based on Nginx, which Luo said he would sponsor at the press conference. This project can expose the interface of Lua script to the outside world. Students who played World of Warcraft after 80 must be familiar with Lua language, and the famous plug-in mechanism of World of Warcraft is completed with Lua. With the advent of OpenResty, we can finally use Lua scripting language to operate our Nginx servers, where Lua also uses the concept of “coroutines” for concurrency, consistent with the Go language. In addition, OpenResty changes to the server configuration take effect in a timely manner without the need to restart the server. Greatly improve the efficiency of operation and maintenance. Wait, wait, wait…
3. The truth about “queue head congestion” in HTTP
We mentioned server, high concurrency several times before. The server in our impression is strongly associated with the three words of high concurrency. So what does “queue head congestion” mean in HTTP? Let’s start with a picture:
How to understand this picture, which has been circulating on the Internet for a long time? Some students believe that HTTP congestion is caused by THE transport protocol TCP, because TCP is inherently congested. Not all of this is true. Consider the following scenario:
-
The network is good.
-
The client uses the socket to send a set of data A, and 2 seconds later sends data B.
-
The server receives data A and starts processing data A, receives data B and starts processing data B.
-
When data b is processed, the server thread sends the responseb to the client. After a period of time, the server thread finally completes the data processing and sends the responsea to the client.
-
When sending responseB, the client can even send data C, D, and E simultaneously.
The above communication scenario perfectly illustrates TCP’s capability as a full-duplex transmission. This means that the client and the server have two transport channels working. So from this point of view, TCP is not the root cause of HTTP queue congestion. Because we all know that HTTP uses the transport layer protocol is TCP. Only in the case of a bad network environment, TCP as a reliability protocol, will indeed repeatedly send packets and wait for packets to confirm the situation. But this is not the root cause of HTTP “queue head congestion”.
From this graph, it looks like HTTP 1.x will only send a subsequent HTTP request after a response from the previous HTTP request comes back. But from this point of view, the efficiency of the server is not too low? If so, how can we explain the speed at which we open web pages and apps every day? After a while of exploration, I found that the above figure is for single TCP connections, and the so-called HTTP queue head congestion occurs only on a single TCP connection. We often have more than one TCP connection with a domain name on a server. Chrome, for example, allows a maximum of six TCP connections to a domain name by default, so that a server can handle up to six HTTP requests from your IP address at the same time, even if there is congestion.
Why are browsers limited to six? Isn’t the theory that the more TCP connections the faster? However, if each browser opens multiple TCP connections for a single domain name to speed up access, the server will quickly run out of TCP resources and then deny access. However, since the browser limits the number of TCP connections to a single domain name to six, why not just access multiple domains on the same page? In fact, a single page access to multiple domain names is also a point in the front-end optimization, the browser can only limit your single domain name 6 TCP connections, but you can not limit a page can have multiple domain names, we open several domain names is not equivalent to open several TCP connections? This will greatly increase the speed of page access.
Some of us might be wondering here, browsers limit the number of TCP connections to a single domain name, but does Android limit okHTTP that we use every day? How much is it limited? Take a look at the source:
Okhttp limits the number of TCP connections to a single domain name by default to 5, and exposes how to set this value. On a single TCP connection, why should HTTP make a response to the previous message before the subsequent HTTP request can be sent? Is there something wrong with this design? Too slow? Or do we have it all wrong? Is there another possibility:
-
HTTP messages can be continuously sent over a single TCP connection without waiting for the return of a previous HTTP message.
-
After receiving HTTP messages, the server first processes these messages. When the message is processed and ready to send a response, the server must wait until the response of the previously arrived request is sent first, just like a first-in, first-out queue. This also seems to fit the “queue head congestion” design?
With this question in mind, I do a set of experiments. First, we write a piece of server-side code that provides fast and slow2 interfaces, in which slow interface returns messages with a delay of 10 seconds and fast interface with a delay of 5 seconds.
package main
import (
"io"
"net/http"
"os"
"time"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.GET("/slow", slowRes)
e.GET("/fast", fastRes)
e.Logger.Fatal(e.Start(": 1329"))
}
func fastRes(c echo.Context) error {
println("get fast request!!!!!")
time.Sleep(time.Duration(5) * time.Second)
return c.String(http.StatusOK, "fast reponse")
}
func slowRes(c echo.Context) error {
println("get slow request!!!!!")
time.Sleep(time.Duration(10) * time.Second)
return c.String(http.StatusOK, "slow reponse")}Copy the code
Then we deploy the server application in the cloud, and write an Android application, we let the application send HTTP request can only use one TCP connection for a single domain name, and set the timeout time to 20s (otherwise the default OKHTTP response timeout time is too short to wait for the server to return the connection) :
dispatcher = new Dispatcher();
dispatcher.setMaxRequestsPerHost(1);
client = new OkHttpClient.Builder().connectTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).dispatcher(dispatcher).build();
new Thread() {
@Override
public void run() {
Request request = new Request.Builder().get().url("http://www.dailyreport.ltd:1329/slow").build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.v("wuyue"."slow e=="+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.v("wuyue"."slow reponse=="+ response.body().string()); }}); } }.start(); newThread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Request request = new Request.Builder().get().url("http://www.dailyreport.ltd:1329/fast").build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.v("wuyue"."fast e=="+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.v("wuyue"."fast reponse=="+ response.body().string()); }}); } }.start();Copy the code
Be sure to use an enqueue (asynchronous) method to send HTTP requests, otherwise your domain name TCP connection limit will be invalid. Then we use Wireshark to capture the packet:
It can be clearly seen that the two HTTP requests both use the same TCP connection and both use the source port number 60465 to server 1329. Then look at the time. The slow request starts to be sent for about 0s, the HTTP response from SLOW is received for about 10s, and immediately the request from the fast interface is sent, and 5 seconds later the HTTP response from FAST is returned.
If you set the TCP number of the domain name to 1 and change it to 5, you can see:
At this point you can clearly see that this time the FAST was sent after about 2 seconds on the SLOW interface, instead of waiting for the slow interface to return. Also note that the source port numbers used for these two HTTP messages are different: one is 64683, the other is 64684. This means that a different TCP connection is used to transport our HTTP messages.
In summary, we can conclude that HTTP 1.x is “queue head congestion” :
-
HTTP 1. X in the “queue head congestion”, in addition to its own transport layer protocol TCP caused by TCP congestion mechanism, more refers to the HTTP application layer limitation. This limitation is embodied in that, for HTTP, the client can send subsequent requests only after ensuring that the response of the previous request is returned on a single TCP connection.
-
Queue congestion in HTTP 1.x is essentially guaranteed by HTTP clients.
-
If you’re visiting a web page where all requests are directed to the same domain name, no matter how high the server’s concurrency, it can only handle six of your HTTP requests at the same time, because most browsers limit TCP connections to a single domain name. The only way to overcome this limitation and improve page loading speed is to enable multiple domain names.
-
Although okHTTP exposes the number of TCP connections in a single domain, there is no way to increase the speed of your application’s response by setting this value very high, because most servers limit the number of TCP connections in a single IP address, such as Nginx’s default setting of 10. So it doesn’t matter if your client sets the value too high unless you have an agreement with the server. But this is not as convenient as using multiple domain names.
As a result of the above analysis, HTTP 1.x did not fully realize the potential of TCP’s full-duplex channel, which is what Pipelining (Pipelining) has been called since 1.1. This convention allows HTTP clients to send subsequent requests without waiting for a response from a previous request. But for various reasons, modern browsers don’t have this feature enabled (see Pipelining keyword for more information, no copy and paste here). Curious, I searched okhttp’s code to see if they had a similar implementation. Finally we find a clue in this class:
This tunnel is called pipelining in http1.1. Is it possible to use this browser off by default technology in OKHTTP? Moving on to the code:
We see that the place where this value is used is from the connectTunnel method. Let’s see that this method is called in the connect method:
Let’s look at the implementation of this method:
/**
* Returns true if this route tunnels HTTPS through an HTTP proxy. See <a
* href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>.
*/
public boolean requiresTunnel() {
returnaddress.sslSocketFactory ! = null && proxy.type() == Proxy.Type.HTTP; }Copy the code
As you can see from the comments and RFC documentation, to enable the so-called tunnel function, you need your destination address to be HTTPS, specifically TLS for packet transmission, and an HTTP proxy server. This part of the code will not fire until both conditions are met. This part involves the TLS protocol related knowledge, we will put the content of this section in the third chapter to explain. The tunnel is used to directly forward TCP packets at the transport layer to the target server, rather than the HTTP proxy server for forwarding packets at the application layer.
4. The nature of HTTP packet transmission
Referer (I Google Github and click on the github link and see the request)
This field is often used for anti-theft, page source statistical analysis, cache optimization, and so on. Note, however, that the Referer browser has a policy for automatically adding it for us: Either the source is HTTP and the destination is HTTP, or the source is HTTPS and the destination is HTTPS, and if it happens to be HTTP and the destination is HTTPS or vice versa, the browser won’t help us add this field.
In addition, when HTTP packets are transmitted, fixed-length packets and indeterminate packets use different units.
For example, the unit after the content-Length field is base 10. That’s the “Hello, World! . However, for Chunk non-fixed-length packets, the unit is hexadecimal. In addition, for Chunk transmission, some headers of response are transmitted after the body transmission is completed. Let’s write a simple example on the server side, return a response called Hellowuyue, but use the chunk transmission mode. Here I simply use Go language to complete the corresponding code.
package main
import (
"net/http"
"github.com/labstack/echo"
)
func main() {e := echo.New() {e := echo."/", func(c echo.Context) error {
c.Response().WriteHeader(http.StatusOK)
c.Response().Write([]byte("hello"))
c.Response().Flush()
c.Response().Write([]byte("wuyue"))
c.Response().Flush()
return nil
})
e.Logger.Fatal(e.Start(": 1323"))}Copy the code
Let’s visit network in the browser to see the display information:
Then we used Wireshark to take a look at the chunk transfer mechanism in detail: it should be noted here that I did not choose to deploy our server code on the extranet server, but simply on the local, so we need to select the loopback address instead of the local connection. Also monitor port 1323. And do the filter for Port 1323.
Let’s take a look at the complete wireshark restore process:
If you look at the structure of this chunk, each chunk ends with a hexadecimal 0d0A, which we can understand as /r/ N or CRLF newline character. And then let’s see when the chunk is all done there’s an end chunked which also contains an 0d0A. (Space does not allow for the ABNF paradigm’s use of the chunk specification. Wireshark = wireshark = Wireshark = Wireshark = Wireshark = Wireshark = Wireshark
Finally, let’s take a look at how the browser and server interact when uploading files using form forms and how OKHTTP performs similar functions to deepen our understanding of package transfer. First we define a very simple HTML that provides a form.
<! doctype html> <html lang="en">
<head>
<meta charset="utf-8"> <title> Upload file </title> </head> <body> <h1> Upload file </h1> <form action="/uploadresult" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Files: <input type="file" name="file"><br><br>
Files2: <input type="file" name="file2"><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>Copy the code
Then define our server:
package main
import (
"io"
"net/http"
"os"
"time"
"github.com/labstack/echo"
)
func main() {e := echo.New() {e := echo."/uploadtest", func(c echo.Context) error {
return c.File("html/upload.html"}) // HTML is pre-defined to click upload to jump to the uri e.post ("/uploadresult", getFile)
e.Logger.Fatal(e.Start(": 1329"))
}
func getFile(c echo.Context) error {
name := c.FormValue("name")
println("name==" + name)
file, _ := c.FormFile("file")
file2, _ := c.FormFile("file2")
src, _ := file.Open()
src2, _ := file2.Open()
dst, _ := os.Create(file.Filename)
dst2, _ := os.Create(file2.Filename)
io.Copy(dst, src)
io.Copy(dst2, src2)
return c.String(http.StatusOK, "Upload successful")}Copy the code
Then we go to the form, upload the file and use Wireshark to grab a package to see what the browser is doing for us behind the scenes.
Those interested in content-disposition can do their own search for its meaning.
Okhttp: okhttp: okhttp: okhttp: okhttp: okhttp: okhttp
// Note that contentType is set manually, RequestBody requestBody1 = requestBody.create (MediaType. Parse ()"image/gifccc"), new File("/mnt/sdcard/ccc.txt"));
RequestBody requestBody2 = RequestBody.create(MediaType.parse("text/plain111"), new File("/mnt/sdcard/Gif.gif"));
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM).addFormDataPart("file2"."Gif.gif", requestBody1)
.addFormDataPart("file"."ccc.txt", requestBody2)
.addFormDataPart("name"."Wu yue")
.build();
Request request = new Request.Builder().get().url("http://47.100.237.180:1329/uploadresult").post(requestBody).build();Copy the code
Five, the summary
This topic describes how to use the Network panel of Chrome and the Wireshark to analyze HTTP, and focuses on the concept of queue congestion in HTTP1. x, solutions to this problem, and limiting policies of browsers. In the second chapter, we will cover HTTP caching, DNS, and Websocket in detail. In the third chapter, we will examine every detail of the HTTP2 and TLS protocols.
For more content, please pay attention to vivo Internet technology wechat public account
Note: To reprint the article, please contact our wechat account: LABs2020.