完整HTTP请求

· 概述

在详细学习https(代指http和https)之前,都觉得这些技术没什么大不了的,只不过是一套网络协议,和我早年学习xmpp、sip、rtp一样,遇到问题先百度,没有答案就上rfc。

以下信息部分来之互联网。

当我下定决心彻底掌握https时,发现事情并没有想象那么简单,随着学习的深入,需要掌握的知识点越来越多,从下往上,可以基本按照四层网络模型来划分,物理链路层->网络层->传输层→应用层,全文不做物理硬件相关的介绍,例如网卡、路由器、F5等等设备,只阐述其网络中起到的作用。

  1. 物理链路层涉及到网络最底层相关的知识,它主要负责网络数据的收发,有以太帧、网卡驱动、DMA、中断与软中断、CRC32。
  2. 网络层在四层网络模型中,起到标识各个节点的作用,为了保证定位节点的准确,涉及到的协议有IP协议(IPv4、IPv6)、ARP协议、DNS协议。
  3. 传输层如它字面意思一样,用来传输数据,目前互联网中广泛使用的有tcp和udp两种协议,tcp是有状态可靠协议,udp是无状态的,为了保证数据的安全性,用到tls协议对数据进行加密。udp很多时候又被二次制定为符合需求的其他有状态协议,如为了解决移动网络下网络波动延迟大而制定的kcp协议,google开发的quic网络传输协议等等,这里不对这些拓展协议做介绍,有兴趣的同学可以自行了解,相信你会打开新世界的大门。
  4. 应用层的作用就是处理网络层解包后的数据,就是http协议。

除了上面介绍的知识点外,还有:

  1. url与uri、万维网、域名组成。
  2. 数字签名证书、证书格式、证书链校验。
  3. RSA、ECC、DH三种非对称加密算法、AES对称加密算法、SHA算法。
  4. 大数计算、欧拉公式、二次探测、miller-rabin算法等。
  5. 多线程与PV操作,同步与异步。
  6. linux下用户态与内核态、0拷贝、epoll、socket。
  7. 广域网、局域网、以太网,nat与穿透技术。
  8. 正向代理、反向代理、osi四层\七层代理。

以上内容是我整理而来,知识点比较多,为了保证可读性和连续性,将从上往下的展开。

· 第一次请求

打开浏览器,输入http://www.baidu.com,按下回车。

URL与URI

URL = Universal Resource Locator 统一资源定位符,一种定位资源的主要访问机制的字符串。 

URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。 

仅从字面意思,难以理解定位、标志的却别,从下面这个例子将很直观的表达它们间的却别:

如果要找到张三此人所在的地址,根据URL的规则则是,定位协议://地球/中国/广东省/深圳市/南山区/生态园/9栋-4B-17/张三.人。

URI则是标识张三这个人,那么ID:430623199911294321,这个ID号标识张三这个人,见ID如见人。

同理,URI可以标识世界上任何事物,例如:

  • telephone:+86-13216732123,手机号码
  • isbn:7-105-5655,国际标准书号
  • ip:192.168.1.1,IPv4地址
  • code:518000,深圳邮编
  • http://username:password@gitlab.yungehuo.com/hhy/java/hhy.git,代码地址
  • email:329137445@qq.com,电邮地址

通过以上例子,很清楚看出URI对资源的标志作用。

URL是URI的一个子集,如 定位协议://地球/中国/广东省/深圳市/南山区/生态园/9栋-4B-17/张三.人,同时具有定位和标识作用。

http://username:password@localhost:8049/order/cargo/audit-detail?orderId=161612707361#page635,是一个典型的URL地址,它同时也符合URI定义。

URI结构由scheme、authority、path、query、fragment五部分组成,special part由authority、path组成,分解开看:

scheme → http

authority → username:password@localhost:8049

path → /order/cargo/audit-detail

query → orderId=161612707361

fragment → #page635

除了http有上述格式外,还有ftp、dubbo、rmi等等,这些协议一般都用uri规范来设计。

观察下面代码

    public static void main(String[] args) throws MalformedURLException, URISyntaxException {
        String str = "http://username:password@localhost:8049/order/cargo/audit-detail?orderId=161612707361#page635";
        URL url = new URL(str);
        System.out.println("url:");
        System.out.println(url.getProtocol());
        System.out.println(url.getAuthority());
        System.out.println(url.getPath());
        System.out.println(url.getQuery());
        System.out.println(url.getRef());

        System.out.println();
        
        URI uri = new URI(str);
        System.out.println("uri:");
        System.out.println(uri.getScheme());
        System.out.println(uri.getAuthority());
        System.out.println(uri.getPath());
        System.out.println(uri.getQuery());
        System.out.println(uri.getFragment());
    }

输出如下:

url:
http
username:password@localhost:8049
/order/cargo/audit-detail
orderId=161612707361
page635

uri:
http
username:password@localhost:8049
/order/cargo/audit-detail
orderId=161612707361
page635

URI和URL方法名不同外,有着相同的输出,说明他们都能解析http://username:password@localhost:8049/order/cargo/audit-detail?orderId=161612707361#page635网址。

但是对于不是URL格式的字符串来说,将会报错。

String str = "telephone:+86-13265487458"; // 修改str为电话格式

URL将会报错,而URI能正常使用,电话号码+86-13265487458属于special part。

DNS解析

http://www.baidu.com url地址解析出host=www.baidu.com和协议=http,因为host是域名地址,tcp连接需要ip地址,调用系统函数getaddrinfo可以得到ip地址。

getaddrinfo函数是一个底层C语言posix(可移植)规范函数,用来获取地址、链接、协议信息,为后续socket使用。

信息如下:

host:   www.baidu.com
service:    http
ip:     14.215.177.38
port:   80
family:     inet
socket_type:    stream
protocol:   hopopts
addrinfo:   cdata<struct addrinfo *>: 0x007ae3f8
flags: 
ai_flags:   0
ai_family:  2
ai_socktype:    1
ai_protocol:    0
ai_addrlen:     16
ai_canonname:   cdata<char *>: NULL
ai_addr:    cdata<struct sockaddr *>: 0x007bcff8
ai_next:    cdata<struct addrinfo *>: 0x007ae510

函数将host转换程socket使用的数据结构,其中有一个重要的参数ip。

在dns解析前,需要先明白几个专有名词:根域名服务器顶级域名服务器权威域名服务器

根域名服务器管理com、org、net等一级域名的服务器,他们数量有限,有多少个一级域名,就有多少个根域名服务器。

顶级域名服务器管理baidu.com、qq.com、midix.net、lastcoder.top这样的二级域名,它可以返回这些域名的ip地址,这样的服务器较根域名要多一些。

权威域名服务器管理www.baidu.com、tieba.baidu.com、music.qq.com等这样的三级域名,他们都是由权威机构管理,例如百度自己的域名解析服务器不仅可以解析自己的三级域名,也可以解析其他域名。

非权威域名服务器许多大公司、网络运行商都会建立自己的 DNS 服务器,作为用户 DNS 查询的代理,代替用户访问核心 DNS 系统,例如各市区地区dns解析等,这样的域名服务器是非权威的,存在域名劫持的风险。

域名解析遵循先深度再广度进行查询,大致流程如下:

  1. 域名解析会先从浏览器本地缓存查找,找不到就调用getaddrinfo函数。
  2. 调用getaddrinfo函数,会先从本地dns缓存查找,找不到就去hsots获取ip地址,windows电脑,可以在windows/system32/drivers/etc/hosts里看到。
  3. 本地dns缓存地址不存在,如果是自动域名解析,则会去网关询问,如果是手动配置,则去此地址询问。
  4. 如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。

  5. 如果LDNS仍然没有命中,就直接跳到根域名服务器请求解析。

  6. 根域名服务器返回给LDNS一个所查询域的顶级域名服务器地址。

  7. 此时LDNS再发送请求给上一步返回的gTLD。

  8. 接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器。

  9. Name Server根据映射关系表找到目标ip,返回给LDNS。

  10. LDNS缓存这个域名和对应的ip。

  11. LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束。

得到了域名对应的ip地址,现在浏览器可以尝试和服务器建立socket连接。

ARP地址解析协议

前面经历了URL解析、域名解析,得到了www.baidu.com的ip地址14.215.177.38。

浏览器遵循rfc协议标准设计,因此会默认使用80端口(port)尝试去建立连接。

IP协议是网络层协议,传输层才需要端口,这里根据http协议规范,采用tcp连接。socket建立tcp连接,操作起来很容易,只需要ip和port,就能建立一个可靠的连接。

一切看起来简直太简单了,似乎漏掉了什么。回到文章开头的以太帧,根据其定义,不难发现,这里我们还少了MAC地址。

本文所说以太帧是IEEE 802.1Q──虚拟局域网(Virtual LANs;VLAN) ethernet || 型 以太网帧,其格式如下:

前导码帧开始符MAC 目标地址MAC 源地址802.1Q标签 (可选)以太类型负载冗余校验帧间距

10101010 7个octet

10101011 1个octet

6 octets

6 octets

(4 octets)

2 octets

46–1500 octets

4 octets

12 octets

根据以太帧的定义,要想建立tcp连接,必须要mac源地址合mac目标地址,可现在是不知道目标mac地址。

为了解决这个问题,互联网工程任务组(IETF)在1982年11月发布RFC 826制定的地址解析协议(ARP),地址解析协议是IPv4中必不可少的协议,而IPv4是使用较为广泛的互联网协议版,需要注意的是IPv6协议中,是没有了ARP协议的,采用了更加先进的ND协议(邻居发现协议)。

arp协议工作原理和dns有点类似,都是先从本地缓存查找,其整个流程如下:

过程1:假设A与C在同一局域网,A要和C实现通信,A会查询本地的ARP缓存表,找到C的IP地址对应的MAC地址后,就会进行数据传输。如果未找到,A会找广播地址(网关)广播一个ARP请求报文,所有主机都收到ARP请求,只有C主机会回复一个数据包给A,同时A主机会将返回的这个地址保存在ARP缓存表中。如下图所示:

过程2:如果A与C在不同的局域网内,那么将A、C所在的网关看作是一个广域网,A网关在广域网上广播寻找IP地址14.215.177.38的MAC地址,C的网关收到后,将自己的MAC地址发送给A网关,其过程同过程1类似,将广域网看作一个大的“局域网”。

这里又存在一个问题,发送以太帧需要目的MAC地址,现在还是没有,ARP协议没法发送数据,这里RFC上已经详细说明了,对于以太帧类型为arp的数据,mac目的地址自动补ff-ff-ff-ff-ff-ff:

当C收到arp请求收,将向A发送应答包,不仅以太帧有mac地址,IP协议层也加上了自己mac地址:

arp地址很好的解决了找到mac地址的问题,但是因为协议设计较早,没有考虑nat网络情况,导致nat局域网存在安全风险,例如著名的arp伪装攻击、arp广播攻击,就是利用了arp协议漏洞。

SOCKET

得到MAC目的地址,目的IP,一切准备就绪,现在是时候建立一个可靠的tcp连接了。

解析http://www.baidu.com用到dns协议,解析mac地址用到arp协议,它们都是ipv4协议族协议,都使用了socket进行了数据的收发,使用系统api函数进行数据收发,但和建立tcp连接内部逻辑有很大不同。

