网络编程套接字 | UDP套接字

news2024/10/3 8:25:05

前面的文章中我们叙述了网络编程套接字的一些预备知识点,从本文开始我们就将开始UDP套接字的编写。本文中的服务端与客户端都是在阿里云的云服务器进行编写与测试的。

udp_v1

在v1的版本中我们先来使用一下前面讲过得一些接口,简单的构建一个udp服务器:

// udp_server.cc
#include "udp_server.hpp"
#include <memory>

using namespace std;
using namespace ns_server;
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082)); // 通过智能指针控制服务器的资源管理,并且向程序传入ip与端口号
    usvr->InitServer(); //服务器的初始化
    usvr->Start();
    return 0;
}
// udp_server.hpp
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace ns_server
{
	// 用于展示返回对应的错误提示
    enum{
        SOCKET_ERR = 1,
        BIND_ERR
    };
    
    const static uint16_t default_port = 8080;
    
    class UdpServer
    {
    public:
        UdpServer(std::string ip, uint16_t port = default_port) : ip_(ip), port_(port)
        {
            std::cout << "server addr: " << ip << " : " << port_ << std::endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口,打开网络文件
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
            if (sock_ < 0)
            {
                std::cerr << "create socket error: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "create socket success: " << sock_ << std::endl; // 3
            // 2. 给服务器指明IP地址和端口号Port
            struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中! 
            bzero(&local, sizeof(local)); // 清空上述字段ud
            local.sin_family = AF_INET; // PF_INET 初始化socketaddr_in结构
            local.sin_port = htons(port_); // 本地主机序列构建的port_,需要从主机序列转变成网络序列
            // inet_addr: 1,2
            // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
            // 2. 需要将主机序列转化成为网络序列
            local.sin_addr.s_addr = inet_addr(ip_.c_str()); // sin_addr C++中的结构体在C++中可以进行转化,但是在C语言中不行
            // 这里需要将字符串转换uint32_t的类型,并且同时进行将主机序列转换成网络序列
            // inet_addr 函数将包含 IPv4 点十进制地址的字符串转换为IN_ADDR结构的正确地址。而在in_addr结构之中有in_addr_t s_addr的一个数据结构
            if (bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0) // 然后是绑定相关的套接字文件,此时需要就将前面在帧栈上定义的local进行绑定
            {
                std::cerr << "bind socket error: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind socket success: " << sock_ << std::endl; //3
        }
        void Start() {}
        ~UdpServer() {}
    private:
        int sock_;
        uint16_t port_;
        std::string ip_; // 后面需要去掉这个ip
    };
}

然后运行上述的程序会出现一个问题就是:

server addr: 1.1.1.1 : 8082
create socket success: 3
bind socket error: Cannot assign requested address

bind socket error: Cannot assign requested address 云服务器不需要bind ip地址,需要让服务器自己制定ip地址
云服务器,或者一款服务器,一般不要指明某一个确定的IP – 服务器可能有多张网卡,可能配有多个IP,我们要让我们的udpserver启动的时候bind本主机上的任意IP,然后我们对上述的v1版本进行修改。

// 需要修改的地方就是:
local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,bind本主机上的任意IP

server addr: 1.1.1.1 : 8082
create socket success: 3
bind socket success: 3

此时就可以正确的进行bind操作。

udp_v2

下面我们将上述的程序进行完善,添加上服务器正常工作的程序(我们想要完成的是客户端发送消息,服务端接收到消息并答应在终端上,同时将消息返回给客户端)

void Start()
{
    // 服务器的正常工作
    char buffer[1024];
    while (true)
    {
        // 收
         // ssize_t 实际写入的大小 recvfrom(int sockfd 绑定的套接字, void *buf 接受数据存放的缓冲区, size_t len 缓冲区长度, int flags 读取方式(0), struct sockaddr *src_addr 需要知道client的IP和PORT 输入接收缓冲区, socklen_t *addrlen 实际结构体的大小); 输入输出型参数 需要知道谁发的数据
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer); // 这里一定要写清楚,未来传入的缓冲区的大小
        int n = recvfrom(sock_, buffer, sizeof(buffer)-1/*因为这是以字符作为消息的类型,所以缓冲区预留一部分空间*/, 0, (struct sockaddr*)&peer, &len); // 消息的类型需要程序员来定义
        if (n > 0) buffer[n] = '\0';
        else continue;
        // 提取client信息
        std::string clientip = inet_ntoa(peer.sin_addr); // 把一个四字节的IP转化为字符串
        uint16_t clientport = ntohs(peer.sin_port); // 将从网络中获取的端口号转换成主机
        std::cout << clientip << "-" << clientport << "#" << buffer << std::endl;
        // 发
        sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len); // 往文件中去写的时候,不需要携带\0
    }
}

