C++ 网络编程学习三

C++ 网络编程学习三

    • 用智能指针延长session的生命周期
    • 处理粘包问题


用智能指针延长session的生命周期

问题:

  • 客户端断开后:会触发服务器对应session的写或读事件,由于是异步编程,需要在回调中对读写事件进行处理。
  • 客户端断开, 则应该析构掉该session。但是此时该session在asio底层回调队列中可能还有很多读写函数对象在排队等着执行 。 如果在某个读写回调对象把这个session析构掉了,那之后执行的读写回调函数可能会再次析构这个session。
  • 所以我们需要保证, 在该session对应asio底层回调队列中,还存在将要执行的读写回调函数时,该session不被析构。通过智能指针来实现伪闭包,延长session的生命周期。
  • 智能指针传给函数对象,函数对象不释放,智能指针也就不会被释放掉。

  • 把智能指针传递给session用的回调函数,函数内部再使用智能指针,这个时候智能指针就不被释放。

  • 假如包含智能指针的函数没有调用怎么办?用lambda表达式和bind强制将智能指针中的shared_ptr加1。

  • 构造一个伪闭包:

    • 利用智能指针被复制或使用引用计数加一的原理保证内存不被回收
    • bind操作可以将值绑定在一个函数对象上生成新的函数对象,如果将智能指针作为参数绑定给函数对象,那么智能指针就以值的方式被新函数对象使用,那么智能指针的生命周期将和新生成的函数对象一致,从而达到延长生命的效果。
