文章目录
- 一、什么是FastDFS
- 二、FastDFS的架构
- 2.1 跟踪服务器(tracker server)
- 2.2 存储服务器(storage server)
- 2.3 客户端(client)
- 三、 FastDFS功能逻辑分析
- 3.1 upload file(上传文件)原理
- 3.2 download file(下载文件)逻辑
- 四、海量小文件存储
- 4.1 海量小文件存储的问题
- 4.2 小文件机制配置
- 4.3 合并存储文件
- 4.3.1 启动小文件存储时服务返回给客户端的fileid变化
- 4.3.2 Trunk文件存储结构
- 4.3.3 小文件存储平衡树
- 五、FastDFS文件同步
- 5.1 同步规则
- 5.2 binlog
- 5.3 mark
- 5.4 binlog同步过程
- 5.5 新增节点同步
- 5.6 高可用和高并发
- 六、一些面试要点
一、什么是FastDFS
FastDFS(Fast Distributed File System)是一个开源的分布式文件系统,它旨在提供高性能、高可靠性和可扩展性的文件存储解决方案,解决海量数据存储问题。其主要的功能包括:文件存储,同步和访问。特别适合以中小文件(建议范围:4KB < file_size <500MB)为载体的在线服务,如图片分享和视频分享网站。
举个例子,想象你有一个大型的网站,用户可以上传和下载大量的图片和视频文件。如果这些文件都存储在单个服务器上,可能会导致服务器负载过高、存储空间不足以及访问速度慢等问题。这时就需要将这些文件分布式地存储在多台服务器上,以提高整个系统的性能和可靠性。
FastDFS 就是为了解决这个问题而设计的,它可以将大文件切分成小块,并将这些小块分散存储在多个服务器上,实现文件的分布式存储。同时,FastDFS 还提供了文件上传、下载、删除等操作的接口,使得开发人员可以方便地操作这些分布式文件。
FastDFS开源地址
二、FastDFS的架构
FastDFS由 跟踪服务器(tracker server)、存储服务器(storage server)和客户端(client) 三个部分组成。
2.1 跟踪服务器(tracker server)
跟踪服务器,主要做调度工作,起负载均衡的作用。在内存中记录集群中所有存储组和存储服务器的状态信息,是客户端和数据服务器交互的枢纽。
Tracker是FastDFS的协调者,主要做调度工作,负责管理所有的storage server和group。
1)每个storage在启动后会连接Tracker,告知自己所属的group等信息,并保持周期性的心跳。tracker根据storage的心跳信息,建立group ==> [storage server list]的映射表。
2)client 访问 storage server 之前,必须先访问 tracker server,动态获取到 storage server 的连接信息,最终数据是和一个可用的 storage server 进行传输。
Tracker需要管理的元信息很少,会全部存储在内存中;另外tracker上的元信息都是由storage汇报的信息生成的,本身不需要持久化任何数据,这样使得tracker非常容易扩展,直接增加tracker机器即可扩展为tracker cluster来服务,cluster里每个tracker之间是完全对等的,所有的tracker都接受stroage的心跳信息,生成元数据信息来提供读写服务。
2.2 存储服务器(storage server)
Storage server以组(卷,group或volume)为单位组织,一个group内包含多台storage机器,数据互为备份。存储空间以group内容量最小的storage为准。所以建议group内的多个storage尽量配置相同,以免造成存储空间的浪费。
以group为单位组织存储能方便的进行应用隔离、负载均衡、副本数定制(group内storage server数量即为该group的副本数)。
1)将不同应用数据存到不同的group就能隔离应用数据
2)根据应用的访问特性来将应用分配到不同的group来做负载均衡;
缺点是group的容量受单机存储容量的限制,同时当group内有机器坏掉时,数据恢复只能依赖group内地其他机器,使得恢复时间会很长。
group内每个storage的存储依赖于本地文件系统,storage可配置多个数据存储目录,比如有10块磁盘,分别挂载在/data/disk1 到 /data/disk10,则可将这10个目录都配置为storage的数据存储目录。
storage接受到写文件请求时,会根据配置好的规则,选择其中一个存储目录来存储文件。为了避免单个目录下的文件数太多,在storage第一次启动时,会在每个数据存储目录里创建2级子目录,每级256个,总共65536个文件,新写的文件会以hash的方式被路由到其中某个子目录下,然后将文件数据直接作为一个本地文件存储到该目录中。
这种方式会加快文件索引。
2.3 客户端(client)
FastDFS向使用者提供基本文件访问接口,比如monitor、upload、download、append、delete等,以客户端库的方式提供给用户使用。
三、 FastDFS功能逻辑分析
3.1 upload file(上传文件)原理
1、选择tracker server
当集群中不止一个tracker server时,由于tracker之间是完全对等的关系,客户端在upload文件时可以任意选择一个trakcer。
2、选择存储的group
当tracker接收到upload file的请求时,会为该文件分配一个可以存储该文件的group,支持如下选择group的规则:
1)Round robin,所有的group间轮询
2)Specified group,指定某一个确定的group
3)Load balance,选择最大剩余空 间的组上传文件
3、选择storage server
当选定group后,tracker会在group内选择一个storage server给客户端,支持如下选择storage的规则:
1)Round robin,在group内的所有storage间轮询
2)First server ordered by ip,按ip排序
3)First server ordered by priority,按优先级排序(优先级在storage上配置)
4、选择storage path
当分配好storage server后,客户端将向storage发送写文件请求,storage将会为文件分配一个数据存储目录,支持如下规则:
1)Round robin,多个存储目录间轮询
2)剩余存储空间最多的优先
5、生成Fileid
选定存储目录之后,storage会为文件生一个Fileid,由:storage server ip、文件创建时间、文件大小、文件crc32(文件内容的检验码)和一个随机数(防止生成重名文件)拼接而成,然后将这个二进制串进行base64编码,转换为可打印的字符串。
6、选择两级目录
当选定存储目录之后,storage会为文件分配一个fileid,每个存储目录下有两级256*256的子目录,storage会按文件fileid路由到其中一个子目录,然后将文件以fileid为文件名存储到该子目录下。
7、生成文件名
当文件存储到某个子目录后,即认为该文件存储成功,接下来会为该文件生成一个文件名,文件名由:group、存储目录、两级子目录、fileid、文件后缀名(由客户端指定,主要用于区分文件类型)拼接而成。
3.2 download file(下载文件)逻辑
客户端upload file成功后,会拿到一个storage生成的文件名,接下来客户端根据这个文件名即可访问到该文件。
1、跟upload file一样,在download file时客户端可以选择任意tracker server。
2、clinet发送download请求给某个tracker,必须带上文件名信息,tracke从文件名中解析出文件的group、大小、创建时间等信息,然后为该请求选择一个storage用来服务读请求。
一致性问题
弱一致性:先返回存储结果,再同步,速度快,不可靠
强一致性:先同步,再返回存储结果,速度慢,安全可靠,非商业付费。
FastDFS 是弱一致性,先返回存储结果,再同步。可能发生同一组内的 storage 未同步的情况。
具体来说,由于group内的文件同步是在后台异步进行的,所以有可能出现:在读的时候,文件还没有同步到某些storage server上。为了尽量避免访问到这样的storage,tracker按照如下规则选择group内可读的storage:
1)该文件上传到的源头storage :源头storage只要存活着,肯定包含这个文件,源头的地址被编码在文件名中。
2)根据时间。在同group下,获取最小的一个同步时间点(各个storage在同一时间,同步完成的时间点不一样)。若在最小同步时间点之前的文件,按照用户的规则随意选择一个storage。若在最小同步时间点之后的文件,选择源storage提供给客户端。
四、海量小文件存储
4.1 海量小文件存储的问题
通常我们认为大小在1MB以内的文件称为小文件,百万级数量及以上称为海量,由此量化定义海量小文件问题。 如社交网站、电子商务、广电、网络视频、高性能计算比如
1)Facebook存储了600亿张以上的图片,推出了专门针对海量小图片定制优化的Haystack进行存储。
2)淘宝目前应该是最大C2C电子商务网站,存储超过200亿张图片,平均大小仅为15KB,也推出了针对小文件优化的TFS文件系统存储这些图片,并且进行了开源。
那么海量小文件存储会带来哪些问题呢?
首先我们先了解一下Linux文件存储结构。
Linux自动将硬盘分成两个区域,
1)一个是inode区(inode table),相当于索引,存放inode所包含的信息,即存放的文件信息。
2)一个是bock数据区,存放文件数据。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。
假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB数据就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。也就是说,inode越多越浪费磁盘资源。
因此,海量小文件主要有2个问题:
1)如果小文件都小于1KB,而系统初始化的时候每个2K设置一个node,则此时一个文件还是至少占用2K的空间,最后导致磁盘空间利用率不高,小于50%。
2)大量的小文件,导致在增加、查找、删除文件的时候需要遍历过多的node节点,影响效率。
4.2 小文件机制配置
对于小文件,FastDFS采用合并存储的方式,合并存储后的文件称为 trunk 文件。
合并文件存储相关的配置都在tracker.conf中。配置完成后,重启tracker和storage server。
支持小文件存储,只需要设置tracker的use_trunk_file=true, store_server=1, 其他保持默认即可,但也要注意slot_max_size 的大小,这样才知道多大的文件触发小文件存储机制。
#是否启用trunk存储,缺省false,需要打开配置
use_trunk_file = true
#trunk文件最小分配单元 字节,缺省256,即使上传的文件只有10字节,也会分配这么多空间。
slot_min_size = 256
#trunk内部存储的最大文件,超过该值会被独立存储,默认16M,
#超过这个size的文件,不会存储到trunk file中,而是作为一个单独的文件直接存储到文件系统中。
slot_max_size = 1MB
#trunk文件大小,默认 64MB,不要配置得过大或者过小,最好不要超过256MB。
trunk_file_size = 64MB
4.3 合并存储文件
向FastDFS上传文件成功时,服务器返回该文件的存取ID fileid。当没有启动合并存储时该fileid和磁盘上实际存储的文件一 一对应。当采用合并存储时就不再一 一对应,而是多个fileid对应的文件被存储成一个大文件。
合并存储后的大文件统称为Trunk文件,没有合并存储的文件统称为源文件。
Trunk文件文件名格式:fdfs_storage1/data/00/00/000001 文件名从1开始递增,类型为int。
4.3.1 启动小文件存储时服务返回给客户端的fileid变化
1、独立文件存储的file id
文件名(不含后缀名)采用Base64编码,包含如下5个字段(每个字段均为4字节整数):
group1/M00/00/00/wKgqHV4OQQyAbo9YAAAA_fdSpmg855.txt
这个文件名中,除了.txt为文件后缀,wKgqHV4OQQyAbo9YAAAA_fdSpmg855 这部分是一个base64编码缓冲区,组成如下:
1)storage_id(ip的数值型)源storage server ID或IP地址
2)timestamp(文件创建时间戳)
3)file_size(若原始值为32位则前面加入一个随机值填充,最终为64位)
4)crc32(文件内容的检验码)
5)随机数 (引入随机数的目的是防止生成重名文件)
wKgqHV4OQQyAbo9YAAAA_fdSpmg855
| 4bytes | 4bytes | 8bytes |4bytes | 2bytes |
| ip | timestamp | file_size |crc32 | 随机数 |
2、小文件存储的file id
如果采用了合并存储,生成的文件ID将变长,文件名后面多了base64文本长度16字符(12个字节)。
这部分同样采用Base64编码,包含如下几个字段:
group1/M00/00/00/eBuDxWCwrDqITi98AAAA-3Qtcs8AAAAAQAAAgAAAAIA833.txt
采用合并的文件ID更长,因为其中需要加入保存的大文件id以及偏移量,具体包括了如下信息:
1)storage_id(ip的数值型)源storage server ID或IP地址
2)timestamp(文件创建时间戳)
3)file_size:实际的文件大小
4)crc32:文件内容的crc32码
5)trunk file ID:大文件ID如000001
6)offset:文件内容在trunk文件中的偏移量
7)alloc_size:分配空间,大于或等于文件大小
8)随机数 (引入随机数的目的是防止生成重名文件
eBuDxWCwrDqITi98AAAA-3Qtcs8AAAAAQAAAgAAAAIA833
| 4bytes | 4bytes | 8bytes |4bytes | 4bytes | 4bytes | 4bytes |2bytes |
| ip | timestamp | file_size |crc32 | trunk ID | offset | alloc_size | 随机数|
4.3.2 Trunk文件存储结构
trunk内部是由多个小文件组成,每个小文件都会有一个trunkHeader,以及紧跟在其后的真实数据,结构如下:
|||——————————————————————— 24bytes ——————————————————————————————————————|||
|—1byte —|— 4bytes —|— 4bytes —|—4bytes— |—4bytes—|———— 7bytes ————|
|—filetype—|—alloc_size—|—filesize—|—crc32 —|—mtime —|—formatted_ext_name—|
|||—————————————————————— file_data filesize bytes ——————————————————————|||
|———————————————————————— file_data ———————————————————————————————————————|
Trunk文件为64MB(默认),因此每次创建一次Trunk文件总是会产生空余空间.比如为存储一个10MB文件,创建一个Trunk文件,那么就会剩下接近54MB的空间(TrunkHeader 会24字节,后面为了方便叙述暂时忽略其所占空间)。下次要想再次存储10MB文件时就不需要创建新的文件,存储在已经创建的Trunk文件中即可。当 trunk 文件中的小文件被全部删除后,trucnk 文件才会被删除。
4.3.3 小文件存储平衡树
如何查询空闲块
在 storage 内部会为每个 store_path 构造一棵以空闲块大小作为关键字的空闲平衡树,相同大小的空闲块保存在链表中。每当需要存储一个文件时会首先到空闲平衡树中查找大于并且最接近的空闲块,然后试着从该空闲块中分割出多余的部分作为一个新的空闲块,加入到空闲平衡树中。若找不到满足的空闲块,则新建一个 trunk 文件 64MB,并加入到空闲平衡树,再一次执行上述查找操作。
五、FastDFS文件同步
文件上传成功后,其它的storage server才开始同步,其它的storage server怎么去感知,tracker server是怎么通知storage server?
1、storage定时发送心跳包到tracker,并附带同步的时间节点。
2、tracker收到后,向其他的 Storage Server 发送同步命令,通知它们去复制保存该文件。3、接收到 tracker 的同步命令后,其他的 storage 将请求源 storage 获取文件数据,并在本地进行保存,确保所有的 storage 都拥有相同的文件副本。
5.1 同步规则
- 只在本组内的storage server之间进行同步;
- 源头数据才需要同步,备份数据不需要再次同步,否则就构成环路了,源数据和备份数据区分是用binlog的操作类型来区分,操作类型是大写字母,表示源数据,小写字母表示备份数据;
- 当先新增加一台storage server时,由已有的一台storage server将已有的所有数据(包括源头数据和备份数据)同步给该新增服务器。
5.2 binlog
FastDFS文件同步采用binlog异步复制方式。storage server使用binlog文件记录文件上传、删除等操作,根据binlog进行文件同步。binlog中只记录文件ID和操作,不记录文件内容。例如
1646123002 C M00/00/00/oYYBAF285cOIHiVCAACI-7zX1qUAAAAVgAACC8AAIkT490.txt
1646123561 d M00/00/00/oYYBAF285luIK8jCAAAJeheau6AAAAAVgABI-cAAAmS021.xml
binlog文件有三列,依次为:
1)时间戳
2)操作类型
3)文件ID(不带group名称)
∘
\quad \circ
∘ Storage_id(ip的数值型)
∘
\quad \circ
∘ timestamp(创建时间)
∘
\quad \circ
∘ file_size(若原始值为32位则前面加入一个随机值填充,最终为64位)
∘
\quad \circ
∘ crc32(文件内容的检验码)
文件操作类型采用单个字母编码,其中源头操作用大写字母表示,被同步的操作为对应的小写字母。文件操作字母含义如下
binlog 位置:base_path/data/sync/,例如
root@xx:/home/fastdfs/storage_group1_23000/data/sync#
5.3 mark
为解决文件同步如何推送给不同的 storage,文件同步采用增量方式,记录已同步的位置到mark文件中。mark文件存放路径为:$base_path/data/sync/。
增量同步是指在文件系统中只同步发生变化的部分,而不是对整个文件进行完全复制或传输。
//binlog索引id 表示上次同步给114.215.169.67机器的最后一条binlog文件索引
binlog_index=0
//当前时间binlog 大小 (单位是字节)表示上次同步给114.215.169.67机器的最后一条binlog偏移量,
// 若程序重启了,也只要从这个位置开始向后同步即可。
binlog_offset=3944
//是否需要同步老数据
need_sync_old=1
//是否同步完成
sync_old_done=1
//同步已有数据文件的截至时间
until_timestamp=1621667115
//扫描记录数
scan_row_count=68
//每次同步操作时从源 Storage Server 读取的数据行数
sync_row_count=53
5.4 binlog同步过程
在FastDFS之中,每个Storaged之间的同步都是由一个独立线程负责的,该线程中的所有操作都是以同步方式执行的。比如一组服务器有A、B、C三台机器,那么在每台机器上都有两个线程负责同步,如A机器,线程1负责同步数据到B,线程2负责同步数据到C。
第一步:获取组内的其他Storage信息,并启动同步线程 ---- tracker_report_thread_entrance
在Storage.conf配置文件中,只配置了Tracker的IP地址,并没有配置组内其他的Storage。因此同组的其他Storage必须从Tracker获取。具体过程如下:
1)Storage启动时为每一个配置的Tracker启动一个线程负责与该Tracker的通讯。
2)默认每间隔30秒,与Tracker发送一次心跳包,在心跳包的回复中,将会有该组内的其他Storage信息。
3)Storage获取到同组的其他Storage信息之后,为组内的每个其他Storage开启一个线程负责同步。
第二步:同步线程执行过程 ---- storage_sync_thread_entrance
每个同步线程负责一台Storage的同步,以阻塞方式进行。
1)打开对应Storage的mark文件,如负责114.215.169.67的同步则打开
114.215.169.67_23000.mark文件,从中读取binlog_index、binlog_offset两个字段值,如取到值为:0、100,那么就打开binlog.000文件,seek到100这个位置。
2)进入一个while循环,尝试着读取一行,若读取不到则睡眠等待。若读取到一行,并且该行的操作方式为源操作,如C、A、D、T(大写的都是),则将该行指定的操作同步给对方(非源操作不需要同步),同步成功后更新binlog_offset标志,该值会定期写入到114.215.169.67_23000.mark文件之中。
第三步:若遇到同步前删除
假如同步较为缓慢,那么有可能在开始同步一个文件之前,该文件已经被客户端删除,此时同步线程将打印一条日志,然后直接接着处理后面的Binlog。
5.5 新增节点同步
在已有节点A、B上,新增节点 storage C。A、B竞争关系,主动请求做同步推送
5.6 高可用和高并发
通过使用 tracker 集群,storage 集群,即冗余服务,实现高可用。
高并发问题:
1)写并发
增加 group,水平扩展
注意:增加 storage 没有用,同组 storage 需要推送文件给其他 N-1 个 storage。
2)读并发
增加 storage,适用于少写多读的场景
增加 group,水平扩展
六、一些面试要点
1、storage如何分组
在 FastDFS 中,可以通过分组(Group)的方式对 Storage Server 进行分组管理。每个分组(Group)由一个或多个 Storage Server 组成,它们共享一个组名相同的存储路径。
要进行 Storage 分组,需要进行以下步骤:
1)在 Tracker Server 的配置文件 tracker.conf
中,设置group_name
参数为你想要的分组名。例如:group_name=group1
。
2)在每个 Storage Server 的配置文件storage.conf
中,设置 group_name
参数为相同的分组名。例如:group_name=group1
。
3)在 storage.conf
中,通过配置 store_path0
或 store_path1
参数指定存储路径,以供分组内的 Storage Server 共享。例如:store_path0=/data/fastdfs/group1
。
4)启动并注册每个 Storage Server 到 Tracker Server。
通过这样的配置和启动步骤,多个 Storage Server 将会被分组,它们共享相同的组名和存储路径。这样的分组管理可以提高系统的可靠性和容错性,并能够实现数据的冗余备份与负载均衡。
需要注意的是,在 FastDFS 中,Tracker Server 并不直接管理分组,而是通过存储路径来识别和管理不同的分组。因此,每个分组应该使用唯一的存储路径,以确保分组的正确识别和管理。
2、不同group里的storage能否做同步
在 FastDFS 中,不同 Group 中的 Storage Server 之间是无法直接进行数据同步的。每个 Group 内的 Storage Server 是通过共享相同的存储路径来实现数据的冗余备份和负载均衡,但不同 Group 之间的数据同步需要通过其他方式来实现。
如果你需要在不同的 Group 之间进行数据同步,可以考虑使用 FastDFS 提供的文件复制功能。文件复制功能允许将一个文件从一个 Group 中的 Storage Server 复制到另一个 Group 中的 Storage Server。你可以使用 FastDFS 提供的命令行工具 fdfs_storaged 来进行文件复制操作。
3、存储的storage服务器宕机了,其他storage如何同步
当一个存储(Storage)服务器宕机时,FastDFS 中的其他存储服务器无法自动进行数据同步。但是,FastDFS 提供了一些手动操作来实现数据的同步和恢复。
以下是一种常见的方案来处理存储服务器宕机的情况:
1)检测存储服务器宕机:可以通过监控脚本或其他方式来检测存储服务器的可用性,一旦发现宕机情况,就可以触发后续的同步操作。
2)从备份中恢复数据:如果存储服务器宕机导致数据丢失,可以从备份中恢复数据。FastDFS 支持使用 fdfs_storaged 命令行工具进行备份和还原操作。可以使用备份文件将数据还原到其他存储服务器上。
3)手动同步文件:如果存储服务器没有数据丢失,只是处于离线状态,可以手动将该存储服务器上的文件同步到其他存储服务器。可以使用 fdfs_storaged 工具的 sync_file 命令来实现文件的手动同步。
4)更新 Tracker Server:在恢复和同步操作完成后,需要更新 Tracker Server 的状态信息,以确保它能正确指导客户端访问可用的存储服务器。
4、FastDFS如何应对单点故障
FastDFS 的 Tracker Server 是支持高可用性的,可以使用多个 Tracker Server 构成一个集群来实现系统的容错和负载均衡。但是,在默认情况下,每个 Group 中只能有一个 Tracker Server 用于管理该 Group 中的 Storage Server,因此,如果该 Tracker Server 发生故障将会影响到整个 Group 的运行。
为了应对这种单点故障,可以采取以下措施:
1)启用多个 Tracker Server:将多个 Tracker Server 构成一个集群,同时为每个 Group 分配多个 Tracker Server 来管理该 Group 中的 Storage Server。这样可以增加系统的可靠性和容错性,即使某个 Tracker Server 发生故障,其他 Tracker Server 仍然可以继续工作。
2)使用 Keepalived 进行负载均衡:可以在多个 Tracker Server 上部署 Keepalived,如果某个 Tracker Server 发生故障,Keepalived 会自动将虚拟 IP 转移到其他可用的 Tracker Server 上,以保证服务的连续性和可靠性。
3)定期备份数据:可以定期备份 Tracker Server 的数据,以便在发生故障时能够快速恢复数据。可以使用 FastDFS 自带的 fdfs_storaged 工具进行数据备份和还原。
5、如何防止盗链
FastDFS内置防盗链采用Token的方式。
token是带时效的,包含了文件ID、时间戳ts和密钥。
在设定的时间范围内,比如5分钟内,token是有效的。
FastDFS在URL中带上当前时间戳和带时效的token,参数名分别为ts和token。
FastDFS API中提供了生成token的算法,扩展模块中会对token进行检验。
token的生成和检验都在服务器端,因此不会存在安全问题。
http.conf中防盗链相关的几个参数如下:
http.anti_steal.check_token:是否做token检查,缺省为false
http.anti_steal.token_ttl:token TTL,即生成token的有效时长
http.anti_steal.secret_key:生成token的密钥,尽量设置得长一些,千万不要泄露出去
http.anti_steal.token_check_fail:token检查失败,返回的文件内容,需指定本地文件名
配置示例如下:
# if use token to anti-steal
# default value is false (0)
http.anti_steal.check_token=false
# token TTL (time to live), seconds
# default value is 600
http.anti_steal.token_ttl=900
# secret key to generate anti-steal token
# this parameter must be set when http.anti_steal.check_token set to true
# the length of the secret key should not exceed 128 bytes
http.anti_steal.secret_key=FastDFS1234567890
# return the content of the file when check token fail
# default value is empty (no file sepecified)
http.anti_steal.token_check_fail=/home/yuqing/fastdfs/conf/anti-steal.jpg
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务器课程