UNP 简介

news2024/11/16 8:25:15

目录

从一个简单的时间获取客户端开始

socket

指定服务器 IP 地址与端口

与服务器建立连接并读取数据

简单的时间获取服务端

Unix 标准


从一个简单的时间获取客户端开始

接下来,将从一个使用 TCP 连接的获取时间的客户端开始。

// 以下代码与 UNP intro/daytimetcpcli.c 等价
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// inet_pton/3, htons/1
#include <arpa/inet.h>

// struct sockaddr_in
#include <netinet/in.h>

// errno
#include <errno.h>

// socket/3, connect/3
#include <sys/socket.h>
#include <sys/types.h>

// read/3
#include <unistd.h>

#define MAXLINE 4096

void err_sys(const char* fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  char buffer[MAXLINE + 1] = {0};
  vsnprintf(buffer, MAXLINE, fmt, ap);
  int n = strlen(buffer);
  snprintf(buffer + n, MAXLINE - n, ":%s", strerror(errno));
  strcat(buffer, "\n");
  fflush(stdout);
  fputs(buffer, stderr);
  fflush(stderr);
  va_end(ap);
  exit(1);
}

void err_quit(const char* fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  char buffer[MAXLINE + 1] = {0};
  vsnprintf(buffer, MAXLINE, fmt, ap);
  strcat(buffer, "\n");
  fflush(stdout);
  fputs(buffer, stderr);
  fflush(stderr);
  va_end(ap);
  exit(1);
}

int main(int argc, char** argv) {
  if (argc != 2) {
    err_quit("usage: a.out <IPaddress>");
  }
  int sockfd;
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    err_sys("socket error");
  }
  struct sockaddr_in servaddr = {
    .sin_family = AF_INET,
    .sin_port = htons(13),
  };
  if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
    err_quit("inet_pton error for %s", argv[1]);
  }
  if (connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0) {
    err_sys("connect error");
  }
  int n;
  char recvline[MAXLINE + 1];
  while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
    recvline[n] = 0;
    if (fputs(recvline, stdout) == EOF) {
      err_sys("read error");
    }
  }
  if (n < 0) {
    err_sys("read error");
  }
  return 0;
}

当然需要自行编译一下 daytimetcpsrv 并启动它,然后就可以顺利启动 client 就可以看到获取的时间了。

socket

我们最先遇到的 API 是 socket/3,这会帮助我们启动一个网络服务,我们可以自己指定 domaintype ,还有一个特殊的 protocol 参数用于指定 socket 一起使用的特定协议,但这个参数通常情况下为 0。下表展示了常用的 domain 与 type 值,摘自 Linux Kernel 5.3.18。

 

我们往往使用 AF_INETSOCK_STREAM 来启动一个 TCP 连接。而该 API 会返回一个整数作为返回结果,成功时将返回新 socket 的文件描述符 (fd),而失败时返回 −1-1−1 并设置 errno,而 errno 可以让我们得知具体发生了什么错误。

// 构建 IPv4 socket
tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
raw_socket = socket(AF_INET, SOCK_RAW, protocol);
// 构建 IPv6 socket
tcp6_socket = socket(AF_INET6, SOCK_STREAM, 0);
raw6_socket = socket(AF_INET6, SOCK_RAW, protocol);
udp6_socket = socket(AF_INET6, SOCK_DGRAM, protocol);

 

指定服务器 IP 地址与端口

使用 sockaddr_in 数据结构 (头文件 netinet/in.h 中) 保存服务器的 IPv4 信息,结构体如下

// IPv4
struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr {
    in_addr_t      s_addr;     /* address in network byte order */
};

// Ipv6
struct sockaddr_in6 {
    sa_family_t     sin6_family;   /* AF_INET6 */
    in_port_t       sin6_port;     /* port number */
    uint32_t        sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr;     /* IPv6 address */
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */
};

struct in6_addr {
    unsigned char   s6_addr[16];   /* IPv6 address */
};