先来看与建立socket相关的函数,他们按照客户端和服务端划分,服务端:socket、bind、listen、accept、send、recv,客户端:socket、connect、send、recv,在linux系统(后面所有内容都是指linux系统,除非特别指出windows)里为了提高io效率服务端还有select、poll、epoll。

select、poll、epoll需要文件句柄来操作,linux系统下,建立socket相当于创建一个文件,通过ll /proc/[pid]/fd 查看进程打开的文件有哪些,下面例子是查看pid=3407进程打开的文件,最下面有几个socket,这方面的内容和本文关联不大,属于linux系统编程范畴,有兴趣的同学可以自行了解。

回到socket系统api,这几个api是可移植的(posix),基本上所有系统都是这样设计。这里只需要考虑与客户端相关的函数,因为浏览器作为客户端连接服务器。

按字面意思来理解:

socket,使用tcp协议,构造一个socket句柄。

connect,使用ip14.215.177.38,port80,建立连接。

send,发送数据。

recv,收取数据。

四个函数,分工明确,这里简要概述下socket如何发送数据,

  • 在用户态环境申请一块内存缓存zbuff,并与本地端口形成映射关系。
  • 调用send函数向zbuff写数据,写完后立即返回长度。
  • 内核态从zbuff读取数据到ringbuff,并通知网卡驱动程序。
  • 网卡驱动通过dma方式从ringbuff读取数据,并发送。

socket采用了门面模式(外观模式),是系统对外界提供单一的接口,外部不需要了解内部的实现,这样的好处,大大减少了系统函数,同时保持了兼容、拓展特性,如增加新的协议,只需要增加一个参数,外部调用保持一致。

DMA(直接内存访问)

DMA 传输将数据从一个地址空间复制到另外一个地址空间。

当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成,也就是说除了初始化的时候需要cpu工作,传输的过程是脱离CPU的,是不需要CPU工作的。

在实现DMA传输时,是由DMA控制器直接掌管总线,因此,存在着一个总线控制权转移问题。即DMA传输前,CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应立即把总线控制权再交回给CPU。一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束4个步骤。

这样带来好处,可以让CPU从繁重的数据拷贝工作中抽出来,更多提高系统的IO处理能力。

坏处是增加了内存开销,如果存在大量的小包,会导致cpu中断频繁,上下文切换,反而降低了吞吐率。

这是一种典型的空间换时间的案例。

回到主题,socket中有read-buffer和write-buffer,用户态用户写入数据后,起始就是打开链接,尝试tcp链接,发送第一个tcp包p1。

p1数据写入write-buffer,内核态从write-buffer读取数据,设置端口等tcp协议数据,包装成tcp包,通过DMA方式写入网卡驱动ring-buffer(环形buff)。

硬中断与软中断

硬中断:

1. 硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。

2. 处理中断的驱动是需要运行在CPU上的,因此,当中断产生的时候,CPU会中断当前正在运行的任务,来处理中断。在有多核心的系统上,一个中断通常只能中断一颗CPU(也有一种特殊的情况,就是在大型主机上是有硬件通道的,它可以在没有主CPU的支持下,可以同时处理多个中断。)。

3. 硬中断可以直接中断CPU。它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。

4. 对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程来运行。它的存在是为了让调度代码(或称为调度器)可以调度多任务。

软中断:

1. 软中断的处理非常像硬中断。然而,它们仅仅是由当前正在运行的进程所产生的。

2. 通常,软中断是一些对I/O的请求。这些请求会调用内核中可以调度I/O发生的程序。对于某些设备,I/O请求需要被立即处理,而磁盘I/O请求通常可以排队并且可以稍后处理。根据I/O模型的不同,进程或许会被挂起直到I/O完成,此时内核调度器就会选择另一个进程去运行。I/O可以在进程之间产生并且调度过程通常和磁盘I/O的方式是相同。

3. 软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。一些内核允许设备驱动的一些部分存在于用户空间,并且当需要的时候内核也会调度这个进程去运行。

4. 软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。有一个特殊的软中断是Yield调用,它的作用是请求内核调度器去查看是否有一些其他的进程可以运行。

数据被写入到ring-buffer后,当前进程触发软中断,让出cpu给网卡驱动进行DMA操作,DMA指令完成后,恢复进程继续往下执行,同时DMA设备并行拷贝数据到ring-buffer,之前网卡驱动给数据加上ip地址,组成ip包(网络层)。

中断是一种异步的事件处理机制,用来提高系统的并发处理能力。中断事件发生,会触发执行中断处理程序,而中断处理程序被分为上半部和下半部这两个部分。上半部对应硬中断,用来快速处理中断;下半部对应软中断,用来异步处理上半部未完成的工作。Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,我们可以查看 proc 文件系统中的 /proc/softirqs ,观察软中断的运行情况。在 Linux 中,每个 CPU 都对应一个软中断内核线程,名字是 ksoftirqd/CPU 编号。当软中断事件的频率过高时,内核线程也会因为 CPU 使用率过高而导致软中断处理不及时,进而引发网络收发延迟、调度缓慢等性能问题。

下图是linux

以太帧

通过DMA方式,数据从内存写入到网卡缓存,这时网卡驱动会去对数据进行网络层协议包装(IP协议),写入IPv4或者IPv6的地址等信息,之后将数据丢给物理设备——网卡。

网卡使用MAC地址信息、CRC32校验码等信息包装数据为以太帧。以太帧的数据并不是一样的,不同的协议有不同的格式。

本文所说以太帧是IEEE 802.1Q──虚拟局域网(Virtual LANs;VLAN) ethernet || 型 以太网帧,其格式如下:

前导码帧开始符MAC 目标地址MAC 源地址802.1Q标签 (可选)以太类型负载冗余校验帧间距

10101010 7个octet

