☰
DNS 与 HTTP:两大应用层协议的实现与对比
2026/10/4 4:29:17 网站建设 项目流程

一、网络协议

在浏览器之中输入域名www.baidu.com浏览器可以顺利打开百度,点击按钮可以让数据发送到服务器,这些日常之中看似理所应当的从操作背后是一整套网络协议的支撑。在我们输入www.baidu.com之后,浏览器会先解析我们输入的URL,即https://www.baidu.com,拆分出协议,域名,端口和路径。现在本地检查是否存在缓存,如果本地之中没有缓存就会走DNS协议发送请求报文,DNS协议会将域名解析为IP地址,在本机和服务器建立三次握手之后会发送http请求并等待服务器返回的http响应,然后我们会得到我们需要的;浏览器信息界面。

在理解一整套的协议之前,我们需要先理解网络是分层的,实际开发之中一般链路层管物理传输,网络层负责寻址和路由,传输层管理端到端通信,应用层则是我们最长打交道的一层,也就是负责“数据长什么样,怎么组织数据,该怎么解析数据”,上文提到的DNS协议和HTTP协议就是应用层常见的协议,前者负责找到需要寻找的网页,后者负责和对方进行交谈,任何一次的网页访问都是先进行DNS,再HTTP。

DNS常见指令是nslookup,是systemd-resolved的本地存根解析器,是一个网络查询工具,主要用来查询DNS,也就是实现域名和IP地址之间的相互转换。ubuntu默认存在,在bash中输入:

nslookup www.baidu.com

输出结果:

前面两行的显示的127.0.0.53是本机使用的DNS服务器,端口是53(DNS标准服务端口)。Non-authoritative answer:表示这个结果不是从baidu.com的权威服务器之中得到的,而是本地DNS上游地柜DNS查询或者缓存之后得到的结果。www.baidu.com canonical name = www.a.shifen.com.代表www.baidu.com不是直接配置IP地址,而是一个CNAMEwww.a.shifen.com的别名记录。接下来四行表示www.a.shifen.com有两个IPv4地址:

182.61.200.108 182.61.200.110

这两个IPv4地址表示本次DNS查询得到的百度服务IP,属于百度服务器集群之中的两个具体实例的体现。这是一种常见的服务器设置操作,被称之为DNS轮询,主要是为了负载均衡和高可用,当一个IP地址的服务器或者网络节点发生故障的时候访问就可能受到影响,存在多台IP地址可以查询就可以最大程度避免前面的情况。

nslookup指令也可以做反向DNS查询,使用IP地址查询可以找到对应的域名。

nslookup 8.8.8.8

查询结果如下:

DNS服务可以将www.baidu.com作为一个域名和182.61.200.108相互对应,DNS就像一个自动的电话号码簿,可以拨打182.61.200.108的名字www.baidu.com来代替电话号码(IP地址),根据名字查电话号码的过程就是DNS查询,并且会暂时记住号码做缓存处理。

curl命令可以发送HTTP请求,看得到的响应。

curl http://www.baidu.com

最简单的指令就是发送GET可以看到服务器返回的网页源码:

-I参数可以看到HTTP请求响应头:

这个命令行操作全过程:

www.baidu.com ↓ DNS解析 182.61.200.108 / 182.61.200.110 ↓ 建立 TCP 连接 目标服务器:80 ↓ HEAD / HTTP/1.1 ↓ 服务器返回 HTTP 响应头

然后得到:

HTTP/1.1 200 OK

标明当前的HTTP协议版本是HTTP/1.1,状态码200表示成功,OK则是当前状态的说明。

二、DNS协议和UDP编程

DNS协议默认使用UDP 53端口,但是在部分大响应和区域传送的情形之下会采用TCP 53端口,127.0.0.53#53就是表示默认走53端口。在DNS之中通常把每一段叫做label,这些label之间采用.分隔。当前,对于每一级域名长度的限制是63个字符,即单个label不能超过63个字符,域名总长度(包括.)则不能超过253个字符。