另外系统使用大端序或小端序,而网络编程中往往需要使用网络序,因此提供了一系列将系统序 (host byte order) 转换为网络序 (network byte order) 的函数 (头文件 arpa/inet.h),不过在有些系统中这些函数被包含在 netinet/in.h 中。使用这些函数就可以自由的定义端口值。

uint32_t htonl(uint32_t hostlong);   // 将 unsigned int 类型 host to network
uint16_t htons(uint16_t hostshort);  // 将 unsigned short 类型 host to network
uint32_t ntohl(uint32_t netlong);    // 将 unsigned int 类型 network to host
uint16_t ntohs(uint16_t netshort);   // 将 unsigned short 类型 network to host

最后一个与IP信息有关的函数就是 inet_pton/3 (presentation to numeric,头文件 arpa/inet.h 中),这是一个伴随着 IPv6 诞生的 POSIX 函数,可以将 IPv4 的点分十进制字符串形式或 IPv6 的字符串行形式地址转换为 in_addrin6_addr 的字节形式。这个函数会返回一个整数用来确定函数是否成功,111 代表了成功,而 000 代表没有包含有效的地址,−1-1−1 是最为严重的,表示不是有效的协议族,这还会将 errno 设置为 EAFNOSUPPORT。与这个函数相关的还有一个 inet_ntop/4,它与前一个函数作用相反,最后一个参数 size 则指示了 buffer 可以接收的字节大小。下面一段程序是 Manual 中给出的示例,于此贴出。

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    unsigned char buf[sizeof(struct in6_addr)];
    int domain, s;
    char str[INET6_ADDRSTRLEN];
    if (argc != 3) {
	fprintf(stderr, "Usage: %s {i4|i6|<num>} string\n", argv[0]);
	exit(EXIT_FAILURE);
    }
    domain = (strcmp(argv[1], "i4") == 0) ? AF_INET :
	     (strcmp(argv[1], "i6") == 0) ? AF_INET6 : atoi(argv[1]);
    s = inet_pton(domain, argv[2], buf);
    if (s <= 0) {
	if (s == 0) {
	    fprintf(stderr, "Not in presentation format");
	} else {
	    perror("inet_pton");
	}
	exit(EXIT_FAILURE);
    }
    if (inet_ntop(domain, buf, str, INET6_ADDRSTRLEN) == NULL) {
	perror("inet_ntop");
	exit(EXIT_FAILURE);
    }
    printf("%s\n", str);
    exit(EXIT_SUCCESS);
}
// ./a.out i4 127.0.0.1
// 127.0.0.1
// ./a.out i6 0:0:0:0:0:0:0:0
// ::
// ./a.out i6 1:0:0:0:0:0:0:8
// 1::8
// ./a.out i6 0:0:0:0:0:FFFF:204.152.189.116
// ::ffff:204.152.189.116

 以前常用的函数则是支持 IPv4 的 inet_addr/1 系列函数

 

与服务器建立连接并读取数据

建立连接使用 connect/3 完成,先看一下函数原型

// #include <sys/socket.h>
// #include <sys/types.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该函数接收 sockfd 用于监听主机端口,并使用 addr 接收服务器的地址、端口等信息,最后一个参数则是 addr 的数据类型大小。

当然 socket 类型会影响该函数的行为:SOCK_DGRAM 类型的 socket,addr 是默认发送和接收数据报的地址;而 SOCK_STREAM 和 SOCK_SEQPACKET 类型的 socket,将会试图与指定地址建立连接。通常来说,基于连接的 socket 只允许调用成功一次该函数,而无连接的 socket 可以多次调用该函数来更改关联的地址,并且自 Linux Kernel 2.2 以后,可以通过连接到将 sa_family 设置为 AF_UNSPEC 来解除地址关联。

读取一个文件的数据往往采用 POSIX 函数 read/3 ,接收 fd、buffer、接收的字节长度 size,最终会返回实际读取的字节长度,但是返回 -1 时代表发生错误并会设置 errno