10101011 1个octet

6 octets

6 octets

(4 octets)

2 octets

46–1500 octets

4 octets

12 octets

前导码:一个帧以7个字节的前导码和1个字节的帧开始符作为帧的开始。快速以太网之前,在线路上帧的这部分的位模式是10101010 10101010 10101010 10101010 10101010 10101010 10101010 10101011。由于在传输一个字节时最低位最先传输(LSB),因此其相应的16进制表示为0x55 0x55 0x55 0x55 0x55 0x55 0x55 0xD5。

MAC地址:占6个字节,参考上面MAC地址解释。

    802.1Q标签:忽略。

    以太类型:以太帧有很多种类型。不同类型的帧具有不同的格式和MTU值。但在同种物理媒体上都可同时存在。

  • 以太网第二版或者称之为Ethernet II 帧,DIX帧,是最常见的帧类型。并通常直接被IP协议使用。

  • Novell的非标准IEEE 802.3帧变种。

  • IEEE 802.2逻辑链路控制(LLC) 帧

  • 子网接入协议(SNAP)帧

    所有四种以太帧类型都可包含一个IEEE 802.1Q选项来确定它属于哪个VLAN以及他的IEEE 802.1p优先级(QoS)。这个封装由IEEE 802.3ac定义并将帧大小从64字节扩充到1522字节(注:不包含7个前导字节和1个字节的帧开始符以及12个帧间距字节)。
IEEE 802.1Q标签,如果出现,需要放在源地址字段和以太类型或长度字段的中间。这个标签的前两个字节是标签协议标识符(TPID)值0x8100。这与没有标签帧的以太类型/长度字段的位置相同,所以以太类型0x8100就表示包含标签的帧,而实际的以太类型/长度字段则放在Q-标签的后面。TPID后面是两个字节的标签控制信息(TCI)。(IEEE 802.1p 优先级(QoS)和VLANID)。Q标签后面就是通常的帧内容。

负载:也就是内容,最少46字节,最大1500字节。

    根据CSMA/CD要求,为保证碰撞检测以太网最小帧长为64字节,其中以太网帧头+帧尾共18字节,所以以太网的data(IP,arp,rarp数据报) 至少为46字节,而arp或者rarp为28字节,为达到46字节需要填充18字节。

    最小帧长度保证有足够的传输时间用于以太网网络接口卡精确地检测冲突,这一最小时间是根据网络的最大电缆长度和帧沿电缆长度传播所要求的时间确定的。

冗余校验:帧校验码是一个32位循环冗余校验码,以便验证帧数据是否被损坏,如果数据损坏,直接丢弃,对于TCP等有状态的连接,可能会有请求peer重发,这个过程在网卡层完成。

帧间距:当一个帧发送出去之后,发送方在下次发送帧之前,需要再发送至少12个octet的空闲线路状态码。

其中前导码、帧开始符、帧间距是没法在wireshark等抓包软件抓到的,因为这部分信息是用来包装以太帧的,网卡物理层已过滤掉。

NAT网络

在建立tcp之前,还需要了解什么是nat网络。

因为IPv4地址紧张,为了让设备连接互联网,就需要复用IP地址,这里就需要nat网络。

nat网络并不是仅仅因为IPv4紧张,这只是个应用场景,还有安全等场景。

理解nat网络只需要知道1个概念和4种类型。

概念:内部主机要访问公网,需要进行端口\IP映射,内网主机IP:端口1 ---> 公网IP:端口2。

根据此概念会出现4中情况,分别如下:

NAT的原理与类型

NAT是IETF标准,它通过将局域网内的主机IP地址映射为Internet上有效的公网IP地址,从而实现了网络地址的复用。使用NAT技术,局域网内的多台PC可以共享单个、全局路由的IP地址,减少了所需的IP地址的数量。

NAT主要可以分为两类:基本NAT和NAPT ( Network Address Port Translation )。

基本NAT一般是用于NAT拥有多个公网IP的情形下,将公网IP地址与内网主机进行静态绑定,基本上这种类型的NAT设备已经很少了。或许根本我们就没机会见到。

NAPT(Network Address/Port Translators):其实这种才是我们常说的 NAT。NAPT将内部连接映射到外部网络中的一个单独IP地址上,同时在该地址上加上一个由NAT设备选定的端口号。根据映射方式不同,NAPT可以分为圆锥型NAT和对称性NAT,其中圆锥型NAT包括完全圆锥型NAT、地址限制圆锥型NAT和端口限制圆锥型NAT。
1 完全圆锥型NAT( Full Cone NAT )

完全圆锥型NAT,将从一个内部IP地址和端口来的所有请求,都映射到相同的外部IP地址和端口。并且,任何外部主机通过向映射的外部地址发送报文,都可以实现和内部主机进行通信。这是一种比较宽松的策略,只要建立了内部网络的IP地址和端口与公网IP地址和端口的映射关系,所有的Internet上的主机都可以访问该NAT之后的主机。

2 地址限制圆锥型NAT( Address Restricted Cone NAT )

地址限制圆锥型NAT也是将从相同的内部IP地址和端口来的所有请求映射到相同的公网IP地址和端口。但是与完全圆锥型NAT不同,当且仅当内部主机之前已经向公网主机发送过报文,此时公网主机才能向内网主机发送报文。


3 端口限制圆锥型NAT( Port Restricted Cone NAT )

类似与地址限制圆锥型NAT,但是更严格。端口受限圆锥型NAT增加了端口号的限制,当前仅当内网主机之前已经向公网主机发送了报文,公网主机才能和此内网主机通信。


4 对称型NAT( Symmetric NAT)