根据RFC 1035文档,DNS协议报文格式如下:

整体而言DNS协议报文分为头部(固定12个字节),后面的Queries区域、Answer区域、Authority区域、Additional区域,除去头部以外,后面四个区域均是变长区域。

Flags区域主要布局如下:

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 1 4 1 1 1 1 3 4

头部主要字段:

字段长度含义
Transaction ID16 bit查询事务编号
QR1 bit0查询,1响应
Opcode4 bit操作类型,普通查询通常为 0
AA1 bit是否为权威回答
TC1 bit报文是否被截断
RD1 bit是否希望递归查询
RA1 bit服务器是否支持递归
Z3 bit保留位,通常为 0
RCODE4 bit返回状态码
Questions16 bitQuestion 数量
Answer RRs16 bitAnswer 记录数量
Authority RRs16 bitAuthority 记录数量
Additional RRs16 bitAdditional 记录数量

Queries查询区域主要字段结构:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / QNAME / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QTYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QCLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

第一部分QNAME是变长字段,也是Queries区域变长的主要原因。他的长度主要是由域名长度决定的。比如www.baidu.com就会被拆分为:

03 'www' 05 'baidu' 03 'com' 00

最终会被记录为如下的二进制结构:

03 77 77 77 05 62 61 69 64 75 03 63 6f 6d 00

所以说域名越短QNAME字段越短,域名越长,则QNAME越长。剩下的QTYE和QCLASS则各为两个字节。

QTYPE的值:

值名称含义
1AIPv4 地址
2NS权威名称服务器
5CNAME规范名称(别名)
6SOA授权起始记录
12PTR指针记录(反向解析)
15MX邮件交换
16TXT文本记录
28AAAAIPv6 地址
33SRV服务记录
255ANY / *请求所有记录(服务器有多少给多少)

QCLASS(查询类别)占 2 个字节,用来指定查询的协议类别。它和 QTYPE 一起构成 Question 区域的最后两个字段。下表列出了核心 QCLASS 值。

值名称含义
1INInternet(最常用,99.99% 的查询都是用IN)
3CHChaos(早期用于 Chaosnet,现在几乎不用)
4HSHesiod(MIT 的 Hesiod 系统,比较罕见)
255ANY / *请求所有类别

Answer / Authority / Additional 三个区域共用下面格式,数量可变的资源记录,其中记录的条数由头部 之中对应的计数字段指定,每一条资源记录具有以下格式:

1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / NAME / ← 域名(变长) | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TYPE | ← 2 字节 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | CLASS | ← 2 字节 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TTL | ← 4 字节 | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | RDLENGTH | ← 2 字节 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| / RDATA / ← 变长 / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

字段部分的说明:

字段宽度说明
NAME变长记录所属的域名(label 编码,可压缩)
TYPE2 字节记录类型(A/CNAME/MX...),决定 RDATA 含义
CLASS2 字节类别(IN=1)
TTL4 字节32 位无符号整数,缓存时间(秒)
RDLENGTH2 字节RDATA 的字节数
RDATA变长实际数据,随 TYPE 变化

你发一个 DNS 查询报文,DNS 服务器返回一个 DNS 响应报文(Question 原样回显,Answer 填上答案)。

关键区别:

发送请求服务器回的响应
Question 段你填的原样回显
Answer 段空服务器填的
头部 flagsQR=0(查询)QR=1(响应)

返回的报文是服务器重新生成一个响应报文,把查询的问题原样抄进去Queries区域,在Answer区域填上答案,将flags 的 QR 位:改成 1。Transaction ID则和你请求的 ID 保持一致(用来配对)。

下面一份代码使用C语言实现了DNS客户端查询,采用UDP编程方式,编译出的程序能够查询域名,发送UDP查询,接收响应,解析IP:

#include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> ​ #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> ​ #define DNS_SERVER_PORT 53 #define DNS_SERVER_IP "114.114.114.114" ​ #define DNS_HOST 1 #define DNS_CNAME 5 ​ struct dns_header { unsigned short id; unsigned short flags; ​ unsigned short questions; //1 unsigned short answer; ​ unsigned short authority; unsigned short additions; }; ​ struct dns_questions { int length; unsigned short qtype; unsigned short qclass; unsigned char *name; }; ​ struct dns_item { char* domain; char* ip; }; ​ //! \brief initialize a DNS header, prepare to send a query. //! \param header pointer to the DNS header to be initialized. int dns_create_header(struct dns_header* header) { ​ if (header ==NULL) return -1; memset(header, 0, sizeof(struct dns_header)); ​ //random srandom(time(NULL)); header->id = random(); ​ header->flags = htons(0x0100); header->questions = htons(1); return 0; } ​ //! \brief initialize a DNS question section //! \param question pointer to the DNS question structure to be filled //! \param hostname the domain name to query //! \return 0 on success, -1 on invalid arguments or encoding failure,-2 on empty name int dns_create_questions(struct dns_questions* question, const char* hostname) { ​ if (question == NULL || hostname == NULL) return -1; memset(question, 0, sizeof(struct dns_questions)); ​ question->name = (char*)malloc(strlen(hostname) + 2); if (question->name == NULL) { return -2; } ​ question->length = strlen(hostname) + 2; question->qtype = htons(1); question->qclass = htons(1); ​ const char delim[2] = "."; char* qname = question->name; char* hostname_dup = strdup(hostname); // malloc char* token = strtok(hostname_dup, delim); ​ while (token != NULL) { size_t len = strlen(token); ​ *qname = len; qname++; ​ strncpy(qname, token, len+1); qname += len; token = strtok(NULL, delim); } free(hostname_dup); } ​ //! \brief build a DNS request packet, including the header and question section //! \param header pointer to the DNS header structure //! \param question pointer to the DNS question structure //! \param request pointer to the buffer where the request will be built //! \param rlen length of the request buffer //! \return the length of the built request on success, -1 on invalid arguments int dns_build_request(struct dns_header* header, struct dns_questions* question,char* request, int rlen) { if (header == NULL || question == NULL || request == NULL) return -1; memset(request, 0, rlen); // header --> request; ​ memcpy(request, header, sizeof(struct dns_header)); int offset = sizeof(struct dns_header); ​ // questions --> request; memcpy(request + offset, question->name, question->length); offset += question->length; memcpy(request + offset, &question->qtype, sizeof(question->qtype)); offset += sizeof(question->qtype); memcpy(request + offset, &question->qclass, sizeof(question->qclass)); offset += sizeof(question->qclass); return offset; } ​ static int is_pointer(int in) { return (in & 0xC0) == 0xC0; } ​ static void dns_parse_name(unsigned char *chunk, unsigned char *ptr, char *out, int *len) { ​ int flag = 0, n = 0, alen = 0; char *pos = out + (*len); ​ while (1) { ​ flag = (int)ptr[0]; if (flag == 0) break; ​ if (is_pointer(flag)) { n = (int)ptr[1]; ptr = chunk + n; dns_parse_name(chunk, ptr, out, len); break; } else { ​ ptr ++; memcpy(pos, ptr, flag); pos += flag; ptr += flag; ​ *len += flag; if ((int)ptr[0] != 0) { memcpy(pos, ".", 1); pos += 1; (*len) += 1; } } } } ​ static int dns_parse_response(char *buffer, struct dns_item **domains) { ​ int i = 0; unsigned char *ptr = buffer; ​ ptr += 4; int querys = ntohs(*(unsigned short*)ptr); ​ ptr += 2; int answers = ntohs(*(unsigned short*)ptr); ​ ptr += 6; for (i = 0;i < querys;i ++) { while (1) { int flag = (int)ptr[0]; ptr += (flag + 1); ​ if (flag == 0) break; } ptr += 4; } ​ char cname[128], aname[128], ip[20], netip[4]; int len, type, ttl, datalen; ​ int cnt = 0; struct dns_item *list = (struct dns_item*)calloc(answers, sizeof(struct dns_item)); if (list == NULL) { return -1; } ​ for (i = 0;i < answers;i ++) { bzero(aname, sizeof(aname)); len = 0; ​ dns_parse_name(buffer, ptr, aname, &len); ptr += 2; ​ type = htons(*(unsigned short*)ptr); ptr += 4; ​ ttl = ntohl(*(unsigned int*)ptr); ptr += 4; ​ datalen = ntohs(*(unsigned short*)ptr); ptr += 2; ​ if (type == DNS_CNAME) { ​ bzero(cname, sizeof(cname)); len = 0; dns_parse_name(buffer, ptr, cname, &len); ptr += datalen; } else if (type == DNS_HOST) { ​ bzero(ip, sizeof(ip)); ​ if (datalen == 4) { memcpy(netip, ptr, datalen); inet_ntop(AF_INET , netip , ip , sizeof(struct sockaddr)); ​ printf("%s has address %s\n" , aname, ip); printf("\tTime to live: %d minutes , %d seconds\n", ttl / 60, ttl % 60); ​ list[cnt].domain = (char *)calloc(strlen(aname) + 1, 1); memcpy(list[cnt].domain, aname, strlen(aname)); list[cnt].ip = (char *)calloc(strlen(ip) + 1, 1); memcpy(list[cnt].ip, ip, strlen(ip)); cnt ++; } ptr += datalen; } } ​ *domains = list; ptr += 2; ​ return cnt; } ​ ​ int dns_client_commit(const char* domain) { ​ int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { return -1; } ​ struct sockaddr_in servaddr = {0}; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(DNS_SERVER_PORT); servaddr.sin_addr.s_addr = inet_addr(DNS_SERVER_IP); ​ int ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); printf("connect ret: %d\n", ret); ​ struct dns_header header; dns_create_header(&header); ​ struct dns_questions question; dns_create_questions(&question, domain); ​ char request[1024] = {0}; int length = dns_build_request(&header, &question, request, 1024); ​ // request int slen = sendto(sockfd, request, length, 0, (struct sockaddr*)&servaddr, sizeof(servaddr)); ​ // receive the response from the DNS server char response[1024] = {0}; struct sockaddr_in addr; size_t addr_len = sizeof(struct sockaddr_in); ​ int n = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr*)&addr, (socklen_t*)&addr_len); printf("recvfrom ret: %d\n", n); struct dns_item *dns_domain = NULL; dns_parse_response(response, &dns_domain); ​ free(dns_domain); #if 1 for (int i = 0; i < n; i++) { printf("%02x ", (unsigned char)response[i]); if ((i + 1) % 16 == 0) { printf("\n"); } } printf("\n"); ​ #endif return n; } ​ int main(int arg, char* argv[]) { if (arg < 2) return -1; ​ dns_client_commit(argv[1]); }