然后对udp_server.cc文件进行修改,我们想要使用./udp_server port的形式来运行程序,在运行程序的时候将端口号进行传入

static void usage(string proc)
{
    std::cout << "Usage:\n\t" << proc << " port\n" << std::endl;
}
// ./udp_server port
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]); // 此处为char*类型的数据若是要传入port需要进行转换
    unique_ptr<UdpServer> usvr(new UdpServer(port));
    // bind socket error: Cannot assign requested address 云服务器不需要bind ip地址,需要让服务器自己制定ip地址
    // 自己本地装的虚拟机或者是物理机器是允许的,
    usvr->InitServer(); //服务器的初始化
    usvr->Start();
    return 0;
}

然后是服务端的代码

// udp_client.cc
#include <string>
#include <cstring>
// 127.0.0.1 本地回环,表示的就是当前的主机,通常用来进行本地通信或者测试
static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}
// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_ERR);
    }
    // client 这里要不要bind? 要的!socket通信的本质[clientip:clientport, serverclient. serverport]
    // 要不要自己bind?不需要自己bind,os自动给我们进行bind!-- 为什么?client的port要随机让os分配防止client出现启动冲突
    // server为什么要自己bind -- 1. server的端口不能随意改变, 众所周知且不能随意改变;2. 同一家公司的port需要统一进行管理
    // 明确server是谁?
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    while (true)
    {
        // 用户输入
        std::string message;
        std::cout << "please Enter# ";
        std::cin >> message;
        // 什么时候bind?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport + 自己的IP 1. bind 2. 构建发送的数据报文
        // 发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
        // 接收
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}

在这里插入图片描述
这样我们就完成了一个简单的UDP网络通信程序。

udp_v3

下面我们还需要对上述的UDP网络通信进行修改,在上述的网络通信程序中服务端在接收到客户端的信息之后立即进行了处理,然后将信息进行返回,但是我们想要让信息的处理与网络IO进行分离,在第三版中我们就进行对应的修改。我们在工作空间中定义了using func_t = std::function<std::string (std::string)>;这样的参数为string,返回值为string的一个包装器,在udp_server类中新增了一个成员func_t service_;来进行业务处理。在编写构造函数的时候将这个成员初始化,那么我们就可以在外部传入一个执行业务的方法,在外部定义好了这个执行的方法,在外部即udp_server.cc中将业务处理完毕之后再进行网络IO。
下面就是对上述版本的修改

// udp_server.hpp
// 在Start()中发送消息前执行业务处理
void Start()
{
    // ...
    // 做业务处理
    std::string response = service_(buffer);
    // 发
    sendto(sock_, response.c_str(), response.size(), 0, (struct sockaddr*)&peer, len); // 往文件中去写的时候,不需要携带\0
}

然后我们就在cpp文件中处理上层业务

// udp_server.cc
std::string transactionString(std::string request)
{
    std::string result;
    char c;
    for(auto & r : request)
    {
        if (islower(r)) 
        {
            c = toupper(r);
            result.push_back(c);
        }
        else 
        {
            result.push_back(r);
        }
    }
    return result;
}

static bool isPass(const std::string& command)
{
    bool pass = true;
    auto pos = command.find("mv");
    if (pos != std::string::npos) pass = false;

    pos = command.find("rm");
    if (pos != std::string::npos) pass = false;

    return pass;
}

std::string execteCommand(std::string command)
{
    // FILE *popen(const char *command, const char *type);
    // 1. 创建管道
    // 2. 创建子进程
    // 3. 通过FILE*将结果直接返回,可以让用户以读取文件的访问,获得命令执行的结果

    // 安全检查
    if (!isPass(command)) return "bad";
    
    // 业务逻辑处理
    FILE* fp = popen(command.c_str(), "r");
    if (fp == nullptr) return "None";
    
    // 获取结果
    char line[2048];
    std::string result;
    while (fgets(line, sizeof(line), fp) != NULL)
    {
        result += line;
    }
    return result;

    fclose(fp);
}

