http基本原理与客户端请求
一、http基础概念与工作流程
1、介绍
HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写。它的发展是万维网协会(World Wide Web Consortium)和Internet工作小组IETF(Internet Engineering Task Force)合作的结果,(他们)最终发布了一系列的RFC,RFC 1945定义了HTTP/1.0版本。其中最著名的就是RFC 2616。RFC 2616定义了今天普遍使用的一个版本——HTTP 1.1。
HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。
HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议。
2、在TCP/IP协议栈中的位置
HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:
默认HTTP的端口号为80,HTTPS的端口号为443。
3、HTTP的请求响应模型
HTTP协议永远都是客户端发起请求,服务器回送响应。见下图:
这样就限制了使用HTTP协议,无法实现在客户端没有发起请求的时候,服务器将消息推送给客户端。
HTTP协议是一个无状态的协议,同一个客户端的这次请求和上次请求是没有对应关系。
4、工作流程
一次HTTP操作称为一个事务,其工作过程可分为四步:
- 首先客户机与服务器需要建立连接。只要单击某个超级链接,HTTP的工作开始。
- 建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。
- 服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。
- 客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器断开连接。
如果在以上过程中的某一步出现错误,那么产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP自己完成的,用户只要用鼠标点击,等待信息显示就可以了。
5、抓包分析
1、建立TCP连接
2、在TCP连接,socket的基础上,发送HTTP协议请求
3、服务器在TCP链接socket,返回一个HTTP协议的response
在上图中,可清晰的看到客户端浏览器(ip为192.168.2.33)与服务器的交互过程:
- No1:浏览器(192.168.2.33)向服务器(220.181.50.118)发出连接请求。此为TCP三次握手第一步,此时从图中可以看出,为SYN,seq:X (x=0)
- No2:服务器(220.181.50.118)回应了浏览器(192.168.2.33)的请求,并要求确认,此时为:SYN,ACK,此时seq:y(y为0),ACK:x+1(为1)。此为三次握手的第二步;
- No3:浏览器(192.168.2.33)回应了服务器(220.181.50.118)的确认,连接成功。为:ACK,此时seq:x+1(为1),ACK:y+1(为1)。此为三次握手的第三步;
- No4:浏览器(192.168.2.33)发出一个页面HTTP请求;
- No5:服务器(220.181.50.118)确认;
- No6:服务器(220.181.50.118)发送数据;
- No7:客户端浏览器(192.168.2.33)确认;
- No14:客户端(192.168.2.33)发出一个图片HTTP请求;
- No15:服务器(220.181.50.118)发送状态响应码200 OK
二、http客户端请求
1、www.baidu.com --> 翻译为IP地址。(DNS)
2、创建sockfd并与服务器连接
3、发送http协议请求并接收
客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <fcntl.h>
#define HTTP_VERSION "HTTP/1.1"
#define CONNETION_TYPE "Connection: close\r\n"
#define BUFFER_SIZE 4096
将域名翻译为IP地址(DNS)
char *host_to_ip(const char *hostname) {
struct hostent *host_entry = gethostbyname(hostname); //dns
// 14.215.177.39 -->
// inet_ntoa: (unsigned int) --> char *如下
// 0x12121212 --> "18.18.18.18"
if (host_entry) {
return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list);//无符号int型数据转换成char*(字符串)
}
return NULL;
}
创建sockfd并与服务器连接
int http_create_socket(char *ip) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin = {0};// 用来形容服务器的IP 地址
sin.sin_family = AF_INET;// address family (地址族/网络协议栈/我们使用的协议栈),AT_INET代表的是TCP/IP协议族
sin.sin_port = htons(80); //端口,大部分http协议默认80端口
sin.sin_addr.s_addr = inet_addr(ip);// IP地址,char*(字符串)转换为无符号int型数据,正好与inet_ntoa
if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {
return -1;
}
fcntl(sockfd, F_SETFL, O_NONBLOCK);//把io设置成非阻塞
return sockfd;//sockfd是文件的句柄,文件操作的fd
}
发送http协议请求并接收
char * http_send_request(const char *hostname, const char *resource) {
char *ip = host_to_ip(hostname); // 获取IP地址
int sockfd = http_create_socket(ip);//sockfd建立好之后,就已经和服务器之间建立好了连接
char buffer[BUFFER_SIZE] = {0};//buffer里面的数据格式就是http的协议格式
sprintf(buffer,
"GET %s %s\r\n\
Host: %s\r\n\
%s\r\n\
\r\n",
resource, HTTP_VERSION,
hostname,
CONNETION_TYPE
);
//上面每行最后一个"\"是占位符,表示那不是换行
send(sockfd, buffer, strlen(buffer), 0);
// 下面是接收
// select
// 来监听receive的io里有没有可读的数据(也可以同时监听多个io)
fd_set fdread;// 集合,0/1标志位,1有0没有
FD_ZERO(&fdread);// 置空
FD_SET(sockfd, &fdread);// 把监听的io置为一个我们监听的状态
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
char *result = malloc(sizeof(int));
memset(result, 0, sizeof(int));//malloc后要memset,防止有脏数据
while (1) {// 不断遍历select,一旦有数据,+1
// select(maxfd + 1, &rset, &wset, &eset, NULL);
int selection = select(sockfd+1, &fdread, NULL, NULL, &tv);//(判断有多少可读的fd,一个可读的集合,可写的集合,哪些io出错,多长时间轮询一次遍历所有io)
if (!selection || !FD_ISSET(sockfd, &fdread)) {
break;
} else {
memset(buffer, 0, BUFFER_SIZE);
int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
if (len == 0) { // disconnect,对端已经关闭
break;
}
result = realloc(result, (strlen(result) + len + 1) * sizeof(char));//因为buffer一次可能读不完,所以重新分配result,把数据放到result中存储
strncat(result, buffer, len);// 把buffer数据copy到result里
}
}
return result;
}
main
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);
}
三、深入了解
1、Cookie和Session
Cookie和Session都是为了用来保存状态信息,都是保存客户端状态的机制,他们都是为了解决HTTP无状态的问题二做的努力。
Session可以用Cookie来实现,也可以用URL回写的机制来实现。用Cookie来实现的Session可以认为是对Cookie的更高级应用。
1.1 两者比较
Cookie和Session有一家明显的不同点:
- Cookie将状态保存在客户端,Session将状态保存在服务器端
- Cookie是服务器在本地机器上存储的小段文本,并随每一个请求发送至同一个服务器。网络服务器用HTTP投向客户端发送cookies,在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将同一服务器的任何请求缚上这些cookies。Cookie最早在RFC2109中实现,后续RFC2965做了增强。Session并没有在HTTP的协议中定义
- Session是针对每一个用户的,变量的值保存在服务器上,用一个sessionID来区分实发个用户session变量。这个值是通过用户浏览器在访问的时候返回给服务器的。当客户禁用cookie时,这个值也可能设置为由get来返回给服务器
- 就安全性来说:当你访问一个使用session的站点,同时在自己机子上建立一个cookie,在服务器端的session更安全些。因为它不会任意读取客户存储信息。
1.2 Session机制
Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个sessionID,如果已包含一个sessionID则说明以前已经为此客户端创建过session,服务器就按照sessionID把这个session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含sessionID,则为此客户端创建一个session并且生成一个与此session相关联的sessionID。sessionID的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 sessionID将被在本次响应中返回给客户端保存。
1.3 Session的实现方式
1.3.1使用Cookie来实现
服务器给每个Session分配一个唯一的JESSIONID,并通过Cookie发送给客户端。
当客户端发起新的请求的时候,将在Cookie头中携带这个JESSIONID。这样服务器能够找到这个客户端对应的Session。
流程如下图所示:
1.3.2 使用URL回写来实现
URL回写是指服务器在发送给浏览器页面的所有链接中都携带JSESSIONID的参数,这样客户端点击任何一个链接都会把JSESSIONID带会服务器。
如果直接在浏览器输入服务端资源的url来请求该资源,那么Session是匹配不到的。
Tomcat对Session的实现,是一开始同时使用Cookie和URL回写机制,如果发现客户端支持Cookie,就继续使用Cookie,停止使用URL回写。如果发现Cookie被禁用,就一直使用URL回写。jsp开发处理到Session的时候,对页面中的链接记得使用response.encodeURL() 。
1.4 与Cookie相关的HTTP扩展头
- Cookie:客户端将服务器设置的Cookie返回到服务器;
- Set-Cookie:服务器向客户端设置Cookie;
- Cookie2 (RFC2965)):客户端指示服务器支持Cookie的版本;
- Set-Cookie2 (RFC2965):服务器向客户端设置Cookie。
1.5 Cookie的流程
服务器在响应消息中用Set-Cookie头将Cookie的内容回送给客户端,客户端在新的请求中将相同的内容携带在Cookie头中发送给服务器。从而实现会话的保持。
流程如下图所示:
2、缓存的实现原理
2.1 什么是Web缓存
WEB缓存(cache)位于Web服务器和客户端之间。
缓存会根据请求保存输出内容的副本,例如html页面,图片,文件,当下一个请求来到的时候:如果是相同的URL,缓存直接使用副本响应访问请求,而不是向源服务器再次发送请求。
HTTP协议定义了相关的消息头来使WEB缓存尽可能好的工作。
2.2 缓存的优点
- 减少相应延迟:因为请求从缓存服务器(离客户端更近)而不是源服务器被相应,这个过程耗时更少,让web服务器看上去相应更快。
- 减少网络带宽消耗:当副本被重用时会减低客户端的带宽消耗;客户可以节省带宽费用,控制带宽的需求的增长并更易于管理。
2.3 客户端缓存生效的常见流程
服务器收到请求时,会在200OK中回送该资源的Last-Modified和ETag头,客户端将该资源保存在cache中,并记录这两个属性。当客户端需要发送相同的请求时,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器通过这两个头判断本地资源未发生变化,客户端不需要重新下载,返回304响应。常见流程如下图所示:
【注】:
1、如果socket是阻塞的,当我们read阻塞io的时候,一旦socket中没有数据,整个线程就会挂起,等待数据的到来
如果是非阻塞的,我们read的时候,即使没有数据,他也会立马返回。所以一般会选择非阻塞的io
2、realloc(void *ptr,size_t new_size); ptr是指向原来地址的指针,这个函数用于修改一个原先已经分配内存块的大小。
如果在该存储区后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充,并返回传送给它的同样的指针值。如果在原存储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现存的内容复制到新分配的存储区。
如果realloc中的第一个参数如果为空则和malloc一样。
3、gethostbyname()返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。结构的声明与gethostbyaddr()中一致。
4、
struct hostent
{
char *h_name;
char ** h_aliases;
short h_addrtype;
short h_length;
char ** h_addr_list;
};
5、inet_ntoa()将一个十进制网络字节序转换为点分十进制IP格式的字符串。
6、inet_addr()若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址
7、
struct sockaddr_in
{
sa_family_t sin_faily; // 地址族
uint16_t sin_port; // 16位TCP/UDP/……端口号
strcut in_addr sin_addr; // 32位IP地址
char sin_zero[8]; // 不使用
};
strcut addr_in
{
In_addr_t s_addr; // 32位IP地址
};
8、socket(int af, int type, int protocol)函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源。如果协议protocol未指定(等于0),则使用缺省的连接方式。
af:一个地址描述。仅支持AF_INET格式,也就是说ARPA Internet地址格式。
type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
9、send(int sockfd, const void *buf, size_t len, int flags)是一个计算机函数,功能是向一个已经连接的socket发送数据,如果无错误,返回值为所发送数据的总数,否则返回SOCKET_ERROR。
10、int select (int maxfd + 1,fd_set *readset,fd_set *writeset, fd_set *exceptset,const struct timeval * timeout);
该函数用于监视文件描述符的变化情况——读写或是异常。
参数一:最大的文件描述符加1。
参数二:用于检查可读性,
参数三:用于检查可写性,
参数四:用于检查带外数据,
参数五:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。
11、strncat(result, buffer, len);把buffer数据copy到result里