【Linux】38.网络基础(2.1)
文章目录
- 1. 应用层
- 1.1 再谈 "协议"
- 1.1.1 网络版计算器
- 1.2 HTTP协议
- 1.2.1 认识URL
- 1.2.2 urlencode和urldecode
- 1.2.3 HTTP协议格式
- 1.2.4 HTTP的方法
- 1.2.5 HTTP的状态码
- 1.2.6 HTTP常见Header
- 1.2.7 最简单的HTTP服务器
1. 应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
1.1 再谈 “协议”
协议是一种 “约定”。socket api的接口,在读写数据时,都是按 “字符串” 的方式来发送接收的。如果我们要传输一些"结构化的数据" 怎么办呢?
1.1.1 网络版计算器
例如, 我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
约定方案一:
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
…
约定方案二:
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
这个过程叫做 “序列化” 和"反序列化"
序列化和反序列化是将数据结构或对象转换的过程:
序列化(Serialization):
- 将内存中的数据结构/对象转换为可存储或传输的格式
- 常见格式: JSON、XML、二进制等
- 目的: 便于存储和网络传输
反序列化(Deserialization):
- 将存储/传输格式的数据恢复为内存中的数据结构/对象
- 是序列化的逆过程
- 使数据可被程序再次使用
使用场景:
- 网络传输数据
- 数据持久化存储
- 缓存
- 跨语言/平台数据交换
// proto.h - 协议头文件
// 定义客户端和服务器之间通信用的数据结构
// 请求结构体 - 客户端发送给服务器的数据格式
typedef struct Request {
int a; // 第一个操作数
int b; // 第二个操作数
} Request;
// 响应结构体 - 服务器返回给客户端的数据格式
typedef struct Response {
int sum; // 计算结果
} Response;
// client.c - 客户端程序
Request request; // 创建请求结构体实例
Response response; // 创建响应结构体实例
// 从用户输入获取两个整数,格式为"a,b"
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的。这种约定, 就是应用层协议。
1.2 HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的。
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。
1.2.1 认识URL
平时我们俗称的 “网址” 其实就是说的 URL
1.2.2 urlencode和urldecode
urlencode和urldecode是用于URL编码和解码的方法:
urlencode:
- 将普通文本转换为URL编码格式
- 把特殊字符(如空格、中文等)转换为%加上两位16进制数
- 例如:
- 空格转换为%20
- 中文"你好"转换为%E4%BD%A0%E5%A5%BD
urldecode:
- 将URL编码格式转换回普通文本
- 把%加两位16进制数转换为对应的字符
- 是urlencode的反向操作
使用场景:
- URL传参时对参数值进行编码
- 处理包含特殊字符的URL
- 表单提交时对数据进行编码
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了。因此这些字符不能随意出现。
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例如:
这里的"+" 被转义成了 “%2B”
1.2.3 HTTP协议格式
协议格式
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度
HTTP请求
HTTP响应
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中
1.2.4 HTTP的方法
其中最常用的就是GET方法和POST方法
1.2.5 HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
1.2.6 HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: 正文Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
- User-Agent: 声明用户的操作系统和浏览器版本信息
- referer: 当前页面是从哪个页面跳转过来的
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
- Cookie: 用于在客户端存储少量信息,通常用于实现会话(session)的功能
1.2.7 最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到
// 包含必要的系统头文件
#include <sys/socket.h> // Socket接口
#include <netinet/in.h> // 网络地址结构
#include <arpa/inet.h> // IP地址转换函数
#include <unistd.h> // UNIX标准函数
#include <stdio.h> // 标准输入输出
#include <string.h> // 字符串处理
#include <stdlib.h> // 标准库函数
// 打印使用说明
void Usage() {
printf("usage: ./server [ip] [port]\n");
}
int main(int argc, char* argv[]) {
// 检查命令行参数
if (argc != 3) { // 需要IP地址和端口两个参数
Usage();
return 1;
}
// 创建TCP Socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket"); // 打印错误信息
return 1;
}
// 配置服务器地址结构
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 使用IPv4
addr.sin_addr.s_addr = inet_addr(argv[1]); // 设置IP地址
addr.sin_port = htons(atoi(argv[2])); // 设置端口号
// 绑定Socket到指定地址和端口
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
perror("bind");
return 1;
}
// 开始监听连接请求
ret = listen(fd, 10); // 最多排队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}; // 10KB的缓冲区
ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);
if (read_size < 0) {
return 1; // 读取失败
}
// 打印收到的HTTP请求
printf("[Request] %s", input_buf);
// 构造HTTP响应
char buf[1024] = {0};
const char* hello = "<h1>hello world</h1>"; // 响应体内容
// 构造HTTP响应头和响应体
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”
http://[云服务器公网IP]:[端口号]
- 进入轻量应用服务器,点击这里的管理规则
- 点击添加规则,自定义规则,协议TCP,端口号9090,添加备注,点击确定
-
编译,运行程序mian.cc,生成main文件,执行main文件:
./main 0.0.0.0 9090
-
浏览器输入网址,http://[ip]:[port], 就能看到显示的结果 “Hello World”
备注:
此处我们使用 9090 端口号启动了HTTP服务器。虽然HTTP服务器一般使用80端口, 但这只是一个通用的习惯,并不是说HTTP服务器就不能使用其他的端口号。
使用Edge测试我们的服务器时, 可以看到服务器打出的请求中还有一个 GET / HTTP/1.1 这样的请求