往往在读取服务器数据时,采用循环的方式读取,直到读到的数据大小为 0 才认为此次读取完成。虽然这个程序中每次读取都会直接读完所有数据 (因为数据只有 26 byte,而一次可以接收 4096 byte)。

在这个示例中,数据传输完成由服务器关闭连接表示,这与 HTTP/1.0 是类似的;而 SMTP 采用两个字节序的 ASCII 回车符后跟 ASCII 换行符表示数据结束;Sun RPC 和 DNS 则是用一个包含数据大小的字段来确定何时结束数据。这些结果的背后是 TCP 不提供数据标记 ,你需要自己选择如何记录数据结束。

简单的时间获取服务端

这里的服务端是对应上一节中的客户端,为客户端提供获取时间的服务。

// 以下代码与 UNP intro/daytimetcpsrv.c 等价
#include <stdbool.h>
#include <stdio.h>
#include <time.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h>

#define LISTENQ 1024
#define MAXLINE 4096

int main(int argc, const char* argv[]) {
  int listenfd = socket(AF_INET, SOCK_STREAM, 0);
  if (listenfd == -1) {
    err_sys("listen error");
  }
  struct sockaddr_in servaddr = {
    .sin_family = AF_INET,
    .sin_port = htons(13),
    .sin_addr.s_addr = htonl(INADDR_ANY),
  };
  if (bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) == -1) {
    err_sys("bind error");
  }
  if (listen(listenfd, LISTENQ) == -1) {
    err_sys("listen error");
  }
  while (true) {
    int connfd = accept(listenfd, (struct sockaddr*) NULL, NULL);
    if (connfd == -1) {
      err_ret("connect error");
      continue;
    }
    time_t ticks = time(NULL);
    char buffer[MAXLINE + 1] = {0};
    snprintf(buffer, sizeof(buffer), "%.24s\r\n", ctime(&ticks));
    if (write(connfd, buffer, strlen(buffer)) == -1) {
      err_ret("write error");
    }
    if (close(connfd) == -1) {
      err_ret("close error");
    }
  }
}

 可以看到与客户端不同的是,服务器在申请 socket 时没什么变化,但接下来服务器填写了自己的 IP 与 Port 信息,Port 用于指示接下来监听的端口,而 IP 则是指定为了 INADDR_ANY,这是系统中预定的 IP 地址的值。

 

有趣的是,如果使用 INADDR_ANY 的话,客户端可以使用路由器、网线提供的 DHCP 地址访问服务器,而改为 INADDR_LOOPBACK 时仅可通过 127.0.0.1 进行访问。当然这些数值都需要通过 htonl/1 转换为网络序。

可能初看时不是很理解 bind 的含义,而且这个函数和 connect 的参数几乎一致。不过不同的是,connect 用于客户端向服务器发起申请连接,而 DGRAM 类型则是将 fd 与 IP 信息绑定。这里 bind 也是将 IP 信息与 fd 进行绑定,根据 Manual 的解释,sockfd 申请之后存在于地址族的名称空间之中,但没有绑定地址,因此需要此函数对 fd 与 addr 进行绑定。

最后一个 listen/2 就是监听给出的 addr 及 port,这里的 sockfd 主要是 SOCK_STREAM 与 SOCK_SEQPACKET 类型的 socket。(SOCK_DGRAM 又一次被排除在外了) listen 的第二个参数,根据 Manual 的 NOTE 章节,目前版本的 Linux 代表了成功 accept 的 TCP 连接队列的长度。

在与客户端通信时,往往使用 accept/3 接收来自客户端的连接所创建的 connfd,经过这一步后,TCP 完成连接从而状态改变为 ESTABLISHED。当数据被准备好之后,服务器就可以通过 write/3 将数据写入 connfd 发送给客户端,这与平时向文件中写内容是类似的。最后我们只需要像关闭文件一样关闭 connfd 就可以了。

但是这个程序存在一些问题, while (true) 可以让服务器一直循环等待客户端请求到来,但是一次只能接收一个请求并处理,这对大量请求情况下显然是不合适的。但在示例中,我们仅使用了标准库函数 time 和 ctim,运行速度很快。但现实中我们可能需要几十秒甚至一分钟处理一个请求,这时这样的服务器是不可接受的。

