文章目录
应用层
我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。
1. 再谈 “协议”
协议是一种 “约定”,socket api的接口,在读写数据时,都是按 “字符串” 的方式来发送接收的。
1.1 自定制协议
- 工作在应用层,自己定制的协议;
- 双方严格遵守协议内用;
- 增加定长的报头;
- 对一条数据增加分隔符,通常是
/r/n
; - 序列化与反序列化。
2. HTTP协议
只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析,这种约定,就是应用层协议。HTTP(超文本传输协议)就是其中之一。
2.1 认识URL
平时我们俗称的 “网址” 其实就是说的 URL。
URL组成:
- 协议方案名:例如http或https;
- 登录信息(认证);
- 服务器地址:域名或ip;
- 服务器端口号:http中不显示就默认80端口;
- 带层次的文件路径:服务器提供的服务;
- 查询字符串:?为分界点,以键值对形式表现。键1=值1&键2=值2…
- 片段标识符:前端用来做页面定位使用的。
2.2 urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。"+" 被转义成了 “%2B”。
urldecode就是urlencode的逆过程。
2.3 HTTP协议格式
2.3.1 HTTP请求
GET /xxgk/about1.htm HTTP/1.1
Host: www.xiyou.edu.cn
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://www.xiyou.edu.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=EE5774BE918212AACACF850083AFAF54
If-None-Match: "6edd-5b6ef7e4b1bdc-gzip"
If-Modified-Since: Mon, 21 Dec 2020 01:36:55 GMT
- 首行: [方法] + [url] + [版本];
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束;
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度。
2.3.2 HTTP响应
HTTP/1.1 200 OK
Server: VWebServer/6.0.0
Date: Tue, 22 Dec 2020 02:25:01 GMT
Expires: Tue, 22 Dec 2020 02:33:36 GMT
Cache-Control: max-age=600
Content-Type: text/html
Content-Length: 9857
X-Frame-Options: SAMEORIGIN
Last-Modified: Mon, 21 Dec 2020 01:36:55 GMT
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Encoding: gzip
ETag: "6edd-5b6ef7e4b1bdc-gzip"
Content-Language: zh-CN
- 首行: [版本号] + [状态码] + [状态码解释];
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束;
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中。
2.4 HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
2.5 HTTP的方法
方法 | 说明 | 支持的HTTP协议版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获取报文首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINK | 断开连接关系 | 1.0 |
最常用的就是GET方法和POST方法。
GET方法和POST方法的区别:
- 数据长度限制:GET方法发送数据是向URL添加数据,但URL的长度是受限制的(URL最大长度是2048个字符)。POST方法没有限制。
- 安全性:GET方法发送的数据是URL的一部分。POST方法传输数据的参数不会被保存在浏览器历史或web服务器日志中,所以POST更安全。
- 可见性:GET方法数据都在URL中,URL对所有人都可见。POST方法数据不会显示在URL中。
2.6 HTTP的状态码
最常见的状态码:
- 200:服务器正确处理完请求,并返回响应。
- 301/302/307:重定向(服务端响应3XX重定向状态码,location报头,然后浏览器会自行跳转到location)。
- 400:客户端错误(客户端请求数据错误–数据格式/数据类型错误)。
- 404:找不到资源(服务器没有提供该URL地址请求的服务)。
- 405:Method Not Allow 不支持的客户端请求方法(服务器的这个服务不支持请求报头的请求方法)。
- 500:服务器错误(一般是服务器后端错误–抛出异常)。
- 502:Bad Gateway 网关错误。
3. 实现简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 “Hello World”。
只要我们按照HTTP协议的要求构造数据, 就很容易能做到。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sstream>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 5);
if(ret < 0)
{
perror("listen");
return -1;
}
int newsockfd = -1;
while(1)
{
newsockfd = accept(sockfd, NULL, NULL);
if(newsockfd < 0)
{
perror("accept");
return -1;
}
// <html>Hello World</html> text/html
// 首行
// 响应报头
// 空行
// 响应体
std::stringstream ss;
ss << "http/1.1 200 OK\r\n";
ss << "Content-Type: text/html\r\n";
ss << "Content-Length: "<< body.size() << "\r\n";
ss << "\r\n"; //空行
std::string body = "<html>Hello World</html>";
ret = send(newsockfd, ss.str().c_str(), ss.str().size(), 0); // 发头部
if(ret < 0)
{
perror("send");
return -1;
}
ret = send(newsockfd, body.data(), body.size(), 0); // 发正文
if(ret < 0)
{
perror("send");
return -1;
}
}
close(newsockfd);
close(sockfd);
return 0;
}
编译, 启动服务. 在浏览器中输入 http://[ip]:[port], 就能看到显示的结果 “Hello World”。
备注:
此处我们使用 8080 端口号启动了HTTP服务器。
虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯。
并不是说HTTP服务器就不能使用其他的端口号。
4. 测试不同的状态码
把返回的状态码改成302, 404, 403, 504等, 看浏览器上分别会出现什么样的效果。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <sstream>
int main(int argc, char* argv[])
{
if(argc != 3)
{
//./http_test [ip] [port] 或者 ./http_test 0.0.0.0 9090
printf("using \"./http_test [ip] [port]\"\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 5);
if(ret < 0)
{
perror("listen");
return -1;
}
int newfd = accept(sockfd, NULL, NULL);
if(newfd < 0)
{
perror("accept");
return -1;
}
while(1)
{
char buf[1024] = { 0 };
ret = recv(newfd, buf, sizeof(buf) - 1, 0);
if(ret < 0)
{
perror("recv");
continue;
}
else if(ret == 0)
{
printf("peer shutdown\n");
close(newfd);
close(sockfd);
return -1;
}
printf("chrome say: %s\n", buf);
memset(buf, '\0', sizeof(buf));
//模拟http协议:
//buf==>响应报头,body==>响应正文
std::string body = "<html>Hello World</html>";
std::stringstream header;
header << "HTTP/1.1 200 OK\r\n";
header << "Content-Type: text/html\r\n";
header << "Content-Length: " << body.size() << "\r\n";
header << "\r\n";
//header << "HTTP/1.1 302 Redirect\r\n";
//header << "Content-Type: text/html\r\n";
//header << "Content-Length: " << body.size() << "\r\n";
//header << "Location: https://www.baidu.com\r\n"; // 重定向
//header << "\r\n";
//header << "HTTP/1.1 404 page not found\r\n";
//header << "Content-Type: text/html\r\n";
//header << "Content-Length: " << body.size() << "\r\n";
//header << "\r\n";
send(newfd, header.str().c_str(), header.str().size(), 0);
send(newfd, body.c_str(), body.size(), 0);
}
close(newfd);
close(sockfd);
return 0;
}