// 包含智能指针的Server类。
class CServer
{
public:// 构造函数CServer(boost::asio::io_context& io_context, short port);void ClearSession(std::string uuid);
private:void HandleAccept(std::shared_ptr<CSession>, const boost::system::error_code& error);void StartAccept();boost::asio::io_context& _io_context;// 上下文short _port;// 端口tcp::acceptor _acceptor;// 通过智能指针方式管理Session类,将acceptor接收的连接保存在Session类型的智能指针里。// 在Server类中添加成员变量,该变量为一个map类型,key为Session的uid,value为该Session的智能指针。std::map<std::string, std::shared_ptr<CSession>> _sessions;// 通过Server中的_sessions这个map管理链接,可以增加Session智能指针的引用计数,只有当Session从这个map中移除后,Session才会被释放。};class CSession :public std::enable_shared_from_this<CSession> {
public:// 上下文初始化CSession,socket绑定上下文CSession(boost::asio::io_context& io_context, CServer* server);tcp::socket& GetSocket() { return _socket; }std::string& GetUuid() { return _uuid; }void Start();void Send(char* msg, int max_length);
private:enum { MAX_LENGTH = 1024 };void HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> _self_shared);void HandleWrite(const boost::system::error_code& error, std::shared_ptr<CSession> _self_shared);tcp::socket _socket;std::string _uuid;char _data[MAX_LENGTH];CServer* _server;std::queue<std::shared_ptr<MsgNode> > _send_que;std::mutex _send_lock;};
// CServer类
CServer::CServer(boost::asio::io_context& io_context, short port): _io_context(io_context), _acceptor(io_context, tcp::endpoint(tcp::v4(), port)) {cout << "Server start success, on port: " << port << endl;StartAccept();
}void CServer::StartAccept() {// new_session虽然是一个局部变量,但是通过智能指针和bind操作,将new_session作为数值传递给bind函数。// bind函数返回的函数对象内部引用了该new_session,所以引用计数加1,这样保证了new_session不会被释放。std::shared_ptr<CSession> new_session = make_shared<CSession>(_io_context, this);// placeholders::_1 占位符的作用是给HandleAccept函数一个错误码关键字。_acceptor.async_accept(new_session->GetSocket(), std::bind(&CServer::HandleAccept, this, new_session, placeholders::_1));
}void CServer::HandleAccept(std::shared_ptr<CSession> new_session, const boost::system::error_code& error) {if (!error) {new_session->Start();// 在接收连接的逻辑里将Session放入map_sessions.insert(make_pair(new_session->GetUuid(), new_session));// make_pair是圆括号,不是尖括号}else {cout << "session accept failed, error is " << error.what() << endl;}StartAccept();// 继续接收连接就是了
}// 将session从map中移除,当其引用计数为0时,自动释放
void CServer::ClearSession(std::string uuid) {_sessions.erase(uuid);
}// CSession类
// 构造函数
CSession::CSession(boost::asio::io_context& ioc, CServer* server) :_socket(ioc), _server(server) {boost::uuids::uuid  a_uuid = boost::uuids::random_generator()(); //boost提供的生成唯一id的函数_uuid = boost::uuids::to_string(a_uuid);// 将随机数转成string
}void CSession::Start() {memset(_data, 0, MAX_LENGTH);// 数据读到_data中,触发HandleRead回调,注意不能再定义一个智能指针。// shared_from_this()函数可以还用当前的智能指针。_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::HandleRead,this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> _self_shared) {if (!error) {cout << "read data is " << _data << endl;//发送数据Send(_data, bytes_transferred);// 继续read,重复调用HandleReadmemset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this,std::placeholders::_1, std::placeholders::_2, _self_shared));}else {std::cout << "handle write failed, error is " << error.what() << endl;_server->ClearSession(_uuid);// 杀死session}
}void CSession::HandleWrite(const boost::system::error_code& error, std::shared_ptr<CSession> _self_shared) {if (!error) {// 写数据的时候上锁std::lock_guard<std::mutex> lock(_send_lock);// 调用HandleWrite,说明肯定已经发送完一个数据,这个时候弹出一下,后面只要发送队列不为空,就一直发送。_send_que.pop();if (!_send_que.empty()) {auto& msgnode = _send_que.front();boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_msg, msgnode->_total_len),std::bind(&CSession::HandleWrite, this, std::placeholders::_1, _self_shared));}}else {std::cout << "handle write failed, error is " << error.what() << endl;_server->ClearSession(_uuid);// 杀死session}
}// 实现发送接口
void CSession::Send(char* msg, int max_length) {bool pending = false; // pending为true表示上一次数据没有发完。std::lock_guard<std::mutex> lock(_send_lock);if (_send_que.size() > 0) {pending = true;}_send_que.push(make_shared<MsgNode>(msg, max_length)); // 队列里有数据,就不发送了,让队列里面的回调函数发送就行了。if (pending) {return;}boost::asio::async_write(_socket, boost::asio::buffer(msg, max_length),std::bind(&CSession::HandleWrite,this,std::placeholders::_1,shared_from_this()));
}

处理粘包问题

粘包问题:当客户端发送多个数据包给服务器时,服务器底层的tcp接收缓冲区收到的数据为粘连在一起的,是服务器的问题,不是客户端的问题。
客户端发送: hello world! hello world!
服务器接收:hello world! hello world!
客户端给服务器发送了两个hello world! 服务其TCP缓冲区接收了两次,但是第一次接收的数据粘包了。

粘包原因:TCP发送数据的时候,数据逻辑性出了问题。

  • TCP底层通信是面向字节流的,TCP只保证发送数据的准确性和顺序性,字节流以字节为单位。
  • 客户端每次发送N个字节给服务端,N取决于当前客户端的发送缓冲区是否有数据。比如发送缓冲区总大小为10字节,当前有5字节未发送完,那么此时只有5个字节的空闲时间。
  • 此时调用接口发送hello world!, 就只能发送hello给服务器,那么服务器这次接收到的数据很可能就是连着其他数据的hello,下次才能收到world!。

还有其他产生粘包问题的原因:

  1. 客户端的发送频率远高于服务器的接收频率,服务器接收不过来,就会导致数据在服务器的tcp接收缓冲区滞留形成粘连。
  2. tcp底层的安全和效率机制不允许字节数特别少的小包发送频率过高,tcp会在底层累计数据长度到一定大小才一起发送。
  • 处理粘包的方法主要采用应用层定义收发包格式的方式,这个过程俗称切包处理。用消息id+消息长度+消息内容的tlv协议去切割数据包。

在代码中对粘包进行处理:

  • 定义新的数据结构体,数据包含两部分:消息长度+消息内容,用额外的2字节去存储当前消息的长度。
  • 接收消息数据的CSession类也需要更新。
  • 数据初始化的时候,就要初始化头部信息。

完善加上粘包处理后的逻辑:

  • 头部未解析:
    • 收到的数据不是满足头部的大小:未处理的数据加上头部当前缓存的数据,如果小于2字节,就说明头部数据没有接收完。
    • 收到的数据比头部多:头部的信息已经接收完,取出头部信息。定义数据节点,取出数据信息。
      • 若数据节点的长度< 头部信息长度:数据还没收完。将数据放到接收节点中,更新信息。
      • 若数据节点的长度大于等于头部信息长度:取出首包全部数据,头部节点清楚一下,轮询切包。
  • 头部已解析:已经处理完头部,消息体没有接收完。
    • 消息体还没有接收全:当前数据拷贝到消息节点里,继续监听对方发送。
    • 消息体长度够了,拷贝信息到消息节点,更新变量,把剩下的数据轮询切包。
      在这里插入图片描述
void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> shared_self) {if (!error) {/** copy_len 已经移动的字符数:调用一次HandleRead:会返回总共收到的字节数,会从零开始处理到bytes_transferred这么大,中间会有一些其他的处理,copy_len表示处理到哪里了。*/ int copy_len = 0; //copy_len表示处理到哪里了while (bytes_transferred > 0) {if (!_b_head_parse) { // 最开始的时候头部肯定还没有被解析// 先判断收到的数据是不是满足头部的大小:未处理的数据加上头部当前缓存的数据,如果小于2字节,就说明头部数据没有接收完if (bytes_transferred + _recv_head_node->_cur_len < HEAD_LENGTH) {// 将数据全部拷贝到头部节点memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, bytes_transferred);_recv_head_node->_cur_len += bytes_transferred;//已经拷贝了,头部节点已经处理的长度就要更新::memset(_data, 0, MAX_LENGTH);//清空// 继续去监听读事件_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));return;}else {// 收到的数据比头部多// 头部剩余未复制的长度int head_remain = HEAD_LENGTH - _recv_head_node->_cur_len;memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, head_remain);// 更新已处理的data长度copy_len 和 剩余未处理的长度bytes_transferredcopy_len += head_remain;bytes_transferred -= head_remain;// 获取头部数据 打印数据长度short data_len = 0;memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);cout << "data_len is " << data_len << endl;//头部长度非法 断开连接if (data_len > MAX_LENGTH) {std::cout << "invalid data length is " << data_len << endl;_server->ClearSession(_uuid);return;}_recv_msg_node = make_shared<MsgNode>(data_len); //数据节点// 消息的长度小于头部规定的长度,说明数据没有收全,则先将部分消息放在接收节点里if (bytes_transferred < data_len) {// 拷贝到节点里memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);_recv_msg_node->_cur_len += bytes_transferred;::memset(_data, 0, MAX_LENGTH);// 继续去接收读事件把_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));//头部处理完成_b_head_parse = true;return;}// 消息的长度大于等于头部规定的长度,说明这一节数据已经收齐了,可以读取接收了,需要进行切包。memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, data_len);_recv_msg_node->_cur_len += data_len;copy_len += data_len;bytes_transferred -= data_len;_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0'; //第一个消息包的数据取完了cout << "receive data is " << _recv_msg_node->_data << endl;//此处可以调用Send发送测试Send(_recv_msg_node->_data, _recv_msg_node->_total_len);//继续轮询剩余未处理数据_b_head_parse = false;_recv_head_node->Clear();if (bytes_transferred <= 0) {::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));return;}continue;}}else {//已经处理完头部,处理上次未接受完的消息数据//接收的数据仍不足剩余未处理的int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;// 这次接收到的消息体,还不满足整合成一个数据结构体。if (bytes_transferred < remain_msg) {memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);_recv_msg_node->_cur_len += bytes_transferred;::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));return;}// 接收的消息已经满足形成一个数据包结构体了,memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, remain_msg);_recv_msg_node->_cur_len += remain_msg;bytes_transferred -= remain_msg;copy_len += remain_msg;_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';cout << "receive data is " << _recv_msg_node->_data << endl;//此处可以调用Send发送测试Send(_recv_msg_node->_data, _recv_msg_node->_total_len);//继续轮询剩余未处理数据_b_head_parse = false;_recv_head_node->Clear();if (bytes_transferred <= 0) {::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));return;}continue;}}}else {std::cout << "handle write failed, error is " << error.what() << endl;_server->ClearSession(_uuid);// 杀死session}
}
  • 对于客户端:发送和接收数据的时候,也要先发送两个字节的数据长度,再发送数据消息的结构。
try{// 创建上下文服务boost::asio::io_context   ioc;//构造endpointtcp::endpoint  remote_ep(asio::ip::address::from_string("127.0.0.1"), 10086);tcp::socket  sock(ioc);boost::system::error_code   error = boost::asio::error::host_not_found; ;sock.connect(remote_ep, error);if (error) {cout << "connect failed, code is " << error.value() << " error msg is " << error.message();return 0;}std::cout << "Enter message: ";char request[MAX_LENGTH];std::cin.getline(request, MAX_LENGTH); //输入数据size_t request_length = strlen(request);char send_data[MAX_LENGTH] = { 0 };memcpy(send_data, &request_length, 2); // 先首部2字节,构造数据长度memcpy(send_data + 2, request, request_length); // 再构造数据体boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2));char reply_head[HEAD_LENGTH];size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_LENGTH)); // 先接收头部,获取信息长度short msglen = 0;memcpy(&msglen, reply_head, HEAD_LENGTH);char msg[MAX_LENGTH] = { 0 };size_t  msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen)); // 再接收尾部std::cout << "Reply is: ";std::cout.write(msg, msglen) << endl;std::cout << "Reply len is " << msglen;std::cout << "\n";}catch (const std::exception& e){std::cerr << "Exception: " << e.what() << "\n";}

参考列表
https://www.bilibili.com/video/BV1ys4y1D7Mu

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/499679.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(libusb) usb口自动刷新

文章目录 libusb自动刷新程序Code目录结构Code项目文件usb包code包 效果描述重置reset热拔插使用 END libusb 在操作USB相关内容时&#xff0c;有一个比较著名的库就是libusb。 官方网址&#xff1a;libusb 下载&#xff1a; 下载源码官方编好的库github&#xff1a;Release…

@Slf4j 变量log找不到符号,可能是 Gradle 配置文件写得有问题

Slf4j 变量log找不到符号 鄙人在学习 Java 的 spring boot 项目时, 常常因为 maven 配置文件使用 xml 格式过于复杂, 所以更倾向于使用 gradle 作为构建工具. 然而, 在使用 gradle 作为构建工具时, 又需要引用 Lombok 依赖. 有时忘记在初始化项目时添加上 Lombok 依赖, 所以经…

本届挑战赛亚军方案:基于大模型和多AGENT协同的运维

“轻舟已过万重山团队”荣获本届挑战赛亚军&#xff0c;该团队来自华为集团IT-UniAI 产品和openEuler系统智能团队。 方案介绍 自ChatGPT问世以来&#xff0c;AI迎来了奇点iPhone时刻&#xff0c;这一年来大模型深入影响企业办公&#xff0c;金融&#xff0c;广告&#xff0c;…

SDWAN异地组网难在哪?怎么解决?

SD-WAN作为一种先进的网络技术&#xff0c;为企业提供了更加灵活和高效的网络连接方案。然而&#xff0c;在异地组网的过程中&#xff0c;SD-WAN也面临一些挑战。本文将探讨SD-WAN异地组网所面临的难题&#xff0c;并提供相应的解决方案。 挑战一&#xff1a;网络延迟和不稳定性…

ptython迭代器与生成器

迭代器 Python中的迭代器&#xff08;Iterator&#xff09;是一种强大的工具&#xff0c;用于访问集合元素。它是一种可以记住遍历位置的对象&#xff0c;这意味着迭代器不会一次性生成所有的元素&#xff0c;而是可以等到需要的时候才生成&#xff0c;从而节省了大量的内存资…

智慧城市:打造宜居环境,引领未来可持续发展

随着科技的不断进步与创新&#xff0c;我们的城市正步入一个崭新的时代——智慧城市。智慧城市是指运用信息技术和大数据等现代科技手段&#xff0c;对城市基础设施、公共服务和社会管理进行智能化改造&#xff0c;实现城市各领域的智能化、信息化和高效化。今天&#xff0c;就…

SQL函数学习记录

聚合函数 函数是编程语言的基础之一&#xff0c;在对数字的运算中&#xff0c;我们用的最多的就是聚合函数&#xff0c;本篇接下来就详细阐述下SQL中聚合函数的运用。 什么是聚合函数&#xff08;aggregate function&#xff09;&#xff1f; 聚合函数指的是对一组值执行计算…

Pytorch模型训练后静态量化并加载int8量化模型推理

目录 一、源码包准备1.1 源码包获取1.2 代表性验证集1.3 Pytorch模型1.4 推理测试图片 二、环境准备三、模型转换3.1 参数修改3.2 代码3.3 量化转换结果3.4 量化前后模型大小对比 四、量化模型推理4.1 参数修改4.2 代码4.3 推理结果4.4推理时间 五、总结 一、源码包准备 1.1 源…

什么是Sectigo?如何优惠申请?

Sectigo&#xff0c;全球领先的SSL/TLS证书提供商&#xff0c;以其卓越的安全性能和广泛的认可度赢得了业界的一致好评。我们的证书不仅能加密您的网站通信&#xff0c;确保敏感信息传输过程中的绝对安全&#xff0c;还能显著提升您的网站信誉&#xff0c;让访客一眼就能识别出…

价格战打响!阿里云服务器和腾讯云服务器价格对比

2024年阿里云服务器和腾讯云服务器价格战已经打响&#xff0c;阿里云服务器优惠61元一年起&#xff0c;腾讯云服务器62元一年&#xff0c;2核2G3M、2核4G、4核8G、8核16G、16核32G、16核64G等配置价格对比&#xff0c;阿腾云atengyun.com整理阿里云和腾讯云服务器详细配置价格表…

什么是VR紧急情况模拟|消防应急虚拟展馆|VR游戏体验馆加盟

VR紧急情况模拟是利用虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;技术来模拟各种紧急情况和应急场景的训练和演练。通过VR技术&#xff0c;用户可以身临其境地体验各种紧急情况&#xff0c;如火灾、地震、交通事故等&#xff0c;以及应对这些紧急情况的…

第三百七十四回

文章目录 1. 概念介绍2. 实现方法2.1 基本用法2.2 特殊用法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"分享三个使用TextField的细节"相关的内容&#xff0c;本章回中将介绍如何让Text组件中的文字自动换行.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1.…

leetcode刷题(剑指offer) 46.全排列

46.全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2&#xff1a; 输入&#…

STM32F103学习笔记(七) PWR电源管理(原理篇)

目录 1. PWR电源管理简介 2. STM32F103的PWR模块概述 2.1 PWR模块的基本工作原理 2.2 电源管理的功能和特点 3. PWR模块的常见应用场景 4. 常见问题与解决方案 1. PWR电源管理简介 PWR&#xff08;Power&#xff09;模块是STM32F103系列微控制器中的一个重要组成部分&…

C语言 变量

变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型&#xff0c;类型决定了变量存储的大小和布局&#xff0c;该范围内的值都可以存储在内存中&#xff0c;运算符可应用于变量上。 变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头…

导出数据库表结构到文档中

导出效果&#xff1a; 完整代码&#xff1a; Controller层&#xff1a; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotatio…

Android Stdio Execution failed for task ‘:app:compileDebugKotlin‘ 报错解决

具体报错信息如下&#xff1a; compileDebugJavaWithJavac task (current target is 1.8) and compileDebugKotlin task (current target is 17)jvm target compatibility should be set to the same Java version.很显然&#xff0c;这是一个版本冲突问题&#xff0c;compile…

云上攻防-云服务篇弹性计算服务器云数据库实例元数据控制角色AK控制台接管

知识点: 1、云服务-弹性计算服务器-元数据&SSRF&AK 2、云服务-云数据库-外部连接&权限提升 章节点&#xff1a; 云场景攻防&#xff1a;公有云&#xff0c;私有云&#xff0c;混合云&#xff0c;虚拟化集群&#xff0c;云桌面等 云厂商攻防&#xff1a;阿里云&am…

Tomcat服务部署

1、安装jdk、设置环境变量并测试 第一步&#xff1a;安装jdk 在部署 Tomcat 之前必须安装好 jdk&#xff0c;因为 jdk 是 Tomcat 运行的必要环境。 1. #关闭防火墙 systemctl stop firewalld systemctl disable firewalld setenforce 02. #将安装 Tomcat 所需软件包传到/opt…

90%电商APP已沦落至无人下载,用户主观意愿——是真正实用性价值!

90%电商APP已沦落至无人下载&#xff0c;用户主观意愿——是真正实用性价值&#xff01; 文丨微三云营销总监胡佳东&#xff0c;点击上方“关注”&#xff0c;为你分享市场商业模式电商干货。 - 引言&#xff1a;在互联网发展的大时代下&#xff0c;似乎每个月都有新的APP出现…