网络编程基础知识——从基础到实操
1 域名系统
1.1 DNS的构成
域名空间
域名空间是一个树状结构,最顶层是根域(用.表示)。根域下面是顶级域名(Top - Level Domain,TLD),顶级域名分为两类:
- 通用顶级域名(gTLD):如.com(商业机构)、.org(非盈利组织)、.net(网络服务机构)等。这些域名在早期主要是按照机构的性质来划分的。例如,很多商业公司会选择注册.com域名,像www.amazon.com(亚马逊公司)。
- 国家代码顶级域名(ccTLD):如.cn(中国)、.us(美国)、.jp(日本)等。这些域名是按照国家或地区划分的,用于标识该国家或地区的机构、企业或个人的网站。比如www.beijing.gov.cn是一个中国北京市的政府网站域名。
在顶级域名下面还有二级域名、三级域名等。例如www就是一个常见的三级域名,它通常用于标识一个网站的主页面。域名空间的这种层次结构使得域名的管理更加有序,也便于人们理解和记忆。
域名服务器
- 根域名服务器:根域名服务器是整个DNS体系的顶层,它保存着顶级域名服务器的地址信息。当一个DNS查询请求无法在本地域名服务器中找到答案时,就会向根域名服务器发起查询。全球有13组根域名服务器,它们通过分布式部署来保证系统的稳定性和可靠性。
- 顶级域名服务器(TLD服务器):它负责管理顶级域名下的域名信息。例如,对于.com顶级域名,其对应的TLD服务器存储了所有以.com结尾的域名的权威信息,包括这些域名对应的二级域名、三级域名等的解析信息。
- 权威域名服务器:这是为特定域名提供权威解析信息的服务器。一个企业或组织可以有自己的权威域名服务器,用于管理自己域名下的所有记录,如主机记录(A记录)、邮件交换记录(MX记录)等。例如,一个公司有自己的域名example.com,它在自自己的权威域名服务器上设置了mail.example.com对应的IP地址,用于邮件服务器的解析。
- 本地域名服务器(递归域名服务器):这是用户最常接触的域名服务器。当用户在浏览器中输入一个网址时,本地域名服务器会首先查看自己的缓存,如果缓存中有该域名对应的IP地址,就会直接返回结果。如果没有,它会按照一定的顺序向其他域名服务器(如根域名服务器、TLD服务器等)查询,直到获取到正确的IP地址并返回给用户。
工作原理
- 递归查询
当用户向本地域名服务器发起域名解析请求时,本地域名服务器会负责完成整个查询过程。如果本地域名服务器的缓存中没有该域名的解析记录,它会向根域名服务器发送查询请求。根域名服务器会告诉本地域名服务器应该向哪个TLD服务器查询。本地域名服务器再向TLD服务器查询,TLD服务器会告诉它向权威域名服务器查询。最后,权威域名服务器返回域名对应的IP地址给本地域名服务器,本地域名服务器再将这个结果返回给用户。在整个过程中,用户只需要等待本地域名服务器完成查询并返回结果,用户端不需要进行多次查询。 - 迭代查询
在迭代查询中,本地域名服务器只是将查询请求转发给根域名服务器,根域名服务器会告诉本地域名服务器应该向哪个TLD服务器查询,然后本地域名服务器再自己去向TLD服务器查询。如果TLD服务器也没有该域名的解析记录,它会告诉本地域名服务器向权威域名服务器查询。这种查询方式要求用户端的设备(如计算机)自己去跟踪查询过程,直到获取到最终的IP地址。相比递归查询,**迭代查询对本地域名服务器的资源占用较少,但用户端设备需要做更多的工作 **
记录类型
记录类型 | 功能描述 | 示例 |
---|---|---|
A记录 | 将域名映射为IPv4地址。 | www.example.com → 192.0.2.45 |
AAAA记录 | 将域名映射为IPv6地址。 | www.example.com → 2001:0db8:85a3:0000:0000:8a2e:0370:7334 |
CNAME记录 | 将一个域名指向另一个域名。 | blog.example.com → www.example.com |
MX记录 | 指定域名的邮件服务器。 | example.com → mail.example.com |
NS记录 | 指定域名的权威域名服务器。 | example.com → ns1.example.com 、ns2.example.com |
TXT记录 | 包含任意文本信息,用于域名验证、反垃圾邮件等。 | SPF记录:v=spf1 include:_spf.google.com ~all |
DNS编程基础
#include <iostream>
#include <boost/asio.hpp>
int main() {
boost::asio::io_context io_context;
boost::asio::ip::tcp::resolver resolver(io_context);
// 解析域名
auto endpoints = resolver.resolve("www.example.com", "http");
// 遍历解析结果
for (auto& endpoint : endpoints) {
std::cout << "IP Address: " << endpoint.endpoint().address().to_string() << std::endl;
}
return 0;
}
在Boost.Asio中,resolver.resolve方法用于同步解析域名,并返回一个包含解析结果的迭代器范围。这个范围是一个boost::asio::ip::tcp::resolver::results_type类型的对象,它是一个迭代器范围,包含了所有可能的解析结果(即IP地址和端口号的组合)
auto endpoints = resolver.resolve("www.example.com", "http");
resolver:这是一个boost::asio::ip::tcp::resolver对象,用于执行域名解析。
resolve:这是resolver对象的成员函数,用于同步解析域名。
第一个参数是域名(如"www.example.com")。
第二个参数是服务名(如"http"),它通常是一个端口号的字符串表示。对于HTTP服务,默认端口号是80。
endpoints:这是解析结果,类型为boost::asio::ip::tcp::resolver::results_type。它是一个迭代器范围,包含了所有可能的解析结果。
results_type 的结构
results_type是一个迭代器范围,每个迭代器指向一个boost::asio::ip::tcp::resolver::iterator,而每个迭代器又包含一个boost::asio::ip::tcp::endpoint对象。endpoint对象包含了IP地址和端口号的信息。
进程
定义
**进程(Process)**是计算机科学中的一个核心概念,它是指计算机程序在执行时的一个实例。简单来说,进程是程序运行时的动态实体,它包含了程序的代码、数据、运行时的上下文信息(如寄存器状态、堆栈信息等)以及操作系统分配给它的资源(如内存、文件句柄
进程与程序的区别
- 程序(Program):程序是一组指令的集合,是静态的代码和数据的组合,存储在磁盘或其他存储介质中。
- 进程(Process):进程是程序在执行时的一个实例,是动态的,包含了程序运行时的状态和资源分配信息。
进程的状态
进程在其生命周期中会经历多种状态,这些状态反映了进程在操作系统调度中的行为。
-
- 新建(New)
进程刚刚被创建,但尚未被调度运行。此时进程处于初始化阶段,操作系统正在为其分配资源。
- 新建(New)
-
- 就绪(Ready)
进程已经准备好运行,但正在等待CPU时间片。就绪状态的进程被放入就绪队列中,等待调度器分配CPU资源。
- 就绪(Ready)
-
- 运行(Running)
进程正在CPU上执行。一个系统中可能有多个就绪状态的进程,但只有一个进程处于运行状态(单核CPU)。
- 运行(Running)
-
- 阻塞(Blocked)
进程因为等待某些事件(如I/O操作完成、信号量释放等)而暂时无法运行。阻塞状态的进程不会被调度器分配CPU时间,直到它等待的事件发生。
- 阻塞(Blocked)
-
- 终止(Terminated)
进程完成运行或因错误而终止。终止状态的进程将被操作系统回收资源,其状态信息将被清除。
- 终止(Terminated)
僵尸进程
僵尸进程的产生原因
- 子进程先于父进程结束:
当子进程完成执行后,操作系统会保留其状态信息(如退出状态码、资源使用情况等),直到父进程通过wait()或waitpid()等系统调用读取这些信息。
二、僵尸进程的问题 - 资源占用:
僵尸进程会占用系统资源,如进程表条目、文件描述符等。如果僵尸进程过多,可能会导致系统资源耗尽。 - 无法被终止:
僵尸进程不能被kill命令或任何其他信号终止,因为它们已经“死亡”,只是状态信息尚未被清理。 - 影响系统性能:
僵尸进程的存在可能会影响系统的性能和稳定性,尤其是在资源有限的系统上。
编程基础
1.创建进程
#include <unistd.h>
pid_t fork(void);
返回值:
在父进程中返回子进程的PID。
在子进程中返回0。
如果出错,返回-1。
2.wait()函数
父进程调用wait()等待子进程结束,避免子进程成为僵尸进程
3.示例
#include<iostream>
#include<unistd.h> //unix标准文件
int main()
{
using namespace std;
pid_t pid;
cout<<"parent have!"<<endl;
pid = fork();
if(pid == -1)//错误创建
{
perror("fork error");
_exit(1);
}
else if(pid == 0)//子进程
{
cout<<"i am child,pid = "<<getpid()<<" my parent is:"<<getppid()<<endl;
}
else//父进程
{
cout<<"i am parent,pid = "<<getpid()<<" my parent is:"<<getppid()<<endl;
// 等待子进程结束
wait(nullptr);
}
cout<<"both have!"<<endl;
return 0;
}
信号
定义
信号是一种软件中断机制,用于通知进程某些事件的发生。信号可以由操作系统、其他进程或进程自身发送给目标进程。当信号到达时,目标进程可以选择忽略信号、捕获信号并执行自定义的处理函数,或者执行默认的处理动作。
信号的类型
信号通过一个正整数来标识,每个信号都有一个名称和一个默认的处理动作。以下是一些常见的信号及其默认行为:
信号名称 | 信号编号 | 默认行为 | 描述 |
---|---|---|---|
SIGINT | 2 | 终止进程 | 用户通过键盘中断(通常是Ctrl+C)发送给进程 |
SIGTERM | 15 | 终止进程 | 请求进程终止 |
SIGKILL | 9 | 强制终止进程 | 强制终止进程,不能被忽略或捕获 |
SIGSEGV | 11 | 终止进程并生成核心转储 | 进程访问非法内存时发送 |
SIGCHLD | 17/20 | 忽略 | 子进程结束时发送给父进程 |
SIGALRM | 14 | 终止进程 | 由alarm 系统调用设置的定时器到期时发送 |
SIGUSR1 | 30/10 | 终止进程 | 用户自定义信号1 |
SIGUSR2 | 31/12 | 终止进程 | 用户自定义信号2 |
SIGQUIT | 3 | 终止进程并生成核心转储 | 用户通过键盘(通常是Ctrl+\)发送给进程 |
SIGSTOP | 19/17 | 暂停进程 | 暂停进程 |
SIGCONT | 18/19 | 继续进程 | 继续暂停的进程 |
SIGTSTP | 20/18 | 暂停进程 | 用户通过键盘(通常是Ctrl+Z)发送给进程 |
信号的默认行为
每个信号都有一个默认的处理动作,这些动作由操作系统定义。常见的默认行为包括:
终止进程:
- SIGINT、SIGTERM、SIGUSR1、SIGUSR2等信号的默认行为是终止进程。
进程在接收到这些信号后会立即退出。
强制终止进程: - SIGKILL信号的默认行为是强制终止进程,不能被忽略或捕获。
进程在接收到SIGKILL信号后会立即退出,且不会执行任何清理操作。
终止进程并生成核心转储: - SIGSEGV、SIGABRT等信号的默认行为是终止进程并生成核心转储文件。
核心转储文件包含了进程的内存映像,可以用于调试。
忽略信号: - SIGCHLD信号的默认行为是被忽略。
父进程不会接收到子进程结束的信号,除非父进程显式地注册了信号处理函数。
暂停进程: - SIGSTOP信号的默认行为是暂停进程。
进程在接收到SIGSTOP信号后会暂停执行,直到接收到SIGCONT信号。
继续进程: - SIGCONT信号的默认行为是继续暂停的进程。
进程在接收到SIGCONT信号后会继续执行。
进程可以对信号进行以下几种处理:
- 忽略信号:
进程可以选择忽略某些信号,但有些信号(如SIGKILL和SIGSTOP)不能被忽略。
例如,可以通过signal(SIGINT, SIG_IGN)忽略SIGINT信号。 - 捕获信号:
进程可以注册一个信号处理函数,当信号到达时,执行自定义的处理逻辑。
例如,可以通过signal(SIGINT, handle_sigint)注册一个信号处理函数handle_sigint。 - 执行默认动作:
如果进程没有对信号进行特殊处理,操作系统会执行信号的默认动作。
例如,如果进程没有捕获SIGINT信号,操作系统会终止进程。 - 示例
#include <iostream>
#include <csignal>
#include <unistd.h>
// 信号处理函数
void handle_sigint(int sig) {
std::cout << "Received SIGINT (Ctrl+C). Exiting gracefully." << std::endl;
exit(0);
}
int main() {
// 注册信号处理函数
std::signal(SIGINT, handle_sigint);
std::cout << "Process is running. Press Ctrl+C to send SIGINT." << std::endl;
// 模拟长时间运行的进程
while (true) {
sleep(1);
}
return 0;
}
信号的发送
- kill系统调用
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid是目标进程的PID,sig是要发送的信号编号。
信号的阻塞
进程可以通过sigprocmask系统调用阻塞某些信号,使它们暂时不会被处理。阻塞的信号会在适当的时候被处理,但不会立即中断进程的正常执行。
- 信号掩码
信号掩码是一个位掩码,用于表示哪些信号被阻塞。每个信号对应一个位,如果该位被设置为1,则表示该信号被阻塞。
#include <csignal>
#include <cerrno>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:指定如何修改信号掩码,可以是以下值之一:
SIG_BLOCK:将set中的信号添加到当前信号掩码中。
SIG_UNBLOCK:从当前信号掩码中移除set中的信号。
SIG_SETMASK:将当前信号掩码设置为set。
set:指向要设置的信号集。
oldset:指向存储旧信号掩码的变量。
示例
#include <iostream>
#include <csignal>
#include <unistd.h>
int main() {
// 创建信号集
sigset_t set;
sigemptyset(&set); // 初始化信号集
sigaddset(&set, SIGINT); // 添加SIGINT信号到信号集
// 阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &set, nullptr) == -1) {
perror("sigprocmask");
return 1;
}
std::cout << "SIGINT is blocked. Press Ctrl+C to test." << std::endl;
// 模拟长时间运行的进程
sleep(10);
// 解除阻塞SIGINT信号
if (sigprocmask(SIG_UNBLOCK, &set, nullptr) == -1) {
perror("sigprocmask");
return 1;
}
std::cout << "SIGINT is unblocked. Press Ctrl+C to test." << std::endl;
// 模拟长时间运行的进程
while (true) {
sleep(1);
}
return 0;
}
进程间的通信(管道)
定义
管道是一种简单的进程间通信机制,用于在进程之间传递数据。管道可以分为两种类型:
- 匿名管道(Anonymous Pipes):通常用于父子进程之间的通信。
- 命名管道(Named Pipes,也称为FIFO):可以在不相关的进程之间通信。
匿名管道
特点
- 单向通信:数据只能在一个方向上流动。
- 仅限于相关进程:通常用于父子进程之间的通信。
- 简单易用:通过pipe()系统调用创建。
创建匿名管道
#include <unistd.h> int pipe(int pipefd[2]);
pipefd[0]:管道的读端文件描述符。
pipefd[1]:管道的写端文件描述符。
返回值:成功返回0,失败返回-1。
示例:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>
int main() {
int parent_to_child[2];
int child_to_parent[2];
// 创建两个管道
if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
close(parent_to_child[1]); // 关闭父进程到子进程的写端
close(child_to_parent[0]); // 关闭子进程到父进程的读端
char buffer[80];
// 从父进程读取数据
read(parent_to_child[0], buffer, sizeof(buffer));
std::cout << "Child process received from parent: " << buffer << std::endl;
// 向父进程发送数据
const char *msg = "Hello from child\n";
write(child_to_parent[1], msg, strlen(msg));
close(parent_to_child[0]);
close(child_to_parent[1]);
} else {
// 父进程
close(parent_to_child[0]); // 关闭父进程到子进程的读端
close(child_to_parent[1]); // 关闭子进程到父进程的写端
const char *msg = "Hello from parent\n";
// 向子进程发送数据
write(parent_to_child[1], msg, strlen(msg));
// 从子进程读取数据
char buffer[80];
read(child_to_parent[0], buffer, sizeof(buffer));
std::cout << "Parent process received from child: " << buffer << std::endl;
close(parent_to_child[1]);
close(child_to_parent[0]);
wait(nullptr); // 等待子进程结束
}
return 0;
}
命名管道
1.特点
- 双向通信:虽然管道本身是单向的,但可以通过创建两个管道实现双向通信。
- 不相关进程:可以在不相关的进程之间通信。
- 基于文件系统:命名管道在文件系统中有一个特殊文件,可以通过文件名访问。
2.创建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- pathname:命名管道的路径名。
- mode:文件权限,类似于open()中的权限。
返回值:成功返回0,失败返回-1。
3.使用 - 写操作:使用open()打开管道文件,然后使用write()写入数据。
- 读操作:使用open()打开管道文件,然后使用read()读取数据。
4 示例
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
const char *fifo_path1 = "/tmp/fifo1";
const char *fifo_path2 = "/tmp/fifo2";
// 创建命名管道
if (mkfifo(fifo_path1, 0666) == -1 && errno != EEXIST) {
perror("mkfifo");
return 1;
}
if (mkfifo(fifo_path2, 0666) == -1 && errno != EEXIST) {
perror("mkfifo");
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
// 打开管道
int fd1 = open(fifo_path1, O_RDONLY);
if (fd1 == -1) {
perror("open");
return 1;
}
// 读取数据
char buffer[80];
read(fd1, buffer, sizeof(buffer));
std::cout << "Child process received from parent: " << buffer << std::endl;
close(fd1);
// 打开管道
int fd2 = open(fifo_path2, O_WRONLY);
if (fd2 == -1) {
perror("open");
return 1;
}
// 写入数据
const char *msg = "Hello from child\n";
write(fd2, msg, strlen(msg));
close(fd2);
} else {
// 父进程
// 打开管道
int fd1 = open(fifo_path1, O_WRONLY);
if (fd1 == -1) {
perror("open");
return 1;
}
// 写入数据
const char *msg = "Hello from parent\n";
write(fd1, msg, strlen(msg));
close(fd1);
// 等待子进程的响应
int fd2 = open(fifo_path2, O_RDONLY);
if (fd2 == -1) {
perror("open");
return 1;
}
char buffer[80];
read(fd2, buffer, sizeof(buffer));
std::cout << "Parent process received from child: " << buffer << std::endl;
close(fd2);
wait(nullptr); // 等待子进程结束
}
return 0;
}