API使用部分的介绍在末尾,文章前面会有比较长的基础简介,视自己的技术水平可看可不看,丰俭由人~
HTTP协议与TCP/IP协议族的关系
当我们谈到TCP/IP时,会有两个含义,一个是TCP协议和IP协议本身,另一个含义是指TCP/IP协议族。 TCP/IP协议族是互联网相关的各种协议集合的总称。TCP/IP协议族将协议层次分为四层,而OSI标准则分为七层。一般来说,TCP/IP协议是实际的标准,所以一般理解TCP/IP的四层协议模型即可。从上到下,TCP/IP协议族分层具体为:
- 应用层。应用层决定了向用户提供服务时的通信的活动,是最接近用户的层次。这个层次包括用于文件传输的FTP(File Transfer Protocol,文件传输协议),用于解析域名,获取IP地址的DNS(Domain Name System,域名系统)等,以及包括本文主题HTTP协议。
- 传输层。传输层提供处于网络连接中的两台计算机之间的数据传输,为应用层提供服务。具体包括TCP(Transmission Control,传输控制协议)和UDP(User Data Protocol,用户数据报协议)。HTTP是基于TCP协议的应用层协议。
- 网络层(网络互联层)。网络层协议一般特指IP协议(Internet Protocol),主要作用是规定如何把传输层的数据包传送到目标机器上。
- 链路层。规定处理网络硬件,网络相关的设备驱动,操作系统相关的内容。
在HTTP协议通信过程中,HTTP依赖的底层通信协议会加上各种首部,这些首部包含了当前协议的内容和信息,而目标机器则会解析这些首部,达到互相通信的过程。
TCP协议和HTTP协议关系密切,是HTTP协议使用的传输层协议。TCP协议提供字节流服务(Byte Stream Service),即为了方便传输,将大块数据分割为以报文段(segment)为单位的数据包进行管理。另一方面,TCP协议使用了三次握手和四次挥手的机制,并且把数据包发送出去之后,服务端和客户端都会向对方确认是否成功送达,这保证了TCP提供可靠的传输服务。由于HTTP协议基于TCP协议的基础上构建,因此也集成了TCP的一部分特性,包括传输服务可靠,但传输性能相对基于UDP协议的应用层协议可能要差一些,等。另外,和TCP协议一致,HTTP协议的通信双方必定有一方是客户端,另一方是服务器。
HTTP协议通信的基本过程
客户端向服务器发送请求报文,发起HTTP请求;服务端向客户端发送响应报文,进行HTTP响应。请求报文由请求方法、请求URI、协议版本、可选的请求首部字段和内容实体构成;响应报文由协议版本、状态码(表示请求成功或失败的数字代码)、用以解释状态码的原因短语、可选的响应首部字段以及实体主体构成。
请求报文中提到的方法,代表了客户端需要从服务端获得的服务类型。常用方法包括:
- GET方法用来请求访问已被URI识别的资源。指定的资源经服务器端解析后返回响应内容。
- POST方法用于向服务端传输数据,并且可以获取服务端的响应及资源。虽然GET方法可以向URI中加入参数的方式传输简单数据,但传输文件等大块数据,还是需要使用POST方法。
- HEAD方法和GET方法一样,只是不返回报文主体部分。用于确认URI的有效性及资源更新的日期时间等。
- OPTIONS方法来查询针对请求URI指定的资源支持的方法。
- CONNECT方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。主要使用SSL(Secure Sockets Layer,安全套接层)和TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
HTTP首部字段是构成HTTP报文的要素之一。在客户端与服务器之间以HTTP协议进行通信的过程中,无论是请求还是响应都会使用首部字段,它能起到传递额外重要信息的作用。使用首部字段是为了给浏览器和服务器提供报文主体大小、所使用的语言、认证信息等内容。
需要注意,HTTP是无状态(stateless)协议,即不会对请求方和响应方的通信状态进行保存。由于一些web应用需要保留稳定的通信状态信息(比如登陆状态),于是有了一些基于HTTP的扩展机制,包括Cookie和Session机制等。Cookie会根据从服务器端发送的响应报文内的一个叫做Set-Cookie的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie值后发送出去;而Session机制则是由服务端维护的一个对话状态,客户端需要在Cookie中发送SessionId来使用Session机制。
HTTP相关的Web技术
HTTP/1.1规范允许HTTP服务器搭建多个Web站点,使用虚拟主机(Virtual Host)的功能即可实现。服务器可以托管多个域名,客户端请求服务器托管的多个域名都会通过DNS服务映射到服务器的IP地址。
HTTP通信时,除了客户端和服务器外,还有一些用于通信数据转发的应用程序,包括代理,网关和隧道等。
- 代理服务器的基本行为就是接收客户端发送的请求后转发给其他服务器。持有资源实体的服务器被称为源服务器。从源服务器返回的响应经过代理服务器后再传给客户端。使用代理服务器的理由有:利用缓存技术减少网络带宽的流量,组织内部针对特定网站的访问控制,以获取访问日志为主要目的等。
- 网关的工作机制和代理十分相似。而网关能使通信线路上的服务器提供非HTTP协议服务。利用网关能提高通信的安全性,因为可以在客户端与网关之间的通信线路上加密以确保连接的安全。
- 隧道可按要求建立起一条与其他服务器的通信线路,届时使用SSL等加密手段进行通信。隧道的目的是确保客户端能与服务器进行安全的通信。
HTTPS与HTTP相关的安全技术
在安全层面上,HTTP的不足包括:通信使用明文(不加密) 内容可能会被窃听;不验证通信方的身份,因此有可能遭遇伪装;无法证明报文的完整性,所以有可能已遭篡改。
HTTP协议中没有加密机制,但可以通过和SSL(Secure Socket Layer,安全套接层)或TLS(Transport Layer Security,安全层传输协议)的组合使用,加密通信内容。与SSL组合使用的HTTP被称为HTTPS(HTTP Secure,超文本传输安全协议)或HTTP over SSL。SSL采用一种叫做公开密钥加密(Public-key cryptography)的加密处理方式。公开密钥加密使用一对非对称的密钥。一把叫做私有密钥(private key),另一把叫做公开密钥(public key)。顾名思义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。使用公开密钥加密方式,发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。另外,为了保证公开密钥不会被在传输过程中被篡改,可以使用由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。客户端使用对应的算法去验证公开密钥证书上的数字签名,验证通过后,即可确认服务器发送的公开密钥是可信赖的。
虽然使用HTTP协议无法确定通信方,但如果使用SSL则可以。SSL不仅提供加密处理,而且还使用了一种被称为证书的手段,可用于确定方。证书由值得信任的第三方机构颁发,用以证明服务器和客户端是实际存在的。另外,伪造证书从技术角度来说是异常困难的一件事。所以只要能够确认通信方(服务器或客户端)持有的证书,即可判断通信方的真实意图。
由于HTTP协议无法证通信的报文的完整性,因此,在请求或响应送出之后直到对方接收之前的这段时间 ,即请求或响应的内容遭到篡改,也没有办法获悉。请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击称为中间人攻击。HTTPS通信过程中,发送数据时会附加一种叫做MAC(Message Authentication Code)的报文摘要。MAC能够查知报文是否遭到篡改,从而保护报文的完整性。
HTTPS也存在一些问题,那就是当使用SSL时,它的处理速度会变慢。SSL的慢分两点。一种是指通信慢。和使用HTTP相比,网络负载可能会变慢2到100倍。除去和TCP连接、发送HTTP请求,响应以外,还必须进行SSL通信,因此整体上处理通信量不可避免会增加。另一种是指由于大量消耗CPU及内存等资源,导致处理速度变慢。使用SSL让服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲,比起HTTP会更多地消耗服务器和客户端的硬件资源,导致负载增强。此外,要进行HTTPS通信,证书是必不可少的。而使用的证书必须向认证机构(CA)购买。因此,依旧有部分安全性要求不高的个人网站,使用HTTP的通信方式。
HTTP/HTTPS的身份认证机制
HTTP的认证方式主要有:BASIC认证(基本认证)、DIGEST认证(摘要认证)、SSL客户端认证,FormBase认证(基于表单认证)等。BASIC认证和DIGEST认证的安全性不高,一般不会在Web应用中被采用;SSL客户端认证需要第三方机构发放数字证书,有一定成本,使用也比较少。现有的Web应用大部分使用的是各自实现的表单认证。
基于表单的认证方法并不是在HTTP协议中定义的。客户端会向服务器上的Web应用程序发送登录信息(Credential),按登录信息的验证结果认证。所以表单认证的安全性主要看服务端开发者做的是否足够完善。基于表单认证的标准规范尚未有定论,一般会使用Cookie来管理Session(会话)的方式来实现。Session相关的用户认证信息是保存在服务端的,所以通过客户端攻击的风险大大降低。另外,为减轻跨站脚本攻击(XSS)造成的损失,一般会在Cookie内加上httponly属性,禁止JS脚本访问Cookie。至于密码的传输和保存过程如何保证其安全性,一般是通过加盐(salt)的方式实现的。
salt其实就是由服务器随机生成的一个字符串,但是要保证长度足够长,并且是真正随机生成的。然后把它和密码字符串相连接(前后都可以)生成散列值。当两个用户使用了同一个密码时,由于随机生成的salt值不同,对应的散列值也将是不同的。这样一来,很大程度上减少了密码特征,攻击者也就很难利用自己手中的密码特征库进行破解。
WinHTTP的简单使用
HTTP/HTTPS相关的C++网络库有很多,可以使用libcurl、Boost、Qt等提供了HTTP/S通信机制的第三方库(似乎C++新标准中即将加入网络设施,敬请期待)。但对于没有跨平台需求的Windows C++应用程序来说,WinHTTP不管是打包后的可执行文件大小,API获取和使用上的便捷性是最好的。尤其是追求可执行文件大小尽量少的场景下,个人测试引入WinHTTP基本只会增加几十kb的可执行文件大小,相比libcurl增加几百kb大小优势很足。Qt就更不提了,打包大小一直是被诟病的。
以下是WinHTTP API的基本使用流程。至于里面的接口,结合文章前面的基础知识一一简单介绍下。
WinHttpOpen、WinHttpConnect:初始化 WinHTTP 函数的使用并返回 WinHTTP 会话句柄。这里的会话和前文说到的服务端维护的会话不是一回事,个人理解是类似于Socket编程中返回的一个套接字描述符,后续代码利用这个描述符来进行网络编程。在与服务器交互之前,必须通过调用WinHttpOpen来初始化 WinHTTP 。WinHttpOpen创建一个会话上下文来维护有关 HTTP 会话的详细信息,并返回一个会话句柄。使用此句柄,WinHttpConnect函数可以指定目标 HTTP 或安全超文本传输协议 (HTTPS) 服务器。
WinHttpOpenRequest、WinHttpSendRequest:WinHttpOpenRequest函数打开一个特定资源的 HTTP 请求并返回一个HINTERNET句柄,其他 HTTP 函数可以使用该句柄。WinHttpOpenRequest在调用时不会向服务器发送请求。WinHttpSendRequest函数实际上通过网络建立连接并发送请求。
WinHttpSetCredentials:用于设置身份验证相关的信息,可选。
WinHttpAddRequestHeaders:添加HTTP请求首部信息。不过WinHttpSendRequest有一个参数可以指定首部,此函数看情况可以不用。
WinHttpWriteData:用于向服务器发布数据,只用于POST方法和PUT方法,GET方法不需要调用。
WinHttpReceiveResponse:函数等待接收对WinHttpSendRequest发起的 HTTP 请求的响应。如果HTTP设置为异步,则不需要调用。
WinHttpSetStatusCallback:若异步,则安装回调函数,否则调用WinHttpReceiveResponse,阻塞等待网络响应。
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
LPSTR pszOutBuffer;
BOOL bResults = FALSE;
HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;
// Use WinHttpOpen to obtain a session handle.
hSession = WinHttpOpen( L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0 );
// Specify an HTTP server.
if( hSession )
hConnect = WinHttpConnect( hSession, L"www.microsoft.com",
INTERNET_DEFAULT_HTTPS_PORT, 0 );
// Create an HTTP request handle.
if( hConnect )
hRequest = WinHttpOpenRequest( hConnect, L"GET", NULL,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE );
// Send a request.
if( hRequest )
bResults = WinHttpSendRequest( hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0,
0, 0 );
// End the request.
if( bResults )
bResults = WinHttpReceiveResponse( hRequest, NULL );
// Keep checking for data until there is nothing left.
if( bResults )
{
do
{
// Check for available data.
dwSize = 0;
if( !WinHttpQueryDataAvailable( hRequest, &dwSize ) )
printf( "Error %u in WinHttpQueryDataAvailable.\n",
GetLastError( ) );
// Allocate space for the buffer.
pszOutBuffer = new char[dwSize+1];
if( !pszOutBuffer )
{
printf( "Out of memory\n" );
dwSize=0;
}
else
{
// Read the data.
ZeroMemory( pszOutBuffer, dwSize+1 );
if( !WinHttpReadData( hRequest, (LPVOID)pszOutBuffer,
dwSize, &dwDownloaded ) )
printf( "Error %u in WinHttpReadData.\n", GetLastError( ) );
else
printf( "%s", pszOutBuffer );
// Free the memory allocated to the buffer.
delete [] pszOutBuffer;
}
} while( dwSize > 0 );
}
// Report any errors.
if( !bResults )
printf( "Error %d has occurred.\n", GetLastError( ) );
// Close any open handles.
if( hRequest ) WinHttpCloseHandle( hRequest );
if( hConnect ) WinHttpCloseHandle( hConnect );
if( hSession ) WinHttpCloseHandle( hSession );
以上是微软提供的同步HTTP请求的例程。若要使用异步HTTP请求,则必须调用WinHttpSetStatusCallback函数。
参考:
https://learn.microsoft.com/en-us/windows/win32/winhttp/using-winhttp
《图解HTTP》