当前位置: 首页 > news >正文

【linux 多进程并发】0302 Linux下多进程模型的网络服务器架构设计,实时响应多客户端请求

0302 多进程网络服务器架构

专栏内容

  • postgresql使用入门基础
  • 手写数据库toadb
  • 并发编程

个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.

一、概述


在大规模数据处理中,会有大量的客户端接入同一台服务器,每个客户端都需要长时间提供服务。

服务器采用中心化的部署,而客户端往往分散在不同机器上,服务器与客户端之间跨网络通信,一般采用C/S架构。

而服务端的架构需要能应对大量并发客户端,同时可以给每个客户端独占的服务,这就用到了多任务的网络模型架构,下面我们来看看用多进程如何实现。

二、多路复用的网络模型


C/S架构中,处理大量的网络请求,需要一套基于多路复用的网络处理模型。

  • 可以同时处理网络连接请求和网络数据传递;
  • 减少程序的阻塞时间,避免无效的CPU消耗;
  • 适应不同的并发规模;

以此为目标实现如下网络模型。

2.1 服务端网络监听

多路复用模型这里采用了epoll方式,如果自己的平台不支持,可以换为select或者poll的方式。

在这里插入图片描述

代码如下:

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <arpa/inet.h>  
#include <sys/epoll.h>  
#include <fcntl.h>  
#include <errno.h>  #define MAX_EVENTS 10  
#define BUFFER_SIZE 1024  
#define PORT 8080  // 设置文件描述符为非阻塞模式  
int set_nonblocking(int fd) {  int flags, s;  flags = fcntl(fd, F_GETFL, 0);  if (flags == -1) {  perror("fcntl F_GETFL");  return -1;  }  flags |= O_NONBLOCK;  s = fcntl(fd, F_SETFL, flags);  if (s == -1) {  perror("fcntl F_SETFL");  return -1;  }  return 0;  
}  int main() {  int listen_fd, conn_fd, nfds, epoll_fd;  struct sockaddr_in server_addr;  struct epoll_event ev, events[MAX_EVENTS];  char buffer[BUFFER_SIZE];  ssize_t count;  // 创建监听socket  listen_fd = socket(AF_INET, SOCK_STREAM, 0);  if (listen_fd == -1) {  perror("socket");  exit(EXIT_FAILURE);  }  // 设置非阻塞模式  if (set_nonblocking(listen_fd) == -1) {  close(listen_fd);  exit(EXIT_FAILURE);  }  // 绑定地址和端口  server_addr.sin_family = AF_INET;  server_addr.sin_addr.s_addr = INADDR_ANY;  server_addr.sin_port = htons(PORT);  if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {  perror("bind");  close(listen_fd);  exit(EXIT_FAILURE);  }  // 开始监听  if (listen(listen_fd, SOMAXCONN) == -1) {  perror("listen");  close(listen_fd);  exit(EXIT_FAILURE);  }  // 创建epoll实例  epoll_fd = epoll_create1(0);  if (epoll_fd == -1) {  perror("epoll_create1");  close(listen_fd);  exit(EXIT_FAILURE);  }  // 添加监听socket到epoll实例  ev.events = EPOLLIN;  ev.data.fd = listen_fd;  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {  perror("epoll_ctl: listen_fd");  close(listen_fd);  close(epoll_fd);  exit(EXIT_FAILURE);  }  // 主循环  while (1) {  nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);  if (nfds == -1) {  perror("epoll_wait");  exit(EXIT_FAILURE);  }  for (int n = 0; n < nfds; ++n) {  if (events[n].data.fd == listen_fd) {  // 新的连接  conn_fd = accept(listen_fd, NULL, NULL);  if (conn_fd == -1) {  perror("accept");  continue;  }  // 设置非阻塞模式  if (set_nonblocking(conn_fd) == -1) {  close(conn_fd);  continue;  }  // 添加新的连接socket到epoll实例  ev.events = EPOLLIN | EPOLLET;  ev.data.fd = conn_fd;  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {  perror("epoll_ctl: conn_fd");  close(conn_fd);  }  } else {  // 处理读事件  conn_fd = events[n].data.fd;  while ((count = read(conn_fd, buffer, BUFFER_SIZE)) > 0) {  // 处理接收到的数据(这里简单回显)  write(conn_fd, buffer, count);  }  if (count == -1 && errno != EAGAIN) {  // 出现错误或连接关闭  close(conn_fd);  } else if (count == 0) {  // 连接关闭  close(conn_fd);  }  // 从epoll实例中移除已关闭的socket  if (count <= 0 && errno != EAGAIN) {  ev.events = 0;  epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn_fd, &ev);  }}  }  }  close(listen_fd);  close(epoll_fd);  return 0;  
}

