一、网络协议在浏览器之中输入域名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服务器端口是53DNS标准服务端口。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操作类型普通查询通常为 0AA1 bit是否为权威回答TC1 bit报文是否被截断RD1 bit是否希望递归查询RA1 bit服务器是否支持递归Z3 bit保留位通常为 0RCODE4 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% 的查询都是用IN3CHChaos早期用于 Chaosnet现在几乎不用4HSHesiodMIT 的 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 字节类别IN1TTL4 字节32 位无符号整数缓存时间秒RDLENGTH2 字节RDATA 的字节数RDATA变长实际数据随 TYPE 变化你发一个 DNS 查询报文DNS 服务器返回一个 DNS 响应报文Question 原样回显Answer 填上答案。关键区别发送请求服务器回的响应Question 段你填的原样回显Answer 段空服务器填的头部 flagsQR0查询QR1响应返回的报文是服务器重新生成一个响应报文把查询的问题原样抄进去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, len1); 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 0NOERROR成功 │││ │││││└── 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/QCLASS00 01 → QTYPE 1 A 00 01 → QCLASS 1 INAnswer区域之中有两条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.com00 011TYPEA00 011CLASSIN00 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.com00 01A00 01IN00 00 00 20TTL3200 04RDLENGTH46e 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、1012xx成功200成功、201、2043xx重定向301永久重定向、302临时重定向、3044xx客户端错误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负责根据域名找到对方的IPHTTP则是双方进行沟通。通过两份客户端代码可以充分理解UDP编程采用socket、sendto、recvfrom、connect等APITCP编程采用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 1035RFC 2016