对称型NAT把从同一内网地址和端口到相同目的地址和端口的所有请求,都映射到同一个公网地址和端口。如果同一个内网主机,用相同的内网地址和端口向另外一个目的地址发送报文,则会用不同的映射。这和端口限制型NAT不同,端口限制型NAT是所有请求映射到相同的公网IP地址和端口,而对称型NAT是不同的请求有不同的映射。

TCP连接

经过前面的铺垫,现在可以建立一个有状态的TCP连接。

何为有状态,网络层上的数据是没有状态的,有状态的指两个终端建立了一个连接,此连接双反为了保证数据的可靠性,而建立的状态。

TCP的状态有:listen、syn-send、syn-recv、establish、fin-wait-1、close-wait、fin-wait-2、last-ack、time-wait、close。

为了保证数据传输的效率,还有二次握手、连接复用、no-delay、慢启动、超时重传等,涉及到滑动窗口算法、engle算法等等。

建立连接

建立连接前,服务器端首先被动打开其熟知的端口,对端口进行侦听。当客户端要和服务器端建立连接时,发起一个主动打开端口的请求(该端口一般为临时端口);然后进入三次握手的过程。

① A 的 TCP 向 B 发出连接请求报文段,其首部中的同步比特 SYN 应置为1,并选择序号 x,表明传送数据时的第一个数据字节的序号是 x(设置初始段序号SEQ = x ,例如SEQ = 26 500)。

② B 的 TCP 收到连接请求报文段后,如同意,则发回确认。
B 在确认报文段中应将 SYN 置为 1,其确认号ACK应为 x + 1(ACK 26 501),同时也给出自己的选择序号 y(设置初始段序号SEQ = y ,例如SEQ = 29 010)。

③ A 收到此报文段后,向 B 给出确认,其确认号应为 y + 1(ACK = 29011)。
A 的 TCP 通知上层应用进程,连接已经建立。
当运行服务器进程的主机 B 的 TCP 收到主机 A 的确认后,也通知其上层应用进程,连接已经建立。

由于客户对报文段进行了编号,它知道哪些序号是期待的,哪些序号是过时的。当客户发现报文段的序号是一个过时的序号时,就会拒绝该报文段,这样就不会造成重复连接。

释放连接

数据传输结束后,通信双方都可以释放连接。

四次分手的过程:

过程① 结束时,从 A 到 B 的连接就释放了,连接处于半关闭状态。
相当于 A 向 B 说:“我已经没有数据要发送了。但你如果还发送数据,我仍接收。”

过程② 结束后,至此,整个连接已全部释放。

网关(四层\七层)

忽略接入层、汇聚层、核心层的概念,网关就是将外部数据处理后丢给内部主机设备。

四层、七层网关是指OSI网络模型中的第四层和第七层,一般用来做负载均衡,基本上市面上所有的网络请求都有负载均衡网关。

七层网关:

    http请求一般使用七层网关(其实前面也可以加一层4层网关),这是应为http是表示层协议,需要解析后,然后通过代理给目标服务器。

    

四层网关:

    四层网关一般是路由网络层数据包,常见游戏服务器中长连接,为了效率不可能对包进行解析然后在转发,只可能是直接投递给内网服务器。

    四层网络没有存在sync flood,半连接攻击,导致内网服务器拒绝连接,进而导致拒绝服务。在服务器打开连接时,有backlog参数,改参数大小直接决定了当前未建立连接的半连接个数。

对于目前市面上的设备或软件,已经很难区分的明明白白了,例如nginx既可以代理http请求,也能实现udp数据包的转发。同样硬件设备f5一般是4层代理,但是同样具有7层会话保持等功能。但是针对实际应用场景,还是一层做一件事。

14.215.177.38(www.baidu.com)是百度http网关(极大概率4层)服务器IP,收到80/443端口请求后,丢给web服务器bws(baidu web server,极大概率是7层)处理。

NGINX代理与重定向

这里先不管百度bws情况,考虑市面上最常用的开源架构LNMP,其中的N就是nginx。

nginx通过反向代理前端发来的请求,它先处理http请求包,如果发现包不正常则过滤掉,通过配置的路由规则,重新将请求的数据丢给配置的服务器或则是CGI等。

这里nginx会和配置的服务器建立多个长连接,然后复用此连接,如果是目录则会通过restful方式找到文件。

nginx收到80端口的连接,会自动重定向到443端口,这个过程全程由nginx完成。

此时客户端收到一个重定向的302,头字段中location是443端口的地址。

客户端收到此请求后,会重新建立要给tcp连接,开始第二次http请求使用https协议来连接nginx。

TLS(SSL)

由于http是明文传出,基本上没有任何安全性可言,为了解决此问题,http引入Security socket layer(ssl)的概念,ssl经过1.0,2.0,3.0的发展,目前到了tls1.2。

tls是解决通信信息安全问题的,他是开源的。

这里的开源是基于一个事实,整个互联网的应用、服务器,是由不同组织公司主导的,他们之间不可能形成统一的加密方法,对此就需要基于一个统一的协议。

例如IE浏览器支持加密方式A,firefox浏览器支持加密方式B,chrome浏览器支持加密方式C,如果你的B/S应用只针对特定浏览器访问,那么可以使用其中的A\B\C,否则服务器就需要实现3套加密方案。这里3个还好,如果是1000个,10000个,100000个呢,就没有办法适配了。

对此,TLS是一套开源加密方案。

既然是开源,那么破解还不是分分钟。

其实不然,先来看下TLS加密流程:

整个加密过程是先通过非对称密钥交换生成对称密钥的参数信息,有random1,random2,serverkey,clientkey。

保证密钥参数交换安全,需要权威数字签名证书、非对称加密算法(rsa,ecc,dh等)来保证。

RSA等非对称加密

目前互联网能有今天的繁荣,需要特别感谢RSA技术的发明。

RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的 。