说明

  • TCP服务端的基本步骤创建socket,设置为非阻塞模式,绑定IP与端口,开启监听;
  • 这里服务端的socket需要设置为非阻塞模式,因为我们是在单进程中处理多个连接,每个连接不能阻塞等待;
  • 然后加入到epoll监听池中,开始epoll事件的等待;这里只处理接收事件;
  • 如果有服务端socket的接收事件,那么说明有客户端连接消息,进行accep,创建客户端连接的socket;
  • 同样将客户端连接的socket设置为非阻塞,理由同上;加入epoll临听池中,同样也只处理接收事件;
  • 如果有客户端连接的socket上的接收事件,那么说明客户端正在给服务端发消息;
  • 收到客户端消息后,这里只是简单处理,原样再发给客户端;
  • 如果客户端关闭或出错,将客户端连接关闭,并从epoll临听池中移除;

2.2 客户端测试

现在我们来编写一个简单的客户端模拟程序,测试一下多路复用的网络框架。

/** ex020302_client.c*/#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <arpa/inet.h>  #define SERVER_IP "127.0.0.1"  
#define SERVER_PORT 4808  
#define BUFFER_SIZE 1024  #define CLIENT_SEND_CNT 20int main() 
{  int sockfd;  struct sockaddr_in server_addr;  char buffer[BUFFER_SIZE] = {0};  const char *message = "Hello, Server!";  // 创建套接字  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {  perror("socket creation failed");  exit(EXIT_FAILURE);  }  // 配置服务器地址信息  server_addr.sin_family = AF_INET;  server_addr.sin_port = htons(SERVER_PORT);  // 将IP地址从字符串转换为二进制形式  if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {  perror("Invalid address/ Address not supported");  close(sockfd);  exit(EXIT_FAILURE);  }  // 连接到服务器  if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {  perror("Connection Failed");  close(sockfd);  exit(EXIT_FAILURE);  }  for(int i = 0; i < CLIENT_SEND_CNT; i++){// 发送消息到服务器  send(sockfd, message, strlen(message), 0);  printf("Message sent: %s\n", message);  // 接收服务器的响应  int bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);  if (bytes_received < 0) {  perror("Error in receiving");  } else if (bytes_received == 0) {  printf("Server closed the connection\n");  } else {  buffer[bytes_received] = '\0'; // 确保字符串以空字符结尾  printf("Message received from server: %s\n", buffer);  }  sleep(1);}// 关闭套接字  close(sockfd);  return 0;  
}

说明

  • TCP客户端建立的基本步骤,创建socket,初始化服务端地址,连接服务器;
  • 然后向服务端发送相同的消息;
  • 每次发送完成后,等待接收消息;

2.3 客户端测试

可以看到,服务端处理客户端的请求时,都是按照接收到的顺序进行串行处理;

当客户端的数量达到成百上千时,对客户端的响应时间就会出现非常明显的延迟,

这种延迟会随着业务的复杂度而放大。

这时就需要充分利用多核CPU硬件资源,来进行并发任务的处理。

三、多进程服务处理


上面是在单个任务进程中处理了监听和大量任务连接的网络处理,各客户端连接的服务会相互影响,实际是串行化处理的。