示例中的服务器被称为 迭代服务器 (iterative server),对于每个请求都迭代执行一次;同时处理多个请求的服务器被称为 并发服务器 (concurrent server)。而从 shell 终端启动一个服务器可能会一直运行,因此我们常常使用 Unix 守护进程 (daemon),可以在后台运行,而不跟任何终端关联地运行。

 

Unix 标准

Unix 标准通常被称作 POSIX,即 Portable Operating System Interface (可移植操作系统接口)。当然由于当时有多个机构进行标准化工作,因此 Unix 标准又有很多名字 ISO/IEC 9945IEEE Std 1003.1 和单一 Unix 规范 (Signle UNIX Specification),当然经过发展,POSIX 也经历过版本更新,UNP 所介绍到了 POSIX.1-2001,如今最新标准为 POSIX.1-2017

  • POSIX 第一版为 IEEE Std. 1003.1-1988,这一版规范了 Unix 内核的 C 语言接口,这些接口覆盖了进程原语 (fork,exec,signals 等)、进程环境 (user ID,程序组等)、文件与目录 (所有的 I/O 函数)、终端 I/O、系统数据库 (密码文件与组文件) 以及 tar、cpio 等归档格式。不过当时 POSIX 被称为 IEEE-IX ,POSIX 这个名称是 Richard Stallman 建议使用的。
  • IEEE Std. 1003.1-1990 为第二版,这一版正式称为 ISO 标准 ISO/IEC 9945:1990 。这一版的改动很小,添加了新的副标题 “Part 1: System Application Program Interface (API) [C language]”
  • IEEE Std. 1003.2-1992 扩展到了两卷本,第二卷 “Part 2: Shell and Utilities”。这一卷定义了 shell (基于 System V Bourne Shell) 与大约 100 中命令行工具 (awk, vi,yacc等),第二卷往往也被称为 POSIX.2
  • IEEE Std. 1003.1b-1993 升级自 1990 版,添加了 P1003.4 工作组开发的实时扩展部分,并新增了文件同步、异步 I/O、信号量、内存管理 (mmap 与共享内存)、进程调度、始终与定时器以及消息队列。
  • IEEE Std. 1003.1c-1995 又被称为 ISO/IEC 9945-1:1996 ,包含了之前的 API、事实扩展、pthreads (1003.1c-1995) 以及对 1003.1b 的技术性修订。增添了三章关于线程的内容以及关于线程同步 (mutexes 与 condition_variables)、线程调度、线程同步以及信号句柄。将 ISO/IEC 9945 分为三部分
    • Part 1: System API (C language) — 被称为 POSIX.1
    • Part 2: Shell and utilities — 被称为 POSIX.2
    • Part 3: System administration (正在开发)
  • IEEE Std. 1003.1g: Protocol-independent interfaces (PII) (协议无关接口) 定义了两个 API,被称为 Detailed Network Interfaces (DNI,详尽网络接口):
    • DNI/Socket,基于 4.4BSD socket API
    • DNI/XTI,基于 X/Open XPG4 规范

X/Open 公司与开放软件基金会 (OSF) 合并成了 The Open Group,这是一个由厂商、工业界最终用户、政府以及学术机构共同参加的标准化组织。

  • 1989 年 X/Open 发布了 X/Open 可移植指南第三期 (XPG3)
  • 1992 年发布了 XPG4,而第二版于 1994 年发布。这个版本也被称作 Spec 1170 (规范 1170),1170 是 926 个系统 API、70 个头文件 以及 174 个命令的总和。最终被称为 X/Open Signle UNIX Specification,或 Unix 95
  • 1997 年三月发布了 Signle Unix Specification 第二版,又称为 Unix 98 。这一版的魔数上升到了 1434,而工作站魔数直接到了 3030。这是因为该规范包含了 Common Desktop Environment (CDE,公共桌面环境),这导致必须依赖 X Window System 以及 Motif 用户接口。