int main()
{
	// ...
	// 通过传入第一个transactionString函数可以将我们客户端输入的小写字符,在服务端转换成大写然后在返回客户端
	unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port)); 
	// 这个业务方法就是将客户端输入的指令发送到服务端,在服务端执行后再将执行的结果返回给客户端
    // unique_ptr<UdpServer> usvr(new UdpServer(execteCommand, port));
}

大小写转换业务:
在图片描述
读取客户端指令的业务
在这里插入图片描述

udp_v4

上述第三版的程序还有一些问题就是,程序运行的时候收发的处理都在start()函数中,但是这样一旦阻塞在发送或者阻塞在收取的时候,假如有多个客户端要连接服务端,就会有影响。那么在第四版中我们就将结合前面的讲述过的基于环形队列的生产消费模型来将接受与发送分别使用两个线程来处理。在第四版中我们想要实现的是一个简易的udp多人聊天程序。

// udp_server.hpp
class UdpServer
{
public:
    UdpServer(uint16_t port = default_port) :port_(port)
    {
        pthread_mutex_init(&lock, nullptr);
        p = new Thread(1, std::bind(&UdpServer::Recv, this)); // 这里传入接受与发送的函数时如果直接传入会发生报错,以为Recv与Broadcast都是类的方法,而类的方法是有隐含的this指针的,此时就可以使用bind函数将this指针这个成员先绑定,然后就可以正常的运行。
        c = new Thread(2, std::bind(&UdpServer::Broadcast, this));
    }
    void Start()
    {
    	// ...
        c->run();
        p->run();
    }
    void addUser(const std::string& name, const struct sockaddr_in& peer) // 构建一个新用户
    {
        LockGuard lockguard(&lock);
        // onlineuser[name] = peer;
        auto iter = onlineuser.find(name); // 遍历检测是否存在该用户
        if (iter != onlineuser.end()) return;
        onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
    }
    void Recv()
    {
        // 服务器的正常工作
        char buffer[1024];
        while (true)
        {
            // 收
            // ssize_t 实际写入的大小 recvfrom(int sockfd 绑定的套接字, void *buf 接受数据存放的缓冲区, size_t len 缓冲区长度, int flags 读取方式(0), struct sockaddr *src_addr 需
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 这里一定要写清楚,未来传入的缓冲区的大小
            int n = recvfrom(sock_, buffer, sizeof(buffer)-1/*因为这是以字符作为消息的类型,所以缓冲区预留一部分空间*/, 0, (struct sockaddr*)&peer, &len); // 消息的类型需要程序员
            if (n > 0) buffer[n] = '\0';
            else continue;
            std::cout << "recv done ..." << std::endl;
            // 提取client信息
            std::string clientip = inet_ntoa(peer.sin_addr); // 把一个四字节的IP转化为字符串
            uint16_t clientport = ntohs(peer.sin_port); // 将从网络中获取的端口号转换成主机
            std::cout << clientip << "-" << clientport << "#" << buffer << std::endl;
            // 构建一个用户并检查
            std::string name = clientip;
            name += "-";
            name += std::to_string(clientport);
            // 如果不存在就插入,如果存在,什么都不做
            addUser(name, peer);
            std::string message = name + " >> " + buffer;
            rq.push(message); // 将接收到的信息存放入生产消费中
        }
    }
    void Broadcast()
    {
        while (true)
        {
            std::string sendstring;
            rq.pop(&sendstring); // 从生产消费模型中获取信息
            std::vector<struct sockaddr_in> v; // 获取所有的用户使用数组进行记录
            {
                LockGuard lockguard(&lock); // 使用锁进行保护防止产生冲突
                for (auto user : onlineuser)
                {
                    v.push_back(user.second);
                }
            }
            for (auto user : v) // 依次将信息发送出去
            {
                sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&user, sizeof(user));
            }
        }
    }
    ~UdpServer()
    {
        pthread_mutex_destroy(&lock);
        c->join();p->join();
        delete p;delete c;
    }
private:
	// ...
    std::unordered_map<std::string, struct sockaddr_in> onlineuser; // 添加使用用户
    RingQueue<std::string> rq; // 基于环形队列的生产消费模型
    pthread_mutex_t lock; // 互斥锁
    Thread *c, *p; // 创建生产者与消费者线程
};

对于客户端同样可以使用多线程