要让大量的客户端能同时被响应,需要采用多任务的方式,那么在上面的网络模型基础上加入多进程,服务端为每个客户端连接准备一个独立的进程,这样就可以及时响应。

3.1 多进程架构

首先我们利用前面几个章节的介绍,来搭建一个多进程的代码架构,由主进程根据需要进行创建子进程,并且由主进程进行全局的控制。

在这里插入图片描述

/** ex020302_netprocess.c*/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <arpa/inet.h>  
#include <sys/epoll.h>  
#include <fcntl.h>  
#include <errno.h>  #define MAX_EVENTS 10  
#define BUFFER_SIZE 1024  
#define PORT 4808  void daemon_fork()
{int pid = -1;pid = fork();if(pid < 0){printf("fork error[%s]\n",strerror(errno));exit(-1);}else if(pid > 0){// parent exit.exit(0);}else {// child daemonreturn;}
}void subprocess(int sock)
{int pid = -1;pid = fork();if(pid < 0){printf("fork error[%s]\n",strerror(errno));exit(-1);}else if(pid > 0){// parent.close(sock);return;}else {// child close(listen_fd);processMsg(sock);exit(0);}
}

说明

  • daemon服务程序函数,这个前一章节已经介绍过了,服务端以后台进程的方式运行;
  • 子进程任务处理函数;这里创建的是任务子进程,并在子进程中调用消息处理函数;
  • 这里需要注意的是,在子进程中要关闭服务端的socket,同时在父进程中要关闭客户端连接的socket; 因为父子进程会复制内存空间,但是在各自的进程中,已经不再需要;

3.2 并发网络处理模型

现在就可以将上面的多路复用网络处理放入多进程架构中,处理逻辑进行如下切分:

  • 服务端监听socket初始化,多路复用器的初始化等,都放在主进程中,作为服务端网络初始化的一部分;
  • 每个客户端连接的socket,以及它的读写消息处理逻辑,放在子进程中;这样每个客户端连接对应一个后台服务子进程;
  • 创建子进程的时机,也就是在主进程中接收到新连接时,创建新连接成功后,就可以新建子进程进行处理;
  • 而子进程的退出时间,就是客户端断开连接,或者处理出错时;
void initializeServerNet()
{struct sockaddr_in server_addr;  // 创建监听socket  listen_fd = socket(AF_INET, SOCK_STREAM, 0);  if (listen_fd == -1) {  perror("socket");  exit(EXIT_FAILURE);  }  // 绑定地址和端口  server_addr.sin_family = AF_INET;  server_addr.sin_addr.s_addr = INADDR_ANY;  server_addr.sin_port = htons(PORT);  if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {  perror("bind");  close(listen_fd);  exit(EXIT_FAILURE);  }// 开始监听if (listen(listen_fd, SOMAXCONN) == -1){perror("listen");close(listen_fd);exit(EXIT_FAILURE);}
}void closeServerFd()
{close(listen_fd);  
}void dispatchLoop()
{int conn_fd;  // 主循环  while (1) {  // 新的连接  conn_fd = accept(listen_fd, NULL, NULL);  if (conn_fd == -1) {  perror("accept");  sleep(1);continue;  }subprocess(conn_fd);}  
}void processMsg(int sock)
{char buffer[BUFFER_SIZE];  ssize_t count;  printf("serv-process:%d start.\n");while ((count = read(sock, buffer, BUFFER_SIZE)) > 0){// 处理接收到的数据(这里简单回显)write(sock, buffer, count);}if (count == -1 && errno != EAGAIN){// 出现错误或连接关闭close(sock);}else if (count == 0){// 连接关闭close(sock);}printf("serv-process:%d exit.\n");
}

那么主程序实现如下:

void daemon_fork();
void subprocess(int sock);
void processMsg(int sock);void initializeServerNet();
void closeServerFd();
void dispatchLoop();int listen_fd;int main(int argc ,char *argv[])
{daemon_fork();initializeServerNet();dispatchLoop();closeServerFd();return 0;
}
  • 在主进程中先进程服务端初始化;
  • 然后就可以开始监听,并接收客户端的连接;
  • 当有客户端连接时,就创建客户端连接,并启动子进程与该客户端进行网络通信;
  • 子进程在客户端断开连接或出错时,就会退出;

2.3 客户端测试

可以看到将客户端发送次数调大后,开启的客户端越多,服务端启动的子进程也就会越多;

此时,可以看到服务端每个进程的CPU使用率并不是很高;

但是随着客户端数量越来越多,服务端进程数量超过CPU核数时,就会增加系统的负担;

四、总结


本文主要介绍了基于多进程架构的网络服务器的设计与实现,在多进程架构中每个客户端会有一个服务端的进程专门处理通信,增加了对客户端消息的响应效率,提升了并发处理能力。

结尾


非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!

作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。

注:未经同意,不得转载!

相关文章:

【linux 多进程并发】0302 Linux下多进程模型的网络服务器架构设计,实时响应多客户端请求

0302 多进程网络服务器架构 ​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 一、概…...

LTE及EPC技术原理(笔记)

无线网络发展历史 20世纪80年代&#xff1a;模拟技术和FDMA 20世纪90年代&#xff1a;数字技术和TDMA 21世纪初&#xff1a;数字技术和CDMA LTE进步 下行100Mbps&#xff0c;上行50Mbps 用户面时延10-20ms&#xff0c;控制面时延小于100ms 带宽从1.4MHz~20MHz&#xff0…...

穿越数据迷宫

第一章 在未来的世界里&#xff0c;人类的生活已经被高度数字化。互联网不再是简单的信息交换平台&#xff0c;而是成为了一个庞大的虚拟世界——“数据迷宫”。在这个世界里&#xff0c;每个人都有一个独特的数字身份&#xff0c;他们的生活、工作、娱乐都离不开这个虚拟空间…...

FBX福币交易所国际油价突然大涨!美伊针锋相对

11月4日早上,国际原油大幅高开。WTI原油一度涨超2%。 消息面上,主要产油国宣布延长自愿减产措施至12月底 FBX福币凭借用户友好的界面和对透明度的承诺,迅速在加密货币市场中崭露头角,成为广大用户信赖的平台。 石油输出国组织(欧佩克)发表声明说,8个欧佩克和非欧佩克产油国决…...

Java项目管理与SSM框架介绍

Maven简介 Maven是一个项目管理工具。它可以帮助程序员构建工程&#xff0c;管理jar包&#xff0c;编译代码&#xff0c;完成测试&#xff0c;项目打包等等。Maven工具是基于POM&#xff08;Project Object Model&#xff0c;项目对象模型&#xff09;实现的。在Maven的管理下每…...

WorkFlow源码剖析——Communicator之TCPServer(中)

WorkFlow源码剖析——Communicator之TCPServer&#xff08;中&#xff09; 前言 上节博客已经详细介绍了workflow的poller的实现&#xff0c;这节我们来看看Communicator是如何利用poller的&#xff0c;对连接对象生命周期的管理。&#xff08;PS&#xff1a;与其说Communica…...

在做题中学习(73):删除字符串中所有相邻重复项

解法&#xff1a;用栈来模拟 思路&#xff1a;不用真的定义一个栈,用字符串string来模拟栈的行为 入栈&#xff1a;push_back(s[i]) 出栈:s[i] s.back()的时候&#xff0c;并且s.size() > 0&#xff0c;循环结束得到结果 注意&#xff1a;如果真的用stack<char>来…...

springboot 单元测试-各个模块举例

controller单测 import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Moc…...

MS01SF1 精准测距UWB模组助力露天采矿中的人车定位安全和作业效率提升

在当今矿业行业&#xff0c;随着全球对资源需求的不断增加和开采难度的逐步提升&#xff0c;传统的作业方式面临着越来越多的挑战。露天矿山开采&#xff0c;因其大规模的作业环境和复杂的地形特点&#xff0c;面临着作业人员的安全风险、设备调度的高难度以及资源利用率低下等…...

Android亮屏Job的功耗优化方案

摘要: Job运行时会带来持锁的现象,目前灭屏放电Job的锁托管已经有doze和绿盟标准监管,但是亮屏时仍旧存在过长的持锁现象,故为了优化功耗和不影响用户体验下,新增亮屏放电下如果满足冻结和已运行过一次Job,则进行job限制,当非冻结时恢复的策略 1.现象: (gms_schedu…...

React05 样式控制 classnames工具优化类名控制

样式控制 & classnames工具优化类名控制 样式控制1. 行内样式控制2. 外部样式控制 classnames工具优化类名控制 样式控制 1. 行内样式控制 //定义样式 const style {color: red,fontSize: 30px }function App() {return (<div className"App">{/* 行内样…...

OJ-5G网络建设

示例1 输入&#xff1a; 3 3 1 2 3 0 1 3 1 0 2 3 5 0 输出&#xff1a; 4示例2 输入&#xff1a; 3 1 1 2 5 0 输出&#xff1a; -1 示例3 输入&#xff1a; 3 3 1 2 3 0 1 3 1 0 2 3 5 1 输出&#xff1a; 1 分析&#xff1a;压缩路径 顺序&#xff1a;1 2&#xff1b;…...

Linux简介

1.Linux定义 Linux 是免费使用和自由传播的类 Unix 操作系统&#xff0c;是基于 POSIX 和 UNIX 的多用户、多任务、支持多线程和多 CPU 的操作系统。Linux 能运行主要的 UNIX 工具软件、应用程序和网络协议。它支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思…...

android——渐变色

1、xml的方式实现渐变色 效果图&#xff1a; xml的代码&#xff1a; <?xml version"1.0" encoding"utf-8"?> <shape xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools…...

MySQL约束管理

介绍 MySQL约束管理是指在MySQL数据库中定义和管理数据约束的过程。数据约束用于维护数据的完整性和一致性&#xff0c;确保数据在表中的存储符合特定的规则。通过约束&#xff0c;可以防止不符合要求的数据被插入或更新&#xff0c;从而保护数据库的质量。 约束管理的主要内…...

拯救者y7000p 打开XMP

拯救者y7000p 打开XMP 拯救者bios隐藏功能 第一步、开机按F2进入bios 第二步、点击more settings 第三步、依次按Fnrn再按F12保存重启 第四步、再进bios&#xff0c;点击more settings则显示更多可调制选项&#xff0c;可找到内存超频功能&#xff0c;进行xmp超频 如果第三步失…...

2024 Rust现代实用教程Iterator迭代器

文章目录 一、迭代与循环1.循环2.迭代iteration3.区别 二、Intoiterator、Iterator和Iter之间的关系1.Intolterator2.Iterator Trait3. 源码中经常出现的iter 三、获取迭代器的三种方法iter(),iter_mut()和into_iter()1.iter()方法2.iter_mut()方法3.into_iter()方法---尽量写 …...

基于SpringBoot司机信用评价的货运管理系统【附源码】

基于SpringBoot司机信用评价的货运管理系统 效果如下&#xff1a; 系统主页面 系统注册页面 司机注册页面 管理员主页面 订单评价页面 货物信息页面 个人信息页面 研究背景 随着我国物流行业的迅猛发展&#xff0c;货运管理系统的效率与安全性日益受到重视。在货运过程中&am…...

使用PostgreSQL进行高效数据管理

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用PostgreSQL进行高效数据管理 PostgreSQL简介 安装PostgreSQL 在Ubuntu上安装PostgreSQL 在CentOS上安装PostgreSQL 在macOS上…...

数据库条件查询排查——引号故障

一、错误代码 $where_查询职汇总员[$value头[EmpCode]]$value职员[EmpCode]; 二、正常写法 $where_查询职汇总员[EmpCode]$value职员[EmpCode]; 三、原因 前一个是变量嵌套&#xff0c;这里不需要嵌套...

Python爬虫:揭开淘宝商品描述的神秘面纱

在这个信息爆炸的时代&#xff0c;我们每天都在和时间赛跑。作为一名Python开发者&#xff0c;你是否曾梦想拥有超能力&#xff0c;能够瞬间揭开淘宝商品描述的神秘面纱&#xff1f;今天&#xff0c;就让我们一起化身为代码界的“福尔摩斯”&#xff0c;使用Python爬虫技术&…...

动态规划— 一和零

class Solution {public int findMaxForm(String[] strs, int m, int n) {int[][] dp new int[m1][n1];//dp[i][j]表示i个0和j个1时的最大子集int oneNum 0, zeroNum 0;for(String str : strs){oneNum 0;zeroNum 0;for(char c : str.toCharArray()){if(c 0){zeroNum;}els…...

【Android】SharedPreferences存储中没有 Double 类型数据存储的解决方式

项目需求 存储定位数据&#xff0c;需要保存到小数点后10位数据。 需求分析 项目需求看起来很简单&#xff0c;其实实现起来也不难&#xff0c;我们直接使用SharedPreferences 存储一下就好了&#xff0c;反正也没其他要求。 好了&#xff0c;直接使用SharedPreferences 存…...

ffmpeg:视频字幕嵌入(GPU加速)

实现方案 参考指令 ffmpeg -i input_video.mp4 -vf "subtitlessubtitles.srt" output_video.mp4 解决因文件名称复杂导致的指令执行失败问题&#xff08;引号给文件框起来&#xff09; ffmpeg -i "A.mp4" -vf "subtitlesB.srt" "c.mp4&qu…...

DCN网络进行新冠肺炎影像分类

项目源码获取方式见文章末尾! 600多个深度学习项目资料,快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【BiLSTM模型实现电力数据预测】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现mnist手写数字识别】…...

C++中的继承——第二篇

一、继承与友元 友元关系不能够继承&#xff08;就像父亲的朋友不一定是自己的朋友&#xff09; 具体实现起来就是父类的友元可以访问父类的成员&#xff0c;但是不可以访问子类的成员 二、继承与静态成员 子类的静态成员变量本质上与父类的是同一份&#xff0c;存储在静态…...

动态规划探索篇

Leetcode63——不同路径Ⅱ 题目描述&#xff1a; 给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角&#xff08;即 grid[0][0]&#xff09;。机器人尝试移动到 右下角&#xff08;即 grid[m - 1][n - 1]&#xff09;。机器人每次只能向下或者向右移动一步。 网格…...

js中多let与var

在 JavaScript 中&#xff0c;let 和 var 都用于声明变量&#xff0c;但它们有一些关键的区别。主要区别包括作用域、变量提升、可重复声明、以及在全局作用域中的行为。 1. 作用域&#xff08;Scope&#xff09; let&#xff1a;块级作用域。用 let 声明的变量只在其所在的代…...

基于人工智能的搜索和推荐系统

互联网上的搜索历史分析和用户活动是个性化推荐的基础&#xff0c;这些推荐已成为电子商务行业和在线业务的强大营销工具。随着人工智能的使用&#xff0c;在线搜索也在改进&#xff0c;因为它会根据用户的视觉偏好提出建议&#xff0c;而不是根据每个客户的需求和偏好量身定制…...

冷钱包与热钱包的差异 | 加密货币存储的安全方案

随着加密货币的普及&#xff0c;越来越多的人开始重视加密资产的安全存储问题。钱包作为存储数字资产的工具&#xff0c;主要分为冷钱包和热钱包两大类。它们在安全性、便捷性以及适用场景方面各有优劣。了解这两者的差异&#xff0c;有助于投资者根据自己的需求选择合适的钱包…...