推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
基本概念
p2p, 一个让所有投资者脊背发凉的金融概念, 一个去中心化的金融概念,其实它的本质只是一个技术。P2P(Peer-to-Peer)是一种去中心化的网络架构模式,中文通常翻译为"点对点"或"对等网络"。它代表了与传统的客户端-服务器(Client-Server)模型完全不同的网络通信理念。
P2P 的核心概念
- 去中心化:
- 没有中央服务器控制整个网络
- 所有参与者(节点)地位平等
- 每个节点既是客户端又是服务器(称为"对等体")
- 直接通信:
- 节点之间直接连接和交换数据
- 不需要通过中间服务器中转
- 通信路径更短,延迟更低
- 资源共享:
- 每个节点贡献自己的资源(带宽、存储、计算能力)
- 资源分布在整个网络中
- 节点越多,网络整体能力越强
与传统客户端-服务器模型的对比
| 特性 | P2P 网络 | 客户端-服务器模型 |
|---|---|---|
| 架构 | 去中心化 | 中心化 |
| 节点角色 | 既是客户端又是服务器 | 严格区分客户端和服务器 |
| 扩展性 | 节点越多性能越好 | 服务器可能成为瓶颈 |
| 可靠性 | 单点故障不影响整个网络 | 服务器故障导致服务中断 |
| 资源分布 | 资源分散在各个节点 | 资源集中在服务器 |
P2P 的典型应用场景
- 文件共享:
- BitTorrent:用户直接从其他用户下载文件片段
- 早期Napster:音乐文件共享(混合式P2P)
- 加密货币:
- 比特币/以太坊:交易验证通过P2P网络完成
- 区块链技术的基础架构
- 即时通讯:
- 早期Skype:语音通话直接在对等体间建立
- 某些隐私通讯应用
- 内容分发:
- P2P CDN:利用用户设备分发内容
- 直播平台的P2P加速
我所能设想到的一个应用就是 “智能家具” 的设计,我们用手机与智能家居进行点对点的 P2P 连接,直接下命令,而非绕一大圈地经过中央服务器。这样的设计才是系统开销小,用户体验好。
业务拆解
我们在前面的基本概念介绍里面已经说到过 P2P 网络的各个节点既是客户端又是服务器,本篇文章之中,我们要抓住这一个点设计一个点对点通信的简易代码。至于像加密验证等 “高级玩意”,本篇文章是绝对不会涉及的。
问题来了,我们该怎么设计呢?我们可以尝试一下问自己,到底想要什么功能效果。我问过自己,可以分成两大类——主动类和被动类。
主动类的功能:
- 用户之间随时发起信息。
- 用户选择想要连接的对象 IP(可以重置 IP)。
- 自己是一个客户端,可主动发起并实现与对应 IP 的远程连接。
- 结束程序。
被动类的功能:
- 自己本身是服务器,被动监听到来访 IP,并随即分配套接字资源负责对应的 I/O 任务。
- 自动地接收信息。(这里回想起《角头》中白毛对 “憨春” 说:“憨春大,我 BOSS 找你那么多次,你为什么都已读不回呀?啊?”)
为了简化问题,本篇文章所展示的代码,只实现 “一个设备仅有一个连接,如果想要新的连接就必须删掉旧的连接” 的设计。
对于主动类的功能,我将采用 “用户界面” 式的循环交互设计,类似的代码可见我之前写过的一篇关于 “通讯录小项目” 的文章(原文链接 在此)。
对于被动类的功能,我将采用多线程编程的设计。有两个被动类的功能,那就有两个子线程分别负责。这两个子线程因 “一个设备仅有一个连接,如果想要新的连接就必须删掉旧的连接” 的简化,而使用了 “SELECT” 定时关注连接所对应的套接字是否对接收、读写等事件就绪。select 是一种多路复用(multiplexing)I/O 机制,用于同时监视多个文件描述符(file descriptors),以确定哪些文件描述符已经准备好进行 I/O 操作(如读取、写入或异常条件)。类似的代码可见我之前写过的一篇关于多路 I/O 复用的文章(原文链接 在此)。
为了确保程序能够被正常关闭,所建立套接字都是非阻塞的,即定时执行,重复循环计时。
代码实现
准备工作
准备头文件
#include <stdio.h>
#include <stdlib.h> // EXIT_FAILURE 是一个标准宏,exit 函数
#include <string.h>
#include <unistd.h> // close 函数
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h> // 添加select用于超时处理
#include <fcntl.h> // 用于更改套接字的模式,比如非阻塞模式
#include <errno.h> // 这是全局变量 errno,用于健壮的读取功能
准备宏定义
#define MAX_MSG_LEN 1024
#define PORT 5000
声明与定义全局变量,该全局变量能综合、集成所有的运行参数。我们将之命名为 NodeState
// 全局状态结构体
typedef struct {
int server_fd;
int connection_fd;
int running;
pthread_mutex_t lock;
char peer_ip[16]; // 存储点分十进制IP地址
int peer_port;
} NodeState;
// 声明并定义全局变量
// 点号(.)在这里是C99标准引入的指定初始化器语法的一部分。它的作用是明确指定结构体成员的初始化值,而不是依赖于成员在结构体中的顺序。
NodeState node_state = {
.server_fd = -1,
.connection_fd = -1,
.running = 1,
.lock = PTHREAD_MUTEX_INITIALIZER,
.peer_ip = "",
.peer_port = PORT
};
需要注意到的是,点号(.)在这里是C99标准引入的指定初始化器语法的一部分。它的作用是明确指定结构体成员的初始化值,而不是依赖于成员在结构体中的顺序。
紧接着是错误处理函数,
void error(const char *msg) {
perror(msg);
exit(EXIT_FAILURE); // EXIT_FAILURE 是一个标准宏,定义在 <stdlib.h> 中,用于表示程序执行失败。
// 当 exit 函数被调用时,程序会执行以下操作:
// 关闭所有打开的文件:关闭所有通过标准 I/O 函数(如 fopen)打开的文件流。
// 刷新缓冲区:刷新所有标准 I/O 缓冲区,确保所有未写入的数据都被写入目标文件或设备。
// 调用清理函数:执行所有通过 atexit 注册的清理函数(如果有)。
// 终止程序:终止程序的执行,并将 status 参数作为退出状态码返回给操作系统。
}
当我们结束程序的时候,需要定义清理资源函数
// 清理资源
void cleanup() {
pthread_mutex_lock(&node_state.lock);
if (node_state.connection_fd != -1) {
close(node_state.connection_fd);
node_state.connection_fd = -1; // 重置
}
if (node_state.server_fd != -1) {
close(node_state.server_fd);
node_state.server_fd = -1; // 重置
}
pthread_mutex_unlock(&node_state.lock);
printf("[*] Resources cleaned up\n");
return;
}
为了能让程序正常结束,而非让套接字对应的 accept 和 recv 函数在用户选择退出的时候,一直处于阻塞各自的线程之中,故而我们定义了套接字设置函数
// 设置套接字为非阻塞
void set_nonblocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return;
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1

最低0.47元/天 解锁文章
411

被折叠的 条评论
为什么被折叠?