运行:

./dns www.baidu.com

打印结果如下:

原始字节一共63个,和recvfrom ret显示结果一致。

头部字节:

bd 52 81 80 00 01 00 02 00 00 00 00 └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ ID Flags QD AN NS AR

头部字节可以看出来这是一份DNS服务器的响应报文段,回答了一个问题做出了两个回答。

Flags区域的81 80展开成二进制之后解读如下:

1000 0001 1000 0000 │││ ││││││ └────┘ │││ ││││││ RCODE = 0(NOERROR,成功) │││ │││││└── Z = 0 │││ ││││└─── RA = 1(递归可用) │││ │││└──── RD = 1(期望递归) │││ ││└───── TC = 0(未截断) │││ │└────── AA = 0(非权威) │││ └─────── Opcode = 0(标准查询) │└─────────── QR = 1(响应) └──────────── (QR 位)

接下来就是Queries区域的QNAME,是对www.baidu.com字符的十六进制编码:

03 77 77 77 → 长度3 + "www" 05 62 61 69 64 75 → 长度5 + "baidu" 03 63 6f 6d → 长度3 + "com" 00 → 结束

QTYPE/QCLASS:

00 01 → QTYPE = 1 = A 00 01 → QCLASS = 1 = IN

Answer区域之中有两条RR(资源记录),第一条显示如下:

c0 0c 00 01 00 01 00 00 00 20 00 04 6e f2 46 39 └─┬─┘ └─┬─┘ └─┬─┘ └────┬────┘ └─┬─┘ └────┬────┘ NAME TYPE CLASS TTL RDLENGTH RDATA

不同字节的含义:

字节值字段含义
c0 0c压缩指针NAME指回偏移 12 →www.baidu.com
00 011TYPEA
00 011CLASSIN
00 00 00 2032TTL32 秒(4字节)
00 044RDLENGTHRDATA 4 字节
6e f2 46 39110.242.70.57RDATAIP

偏移指针c0 0c表示这条 RR 的 NAME 不是新写的域名,而是"指向报文偏移 12 处的域名"(头部是12字节,偏移12刚好是QNAME的起点)。第二条RR如下:

c0 0c 00 01 00 01 00 00 00 20 00 04 6e f2 45 15

资源字节的含义:

字节值含义
c0 0c压缩指针www.baidu.com
00 01A
00 01IN
00 00 00 20TTL=32
00 04RDLENGTH=4
6e f2 45 15110.242.69.21IP

三、HTTP协议和TCP编程

HTTP协议是应用层协议,用于客户端和服务器之间的数据传输,通常客户端发起一个请求到服务器上指定端口(默认为80)。HTTP协议假定其下层协议提供可靠的传输。因此何能够提供这种保证的协议都可以被其使用,所以其在TCP/IP协议族使用TCP作为其传输层。1999年6月公布的 RFC 2616,定义了HTTP协议中现今广泛使用的一个版本HTTP 1.1。

RFC 9190之中规定的HTTP请求方法如下:

方法作用有无 body幂等
GET获取资源无是
POST提交数据有否
PUT向指定资源位置上传新内容有是
PATCH局部更新有否
DELETE删除标识资源可无是
HEAD只取响应头无是
OPTIONS查询支持的方法无是

对于GET、HEAD方法而言,这些请求不再具备其他意义,即这些方法是被认为是安全的。维基百科上对于幂等的定义如下:“假如在不考虑诸如错误或者过期等问题的情况下,若干次请求的副作用与单次请求相同或者根本没有副作用,那么这些请求方法就能够被视作“幂等(idempotence)”的。”通俗的讲幂等就是无论执行多少次结果不变,反之执行多次结果会导致变化的话则不是幂等的。

HTTP常见状态码:

类别含义常见
1xx信息100、101
2xx成功200成功、201、204
3xx重定向301(永久重定向)、302(临时重定向)、304
4xx客户端错误400(请求语法错误)、401(未认证)、403(无权限)、404(资源不存在)
5xx服务器错误500(服务器错误)、502(网关错误)、503(服务不可用)

HTTP报文之中用来传递附加信息的就是头字段,HTTP之中的常见头字段如下:

头说明头说明
Host目标主机(HTTP/1.1 必需)User-Agent客户端标识
Accept能接受的响应类型Content-Type实体类型
Content-Length实体长度Authorization认证信息
Cookie客户端携带Set-Cookie服务器设置
Cache-Control缓存策略Connection连接管理

常见的Content-Type:text/html、text/plain、image/png等。

HTTP主要有两种报文结构,分别是请求报文和响应报文。

请求报文结构:

GET /index.html HTTP/1.1 ← 请求行 Host: www.example.com ← 请求头 User-Agent: Mozilla/5.0 Accept: text/html ← 空行 (请求体,GET 一般没有) ← 请求体

请求体一般由POST PUT携带。

响应报文结构:

HTTP/1.1 200 OK ← 状态行 Content-Type: text/html ← 响应头 Content-Length: 1024 ← 空行 <html>...</html> ← 响应体

状态行主要是版本+状态码+描述,响应头则是由若干的键值对(头字段和值)组成的。应用层HTTP协议用于请求响应内容,网络层的getaddrinfo通过域名得到IP地址,传输层socket套接字建立TCP连接,在TCP上发送HTTP请求文本可以得到服务端的HTTP响应文本。

下面一份代码是一份基础的HTTP客户端实现,输入域名和路径,发送HTTP GET请求,打印服务器响应:

#include <stdio.h> #include <stdlib.h> #include <string.h> ​ #include <sys/socket.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <netdb.h> #include <sys/select.h> #include <arpa/inet.h> ​ #define HTTP_VERSION "HTTP/1.1" #define CONNECTION_TYPE "Connection: close\r\n" ​ #define BUFFER_SIZE 4096 ​ char* host_to_ip(const char* hostname) { struct hostent* host_entry = gethostbyname(hostname); if (host_entry) { return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list); } return NULL; } ​ int http_create_socket(char* ip) { ​ int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in sin ={0}; sin.sin_family = AF_INET; sin.sin_port = htons(80); sin.sin_addr.s_addr = inet_addr(ip); ​ if (connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)) != 0) { return -1; } ​ fcntl(sockfd, F_SETFL, O_NONBLOCK); ​ return sockfd; } ​ char *http_send_request(const char* hostname, const char* resource) { ​ char* ip = host_to_ip(hostname); int sockfd = http_create_socket(ip); ​ char buffer[BUFFER_SIZE] = {0}; sprintf(buffer, "GET %s %s\r\n" "Host: %s\r\n" "%s\r\n" "\r\n", resource, HTTP_VERSION, hostname, CONNECTION_TYPE); ​ send(sockfd, buffer, strlen(buffer), 0); fd_set fdread; FD_ZERO(&fdread); FD_SET(sockfd, &fdread); ​ struct timeval tv; tv.tv_sec = 5; // 设置超时时间为5秒 tv.tv_usec = 0; ​ char* result = malloc(1); memset(result, 0, sizeof(int)); ​ while (1) { ​ int selection = select(sockfd + 1, &fdread, NULL, NULL, &tv); if (!selection || !FD_ISSET(sockfd, &fdread)) { break; } else { memset(buffer, 0, BUFFER_SIZE); int len = recv(sockfd, buffer, BUFFER_SIZE - 1, 0); if (len == 0) break; ​ result = realloc(result, (strlen(result) + len +1) * sizeof(char)); strncat(result, buffer, len); } } return result; } ​ int main(int argc, char* argv[]) { if (argc < 3) { return -1; } ​ char* response = http_send_request(argv[1], argv[2]); printf("response : %s\n", response); free(response); }

运行结果如下:

显示结果百度返回了完整的HTML首页,包括响应头,响应体。

四、对比和总结

DNS负责根据域名找到对方的IP,HTTP则是双方进行沟通。通过两份客户端代码可以充分理解UDP编程采用socket、sendto、recvfrom、connect等API,TCP编程采用connect、send、recv等API,协议在本质上是值得字节流和格式约定,并非常见的写在纸面上的规范而是可以根据字节流理解的一种通用约定。以及更加深入理解字节序,网络通信更多采用大端序,而现代大部分的x86架构机器均采用小端序,以及要注意htons和htonl以及ntohs和ntohl四个标准API使用,确保本机到网络,以及16位到32位之间的转化正确。

DNS协议和HTTP协议联合起来穿起来了一个应用层的完整链路:在浏览器上输入www.baidu.com等域名,解析URL,浏览器、系统会先查缓存,未命中则通过DNS协议的UDP 53端口拿到IP和服务端建立TCP三次握手,本机发送HTTP请求和接受HTTP响应,本地浏览器再渲染得到的html文件。

更多可以参考以下内容:

  • RFC 1035

  • RFC 2016

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询