应用层
是七层OSI模型的第七层。应用层直接和应用程序接口并提供常见的网络应用服务。应用层也向表示层发出请求。应用层是开放系统的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。
我们程序员写的一个个解决我们实际问题,满足我们我们日常需求的网络程序,都是在应用层。
协议
协议是一种约定。
socket api的接口,在读写数据时,都是按“字符串”的方式来发送接受的,如果我们要传输一些“结构化的数据”该怎么办?
网络版计算器
例如我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端
约定方案一:
- 客户端发送一个形如“1+1”的字符串
- 这个字符串中有两个操作数,都是整形
- 两个数字之间会有一个字符是运算符,运算符只能是 +;
- 数字和运算符之间没有空格
约定方案二:
- 定义结构体来表示我们需要交互的信息
- 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体
- 这个过程叫做“序列化”和“反序列化”
// proto.h 定义通信的结构体
typedef struct Request
{
int a;
int b;
} Request;
typedef struct Response
{
int sum;
} Response;
// client.c 客户端核心代码
Request request;
Response response;
scanf("%d,%d", &request.a, &request.b);
write(fd, request, sizeof(Request));
read(fd, response, sizeof(Response));
// server.c 服务端核心代码
Request request;
read(client_fd, &request, sizeof(request));
Response response;
response.sum = request.a + request.b;
write(client_fd, &response, sizeof(response))
无论我们采用方案一,还是方案二,还是其他方案,只要保证,一端发送时构造的数据,在另一端能够正确的进行解析,这就是ok的。这种约定,就是应用层协议
HTTP协议
HTTP(超文本传输协议)协议就是其中之一
认识URL
平时我们俗称的“网址”其实就是URL
urlencode和urldecode
像/?:这样的字符,已经被url当作特殊意义理解了。因此这些字符不能随意出现。
比如,某个参数中需要带有这些特殊字符,就必须先对字符进行转义
转义的规则如下:
将需要转码的字符转为16进制,然乎从左到右,取4位(不足4位直接处理),每两位做一位,前面加上%,编码成%XY格式
例如:
"+"被转义成了“%2B”
urldecode就是urlencode的逆过程
HTTP协议格式
HTTP请求报文
- HTTP请求由四部分构成,分别是:请求行,请求报头,空行,请求正文
- HTTP请求的属性(Header),是以冒号分割的键值对,魅族属性之间使用\n分隔;遇到空行标识Header部分结束
- 首行:[方法] + [url] + [版本]
- 空行后面的内容都是Body。Body允许位空字符串,如果Body存在,则在Header中会有一个Content-Length大户型来表示Body的长度。
HTTP响应报文
- 在接收和解释请求消息后,服务器返回一个HTTP响应消息。
- HTTP响应报文的组成:状态行,响应报头,空行,响应正文
- 首行:[版本号] + [状态码] + [状态码解释]
- Header:响应的属性,冒号分割的键值对,每组属性之间使用\n分隔,遇到空行表示Header部分结束
- 空行后面的内容都是Body。Body允许位空字符串,如果Body存在,则在Header中会有一个Content-Length属性来表示Body的长度,如果服务器返回了一个html页面,那么hml页面内容是在body中
HTTP请求的方法
- GET方法:在浏览器的地址栏中输入网址的方式访问网页时,浏览器采用GET方法向服务器获取资源。GET方法也可以向服务器传递参数,在wd后面的为传递的参数,常为搜索关键字等。
- POST方法:要求被请求的服务器接受附在请求后面的数据,常用于提交表单。
- HEAD方法:HEAD方法与GET方法几乎是一样的,对于HEAD请求的回应部分来说,它的HTTP头部中包含的信息与通过GET请求所得到的信息是相同的。利用这个方法,不必传输整个资源内容,就可以得到Request-URI所标识的资源的信息。该方法常用于测试超链接的有效性,是否可以访问,以及最近是否更新
HTTP的状态码
状态码由三位数字组成,第一个数字定义了响应的类别,他有五种可能取值:
- 1xx:指示信息-表示请求已接受,继续处理
- 2xx:成功-表示请求已被成功接收并处理
- 3xx:重定向-要完成请求必须进行更近一步的操作
- 4xx:客户端错误-请求有语法错误或请求无法实现
- 5xx:服务器端错误-服务器未能实现合法的请求
常见的状态码
- 200 OK:客户端请求成功
- 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
- 401 Unauthorized:请求未经授权,这个状态码必须和WWW-Authenticate报头域一起使用
- 403 Forbidden:服务器收到请求,但是拒绝提供服务
- 404 Not Found:请求资源不存在,可能是输入了错误的URL
- 500 Internal Server Error:服务器发生不可预期的错误
- 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
HTTP消息包头
包括普通报头,请求报头,响应报头,实体报头。
每一个报头域都是由 名字 + :+空格+值。消息报头的名字是大小写无关的
普通报头:在普通报头中,有少数报头域用于请求和响应消息,但并不用于被传输的实体,只用于传输的消息
请求报头:请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。
常见的请求报头:
- Accpet:请求报头域用于指定客户端接受那些类型的列表。例如:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。
- Accept-Charset:请求报头域用于指定客户端接受的字符集。例如:Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。
- Accept-Encoding:请求报头域类似于Accept,但是它是用于指定可接受的内容编码。eg:Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受
- Authorization:请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。
- Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP的url中提取出来的,发送请求时,该报头是必须的。允许多个域名同处一个IP地址,即虚拟主机
- User-Agent:请求报头域允许将他的操作系统,浏览器和其他属性告诉服务器,不过,这个报头域不是必须的。
响应报头:响应报头允许服务器传递不能放在状态行中附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息
- Locarion响应报头域:用于重定向接收者的一个新的为止。常用于在更换域名的时候
- Server响应报头域:包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的。
实体报头:请求和响应消息都可以传送有一个实体,一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文和请求所标识的资源的源信息。
- Content-Language实体报头域:描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有语言阅读者
- Content-Length实体报头域:用于指明实体正文的长度,以字节方式存储的十进制数字来表示
- Content-Type实体报头域:用于指示资源的最后修改日期和时间
- Expries实体报头域:给出相应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中页面。我们可以使用Exipres实体报头域指定页面过期的时间
- referer:表示当前页面是从哪个页面跳转过来的
- Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能
- Connection:keep—alive
在HTTP1.0官方协议中并没有对keepalive的明确支持,所有的HTTP请求是如下流程:
- client 发起一个请求;
- server收到请求后,回复一个响应;
- 连接关闭。
如果要在HTTP1.0中支持keep alive,你必须明确的在header中加入Connection:keep-alive。
- client 发起一个包含Connection:keep-alive的请求
- server收到请求后,如果server支持keepalive,回复一个包含Connection:keep-alive的响应,不关闭连接,否则回复一个包含Connection:close的响应,关闭连接。
- 如果client收到包含Connection:keep-alive的响应,向同一个连接发送下一个请求,直到一方主动关闭连接。
因为keepalive在很多情况下能够重用连接,减少资源消耗,缩短响应时间。所以在HTTP1.1中缺省就是支持keepalive的,如果响应方不支持keepalive,需要明确的标识Connection:close,Connection:keep-alive就没什么意义了。
GET和POST方法的区别
- GET把请求的数据放在url上,即HTTP协议头上,其格式为:以?分割URL和传输数据,参数之间以&相连。数据如果是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,及”%“加上”字符串的16进制ASCII码“ POST把数据放在HTTP的包体内
- GET提交的数据最大是2k(原则上url长度无限制,限制实际取决于浏览器,大多数浏览器通常回限制url的长度在2K个字节,即使大都数服务器最多处理64K大小的url,但是没什么用) POST理论上没有限制。实际上IIS4中最大量为80KB,IIS5中为100KB
- GET产生一个TCP数据包,浏览器会把HTTP Header和data一起发送过去,服务器响应200(返回数据) POST产生两个TCP数据包,浏览器先发送header,服务器响应 100 continue,浏览器在发送data,服务器响应200 ok(返回数据)
- GET在浏览器回退时是无害的,POST回再次提交请求
- GET产生的URL地址可以被Bookmark,而POST不可以
- GET请求会被浏览器注重cache,而POST不会,除非手动设置
- GET请求只能进行url编码,而POST支持多种编码方式
- GET请求参数会被完整保留在浏览器的历史纪录里面, 而POST中的参数不会被保留
- GET只接受ASCII字符的参数的数据类型,而POST没有限制
那么问题来了,POST那么好为什么还用GET?因为GET效率高!
最简单的HTTP服务器
实现一个最简单的HTTP服务器,只在网页上输出”hello world“;只要按照HTTP协议的要求构造数据,就很容易做到
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
void Usage()
{
printf("usage: ./server [ip] [port]\n");
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage();
return 1;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return 1;
}
ret = listen(fd, 10);
if (ret < 0)
{
perror("listen");
return 1;
}
for (;;)
{
struct sockaddr_in client_addr;
socklen_t len;
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);
if (client_fd < 0)
{
perror("accept");
continue;
}
char input_buf[1024 * 10] = { 0 }; // 用一个足够大的缓冲区直接把数据读完.
ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);
if (read_size < 0)
{
return 1;
}
printf("[Request] %s", input_buf);
char buf[1024] = {0};
const char* hello = "<h1>hello world</h1>";
sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello), hello);
write(client_fd, buf, strlen(buf));
}
return 0;
}
编译, 启动服务. 在浏览器中输入 http://[ip]:[port], 就能看到显示的结果 "Hello World"
备注
- 此处我们使用的是8080端口号启动的服务器,HTTP服务器一般使用80端口,但这只是一个通用的习惯,并不能说HTTP不能使用其他的端口号