应用层
内容概括:
- 理解应用层的作用
- 认识HTTP协议
应用层
协议:通信双方之间的一种自我约定
例如, 我们需要实现⼀个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进⾏计算, 最后再把结果返回给客户端。
// proto.h
1 #pragma once
2
3 /////////////////////////////////////
4 // 应用层协议
5 /////////////////////////////////////
6
7 // C语言中结构体定义:为了后续命名中不加 struct, 所以在定义结构体时前面加typedef
8 // 但 C++ 中已不存在此规定。
9 typedef struct Request
10 {
11 int a;
12 int b;
13 }Request;
14
15 typedef struct Response
16 {
17 int sum;
//plus_server.c 服务器端代码
2 // 应用层协议的定制,包含两个方面:
3 // 1. 制定好请求中包含哪些信息,响应中包含哪些信息。
4 // 2. 制定好如何进行序列化和反序列化。
5 /////////////////////////////////////////////
6 // 方案一:
7 // 约定:
8 // 客户端发送的请求包含两个数字,这两个数字使用','分割。
9 // 服务器给客户端响应的数据,只包含一个数字(也就是两个加数的和)
10 ///////////////////////////////////////////////////////////////
11 // 方案二:
12 // 定义两个结构体:
13 // struct Request
14 // {
15 // int a;
16 // int b;
17 // };
18 //
19 // struct Response
20 // {
21 // int sum;
22 // };
23 //
24 ///////////////////////////////////////////////////
25 // 方案二VS方案一:(在该场景下)
26 // 1.节省空间
27 // 2.解析方便
28 /////////////////////////////////////////////
29
30
31 #include <stdio.h>
32 #include <string.h>
33 #include <stdlib.h>
34 #include <unistd.h>
35 #include <sys/socket.h>
36 #include <netinet/in.h>
37 #include <arpa/inet.h>
38
39 #include "proto.h"
40
41 typedef struct sockaddr sockaddr;
42 typedef struct sockaddr_in sockaddr_in;
43
44 // 处理请求
45 void ProcessRequest(const Request* req, Response* resp)
46 {
47 resp->sum = req->a + req->b;
48 }
49
50 int main(int argc, char* argv[])
51 {
52 if(argc != 3)
53 {
54 printf("Usage ./plus_server [ip] [port]\n");
55 return 1;
56 }
57
58 // 1.创建文件描述符
59 int sock = socket(AF_INET, SOCK_DGRAM, 0);
60 if(sock < 0)
61 {
62 perror("socket");
63 return 1;
64 }
65
66 // 2.绑定端口号
67 sockaddr_in addr;
68 addr.sin_family = AF_INET;
69 addr.sin_addr.s_addr = inet_addr(argv[1]);
70 addr.sin_port = htons(atoi(argv[2]));
71 int ret = bind(sock, (sockaddr*)&addr, sizeof(addr));
72 if(ret < 0)
73 {
74 perror("bind");
75 return 1;
76 }
77
78 // 3.循环的读取数据
79 while(1)
80 {
81 sockaddr_in peer;
82 socklen_t len = sizeof(peer);
83 // 1.读取客户端的请求,把读到的字符串转换成 Request 结构体(反序列化)
84 Request req;
85 // 读到的客户端发送的字符串里面包含着两个加数
86 recvfrom(sock, &req, sizeof(req), 0, (sockaddr*)&peer, &len);
87 printf("Request: a=%d, b=%d\n", req.a, req.b);
88
89 // 2.通过 Request 结构体,计算生成 Response 结构体。
90 // 服务器程序的核心,业务逻辑所在,可能会非常复杂。
91 // 不同的业务场景下,此处的实现也不同。
92 // 处理请求
93 Response resp;
94 ProcessRequest(&req, &resp);
95
96 // 3. 把 Response 结构体转成一个字符串(序列化),再发送给客户端
97 sendto(sock, &resp, sizeof(resp), 0, (sockaddr*)&peer, sizeof(peer));
98 }
99
100 return 0;
101 }
// plus_client.c 客户端代码
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <sys/socket.h>
6 #include <netinet/in.h>
7 #include <arpa/inet.h>
8
9 #include "proto.h"
10
11 typedef struct sockaddr sockaddr;
12 typedef struct sockaddr_in sockaddr_in;
13
14
15 int main(int argc, char* argv[])
16 {
17 if(argc != 3)
18 {
19 printf("Usage ./plus_server [ip] [port]\n");
20 return 1;
21 }
23 // 1.创建文件描述符
24 int sock = socket(AF_INET, SOCK_DGRAM, 0);
25 if(sock < 0)
26 {
27 perror("socket");
28 return 1;
29 }
30
31 sockaddr_in server_addr;
32 server_addr.sin_family = AF_INET;
33 server_addr.sin_addr.s_addr = inet_addr(argv[1]);
34 server_addr.sin_port = htons(atoi(argv[2]));
35
36 // 3.循环的读取数据
37 while(1)
38 {
39 // 1.先从标准输入读入两个整数
40 Request req;
41 printf("input a: ");
42 fflush(stdout);
43 scanf("%d", &req.a);
44 printf("input b: ");
45 fflush(stdout);
46 scanf("%d", &req.b);
47
48 // 2.把 Request 结构体发送给服务器
49 sendto(sock, &req, sizeof(req), 0, (sockaddr*)&server_addr, sizeof(server_addr));
50
51 // 3.从服务器中读取 Response 结构体
52 Response resp;
53 recvfrom(sock, &resp, sizeof(resp), 0, NULL, NULL);
54
55 // 4.将结果显示到标准输出上
56 printf("Response: sum = %d\n", resp.sum);
57 }
58 return 0;
59 }
序列化:将结构体转换成字符串
反序列化:将字符串转换成结构体
将客户端给服务器发送的请求进行序列化的工具有:
google出品的protobuf 、xml 、json
- xml 相比于 html
// 以上面的两个加数相加为例:
// 对客户端发来的两个操作数相加的请求进行序列化:
<request>
<plusnum> 1 </plusnum>
<plusnum> 2 </plusnum>
</request>
优点:可以进行格式化的解析数据,方便人们肉眼去阅读,解析方便,可读性好。
缺点:因为加了很多的标签,导致所占用的空间太大。
2. json :来源于 javascrpt
此处注意:java 和 javascript 毫无关系。
// 对客户端发来的两个操作数相加的请求进行序列化:
{
request:[
{plusnum:1},
{plusnum:2},
]
}
json 和 xml 优缺点一样,两者只是在解析格式上不同。
3. google protobuf:
优点:占用空间小。
缺点:可读性不好,因为全是二进制代码,导致根本不可读。
HTTP协议
超文本传输协议,它是应用层协议。应用层协议是我们程序员⾃⼰定的。
1.认识URL
URL就是我们俗称的“网址”
http://user:pass@www.example.jp:80/dir/index.htm?uid=1#ch1
http://:协议名
user:pass :登录信息
www.example.jp :服务器地址
80:服务器端口号
/dir/index.htm:带层次的文件路径,表示你要请求服务器上的哪个文件
uid=1:查询字符串
例如:搜索“鲜花”,将被解析成:
// 采用键值对的方式进行解析
query=%CF%CA%BB%A8
&_asf=www.sogou.com
&_ast=&w=01019900
&p=40040100&sut=4547
&sst0=1530349658694
&lkt=0%2C0%2C0
// & 是各个键值对之间的分隔符
// = 是键和值之间的分隔符
. #:提醒我们要跳转到浏览器的哪个地方
ch1:片段标识符
拓展:
http默认端口号:80
https默认端口号:443
2. urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
⽐如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进⾏转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不⾜4位直接处理),每2位做⼀位,前⾯加上%,编码成%XY格式。
例如:
“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程。
HTTP协议格式
HTTP请求
// 例如:
GET http://map.baidu.com/newmap=1&s=con%26wd%3D%E6%AF%94%E7%89%B9%E7%A7%91%E6%8A%80%26c%3D233&from=alamap&tpl=mapdots HTTP/1.1
Host: map.baidu.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: https://www.baidu.com/link?url=nW3KaSbHoXM4TNir9xSAoE0Gs8tRzOg375yWJYQfrlsxI_0lf_w0w4FwgdHqpBxvpohpBiWeDWGsL5oY6kAJh8dWLS4o5QmCsWU4vQJnYTbvCTcG_uOTmcYXVKcCBvjvWSkARk0YPt5F1wm2VjtlzSTuXsncnpDoyb74CiVl4KW&wd=&eqid=99acac6a0004ed36000000065b377485
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: BAIDUID=65D331D0049FB2C50A32D0BC56B2FE26:FG=1; BIDUPSID=65D331D0049FB2C50A32D0BC56B2FE26; PSTM=1528800418; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; H_PS_PSSID=1448_21087; PSINO=7; BDRCVFR[7MNccJyLkSD]=aeXf-1x8UdYcs; BDRCVFR[DnMS6Y3XFW_]=aeXf-1x8UdYcs
HTTP请求格式:
- 首行:方法(method:GET/POST) url 版本号
- header:若干行的键值对,每一行是一个键值对。键值对的键和值之间使用“: ”来分割。header部分一共有多少行,这个不是能直接确定的。
- 空行:只包含一个\n,作为header的结束标记。
- body:数据格式取决于header中的conten-Type字段,比较常见的用法是body格式和query-string的格式相同。数据的长度取决于header中的conten-length字段。
HTTP的方法
方法 | 说明 |
---|---|
GET | 获取资源 |
POST | 传输实体主体 |
PUT | 传输文件 |
HEAD | 获得报文首部 |
DELETE | 删除文件 |
OPTIONS | 询问支持的方法 |
TRACE | 追踪路径 |
CONNECT | 要求用隧道协议链接代理 |
LINK | 建立和资源之间的联系 |
UNLINK | 断开连接关系 |
最常用的就是GET方法和POST方法。
对header中的一些重要字段进行解析:
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的⻓度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端⼝上
-User-Agent:包含两个方面:(a)你的操作系统是什么 (b) 你的浏览器是什么 用来判断当前访问我的客户端是什么样的(电脑还是手机) - referer:告知当前页面的来源,是通过哪一个页面跳转而来的。
- location: 搭配3xx状态码使⽤, 告诉客户端接下来要去哪⾥访问;
- cookie:服务器将当前的客户信息写回给浏览器,浏览器临时就存储了这些信息(以字符串的格式),一个cookie一般大小为4k(4094)。浏览器会根据域名去存储这些信息。
HTTP响应
// 例如:
HTTP/1.1 200 OK
Cache-Control: private,max-age=0
Content-Type: text/html;charset=utf-8
Date: Sat, 30 Jun 2018 12:16:23 GMT
Expires: -1
Http_x_bd_logid: 4206575744
Http_x_bd_logid64: 5258179523571509041
Http_x_bd_product: map
Http_x_bd_subsys: webmap
Server: apache
Vary: Accept-Encoding
Content-Length: 160652
<!DOCTYPE HTML>
<html>
<head>
<!--STATUS OK-->
<meta charset="utf-8" />
<meta name="description" content="浏览地图、搜索地点、查询公交驾车线路、查看实时路况,您的出行指南、生活助手。提供地铁线路图浏览,乘车方案查询,以及准确的票价和时间信息。" />
<meta name="keywords" content="景区热力图,景区人流图,景区人多不多,人群分布热力图,景区拥挤指数,春节拥挤指数,热力地图,地铁线路,地铁专题,地铁方案,地铁票价,地铁时间,地铁换乘"/>
<meta http-equiv="imagetoolbar" content="no" />
<meta property="wb:webmaster" content="10fd28588b2f9686" />
<meta name="sogou_site_verification" content="Mtd2WIt6Ne"/>
<meta name="msvalidate.01" content="2D36557CC0E331F85475A3B85DAAA4FC" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
<meta name="renderer" content="webkit" />
<title>百度地图</title>
<script>
var corePerfMonitor={pageStartTime:+new Date,firstRender:true,components:{"CityIndex":1,"PoiSearch":1,"BusTrans":1,"NavWalk":1,"NavTrans":1},componentName:"CityIndex",data:{},timer:null,onTilesLoaded:function(){var mapType=this.getMapType();var tileLoadedTime=(+new Date)-corePerfMonitor.pageStartTime;var mapTypeStr="normal";if(mapType==="B_SATELLITE_MAP"){mapTypeStr="satellite"}else{if(mapType==="B_EARTH_MAP"){mapTypeStr="earth"}}corePerfMonitor.data["z_mapload_first_"+mapTypeStr+"_"+corePerfMonitor.componentName.toLowerCase()]=tileLoadedTime;corePerfMonitor.timer&&clearTimeout(corePerfMonitor.timer);corePerfMonitor.timer=setTimeout(function(){corePerfMonitor.firstRender=false;alog("cus.fire","time",corePerfMonitor.data);map.removeEventListener("tilesloaded",corePerfMonitor.onTilesLoaded);delete corePerfMonitor.timer;delete corePerfMonitor.data},5000)}};
</script>
<script>
HTTP响应格式:
- 首行: [版本号] + [状态码] + [状态码解释]
- header:若干行的键值对,每一行的键值对的键和值之间使用“: ”分割。
- 空行:header的结束标记。
- body:body的数据格式取决于content-Type往往是一个html的格式;数据长度取决于conten-length;set-cookie字段决定了客户端应该保存哪些cookie。
HTTP的状态码
xian | 类别 | 原因短语 |
---|---|---|
1XX | informational(信息性状态码) | 接受的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作已完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常⻅的状态码, ⽐如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)。
HTTP两个重要的面试题:
- GET方法 VS POST方法
GET方法:一般无body
POST方法:有body - HTTP的状态码有哪些?
查看上面的状态码表,并列举出一些常见的状态码。
实现一个最简单的HTTP服务器
// 只在网页上输出"helloworld"
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <pthread.h>
6 #include <sys/socket.h>
7 #include <netinet/in.h>
8 #include <arpa/inet.h>
9
10 typedef struct sockaddr sockaddr;
11 typedef struct sockaddr_in sockaddr_in;
12
13 int ServerInit(const char* ip, short port)
14 {
15 // 1.创建文件描述符
16 int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
17 if(listen_sock < 0)
18 {
19 perror("socket");
20 return -1;
21 }
22 // 2.绑定端口号
23 sockaddr_in addr;
24 addr.sin_family = AF_INET;
25 addr.sin_addr.s_addr = inet_addr(ip);
26 addr.sin_port = htons(port);
27 int ret = bind(listen_sock, (sockaddr*)&addr, sizeof(addr));
28 if(ret < 0)
29 {
30 perror("bind");
31 return -1;
32 }
33 // 3.监听
34 ret = listen(listen_sock, 5);
35 if(ret < 0)
36 {
37 perror("listen");
38 return -1;
39 }
40 return listen_sock;
41 }
42
43 void* ThreadEntry(void* arg)
44 {
45 int64_t new_sock = (int64_t)arg;
46 // 1.读数据并解析,由于我们此处无脑返回 helloworld, 所以只是把数据读出来,并不打算解析。
47 char buf[1024*10]={0};
48 read(new_sock, buf, sizeof(buf)-1);
49 // 2.根据请求计算响应,由于我们此处无脑返回 helloworld, 所以此处跳过了计算步骤,直接写 helloworld 就行了。
50 // 3.把响应写回给客户端
51 const char* first_line = "HTTP/1.1 200 OK\n";
52 const char* blank_line = "\n";
53 const char* body = "<h1>helloworld</h1>";
54 char header[1024]={0};
55 sprintf(header, "Content-Length: %lu\n",strlen(body));//strlen()返回的类型是size_t,为无符号长整型,所以用lu
56 write(new_sock, first_line, strlen(first_line));
57 write(new_sock, header, strlen(header));
58 write(new_sock, blank_line, strlen(blank_line));
59 write(new_sock, body, strlen(body));
60 close(new_sock);
61 return NULL;
62 }
64 int main(int argc, char* argv[])
65 {
66 if(argc != 3)
67 {
68 printf("Usage ./http_server [ip] [port]\n");
69 return 1;
70 }
71
72 //////////////////////////////////////////////////////////
73 // HTTP 服务器往往是基于TCP实现的。
74 // 1. 创建一个TCP服务器
75 // 2. 按照HTTP协议的格式解析请求,并且按照HTTP协议的格式构造响应。
76 //////////////////////////////////////////////////////////
77
78 int listen_sock = ServerInit(argv[1], atoi(argv[2]));
79 if(listen_sock < 0)
80 {
81 printf("ServerInit failed!\n");
82 return 1;
83 }
84 printf("ServerInit OK\n");
85
86 while(1)
87 {
88 sockaddr_in peer;
89 socklen_t len = sizeof(peer);
90 int64_t new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
91 if(new_sock < 0)
92 {
93 perror("accept");
94 return 1;
95 }
96
97 pthread_t tid;
98 pthread_create(&tid, NULL, ThreadEntry, (void*)new_sock);
99 pthread_detach(tid);
100 }
101 return 0;
102 }