一.协议
协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接 收的. 如果我们要传输一些 "结构化的数据" 怎么办呢? 其实,协议就是双方约定好的结构化的数据
像下面,两端都知道数据结构data的结构,我们可以直接传data数据的二进制吗?
理论可以,但如果两边平台不同,不兼容等导致出错。
eg.大小端问题 64位指针大小8 32位指针大小4
所以我们一般不通过二进制协议进行传输
所以我们一个怎么处理更复杂的、结构化的数据的传输呢?
二.序列化和反序列化
1.什么是序列化 反序列化
比如说我们传一个结构体data,里面包含 int x,char oper ,int y。
我们不要一个一个传,可以把成员元素整合成一个字符串,再传。这个就是序列化
但当我们获取到了这个字符串,怎么获取到里面包含的消息呢?
我们可以自己进行规定,每个元素间用空格进行隔开,依次进行获取。
根据制定的规则进行解包,获取元素,就是反序列化。
序列化: 你将结构体的各个成员转换成某种格式的字符串,以便传输或存储。例如,使用空格分隔成员。
反序列化: 接收端根据预定的规则解析字符串,并将其恢复为原始数据结构(结构体、对象等)。
2.JSON 序列化
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字
符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各
种需要处理 JSON 数据的 C++ 项目中。
序列化方法:
1.使用 Json::Value 的 toStyledString 序列化:
优点:将 Json::Value 对象直接转换为格式化的 JSON 字符串
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}
$ . / test.exe
{
"name" : "joe",
"sex" : "男"
}
2.使用 Json::StreamWriter:
优点:提供了更多的定制选项,如缩进、换行符等。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
return 0;
}
$ . / test.exe
{
"name" : "joe",
"sex" : "男"
}
3.使用 Json::FastWriter:
比 StyledWriter 更快,因为它不添加额外的空格和换行符。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ . / test.exe
{ "name":"joe","sex" : "男" }
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
// Json::FastWriter writer;
Json::StyledWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ . / test.exe
{
"name" : "joe",
"sex" : "男"
}
反序列化方法:
使用 Json::Reader:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
// JSON 字符串
std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";
// 解析 JSON 字符串
Json::Reader reader;
Json::Value root;
// 从字符串中读取 JSON 数据 序列化的字符串 进行反序列
bool parsingSuccessful = reader.parse(json_string,root);
if (!parsingSuccessful)
{
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " <<reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 访问 JSON 数据
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
// 输出结果
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;
return 0;
}
$ . / test.exe
Name : 张三
Age : 30
City : 北京
访问 JSON 数据,int age = root["age"].asInt(); 如果是char c=root["age"].asInt(),也是用.asInt,没有.aschar。因为char类型就是整数
三.重新理解 read、write、recv、send 和 tcp 为什么支持全双工
全双工:通信双方可以同时进行双向数据传输。也就是说,发送和接收可以在同一时刻发生。
在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核
中,可以在发消息的同时,也可以收消息,即全双工。
这就是为什么一个 tcp sockfd 读写都是它的原因。
实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议。
在系统内部都有大量的报文,一部分是从发送缓冲区进行传输,另一部分是向接收缓冲区进行传输。如何进行管理,先描述,再组织。每个报头间用链表进行连接。
在socket结构中就包含两个队列,分别是发送队列,接收队列,对这两部份报头进行管理。
这两个队列分别用于存储发送和接收的报文头。每个队列可以通过链表来管理报文的顺序。
发送缓冲区和发送队列:发送缓冲区的内容一般是按照队列的顺序被处理的。数据首先进入发送队列(即缓冲区),然后按顺序被发送到远程主机。在TCP协议中,发送队列不仅仅是一个简单的队列,还会涉及到流量控制、拥塞控制等机制,确保发送方的速度不会超过接收方的处理能力。
接收缓冲区和接收队列:当数据到达本地系统时,接收队列会存放这些数据。接收队列的顺序和缓冲区管理相结合,确保应用程序能够以正确的顺序读取到接收到的数据。
四.ps axj 当前系统的进程信息
PID PGID SID TTY TIME CMD
123 123 123 tty1 00:00:01 bash
234 123 123 tty1 00:00:00 ps
567 567 567 ? 00:00:10 my_process
PID(进程ID):进程的唯一标识符。
PGID(进程组ID):多个进程共享的组ID。(处在同一组的进程PGID相同,一般第一个是组长)
SID(会话ID):进程所属的会话ID。
TTY(终端):进程关联的终端(如果有)。
TIME(CPU时间):进程使用的CPU时间。
CMD:启动该进程的命令。
五.守护进程
1.前台进程 后台进程
像在命令行中直接sleep 100 启动的就是前台进程,如果在后面加& ,sleep 100 &就把这个进程在后台启动。
启动一个前台进程(例如,运行 sleep 100),该进程会占用终端。前台进程会占据整个终端的输入/输出流,因此你无法在命令行中执行其他命令(如 ls)直到这个前台进程完成。
2.fd 任务号 后台进程->前台进程
当我们启动了一个后台进程,怎么再把它放到前台呢?
fd+任务号
使用 jobs 命令可以列出当前 shell 会话中所有的后台任务,并显示每个任务的任务号(Job ID)和当前状态。
3.Ctrl+Z bg 任务号 前台进程->后台进程
1.ctrl+z 挂起前台进程,变为暂停状态。
2.bg+任务号 将挂起的进程放到后台,不能直接把前台进程放在后台。
4.后台进程和守护进程
当我们进行登录,系统会创建一个会话,这个会话中bash进程当作前台进程,与终端直接连接,建立后台进程不会直接接收用户的输入,也不会直接向终端输出数据,一般不输入输出重定向到其它文件。
如果我们退出登录,该会话的前台进程 终端都会关闭,里面的后台进程也会受到影响。
有没有办法退出时让该后端进程不受影响?
把该后端进程放到一个新的会话中,并且持续运行,不与任何终端关联,就是守护进程。
如何创建守护进程?
手动:
1.调用 fork():首先,进程调用 fork() 创建一个子进程,父进程退出,确保子进程不成为孤儿进程。
2.调用 setsid():子进程通过 setsid() 调用成为新会话的会话领导者,这样它就不再与控制终端关联。(不是是进程组的组长调用它。fork()子进程调用)
3.重定向输入输出:守护进程会将输入输出重定向到日志文件或 /dev/null (文件黑洞 不会保存数据),以确保不会干扰终端。
4.持续运行:守护进程通常进入无限循环,保持持续运行,处理后台任务。#pragma once #include <iostream> #include <cstdlib> #include <signal.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #define ROOT "/" #define devnull "/dev/null" void Daemon(bool ischdir, bool isclose) { // 1. 守护进程一般要屏蔽到特定的异常信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // 2. 成为非组长 if (fork() > 0) exit(0); // 3. 建立新会话 setsid(); // 4. 每一个进程都有自己的CWD,是否将当前进程的CWD更改成为 / 根目录 if (ischdir) chdir(ROOT); // 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了 if (isclose) { ::close(0); ::close(1); ::close(2); } else { int fd = ::open(devnull, O_WRONLY); if (fd > 0) { // 各种重定向 dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); } } }
daemon:
int daemon(int nochdir, int noclose);
nochdir:如果为 1,守护进程将不改变当前工作目录;如果为 0,守护进程会将当前工作目录更改为根目录(/),以避免占用一个文件系统的目录。
noclose:如果为 1,守护进程不会关闭文件描述符。如果为 0,守护进程会关闭标准输入、标准输出和标准错误输出文件描述符。#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { // 将进程转变为守护进程 if (daemon(0, 0) == -1) { perror("daemon"); exit(1); } // 守护进程的工作 while (1) { // 这里可以添加守护进程执行的任务 sleep(10); // 每10秒执行一次任务 } return 0; }
在原会话的后台进程会受到终端信号干扰、父进程退出以及会话管理的影响。
通过使用
setsid()
来创建新的会话,守护进程能够脱离终端和父进程的控制,确保在系统后台独立稳定地运行,避免受到干扰。
守护进程处于新的会话:
1.确保进程不被终端信号干扰
在原有会话中运行的进程通常会收到一些终端信号,尤其是当用户退出时,例如
SIGHUP
信号,通常会导致进程终止。守护进程必须避免这些信号的干扰,才能保证其在系统后台长时间稳定运行。调用
setsid()
后,守护进程会脱离原会话,并成为一个新的会话的会话领导者,控制终端不再影响它。这确保了守护进程不会因为终端的关闭或会话的结束而被中断。2.防止守护进程成为孤儿进程
在操作系统中,进程分为父进程和子进程。如果一个进程的父进程终止,该进程会被操作系统的 "init" 进程收养,成为孤儿进程。然而,守护进程希望在系统后台长期运行,它不希望被父进程或任何其他进程“收养”,而是希望有完全的独立性。
通过调用
setsid()
,守护进程脱离了原会话,成为一个新的会话的会话领导者,这样它不再依赖任何父进程,确保它不会成为孤儿进程,也不会因为父进程的退出而终止。