1、在万维网中,资源(网页,图片,音频等)都是使用“统一资源定位符”(URL)来进行标识的。这些资源是通过超文本传输协议(HTTP协议)传送到客户端的。而用户只需要点击链接来获取该资源。
2、 HTTP协议是应用层的一种协议(超文本传输协议),表示了web客户端与服务器端双方之间传送数据的格式和解析数据的格式。HTTP请求定义了客户端如何向服务器端请求万维网文档,以及服务器端如何将万维网文档传输给客户端。
3、HTTP协议是面向事务的应用层协议,它规定了在浏览器和服务器之间请求和响应的格式和规则,是基于客户端/服务端(C/S)的架构模型,通过可靠的连接来交换信息,是一个无状态的请求/响应协议。
4、
约定的内容包括:
- 客户端与服务器之间有哪些信息需要进行交互
- 如何将交互的信息组织成为字符串(进行序列化的过程)
- 如何将字符串信息解析成结构化的信息
1、特点:
- HTTP协议是无连接的:是指使用HTTP协议进行数据和信息的传输前,不需要建立HTTP连接,而是建立TCP连接,使用TCP连接来保证可靠性的传输。无连接指的是不建立HTTP连接。
针对HTTP无连接,人们设计出了非持久连接和持久连接。
(1) 当客户端/服务器的交互运行于TCP协议之上时,应用程序的每个 请求/响应对是由一个单独的TCP连接时,即客户端每请求 一次服务器端,服务器端对该请求进行响应完,就立即断开连接。该连接是非持久连接。
(2) 而当应用程序的每个请求/响应对是由相同的TCP连接时,即客户端和服务器端完成一次响应和请求后,仍然保持连接,等待后续的客户端对服务器的响应,所有的请求和响应都是基于该条连接进行的,该连接是持久连接。
持久连接(HTTP/1.1版本)支持。
- HTTP协议是无状态的,无状态是指协议对于事物处理没有记忆能力,没有办法区分每次请求的不同之处。可能会导致每次传送的数据量增大。但这种情况也保证了服务器端支持高并发的响应请求。
- 支持客户/服务器模式
- 简单/快速
- 灵活
2、HTTP报文的结构
HTTP是面向文本的,报文中的每个字段都是ASCII码串,其报文又分为请求报文和响应报文。
- 请求报文:由客户端发往服务器端,发送的请求报文
- 响应报文:由服务器端发往客户端,发送的响应报文
- 两种报文的区别:开始行是不同的。请求报文的开始行叫做请求行,响应报文的开始行叫做状态行
HTTP请求报文:方法、资源的URL、HTTP版本(常用的方法包括:POST方法、GET方法、HEAD方法、CONNECT方法)
- GET方法:请求读取URL标识的信息
- HEAD方法:请求读取URL标志信息的首部
- POST方法:给服务器添加信息
- CONNECT方法:用于代理服务器
下面是请求报文的实例:
POST http://128.304.0.25/login HTTP/1.1
Host: 128.304.0.25
Connection: keep-alive
Content-Length: 27
Origin: http://123.207.58.25
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Referer: http://123.207.58.25/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=b52hc1l3dddvm61i1cvkkmif93
name=775&passwd=18700798311
(1)首行为:方法+url+版本
- POST方法(请求资源),GET方法(获取资源)、DELETE方法(请求服务器删除指定的资源)
- url:网址是128.304.0.25
- 版本:HTTP/1.1
(2)Header(请求的属性)
是以冒号加空格分隔的键值对,而且每组属性各占一行,结束的标志为一个空行
- Host属性表示主机名
- Connection属性表示连接的属性,keep-alive(持久连接)
- Content-Length属性:Body部分的长度
- Content-Type:指定body部分的格式
- Refer:当前页面是从哪个页面跳转过来的
- Cookie:用于保存用户提交的身份信息等。当我们登录网站时,第一次登陆之后,本地将我们的信息提交给服务器,本地的cookie就会将服务器返回的信息存储下来,下次我们再请求连接时,就不需要再重复上述的操作了。cookie属性的大小上限为4k,当我们登录淘宝网时,cookie会保存我们的身份信息。
(3)Body部分
Header后面紧接着Body部分,多数情况下为HTML格式的信息。
3、HTTP响应报文
HTTP/1.1 200 OK
Server: nginx
Date: Sat, 30 Jun 2018 10:45:42 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
Vary: Accept-Encoding
X-Powered-By: PHP/5.4.45
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: PHPSESSID=b52hc1l3dddvm61i1cvkkmif93; expires=Sat, 30-Jun-2018 16:45:42 GMT; path=/
Set-Cookie: loginStatus=yes; expires=Sat, 30-Jun-2018 16:45:42 GMT; path=/
Content-Length: 27
(1)首行:版本号+状态码+状态码解释
状态码的分类:https://mp.youkuaiyun.com/postedit/87565202(见之前写的一篇博客)
(2)Header部分
- Set-Cookie:从请求报文中所获得的一些属性来设置该属性,再给客户端发送响应时,就会将其发送回去
(3)Body部分:
报文的内容,通常以HTML的形式展示
下面该程序,模拟HTTP服务器
基于TCP服务器端的整个流程:socket->bind->listen->accept->send/recv->closesocket
僵尸进程:子进程执行exit()后,代码执行部分其实已经结束执行了,系统的资源也已归还给系统了,但是其进程的进程控制块(PCB)仍驻留在内存中,因为PCB是进程存在的唯一标志,里面有PID等信息,并没有消亡,因此这样的进程称为僵尸进程
孤儿进程:父进程结束(异常结束),未能及时收回子进程,子进程仍在运行,这样的进程称为孤儿进程。在Linx系统中,孤儿进程一般会被init进程所收养,成为init进程的子进程,由init进程来做善后的处理,所以她不会像僵尸进程一样无人问津,但是大量存在会有危害。init进程是pid=1的进程,因此又叫做1号进程
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
//处理连接的请求响应,addr是存放IPv4型的结构体
int main(int argc,char * argv[])
{
//判断命令行参数是否合法
if(argc != 3)
{
printf("Usage: ./server [IP] [Port]\n");
return 1;
}
struct sockaddr_in addr;//定义结构体addr存放IPv4类型的套接字的相关信息
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);//IP地址
addr.sin_port = htons(atoi(argv[2]));//端口号
socklen_t addr_len = sizeof(addr);
int fd = socket(AF_INET,SOCK_STREAM,0);//使用socket函数打开网卡文件,创建套接字
//绑定IP和端口号
int bind_ret = bind(fd,(struct sockaddr *)&addr,addr_len);
if(bind_ret < 0)
{
perror("bind");
return 2;
}
//使服务器处于监听状态,接收客户端的请求
if(listen(fd,5) < 0 )
{
perror("listen");
return 3;
}
//服务器开始进行循环,不断地接收来自客户端的请求
while(1)
{
//服务器接收连接请求,使用accept函数接受连接,该函数返回客户端网卡文件的文件描述符
int new_socket = accept(fd ,(struct sockaddr *)&addr,&addr_len);
if(new_socket < 0)
{
perror("accept");
continue;
}
pid_t pid = fork();//使用fork()函数创建进程
if(pid < 0)//创建进程失败
{
perror("fork");
return 4;
}
if(pid == 0)//fork成功,返回值为0的为子进程
{
//子进程不用监听socket ,所以可以直接将网卡文件关闭
close(fd);
pid_t id = fork();//子进程中使用fork函数创建进程
if(id < 0)
{
perror("fork");
return 5;
}
if(id > 0)
{//二代子进程,即新fork()后的父进程,其下还有一个子进程
//为一个已经建立好连接的用户提供服务
char read_buf[1024 * 10] = {0};//开辟10k的缓冲区,存放从客户端读取到的请求
char buf[1024 * 10] = {0};//开辟10k缓冲区,进行初始化
while(1)
{
//读取客户端的请求,使用read函数(),new_socket是客户端网卡文件的文件描述符,读取失败时返回-1
if((read(new_socket ,read_buf ,sizeof(read_buf)-1))<0)
{
perror("read");
}
else
{
printf("%s",read_buf);//读取成功时,输出读取到的内容(客户端发送的请求内容)
printf("********************************\n");
}
//现在不关心客户端发送的请求的内容,因此只要按照http响应报文格式回复过去就可以
const char * body = "<html>\n<h1>hello world</h1>\n</html>";//Body部分内容,显示到网页上的信息
const char * first_line = "HTTP/1.1 200 OK";
const char * header_content_type = "Content-Type: text.html;charset=UTF-8";
//构造http响应报文,sprintf函数是打印到buf中,而printf函数则是打印到屏幕上,属性内容之间使用\n隔开
sprintf(buf,"%s\n%s\nContent-Length: %lu\n\n%s",first_line,header_content_type,strlen(body),body);
write(new_socket,buf,strlen(buf)); //将构造好的响应报文写到客户端的网卡文件中
}
close(new_socket);
exit(0);
}
else
{ //一代子进程,新fork()后的子进程
//一代子进程直接退出,使二代子进程成为孤儿进程
//被一号进程收养,避免成为僵尸进程,造成内存泄漏
//并且直接退出,此处可以不用手动关闭文件
exit(0);
}
}
else
{//父进程,pid>0,返回的是子进程的进程号
//在父进程的执行逻辑中,不用new_socket 必须要将其关闭
//因为父进程是一直在执行中,为了防止文件描述符泄漏
close(new_socket);
waitpid(pid,NULL,0);//等待子进程的终止
}
}
close(fd);
}