Linux网络编程总目录(点击下面链接即可到达对应章节)
Linux网络编程_01_网络基础
Linux网络编程_02_socket套接字
Linux网络编程_03_应用层HTTP协议
Linux网络编程_04_传输层UDP和TCP协议
Linux网络编程_05_网络层IP协议
Linux网络编程_06_数据链路层MAC帧协议
Linux网络编程_07_多路转接
文章目录
一. 序列化反序列化
1.1 简述
序列化就是结构化的数据转换成便于网络传输的格式(一般是转换成字符串),反序列化就是将序列化的格式转换成格式化的格式,如下图。
1.2 代码演示
// 注意事项:
// 1. 如果你的服务器没有安装json库的话需要执行以下命令安装
// sudo yum -y install jsoncpp-devel
// 2. 编译时需要在g++或者gcc后面加上-ljsoncpp链接动态库
// 比如:g++ -o $@ $^ -std=c++11 -ljsoncpp
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
using namespace std;
typedef struct Student
{
string name;
int age;
}Student;
int main()
{
// 1. 序列化
Student s1;
s1.name = "李华";
s1.age = 18;
Json::Value root; // json是一种kv结构
root["name"] = s1.name;
root["age"] = s1.age;
// 两种写法:FastWriter StyledWriter
//Json::StyledWriter writer;
Json::FastWriter writer;
string json_string = writer.write(root); // 将root写进字符串
//cout << json_string << endl;
// 2. 反序列化
Json::Reader reader;
Json::Value rroot;
reader.parse(json_string, rroot); // 将字符串的内容解析给rroot
Student s2;
s2.name = rroot["name"].asString();
s2.age = rroot["age"].asInt();
cout << "name:" << s2.name << " age:" << s2.age << endl;
return 0;
}
二. HTTP协议
2.1 认识RUL
如下图所示,一般我们把URL分为三大部分,分别是请求网站使用的协议,我们访问的IP(域名),还有我们需要访问的资源所在的路径。域名可以理解为是IP的一种符号化表示形式,是跟IP绑定起来的,访问这个域名就是访问这个域名对于绑定的IP。公网IP+端口号确定网络上的唯一一个进程,公网IP+路径确定网络上的唯一一份资源。
2.2 encode和decode
encode就是将一些特殊字符转义为16进制,后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。比如’+‘被转义成了’%2B’,decode就是encode的逆过程。
2.3 HTTP协议格式
2.3.1 HTTP请求
首行: [方法] + [url] + [版本]
Header: 请求的属性,冒号分割的键值对,每组属性之间使用\n分隔
空行: 遇到空行表示Header部分结束
Body: 空行后面的内容都是Body, Body允许为空字符串, 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度
2.3.2 HTTP响应
首行:[版本号] + [状态码] + [状态码解释]
Header: 响应的属性,冒号分割的键值对,每组属性之间使用\n分隔
空行: 遇到空行表示Header部分结束
Body: 跟上面一样,如果服务器返回了一个html页面, 那么html页面内容就是在body中.
2.3.3 HTTP的通信格式
由上面的HTTP请求和响应的数据来看**,HTTP请求和响应的内容可以看成是一个大的字符串,用换行符来分隔各种信息。其中空行可以将http报头和正文部分分开,解包和封装也是根据空行来判断的。**
2.3.4 http_demo代码演示
sock.hpp文件代码
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<iostream>
#include<string>
#include<cstdlib>
using namespace std;
class sock
{
public:
static int Socket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
exit(-1);
}
return sockfd;
}
static void Bind(const int& sockfd, const uint16_t& port)
{
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr= INADDR_ANY;
int ret = bind(sockfd, (struct sockaddr*)&server, sizeof(server));
if (ret < 0)
{
cerr << "bind error" << endl;
exit(-1);
}
}
static void Listen(const int& sockfd)
{
if (listen(sockfd, 5) < 0)
{
cerr << "listen error" << endl;
exit(-1);
}
}
static int Accept(const int& sockfd)
{
sockaddr_in client;
socklen_t len;
int accfd = accept(sockfd, (struct sockaddr*)&client, &len);
if (accfd < 0)
{
cerr << "accept error" << endl;
exit(-1);
}
else
{
cout << "get a client... IP --> " << inet_ntoa(client.sin_addr) << " Port --> " << client.sin_port << endl;
return accfd;
}
}
static int Connect(const int& sockfd, const char* serverIP, const uint16_t& serverPort)
{
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
server.sin_addr.s_addr = inet_addr(serverIP);
int confd = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
if (confd < 0)
{
cerr << "connect error" << endl;
exit(-1);
}
else
{
return confd;
}
}
};
http_demo.cpp文件代码
#include"sock.hpp"
void Usage(const char* proc)
{
cout << "Usage:" << proc << "Port" << endl;
}
void* Run(void* arg)
{
int fd = *(int*)arg;
delete (int*)arg;
char buffer[10240];
ssize_t recv_count = recv(fd, buffer, sizeof(buffer), 0); // 接收http请求信息
if (recv_count > 0)
{
buffer[recv_count] = '\0';
cout << buffer; // 打印请求信息
// 构建响应信息(简单模拟一下)
string body_str = "Helloworld, I am body!!!"; // 响应的正文
string send_string = "HTTP/1.0 200 OK\n";
send_string += "Content-Type: text/plain\n"; // 正文的类型,text/plain --> 正文是普通文本
send_string += "Content-Length: "; // 正文的长度,单位是字节
send_string += to_string(body_str.size());
send_string += "\n"; // 这个'\n'是Content-Length属性的换行符,不是空行
send_string += "\n"; // 这里'\n'就是我们所说的空行
send_string += body_str;
send(fd, send_string.c_str(), send_string.size(), 0); // 发送给用户
cout << send_string << endl;
}
else
{
cout << "recv....." << endl;
}
close(fd);
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
int sockfd = sock::Socket();
uint16_t port = (uint16_t)stoi(argv[1]);
sock::Bind(sockfd, port);
sock::Listen(sockfd);
while (true)
{
int fd = sock::Accept(sockfd);
if (fd > 0)
{
pthread_t tid;
int* pfd = new int(fd);
pthread_create(&tid, nullptr, Run, pfd);
}
}
return 0;
}
// 编译运行后,在浏览器地址栏(注意:是地址栏,不是搜索框)输入IP:port
// 比如192.168.1.13:8080
抓包工具显示的请求信息和响应信息
浏览器显示的结果
2.4 GET和POST请求方法
2.4.1 代码
http_demo文件代码
sock.hpp文件代码跟上面的一样
#include"sock.hpp"
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fstream>
#define MAIN_PAGE "./main/index.html"
void Usage(const char* proc)
{
cout << "Usage:" << proc << "Port" << endl;
}
void* Run(void* arg)
{
int fd = *(int*)arg;
delete (int*)arg;
char buffer[10240];
ssize_t recv_count = recv(fd, buffer, sizeof(buffer), 0);
if (recv_count > 0)
{
buffer[recv_count] = '\0';
cout << buffer << endl;
string send_string = "HTTP/1.0 200 OK\n";
struct stat stat_buf;
stat(MAIN_PAGE, &stat_buf);
string body_size = to_string(stat_buf.st_size);
send_string += "Content-Type: text/html\n"; // text/html--> 正文格式是html
send_string += "Content-Length: ";
send_string += body_size;
send_string += "\n"; // 这里的'\n'是Content-Length的换行符,不是空行
send_string += "\n"; // 这里'\n'就是我们上面所说的空行
// 处理正文部分
ifstream in(MAIN_PAGE);
if (in.is_open())
{
string body_string;
string line;
// 按行全部读完
while (getline(in, line))
{
body_string += line;
}
send_string += body_string;
in.close();
}
ssize_t send_size = send(fd, send_string.c_str(), send_string.size(), 0);
if (send_size == (ssize_t)send_string.size())
{
cout << send_string << endl;
}
}
else
{
cout << "recv....." << endl;
}
close(fd);
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
int sockfd = sock::Socket();
uint16_t port = (uint16_t)stoi(argv[1]);
sock::Bind(sockfd, port);
sock::Listen(sockfd);
while (true)
{
int fd = sock::Accept(sockfd);
if (fd > 0)
{
pthread_t tid;
int* pfd = new int(fd);
pthread_create(&tid, nullptr, Run, pfd);
}
}
return 0;
}
index.html文件代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3>登录</h3>
<!--这里的method填上get或者post,然后填上账号密码,登录抓包看请求信息-->
<!--<form action="/" method="post">-->
<form action="/" method="get">
账号: <input type="text" name="name"><br/>
密码: <input type="password" name="passwd"><br/>
<input type="submit" value="登陆">
</form>
</body>
</html>
2.4.2 抓包显示的请求信息
2.4.3 结论
- GET方法叫获取,如果提交参数是通过URL方式进行提交的。POST方法叫推送,提交参数是通过正文提交的。
- 如果有参数POST方法会比较隐蔽一点,但是不代表POST方法安全,因为抓包抓到也是可以直接通过正文看到提交的参数。GET是通过URL提交的,一般URL会有大小限制,而POST以正文方式提交就不怎么在乎空间大小。
- 如果提交参数比较敏感一点或者非常多就使用POST,其他情况用GET就好了
2.5 HTTP状态码
2.5.1 状态码表
类别 | 原因短句 | |
---|---|---|
1xx | Informational(信息性状态码) | 接收的请求正在处理 |
2xx | Success (成功状态码) | 请求正常处理完毕 |
3xx | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4xx | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5xx | Server Error (服务器错误状态码) | 服务器请求出错 |
2.5.2 常见的状态码
200:OK
302:Found(临时重定向)
403:Forbidden(服务器拒绝该次访问,一般是访问权限出现问题)
404:Not Found
504:Gateway timeout(网关超时)
2.6 常见的Header
2.6.1 属性介绍
Content-Type: Body的数据类型(text/html等)
Content-Length: Body的长度,单位为字节
Host: 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上
User-Agent: 声明用户的操作系统和浏览器版本信息
referer: 当前页面是从哪个页面跳转过来的
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问,实现重定向功能
Set-Cookie: 用于在客户端存储少量信息,通常用于实现会话(session)的功能
Connection: keep-alive(表示一个长连接),长连接可以减少频繁建立TCP链接,达到高效的目的HTTP/1.0默认是短连接,HTTP/1.1默认是长连接。
2.6.2 location的使用
// 将上面的http_demo代码的send_string改成下面这样就好了
string send_string = "HTTP/1.0 301 Moved Permanently\n"; // 301为永久重定向,一般用于网站迁移
string send_string = "HTTP/1.0 302 Found\n"; // 302为临时重定向,一般用于页面跳转
send_string += "location:https://www.bilibili.com\n"; // location:后面跟的就是重定向到的url
2.6.4 Cookie和Session
2.6.4.1 Cookie
Cookie是在浏览器上的一个容器,可以帮用户存储访问网页提交的参数,或者其他信息。如图,当客户端第一次访问一个网页时向服务器提供了账号和密码(注意:因为实现起来比较繁琐,为了代码简易,我们没有实现这个,而是在直接在服务器端Set-Cookie,所以你看到下面第一次请求信息是没有提交账号密码的),然后服务器将账号和密码在响应头Header里的Set-Cookie属性里面返回去给客户端的Cookie里面保持着。后面的访问请求头的Header里的Cookie属性会自动携带保持好的账号密码的,也就不需要用户输入了。 Cookie存储内容的形式一般分两种,文件和内存,如果是内存的形式的话关闭浏览器之后,再次打开后就需要重新输入账号密码,文件的形式就看这个文件信息存在多长时间了。
2.6.4.2 Session
我们直接在Cookie文件里面直接保存账号密码,如果被别人获取到了我们的Cookie文件,别人就会知道我们的账号密码。为了避免账号密码在Cookie文件泄露出来,就提出了session会话。如图,当客户端向服务器提交账号密码给服务器后,服务器会形成一个session文件用来保存账号密码,这个session文件有一个会话id,如图中的001就是我们的会话id。服务器在Set-Cookie时不是将账号密码返回去,而是将session_id返回去给Cookie,客户端在后面请求服务器时,通过自动携带Cookie的session_id在服务器端找到对应文件,从而找到对应的账号密码。
三.HTTPS协议
3.1 对称和非对称加密
3.1.1 对称加密
对称加密就是密钥只有一个,即用X加密,用X解密。 就比如说下面的代码,key变量就可以看作是一把密钥,在数据发送时让data异或上key,接收时再异或上key,就可以得到原来的data值了。
#include<iostream>
int main()
{
// 发送方
int key = 10;
int data = 81;
int send = key ^ data;
// 接收方
int ret_data = send ^ key;
std::cout << ret_data << std::endl;
return 0;
}
3.1.2 非对称加密
非对称加密有一对密钥,公钥和私钥。用私钥加密只能用公钥解密,用公钥加密只能用私钥解密。 一般而言,公钥是对全世界公开的,私钥是私密的,不对外公开。相较于对称加密,非对称加密是非常耗时的。
3.2 证书
我们想要发送一份文章给好友,为了确保好友接收到的文章是完整的,即在通信途中没有被修改过,即便是一个标点符号也不能。如图我们首先是申请证书,将文章通过Hash散列处理得到一串唯一的字符序列(数据摘要或者数据指纹),通过加密算法加密得到数字签名,让文章和其对应的数字签名打包起来形成证书。然后将证书发给好友,好友接收到证书之后,将文章通过Hash散列重新生成数字指纹,将数字签名通过加密算法解密得到数字指。最后对比两份数字指纹,一样那就证明文章在途中没有被修改过。
3.3 https
3.3.1 简述
https是在http的基础上,通过TLS/SSL对数据进行加密,在传输的过程中,数据是被保护起来的,避免被抓包导致数据泄露。
3.3.2 对称加密法
如图所示,使用钥匙K对数据进行加密和解密,这种情况下一般需要客户端和服务器内置了K密钥,这种方法是不可取的,因为K太容易泄露了。
3.3.3 非对称加密法
如图所示,当客户端发起请求时,服务器会将公钥S发送给客户端,然后客户端用S对数据data进行加密,发给服务器。服务器收到后会用自己的私钥对数据进行解密,最后得到数据data。
3.3.4 复合加密法
上面我们说过,非对称式加密是非常耗时的,在通信的过程中使用纯非对称加密是不可取的,所以就有一种复合加密法。如图所示,客户端请求之后,服务器会将自己的公钥S给客户端,客户端拿到S并且形成一个对称密钥K。然后用S对对称密钥K加密得到K+,发送给服务器。服务器接K+后用私钥进行解密,得到K,最后他们正常通信的时候就可以用对称密钥K对数据进行加密和解密了。
3.3.5 中间人偷换公钥
无论是纯非对称法还是复合法,在服务器给公钥S的过程中,会存在一个很大的风险。如图所示,有一个中间人把服务器发给客户端的公钥S给截获,然后将自己的公钥M发给客户端。客户端接到M并且生成对称密钥K,用M给K加密成K+,发送给服务器。这时中间人又截获了K+,用自己的私钥M’解密得到K,最后用S对K再次加密得到K+',发送给服务器,服务器用私钥S’解密得到K。后面服务器跟客户端用K对数据进行通信,但是中间人也获取到了K,所以中间人也可以截获数据,然后进行解密。
3.3.6 https最终使用的加密法
上面的中间人偷换公钥使得我们的传输是不安全的,想要解决这个问题我们就要保证客户端拿到的公钥是服务器发来的S,而不是中间人的M。如图,我们可以对服务器的IP,公钥S进行哈希散列处理形成数据指纹,然后证书颁发的公司用私钥A’进行加密得到数字签名,最后将数据和签名合成证书。客户端请求之后,服务器发给客户端的是带有公钥S的证书,在中间即使中间人截取了也不会造成数据被更改。 第一点,上面我们说过的,数据和其数字签名应该是一一对应的,改了其中一个都不行。第二点,能不能两个都改了呢,答案是不能,以为数字签名最后需要客户端用证书公司的公钥A来解锁,这就说明数字签名只能由证书颁发机构的A’私钥来生成,这个中间人是没有的。
下一章节点击我直达 --> Linux网络编程_04_传输层UDP和TCP协议