HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。基于TCP的应用层协议,它不关心数据传输的细节,HTTP(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,只有遵循统一的 HTTP 请求格式,服务器才能正确解析不同客户端发的请求,同样地,服务器遵循统一的响应格式,客户端才得以正确解析不同网站发过来的响应。
这是最早定稿的HTTP版本,这个版本中它的内容非常地简单。
1.0的HTTP版本,是一种无状态,无连接的应用层协议。 HTTP1.0规定浏览器和服务器保持短暂的链接。
浏览器每次请求都需要与服务器建立一个TCP连接,服务器处理完成以后立即断开TCP连接(无连接),服务器不跟踪也每个客户单,也不记录过去的请求(无状态)。
这种无状态性可以借助cookie/session机制来做身份认证和状态记录。
HTTP1.0存在的问题:
这个版本是在HTTP/1.0的基础上增加了一些功能来优化网络连接的过程。
与 HTTP/1.1 相比,它在以下几方面作了改进:
尽管HTTP/2.0解决了很多1.1的问题,但HTTP/2.0仍然存在一些缺陷,这些缺陷并不是来自于HTTP/2协议本身,而是来源于底层的TCP协议。HTTP/3.0选择了使用UDP协议,在UDP的基础上实现多路复用、0-RTT、TLS加密、流量控制、丢包重传等功能,弥补了HTTP/2.0仍然存在一些缺陷,具体介绍见下文QUIC。
QUIC(Quick UDP Internet Connection快速UDP互联网连接)是谷歌制定的一种基于UDP的低时延的互联网传输层协议。它提供了像TCP一样的传输可靠性保证,可以实现数据传输的0-RTT延迟,灵活的设计使我们可以对它的拥塞控制及流量控制做更多的定制,它还提供了传输的安全性保障,以及像HTTP/2一样的应用数据二进制分帧传输。QUIC协议其实就是所谓的HTTP/3.0。
在QUIC的建连时间中大部分为0-ORTT,极少部分是1-RTT。TCP的一个建连包含三次握手,而如果基于HTTPS加密则还需包含TLS一层的一个握手,同时增加1 RTT的时间;综合来看,已完成建连的TCP连接进行握手大概需要2 RTT,而首次建连的TCP则需3 RTT,其中便包括TLS的证书交换。但对于QUIC协议,若客户端之前未建连,其第一次建连时需在客户端直接生成证书与和协议栈相关的配置且附带一个ID,这些数据会与请求数据一起直接发送并保存在服务端,假若服务端已存在以上数据那么系统会直接进行校验操作并直接回复数据;而已建连的客户端则仅需0-RTTs时间,初次建连的QUIC服务端为便于进行建连与校验会把ID保存在服务端本地,待服务端完成校验即可发送证书至客户端并进行客户端校验,校验成功即可直接启动数字交换;且对于第一次建连的QUIC而言只需1 RTT就可完成建连并允许后续的数据交互,以上就是QUIC的建连时间特性——大部分0-RTT、极少部分1-RTT。
基于TCP的HTTP/2.0深受TCP的队首阻塞问题困扰。由于HTTP/2.0在TCP的单个字节流抽象之上多路复用许多流,一个TCP片段的丢失将导致所有后续片段的阻塞直到重传到达,而封装在后续片段中的HTTP/2.0流可能和丢失的片段毫无关系。
由于QUIC是为多路复用操作从头设计的,携带个别流的的数据的包丢失时,通常只影响该流。每个流的帧可以在到达时立即发送给该流,因此,没有丢失数据的流可以继续重新汇集,并在应用程序中继续进行。
QUIC 具有可插入的拥塞控制,且有着比TCP更丰富的信令,这使得QUIC相对于TCP可以为拥塞控制算法提供更丰富的信息。每个包,包括原始的和重传的,都携带一个新的包序列号。这使得QUIC发送者可以将重传包的ACKs与原始传输包的ACKs区分开来,这样可以避免TCP的重传模糊问题。
QUIC ACKs也显式地携带数据包的接收与其确认被发送之间的延迟,与单调递增的包序列号一起,这样可以精确地计算往返时间(RTT)。
最后,QUIC的ACK帧最多支持 256个ack块,因此在重排序时,QUIC相对于 TCP(使用SACK)更有弹性,这也使得在重排序或丢失出现时,QUIC可以在线上保留更多在途字节。客户端和服务器都可以更精确地了解到哪些包对端已经接收。
对于TCP协议来说,标识一个TCP连接需要4个参数,既来源IP、来源端口、目的IP和目的端口。其中的任一参数改变,TCP连接就需要重新创建。这对于传统网络来说影响不大,因为来源和目的IP相对固定。但是在无线网络中,情况就大不相同了。设备在移动过程中,可能会因为网络切换(如从WIFI网络切换到4G网络环境),导致TCP连接需要重新创建。
QUIC协议使用了UDP协议,不再需要这四元组参数。同时QUIC协议实现了自己的会话标记方式,称为连接UUID。当设备网络环境切换时,连接UUID不会发生变化,因此无需重新进行握手。
数据包丢失不仅导致重传耗时,还会使拥塞窗口变小,从而降低吞吐量,影响了数据传输速度。谷歌在QUIC早期采用了前向纠错码(Forward-Error Correction)来减少包丢失现象。在一个Group中带上一个额外的fec包(N+1个包),分组中任意一个包丢失,都可以用其余N个包来恢复。这种做法同时也带来了冗余,可能没10个包中就要额外传输1个冗余包,在网络状况好的时候是一种浪费,目前谷歌已经废弃了QUIC的FEC功能。
同时,对于一些非常重要的包,QUIC在发送后的短时间内如果没收到回包,便会重发请求,以确保重要的节点不被Delay。
QUIC首次连接需要1RTT,具体过程如下:
后续的连接,如果客户端本地的serverConfig没过期(包含了Apg和其他前次协商信息),直接可以计算出初始秘钥K并加密传输数据,实现0RTT握手。
建立完连接后,就可以传输数据。QUIC连接中传输的所有数据,包括加密握手,被作为流(Stream)内的数据发送,流可以由客户端创建,也可以由服务器创建,可以与其它流并行交错地发送数据,且可以取消。如果端点收到一个STREAM帧,但它不想接受流,它可以立即以一个RST_STREAM帧(稍后描述)响应。注意,初始化流的端点可能也已经在那个流上发送了数据,这些数据必须被忽略。
一旦流创建好,它可被用于发送和接收数据。这意味着一系列的流帧可被QUIC端点在那个流上发送,直到流在那个方向上被终止。QUIC连接的任何一端都可以正常地终止一个流。有以下三种方式可以终止流:
QUIC连接,一旦建立,可由以下两种方式中的一种终止
一个端点还可以在连接期间的任何时间发送一个PUBLIC_RESET包来突然地终止活跃的连接。QUIC中的PUBLIC_RESET等价于TCP的RST。
去https://github.com/google/proto-quic
下载时,发现代码已经被清空了,但是在https://github.com/Louis14lan/proto-quic
找到了备份。
添加环境变量和安装依赖
cd proto-quic
export PROTO_QUIC_ROOT=‘pwd‘/src
export PATH=$PATH:‘pwd‘/depot_tools
./proto_quic_tools/sync.sh
cd src
gn gen out/Default && ninja -C out/Default quic_client quic_server net_unittests
完毕后得到quic_server
和quic_client
mkdir /tmp/quic-data
cd /tmp/quic-data
wget -p --save-headers https://www.example.org
下载一份www.example.org
的拷贝,它主要是给quic_server
二进制可执行文件用来提供本地服务的
修改测试数据
cd www.example.org/
vim index.html
获取到的index.html在header里加上X-Original-Url: https://www.example.org/
我们需要生成一个加密证书,这是因为这个DEMO包含了SSL加密的逻辑。
cd net/tools/quic/certs
./generate-certs.sh
将证书部署到系统
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n quic -i / /net/tools/quic/certs/out/2048-sha256-root.pem
导入失败,换一种方法,将证书添加到浏览器中。在地址栏中输入
chrome://settings/certificates
之后点击导入,将~/yangpei/proto-quic/src/net/tools/quic/certs/out/2048-sha256-root.pem
导入,
然后勾选第一项确定即可。
运行服务端程序
./out/Default/quic_server --quic_response_cache_dir=/tmp/quic-data/www.example.org --certificate_file=net/tools/quic/certs/out/leaf_cert.pem --key_file=net/tools/quic/certs/out/leaf_cert.pkcs8 --host=127.0.0.1 --port=6121
新开终端运行客户端程序
./out/Default/quic_client --host=127.0.0.1 --port=6121 https://www.example.org/ --allow_unknown_root_cert
测试成功,获取到了之前下载的www.example.org的index.html
跟据以下几种类型来判断为什么类型的包,来进行相应的处理:
enum QuicFrameType {
PADDING_FRAME = 0,
RST_STREAM_FRAME = 1,
CONNECTION_CLOSE_FRAME = 2,
GOAWAY_FRAME = 3,
WINDOW_UPDATE_FRAME = 4,
BLOCKED_FRAME = 5,
STOP_WAITING_FRAME = 6,
PING_FRAME = 7,
STREAM_FRAME,
ACK_FRAME,
MTU_DISCOVERY_FRAME,
NUM_FRAME_TYPES
};
PADDING_FRAME:为填充字节帧,接收到这个包时会将包剩余部分填充字节。
RST_STREAM_FRAME:当由流的创建者发送时,表示创建者希望关闭流,当由接收者发送时,表示发生错误或者不想接收流,因此流应该被关闭。
CONNECTION_CLOSE_FRAME:连接关闭。
GOAWAY_FRAME:表示流应该被停止使用,因为之后将会被关闭,在使用的流将被继续处理,但是发送者不会在接收流。
WINDOW_UPDATE_FRAME:用于通知对端流量控制端口接收窗口大小的更新。
BLOCKED_FRAME:表示已经准备好发送数据且有数据要发送,但是被阻塞了。
STOP_WAITING_FRAME:通知对端不需要等待包号小于特定值的包。
PING_FRAME:用来验证对端是否保持活跃,且连接是否正常。
STREAM_FRAME:用于发送数据。
ACK_FRAME:通知对端哪些包被接收到了。
quic实现的C/S端代码位于proto-quic\src\net\tools\quic
根据quic C/S端代码,其服务端和客户端代码的main()文件为server_bin.cc和client_bin.cc等文件。
client.main()函数基本实现步骤:
创建QuicClient类client,调用client.Initialize()进行初始化。
Initialize()实现:
{
定义流窗口大小
定义session窗口大小
设置epoll_server超时时间
调用CreateUDPSocket()创建UDP套接字
注册epoll时间回调函数
}
之后调用client.Connect()进行对话的连接
Connect()实现
{
写数据类PacketWrite类创建
创建session类
初始化session,InitializeSession()
WaitForEvent
}
调用client.CreateClientStream()与session中创建一个stream类流,用于发送数据。
调用stream->WriteStringPiece()来进行数据的发送。
调用client.WaitForEvents()等待事件。
调用stream->CloseConnection(net::QUIC_NO_ERROR);来关闭连接。
调用client.Disconnect()关闭client。
服务端与client端基本类似。
最外层的发送数据接口为调用stream流的WriteOrBufferData(body, fin, nullptr)方法其中body是要发的数据,fin是标识是否是改流的最后一个数据。之后会在流中进行相应的判断和处理,如流上是否有足够的空间来发送这个数据,发送窗口大小是否合适,是否阻塞等。如果判断可以进行发送之后便会调用session类的方法WritevData()。
在session类会调用connection类的SendStreamData方法发送数据,并根据实际发送的数据更新相应stream流的数据消费的数值。
在connection类会调用PacketGenerator类的ConsumeData方法来发送数据。其中会根据包来进行ack的绑定。
之后会返回connection类,根据消息队列情况调用WritePacket()进行socket上包的写入,该方法实现于PacketWriter类。
当Server端创建好之后循环调用StartReading(),进行接收包,根据synchronous_read_count_ 来判断是否是CHLO包。
void QuicSimpleServer::StartReading() {
if (synchronous_read_count_ == 0) {
// Only process buffered packets once per message loop.
dispatcher_->ProcessBufferedChlos(kNumSessionsToCreatePerSocketEvent);
}
...
int result = socket_->RecvFrom(
read_buffer_.get(), read_buffer_->size(), &client_address_,
base::Bind(&QuicSimpleServer::OnReadComplete, base::Unretained(this)));
...
OnReadComplete(result);
}
OnReadComplete()中会调用dispatcher的处理包方法
void QuicSimpleServer::OnReadComplete(int result) {
...
dispatcher_->ProcessPacket(
QuicSocketAddress(QuicSocketAddressImpl(server_address_)),
QuicSocketAddress(QuicSocketAddressImpl(client_address_)), packet);
StartReading();
}
void QuicDispatcher::ProcessPacket(const QuicSocketAddress& server_address,
const QuicSocketAddress& client_address,
const QuicReceivedPacket& packet) {
...
framer_.ProcessPacket(packet);
...
}
跳转到Framer类的处理方法
bool QuicFramer::ProcessPacket(const QuicEncryptedPacket& packet) {
...
if (!visitor_->OnUnauthenticatedPublicHeader(public_header)) {
// The visitor suppresses further processing of the packet.
return true;
}
...
}
visitor_指向dispatch类,跳转到
QuicDispatcher::OnUnauthenticatedPublicHeader(){
...
QuicConnectionId connection_id = header.connection_id;
SessionMap::iterator it = session_map_.find(connection_id);
if (it != session_map_.end()) {
DCHECK(!buffered_packets_.HasBufferedPackets(connection_id));
it->second->ProcessUdpPacket(current_server_address_,
current_client_address_, *current_packet_);
return false;
}
...
}
当包头的connection_id 能在session_map里找到时,直接调用connection的ProcessUdpPacket处理,server端的session_map维护在dispatch类里,创建session类都会记录下来。
之后经过处理跳转到Framer类的ProcessFrameData()方法里,其中对stream Framer和ACK Framer分别进行了处理,
如果是stream包,则对其进行解析后会调用OnStreamFrame()抛到上层。
if (!ProcessStreamFrame(reader, frame_type, &frame)) {
return RaiseError(QUIC_INVALID_STREAM_DATA);
}
if (!visitor_->OnStreamFrame(frame)) {
QUIC_DVLOG(1) << ENDPOINT
<< "Visitor asked to stop further processing.";
// Returning true since there was no parsing error.
return true;
}
}
visitor_在Framer类里,由创建connection类时初始化,指向connection类,在到connection类里调用visitor_->OnStreamFrame(),visitor_指向session类,在由session类抛到stream类的OnDataAvailable()将数据进行处理,注意基础stream类里没有实现OnDataAvailable()的方法,需要编写,下面是官方tools文件里的处理。
OnDataAvailable() {
while (HasBytesToRead()) {
struct iovec iov;
if (GetReadableRegions(&iov, 1) == 0) {
// No more data to read.
break;
}
QUIC_DVLOG(1) << "Stream " << id() << " processed " << iov.iov_len
<< " bytes.";
body_.append(static_cast<char*>(iov.iov_base), iov.iov_len);
if (content_length_ >= 0 &&
body_.size() > static_cast<uint64_t>(content_length_)) {
QUIC_DVLOG(1) << "Body size (" << body_.size() << ") > content length ("
<< content_length_ << ").";
SendErrorResponse();
return;
}
MarkConsumed(iov.iov_len);
}
if (!sequencer()->IsClosed()) {
sequencer()->SetUnblocked();
return;
}
// If the sequencer is closed, then all the body, including the fin, has been
// consumed.
OnFinRead();
if (write_side_closed() || fin_buffered()) {
return;
}
}
如果是ACK包,则会对ack进行处理,并进行拥塞算法的运算。
if (!ProcessAckFrame(reader, frame_type, &frame)) {
return RaiseError(QUIC_INVALID_ACK_DATA);
}
if (!visitor_->OnAckFrame(frame)) {
QUIC_DVLOG(1) << ENDPOINT
<< "Visitor asked to stop further processing.";
// Returning true since there was no parsing error.
return true;
}
OnAckFrame()跳转到connection类的OnAckFrame,其中调用QuicSentPacketManager类OnIncomingAck()方法,其中进行了rtt和带宽的更新,并对丢包进行判断。
虽然目前QUIC协议已经运行在一些较大的网站上,但离大范围普及还有较长的一段距离,期待QUIC协议规范能够成为终稿,并在除了谷歌浏览器之外的其他浏览器和应用服务器中也能够实现。
[1] https://tools.ietf.org/html/draft-ietf-quic-transport-31
[2] http://www.chromium.org/quic
[3] https://www.jianshu.com/p/65daa1578808
[4] https://www.jianshu.com/p/f0aa0ae21809
[5] https://www.cnblogs.com/xidongyu/p/6838236.html
[6] https://blog.csdn.net/dxpqxb/article/details/76819992
[7] https://yu.mantoufan.com/202003051019409975
原文:https://www.cnblogs.com/yangpei9915/p/14337689.html