如果使用对称加密技术,将会存在以下几个严重的问题:

1.需要将密钥告诉对方。

2.需要保管大量第三方密钥。

3.密钥泄露。

4.无法大规模接入。

为了解决上面出现的问题,当时密码学界都在朝非对称加密算法努力,基本的思路是,知道A、B、C三个数中两个数,能推出第三个数,例如 A+B=C,A*B=C都能得到ABC的确定关系,但是A mod B = C,却得不到确定关系。

得不到确定关系,就给非对称加密留下了发挥空间。

RSA有几个核心数学概念,互质关系、欧拉函数、欧拉定理、模反元素。

互质关系

如果两个正整数,除了1以外,没有其他公因子,我们就称这两个数是互质关系(coprime)。比如,15和32没有公因子,所以它们是互质关系。这说明,不是质数也可以构成互质关系。

关于互质关系,不难得到以下结论:

1. 任意两个质数构成互质关系,比如13和61。

2. 一个数是质数,另一个数只要不是前者的倍数,两者就构成互质关系,比如3和10。

3. 如果两个数之中,较大的那个数是质数,则两者构成互质关系,比如97和57。

4. 1和任意一个自然数是都是互质关系,比如1和99。

5. p是大于1的整数,则p和p-1构成互质关系,比如57和56。

6. p是大于1的奇数,则p和p-2构成互质关系,比如17和15。

欧拉函数

请思考以下问题:

任意给定正整数n,请问在小于等于n的正整数之中,有多少个与n构成互质关系?(比如,在1到8之中,有多少个数与8构成互质关系?)

计算这个值的方法就叫做欧拉函数,以φ(n)表示。在1到8之中,与8形成互质关系的是1、3、5、7,所以 φ(n) = 4。

φ(n) 的计算方法并不复杂,但是为了得到最后那个公式,需要一步步讨论。

第一种情况

如果n=1,则 φ(1) = 1 。因为1与任何数(包括自身)都构成互质关系。

第二种情况

如果n是质数,则 φ(n)=n-1 。因为质数与小于它的每一个数,都构成互质关系。比如5与1、2、3、4都构成互质关系。

第三种情况

如果n是质数的某一个次方,即 n = p^k (p为质数,k为大于等于1的整数),则

比如 φ(8) = φ(2^3) =2^3 - 2^2 = 8 -4 = 4。

这是因为只有当一个数不包含质数p,才可能与n互质。而包含质数p的数一共有p^(k-1)个,即1×p、2×p、3×p、...、p^(k-1)×p,把它们去除,剩下的就是与n互质的数。

上面的式子还可以写成下面的形式:

可以看出,上面的第二种情况是 k=1 时的特例。

第四种情况

如果n可以分解成两个互质的整数之积,

n = p1 × p2

φ(n) = φ(p1p2) = φ(p1)φ(p2)

即积的欧拉函数等于各个因子的欧拉函数之积。比如,φ(56)=φ(8×7)=φ(8)×φ(7)=4×6=24。

这一条的证明要用到"中国剩余定理",这里就不展开了,只简单说一下思路:如果a与p1互质(a<p1),b与p2互质(b<p2),c与p1p2互质(c<p1p2),则c与数对 (a,b) 是一一对应关系。由于a的值有φ(p1)种可能,b的值有φ(p2)种可能,则数对 (a,b) 有φ(p1)φ(p2)种可能,而c的值有φ(p1p2)种可能,所以φ(p1p2)就等于φ(p1)φ(p2)。

第五种情况

因为任意一个大于1的正整数,都可以写成一系列质数的积。

根据第4条的结论,得到

再根据第3条的结论,得到

也就等于

这就是欧拉函数的通用计算公式。比如,1323的欧拉函数,计算过程如下:

欧拉定理

欧拉函数的用处,在于[欧拉定理]。"欧拉定理"指的是:

如果两个正整数a和n互质,则n的欧拉函数 φ(n) 可以让下面的等式成立:

也就是说,a的φ(n)次方被n除的余数为1。或者说,a的φ(n)次方减去1,可以被n整除。比如,3和7互质,而7的欧拉函数φ(7)等于6,所以3的6次方(729)减去1,可以被7整除(728/7=104)。

欧拉定理的证明比较复杂,这里就省略了。我们只要记住它的结论就行了。

欧拉定理可以大大简化某些运算。比如,7和10互质,根据欧拉定理,

已知 φ(10) 等于4,所以马上得到7的4倍数次方的个位数肯定是1。

因此,7的任意次方的个位数(例如7的222次方),心算就可以算出来。

欧拉定理有一个特殊情况。

假设正整数a与质数p互质,因为质数p的φ(p)等于p-1,则欧拉定理可以写成

这就是著名的费马小定理。它是欧拉定理的特例。

欧拉定理是RSA算法的核心。理解了这个定理,就可以理解RSA。

模反元素

还剩下最后一个概念:

如果两个正整数a和n互质,那么一定可以找到整数b,使得 ab-1 被n整除,或者说ab被n除的余数是1。

这时,b就叫做a的"模反元素"

比如,3和11互质,那么3的模反元素就是4,因为 (3 × 4)-1 可以被11整除。显然,模反元素不止一个, 4加减11的整数倍都是3的模反元素 {...,-18,-7,4,15,26,...},即如果b是a的模反元素,则 b+kn 都是a的模反元素。

欧拉定理可以用来证明模反元素必然存在。

可以看到,a的 φ(n)-1 次方,就是a的模反元素。

==========================================

好了,需要用到的数学工具,全部介绍完了。RSA算法涉及的数学知识,就是上面这些,下一次我就来介绍公钥和私钥到底是怎么生成的。

RSA生成步骤

前面我介绍了一些数论知识。
有了这些知识,我们就可以看懂RSA算法。这是目前地球上最重要的加密算法。