udp_client.cc
void* recver(void* args)
{
    pthread_detach(pthread_self());
    int sock = *static_cast<int*>(args);
    while (true)
    {
        // 接收
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&temp, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }
}
int main(int argc, char *argv[])
{
	// ...
    pthread_t tid;
    pthread_create(&tid, nullptr, recver, &sock);
    while (true)
    {
        // 用户输入
        std::string message;
        std::cerr << "please Enter# "; // 往2号文件发送
        // std::cin >> message;
        getline(std::cin, message);
        // 发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }
    return 0;
}

在这里插入图片描述
如上图所示就可以看到在右图中两个不同的客户端发送的消息都可以被看到。

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

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

相关文章

Oracle数据库环境变量配置以及可能遇到的问题解决

一、如何配置Oracle数据库环境变量&#xff08;以win10为例&#xff09; 1、找到此电脑&#xff0c;鼠标右键&#xff0c;点击属性。 2、点击属性成功后&#xff0c;进入如下页面&#xff0c;找到“高级系统设置”&#xff0c;点击进入。 3、找到环境变量&#xff0c;点击进入…

深眸科技自研轻辙视觉引擎,以AI机器视觉赋能杆号牌识别与分拣

电线杆号牌作为电力行业标识的一种&#xff0c;相当于电线杆的“身份证”&#xff0c;担负着宣传电力知识、安全警示的作用&#xff0c;用于户外使用标记输电线路电压等级、线路名称、杆塔编号等&#xff0c;能够清晰地记录电力线路杆的信息&#xff0c;并为电力线路的更改以及…

小红书下一个爆款趋势锁定 | 秋冬种草指南

今夏「多巴胺」风靡全网&#xff0c;现秋日又捎来一股名为「美拉德」的风……俨然一副“新晋顶流”的架势。 如何抓住这一新趋势&#xff1f;本期千瓜将锁定小红书下一个爆款内容——「美拉德」&#xff0c;剖析笔记与底层逻辑&#xff0c;助力品牌洞见先机&#xff0c;运筹布…

虚拟数字人直播软件实现带货功能,成为新一代直播风口!

随着短视频带货市场的不断发展&#xff0c;虚拟数字人直播技术逐渐成为热门话题。而在现如今的市场趋势下直播带货则成为了一种火热的营销方式。那么&#xff0c;虚拟数字人直播软件是否可以结合起来&#xff0c;实现无人直播带货的效果呢&#xff1f;让我们来了解一下。 灰豚数…

VSCode md绘制图形mermaid记录

官网&#xff1a;Mindmap | Mermaid 测试还是不错&#xff1a;

【Day-31慢就是快】代码随想录-二叉树-中序和后序遍历构造二叉树

根据一棵树的中序遍历与后序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 思路 首先知道怎么画&#xff0c;然后写代码流程。 以 后序数组的最后一个元素为切割点&#xff0c;先切中序数组&#xff0c;根据中序数组&#xff0c;反过来再切后序数组。一层一层切下去…

最佳策略app平台传出的绝密理财法,这是给散户们的好机会

昨天&#xff0c;记者在走访各大超市的时候发现一个奇怪的现象&#xff1a;普遍的市民朋友在选购商品的时候基本上都会选择那些打折促销的&#xff0c;有些促销的商品甚至是很久之后才会用的&#xff0c;他们也会给带上。而对于那些不打折或者价格高昂的商品&#xff0c;基本上…

入耳耳机对耳朵有损害吗?入耳耳机和骨传导耳机哪个好?

先说结论&#xff0c;入耳式耳机对耳朵是有伤害的&#xff0c;骨传导耳机相比于入耳式耳机可以保护更好的保护耳朵健康。 在日常中&#xff0c;经常会看到有佩戴耳机的时尚青年&#xff0c;他们戴着耳机刷视频、听音乐&#xff0c;陶醉在自己的小世界中&#xff0c;但是长时间…

项目经理常用的6种工具,让项目管理变得更高效

项目管理是一个复杂的过程&#xff0c;因为不仅要管人管事&#xff0c;会遇到各种问题&#xff0c;因此用什么工具管理项目&#xff0c;对项目管理是一个很好地辅助&#xff0c;让项目管理变得更有效、更高效。 1、甘特图 甘特图将一个大型项目划分为几个阶段&#xff0c;并…

飞凌嵌入式受邀亮相2023中国国际数字经济博览会

9月6日&#xff0c;由工信部、国家发改委和河北省人民政府共同主办的2023中国国际数字经济博览会在石家庄国际会展中心&#xff08;正定&#xff09;开幕&#xff0c;近500家参展企业携自家的“黑科技”展品集中亮相&#xff0c;赋能智慧应用新场景&#xff0c;为观众带来了一场…

【1】DDR---容量计算

1、容量计算 density&#xff1a;芯片容量&#xff0c;bit为单位 depth&#xff1a;地址空间&#xff0c; width&#xff1a;数据位宽 densitydepth*width 2、三星DDR 4Gbit&#xff08;总容量&#xff09;256M&#xff08;地址空间&#xff09;*16&#xff08;位宽&#xff…

GOOGLE SRE 运维模式解读

一、SRE核心是什么 我总结下来是&#xff1a;通过软件工程的方式开发&#xff08;GOOGLE规定SRE团队必须将50%的精力花在真实的开发工作上&#xff09;一些自动化的工具系统来解放传统运维工程师大量重复和手工操作&#xff0c;从而让新生代的SRE工程师有更多的时间&#xff1…

用户促活留存新方式——在APP中嵌入小游戏

随着APP同类产品的不断出现&#xff0c;APP开发者们面临着激烈的竞争&#xff0c;很多APP下载后被新的APP取代&#xff0c;获客成本越来越高。同时开发者还会面临用户粘性差、忠诚度低、用完即走、留存困难&#xff0c;商业化价值被大大缩减。 在APP中植入小游戏来提高用户活跃…

无涯教程-JavaScript - BESSELJ函数

描述 BESSELJ函数返回贝塞尔函数Jn(x)。 语法 BESSELJ(X, N)争论 Argument描述Required/OptionalXThe value at which to evaluate the function.RequiredNThe order of the Bessel function. If n is not an integer, it is truncated.Required Notes 如果x为非数值,则B…

亚马逊鲲鹏AI智能养号好用吗?怎么使用的?

亚马逊鲲鹏AI智能一键养号可以根据AI功能页面的姓名、年龄、职业、爱好等生成一批不同的AI角色&#xff0c;账号绑定这些角色后就可以自动浏览进行养号了。 功能特点 1、自动生成AI姓名、随机选择角色性别、自由设置AI年龄 2、根据勾选的AI职业、AI爱好进行随机生成AI关键词进…

pdf文档怎么压缩小一点?文件方法在这里

在日常工作和生活中&#xff0c;我们经常会遇到需要上传或者发送pdf文档的情况。但是&#xff0c;有时候pdf文档的大小超出了限制&#xff0c;需要我们对其进行压缩。那么&#xff0c;如何将pdf文档压缩得更小一点呢&#xff1f;下面&#xff0c;我将介绍三种方法&#xff0c;让…

【校招VIP】测试计划之hashmap分析

考点介绍&#xff1a; HashMap是Java程序员使用频率最高的用于映射键值对(key和value)处理的数据类型。随着JDK版本的跟新&#xff0c;JDK1.8对HashMap底层的实现进行了优化&#xff0c;列入引入红黑树的数据结构和扩容的优化等。 测试计划之hashmap分析-相关题目及解析内容可…

雷士明轩好用吗?测评师对比横评书客、雷士、米家哪款好

如今&#xff0c;大多数人的日常工作和学习都离不开电子设备&#xff0c;长时间盯着屏幕容易造成眼睛疲劳和视力下降。全国近视率占多数的还是青少年&#xff0c;护眼台灯作为一种照明设备&#xff0c;具有调节光线亮度和色温的功能&#xff0c;可以有效减少眼睛的疲劳&#xf…

Java【多线程】Callable 是什么, 如何使用并理解 Cllable, 和 Runnable 有什么区别?

文章目录 前言一、Callable 是什么&#xff1f;二、不使用 Callable 如何创建“有返回值的”线程&#xff1f;三、如何使用 Callable&#xff1f;四、如何理解 Callable&#xff1f;总结 前言 &#x1f4d5;各位读者好, 我是小陈, 这是我的个人主页 &#x1f4d7;小陈还在持续努…

将钉钉机器人小程序从一个公司迁移至另一个公司的步骤

引言&#xff1a; 由于我们以前开发的钉钉小程序都在一个公司&#xff0c;想在想应用到另一个公司&#xff0c;这就牵扯出了关于钉钉小程序迁移方面的具体步骤。下面是具体步骤&#xff1a; 1、创建一个钉钉小程序 在这一步你需要有钉钉开放平台的开发者权限&#xff0c;具体…