再之后,单一 UNIX 规范与 POSIX.1 逐渐统一起来,UNP 主要介绍 POSIX.1-2001 (即 IEEE Std 1003.1-2001)。

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

后台管理项目重构为vue3.0

目录前言&#xff1a;为什么要重构项目&#xff1f;重构的目的具体案例下载项目一. 为什么要重构后台管理项目二. 安装项目所需的vue3.0 插件三. 具体代码重构四. 在更改中遇到的bug总结前言&#xff1a; 我们平常玩的游戏有时需要更新出新的内容&#xff0c;我们的项目也需要…

组件化、模块化、集中式、分布式、服务化、面向服务的架构、微服务架构

目录 1.组件化与模块化 1.1.组件化 2.模块化 2.1.模块化和组件化的区别 3.集中式与分布式 3.1.集中式 3.2.分布式 4.服务化 5.面向服务的架构 5.1.什么是SOA 5.2.实现SOA 5.3.面向对象和面向服务的对比 6.微服务架构 6.1.SOA和微服务 7.总结 最近最火的词是什么…

1月份 GameFi 行业报告

Jan. 2023&#xff0c; DanielData Source&#xff1a; January Monthly GameFi Report在经历了艰难的一年之后&#xff0c;1 月是对加密货币市场最有利的月份。虽然可以说的大部分内容适用于其他看涨周期&#xff0c;但有几个统计数据令 1 月在区块链领域非常有趣。例如&#…

花3个月面过京东测开岗,拿个20K不过分吧?

背景介绍 计算机专业&#xff0c;代码能力一般&#xff0c;之前有过两段实习以及一个学校项目经历。第一份实习是大二暑期在深圳的一家互联网公司做前端开发&#xff0c;第二份实习由于大三暑假回国的时间比较短&#xff08;小于两个月&#xff09;&#xff0c;于是找的实习是在…

GPU并行效率问题——通过MPS提升GPU计算收益

现象描述使用V100_32G型号的GPU运行计算程序时&#xff0c;发现程序每5秒能够完成一次任务&#xff0c;耗费显存6G。鉴于V100 GPU拥有32G的显存&#xff0c;还有很多空闲&#xff0c;决定同时运行多个计算程序&#xff0c;来提升GPU计算收益。然而&#xff0c;这一切都是想当然…

Kotlin新手教程一(Kotlin简介及环境搭建)

目录一、 什么是Kotlin&#xff1f;二、为什么要使用Kotlin&#xff1f;三、使用IntelliJ IDEA搭建Kotlin四、Kotlin使用命令行编译一、 什么是Kotlin&#xff1f; Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言&#xff0c;它也可以被编译成为 JavaScript 源代码&…

IDEA入门安装使用教程

一、背景 作为一个Java开发者&#xff0c;有非常多编辑工具供我们选择&#xff0c;比如Eclipse、IntelliJ IDEA、NetBeans、Visual Studio Code、Sublime Text等等&#xff0c;这些有免费也有收费的&#xff0c;但是就目前市场占比来说普遍使用Eclipse和IntelliJ IDEA这两款主…

字节软件测试岗:惨不忍睹的三面,幸好做足了准备,月薪19k,已拿offer

我今年25岁&#xff0c;专业是电子信息工程本科&#xff0c;19年年末的时候去面试&#xff0c;统一投了测试的岗位&#xff0c;软件硬件都有&#xff0c;那时候面试的两家公司都是做培训的&#xff0c;当初没啥钱&#xff0c;他们以面试为谎言再推荐去培训这点让我特别难受。后…

让huggingface/transformers的AutoTokenizer从本地读词表

https://stackoverflow.com/questions/62472238/autotokenizer-from-pretrained-fails-to-load-locally-saved-pretrained-tokenizer

ArcGIS网络分析之构建网络分析数据集(一)