我们通过一个例子,来理解RSA算法。假设爱丽丝要与鲍勃进行加密通信,她该怎么生成公钥和私钥呢?

第一步,随机选择两个不相等的质数p和q。

爱丽丝选择了61和53。(实际应用中,这两个质数越大,就越难破解。)

第二步,计算p和q的乘积n。

爱丽丝就把61和53相乘。

n = 61×53 = 3233

n的长度就是密钥长度。3233写成二进制是110010100001,一共有12位,所以这个密钥就是12位。实际应用中,RSA密钥一般是1024位,重要场合则为2048位。

第三步,计算n的欧拉函数φ(n)。

根据公式:

φ(n) = (p-1)(q-1)

爱丽丝算出φ(3233)等于60×52,即3120。

第四步,随机选择一个整数e,条件是1

爱丽丝就在1到3120之间,随机选择了17。(实际应用中,常常选择65537。)

第五步,计算e对于φ(n)的模反元素d。

所谓"模反元素"就是指有一个整数d,可以使得ed被φ(n)除的余数为1。

ed ≡ 1 (mod φ(n))

这个式子等价于

ed - 1 = kφ(n)

于是,找到模反元素d,实质上就是对下面这个二元一次方程求解。

ex + φ(n)y = 1

已知 e=17, φ(n)=3120,

17x + 3120y = 1

这个方程可以用"扩展欧几里得算法"求解,此处省略具体过程。总之,爱丽丝算出一组整数解为 (x,y)=(2753,-15),即 d=2753。

至此所有计算完成。

第六步,将n和e封装成公钥,n和d封装成私钥。

在爱丽丝的例子中,n=3233,e=17,d=2753,所以公钥就是 (3233,17),私钥就是(3233, 2753)。

实际应用中,公钥和私钥的数据都采用ASN.1格式表达(实例)。

数字证书

TLS密钥交换过程中,必需要一端有数字证书,否则通信将会是不可信的,存在中间人攻击的可能。

对于http服务,一般是服务器提供数字证书,客户端校验数字证书的合法性。

校验数字证书合法性前,先看看数字证书的结构,证书由两部分组成,其中一部分是百度信息数字证书内容,另一部分是对证书内容的签名。

图片上部分是百度的信息,下部分是数字签名的内容,我们继续查看数字证书的内容,

其中①是内容中网站,如果要访问的网站和这个值对不上,会有中间人攻击分享,那么会弹出一个警告。

②是之前rsa算法中的n,③是rsa中的e,组成了公钥。

然后颁发机构对证书使用sha256进行了签名。

数字证书存在校验链,每台设备都存有根证书,百度正常链如下,

以下是跟证书信息

交换密钥

上面TLS过程中存在客户端随机数r1,服务的随机数r2,这两个随机数在生成ECCDH临时非对称密钥匙有用到,我们这里验证以下rsa公钥签名serverkey参数。

public static void main(String[] args) throws Exception {
        // rsa公钥
        String mHex = "00c1a9b0ae471ad257eb1d151f6e5cb2e4f80b20dbea00df29ffa46b89264b9f232fec57b08ab846402a7ebcdc5a45974fad410ebc20864b0c5d552147e2313c57a7ec9947eb470d72d7c8165475efd345110f4bce607a465c2874ae8e1bbed870667ba8934928d2a3769455de7c27f20ff7980cad86dac6aefd9ff0d981329a97e321ee049296e47811e5c4100e10317a4a97a0ebc79bc4da8937a9c337d756b17f52c7d9260ad6af3816b16dfb7379b168790390eb887b8c48919851a5079486a57846798f589be93559a7f17b57310a90cf24ce0d24e792b26ae9e696370ab87c872f74d25ce84b0a5f6618a74186cf26a6088ea549179253b391a5cf53b031";
        BigInteger m = new BigInteger(hex2byte(mHex));
        BigInteger e = BigInteger.valueOf(65537);
        
        // 参数hash256后+ras私钥签名的数据
        String paramsHashRsaHex = "2e45d1e7d9cce5a85b5d2351d3ff45f1a022c9f09e024eb6d44c782f96dfdc26c2c9317f974c4171e47b1729591b54d6ab6533289272109cfb278383d817fd37d3af0f868e43c3005367a3a1af1fbd4a1f25d814fd8d5d149b31c514b143e3b423c78680868e3549fc3133dbcb8099276aaea6694bf9d63014272fabd7ae8c7c7a7da9f2449ec41982ef95212cc3e49f0aee20e2fc699ad4aec864d7c4105848d8bc5c29c2808b9bb01bd1dae134941130393de44945368cf78e975bbeeda2134f8e20c6ab6521b141f5d65a7fa3091189e7f8d3465513f9553ef0b334ecd06cb99ef6e540c308df9589074e6c36daf2df9d482db5ad1302f3c5316c2137605a";
        
        // ras公钥解签后的hash256数据
        BigInteger c = new BigInteger(paramsHashRsaHex, 16);
        BigInteger o = c.modPow(e, m);
        String pkcs1 = bytes2Hex(o.toByteArray());
        System.out.println(pkcs1);
        
        // 对参数hash256
        byte[] params = hex2byte("60b9f62180502c545ea080c927efb2e84730ee815be5f8150289db0e1763b0af" 
                + "60b9f6219084885480e24f0f81b87db560923cf1318eb042095de0864b78072a" 
                + "03001741"
                + "043603d9fccab690a33f7c3b2c909df3c525917aeac0f9edea3f7196439411475d1984a4c07af29758b580c5514c60468445a74952a6073001729172b6f4ac2383");
        byte[] caha256 = SHA(params, "SHA-256");
        System.out.println(bytes2Hex(caha256));

        System.out.println(bytes2Hex(caha256).equals(pkcs1.substring(pkcs1.length()-64, pkcs1.length())));
        
        
    }