说明: 1. 本文主要用于演示网络分析服务的搭建过程。所以在此不会深入讨论网络分析服务的每一个细节,本文的目的就是让初学者学会使用网络分析服务进行基本的分析(主要针对后续的WEB开发):路径分析,最近设施点分析,以及服务区分析。 2.关于OD成本矩阵分析,多路径配送,…

如何解决 Python 错误 NameError: name ‘X‘ is not defined

Python“NameError: name is not defined”发生在我们试图访问一个未定义的变量或函数时&#xff0c;或者在它被定义之前。 要解决该错误&#xff0c;需要确保我们没有拼错变量名并在声明后访问它。 确保你没有拼错变量或函数 下面是产生上述错误的示例代码。 employee {na…

【MySQL进阶】SQL优化

&#x1f60a;&#x1f60a;作者简介&#x1f60a;&#x1f60a; &#xff1a; 大家好&#xff0c;我是南瓜籽&#xff0c;一个在校大二学生&#xff0c;我将会持续分享Java相关知识。 &#x1f389;&#x1f389;个人主页&#x1f389;&#x1f389; &#xff1a; 南瓜籽的主页…

论文阅读笔记《Common Visual Pattern Discovery via Spatially Coherent Correspondences》

核心思想 两组点集中共有的匹配区域通常具备两个特点&#xff1a;1.局部的特征相似&#xff1b;2.特征点在空间上的分布也相似。作者将候选匹配点对作为图的节点&#xff0c;将两种相似性统一到边的权重来表示。通过寻找图中稠密连接的子图来寻找两个点集中的匹配区域&#xff…

2023年浙江建筑特种工(施工升降机)真题题库及答案

百分百题库提供特种工&#xff08;施工升降机&#xff09;考试试题、特种工&#xff08;施工升降机&#xff09;考试预测题、特种工&#xff08;施工升降机&#xff09;考试真题、特种工&#xff08;施工升降机&#xff09;证考试题库等,提供在线做题刷题&#xff0c;在线模拟考…

大数据DataX(一):DataX的框架设计和插件体系

文章目录 DataX的框架设计和插件体系 一、DataX是什么

Python客户端使用SASL_SSL连接Kafka需要将jks密钥转换为pem密钥,需要转化成p12格式再转换pem才能适配confluent_kafka包

证书生成 生成证书以及jks参考以下文章 https://blog.csdn.net/qq_41527073/article/details/121148600 证书转换jks -> pem 需要转化成p12以下转换才能适配confluent_kafka包&#xff0c;直接jks转pem会报错不能使用&#xff0c;具体参考以下文章 https://www.ngui.cc/z…

我的Android启动优化—【黑白屏优化】

简述 在Android App使用过程中&#xff0c;对于应用的优化是一个加分项&#xff0c;举个例子&#xff0c;打开你的App需要2秒&#xff0c;人家0.5秒&#xff0c;这就是很大的用户体验上的优化。 问题的产生 在开发中&#xff0c;我们在启动app的时候&#xff0c;屏幕会出现一…

C语言之函数

函数不可嵌套定义&#xff0c;函数之间为平行关系#include<stdio.h> //比较大小 int max(int x,int y){int z;if(x>y){zx;}else{zy;}return z;//带返回值 }//不带参数 void help(void){printf("**********hello********\n"); }int main(){int num;nummax(3,…

Kubernetes集群编排工具helm3 工作学习记录

文章目录1. helm 介绍、组件、安装和目录结构1.1 helm 介绍1.2 helm 组件1.2.1 helm2 和 helm3 的区别1.3 helm 安装1.4 helm 目录结构2. 编写一个chart 和 helm3 内置对象讲解2.1 创建编写一个chart2.1.1 创建chart,引用内置对象的变量值2.1.2 helm通过各种类型chart包安装一个…

微信上线外卖小程序“门店快送”

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 抖音入局外卖&#xff0c;微信都开始下场做外卖了&#xff0c;难道都知道这块儿特赚钱吗&#xff0c;只能说如今大环境还是不太好&#xff0c;南山必胜客都开始搞兼职了…… 2月15日&#xff0c;有…