结果如下

01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420f73dabd8573ca6262115c7dc9fc9935081844ff7557f20b31e74ff149767de1f
f73dabd8573ca6262115c7dc9fc9935081844ff7557f20b31e74ff149767de1f
true

这里是对eccdh参数的一次验证。

收到客户端发来的clientkey后,通过r1,r2,serverkey,clientkey,双方生成aes密钥,然后使用aes密钥进行数据交换。

不适用rsa进行加秘的原因是,

1.效率比较低。

2.只能加密比rsa中n位数少的数据(因为是求模),能力有限。

http请求

建立tls连接后,客户端发送http请求,请求内容如下

curl 百度一下,你就知道 --verbose                    curl请求
* Rebuilt URL to: 百度一下,你就知道
* Trying 14.215.177.38...
* TCP_NODELAY set
* Connected to www.baidu.com (14.215.177.38) port 443 (#0)
* schannel: SSL/TLS connection with www.baidu.com port 443 (step 1/3)
* schannel: checking server certificate revocation
* schannel: sending initial handshake data: sending 184 bytes...
* schannel: sent initial handshake data: sent 184 bytes
* schannel: SSL/TLS connection with www.baidu.com port 443 (step 2/3)
* schannel: failed to receive handshake, need more data
* schannel: SSL/TLS connection with www.baidu.com port 443 (step 2/3)
* schannel: encrypted data got 4096
* schannel: encrypted data buffer: offset 4096 length 4096
* schannel: encrypted data length: 282
* schannel: encrypted data buffer: offset 282 length 4096
* schannel: received incomplete message, need more data
* schannel: SSL/TLS connection with www.baidu.com port 443 (step 2/3)
* schannel: encrypted data got 65
* schannel: encrypted data buffer: offset 347 length 4096
* schannel: sending next handshake data: sending 126 bytes...
* schannel: SSL/TLS connection with www.baidu.com port 443 (step 2/3)
* schannel: encrypted data got 226
* schannel: encrypted data buffer: offset 226 length 4096
* schannel: SSL/TLS handshake complete
* schannel: SSL/TLS connection with www.baidu.com port 443 (step 3/3)
* schannel: stored credential handle in session cache                      以上是建立tls连接
> GET / HTTP/1.1                      GET表示请求method为GET, / 表示是根路径,HTTP/1.1 表示使用HTTP协议1.1版本
> Host: www.baidu.com           主机地址
> User-Agent: curl/7.55.1         客户端信息
> Accept: */*                             接受任何格式数据,常用application/json,text/html等
>
* schannel: client wants to read 102400 bytes
* schannel: encdata_buffer resized 103424
* schannel: encrypted data buffer: offset 0 length 103424
* schannel: encrypted data got 2872
* schannel: encrypted data buffer: offset 2872 length 103424
* schannel: decrypted data length: 2843
* schannel: decrypted data added: 2843
* schannel: decrypted data cached: offset 2843 length 102400
* schannel: encrypted data buffer: offset 0 length 103424
* schannel: decrypted data buffer: offset 2843 length 102400
* schannel: schannel_recv cleanup
* schannel: decrypted data returned 2843
* schannel: decrypted data buffer: offset 0 length 102400
< HTTP/1.1 200 OK                  200状态码,OK状态码信息
< Accept-Ranges: bytes
< Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform               缓存信息,私有,不需要缓存,不要存储,代理刷新页面,没有转码
< Connection: keep-alive                表示连接是活的
< Content-Length: 2443                 内容长度,和下面的内容大小一样,有这个头,一般表示是静态页面,动态一般有transfer-encoding:chunked
< Content-Type: text/html              内容类型
< Date: Mon, 07 Jun 2021 02:57:25 GMT      时间
< Etag: "58860402-98b"        实体标识
< Last-Modified: Mon, 23 Jan 2017 13:24:18 GMT           实体最后修改时间
< Pragma: no-cache              跟Cache-Control: no-cache相同。Pragma: no-cache兼容http 1.0 ,
< Server: bfe/1.0.8.18            服务器
< Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/            cookie
<
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=百度新闻——海量中文资讯平台 name=tj_trnews class=mnav>新闻</a> <a href=hao123_上网从这里开始 name=tj_trhao123 class=mnav>hao123</a> <a href=百度地图 name=tj_trmap class=mnav>地图</a> <a href=百度视频——业界领先的中文视频搜索引擎之一 name=tj_trvideo class=mnav>视频</a> <a href=百度贴吧——全球领先的中文社区 name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=百度用户服务中心-首页 class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
* Connection #0 to host www.baidu.com left intact

服务器收到请求后,返回html页面。

客户端收到返回内容后,解析html页面内容并渲染展示。

一个完整HTTP请求包括以下几个部分: 1. 起始行:起始行包括请求方法、请求的URL和使用的协议版本。例如,GET /index.html HTTP/1.1。 2. 头部信息:头部信息包括一系列的键值对,用来传递请求的附加信息。常见的头部信息包括User-Agent(用户代理,用于标识浏览器或客户端)、Host(请求的主机名)、Content-Type(请求体的类型)等。 3. 空行:空行用于分隔头部信息和请求体。 4. 请求体:请求体包含了请求的具体内容,例如表单数据或上传的文件。 综上所述,一个完整HTTP请求的格式为: 起始行 头部信息 空行 请求体 引用\[1\] #### 引用[.reference_title] - *1* *3* [一完整HTTP事务是怎样的过程](https://blog.csdn.net/wang35235966/article/details/77863455)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [一次完整http请求过程](https://blog.csdn.net/weixin_48520816/article/details/125406258)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值