Linux--TCP编程--0216 17

news2024/11/18 21:29:12

观前提示:本篇博文的一些接口需要前几篇博文实现的

  • 线程池的实现

Liunx--线程池的实现--0208 09_Gosolo!的博客-CSDN博客

  • 线程池的单例模式

Linux--线程安全的单例模式--自旋锁--0211_Gosolo!的博客-CSDN博客


 1.TCP编程需要用的接口

创建 socket 文件描述符

int socket(int domain, int type, int protocol);

type 给成  SOCK_STREAM 表示是流式套接

listensock=socket(AF_INET,SOCK_STREAM,0);

开始监听socket

int listen(int socket, int backlog);

TCP是面向连接的,listen其实也是一个套接字,不过他的用途在于建立连接,而不真正提供服务。类似拉人的,提供服务的是服务员。

接收请求

int accept(int socket, struct sockaddr* address,socklen_t* address_len);

相当于拉客的和服务员进行了交接,返回值是真正提供服务的套接字(fd)

 建立连接

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

在正式通信之前,需要先建立连接。客户端需要连接到连接。

 2.TCP编程的框架

 2.1 服务端 TcpServer的框架  tcp_server.hpp

class TcpServer
{
private:
    const static int gbacklog = 20;  //后面再说

public:
    TcpServer(uint16_t port, std::string ip="0.0.0.0")
        :_port(port)
        ,_ip(ip)
        ,listensock(-1)
    {}
    void initServer()
    {}

    void Start()
    {}
    ~TcpServer()
    {}
private:
    uint16_t _port;
    std::string _ip;
    int listensock;//listensock套接字仅用于建立连接
};

 头文件在这里一次性给出

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>


//这几个头文件在线程池里面  其中Task.hpp会在用时给出
#include "ThreadPool/log.hpp"
#include "ThreadPool/ThreadPool.hpp"
#include "ThreadPool/Task.hpp"

 2.1.2 TcpServer的调用 tcp_server.cc

#include "tcp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->Start();
    return 0;
}

2.2 客户端 TcpClient的实现 tcp_client.cc

网络间通信首先需要 struct  sockaddr_in 结构体,这里定义一个对象 server

结构对象有了需要对他进行初始化(建议先全部置零,避免出现一些bug)

  • server 有三个地方需要做初始化,sin_familysin_portsin_addr.s_addr

server.sin_family=AF_INET; //跟申请套接字传入的参数一样即可

server.sin_port=htos(serverport); //记得从本地转为网络字节序

server.sin_addr.s_addr=inet_addr(serverip,c_str());

  • connect()接口  客户端需要连接到服务上

 connect(sock, (struct sockaddr *)&server, sizeof(server) );

  •  send() 向服务端发送消息

 ssize_t s = send(sock, line.c_str(), line.size(), 0);

  •  recv() 接收服务器发来的消息

 recv(sock, buffer, sizeof(buffer) - 1, 0);

  •  close() 关闭套接字

 close(sock);

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
 
    int sock = 0;
    
    while (true) // TODO
    {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }
            // client 要不要bind呢?不需要显示的bind,但是一定是需要port
            // 需要让os自动进行port选择
            // 连接别人的能力!
            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());

            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cerr << "connect error" << std::endl;
                exit(3); // TODO
            }
            std::cout << "connect success" << std::endl;
       
        std::cout << "请输入# ";
        std::string line;
        std::getline(std::cin, line);
        if (line == "quit")
        {
            close(sock);
            break;
        }

        ssize_t s = send(sock, line.c_str(), line.size(), 0);
        if (s > 0)
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server 回显# " << buffer << std::endl;
                close(sock);
            }
            else if (s == 0)
            {
                close(sock);
            }
        }
        else
        {
            close(sock);
        }
    }

    return 0;
}

3. 服务端的实现

3.1 TcpServer::initServer() 

  • 初始化,首先需要建立一个监听套接字listensock,这个监听套接字的作用是建立连接。
listensock=socket(AF_INET,SOCK_STREAM,0);

我们是网络之间进行通信,所以需要借助 struct sockaddr_in 类型的对象,local,创建出来之后最好先清空一下,保证不会出现一些奇怪的问题。

  • local 有三个地方需要做初始化,sin_familysin_portsin_addr.s_addr

local.sin_family=AF_INET; //跟申请套接字传入的参数一样即可

local.sin_port=htos(_port); //记得从本地转为网络字节序

local.sin_addr.s_addr=_ip=="0.0.0.0"?INADDR_ANY:inet_addr(_ip.c_str());

  •  接着和udp一样,进行绑定操作

//注意 虽然使用struct sockaddr_in 结构体类型 但是接口中的参数依然是 struct sockaddr*

bind(listensock, (struct sockaddr *)&local, sizeof(local);

  •  (tcp新增)建立连接

 listen(listensock, gbacklog);//这个gbacklog以后再谈

3.1.1 完整代码

class TcpServer
{
private:
    const static int gbacklog = 20;  //后面再说

public:
    TcpServer(uint16_t port, std::string ip="0.0.0.0")
        :_port(port)
        ,_ip(ip)
        ,listensock(-1)
    {}
    void initServer()
    {
        listensock=socket(AF_INET,SOCK_STREAM,0);
        if(listensock<0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", listensock);

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        //local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
        local.sin_addr.s_addr=_ip=="0.0.0.0"?INADDR_ANY:inet_addr(_ip.c_str());
        //inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);

        if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
   
        if(listen(listensock, gbacklog)<0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }
}

3.2 TcpServer::Start()

 要想服务端程序开始运行,首先需要有人来提供服务,记得listensock的作用仅是建立连接吗,那谁来做具体的服务呢?

使用accept接口的返回值!

首先,网络通信需要struct sockaddr_in结构体,先创建一个,struct sockaddr_in src;,len就是这个结构体的长度。

int servicesock=accept(listensock,(struct sockaddr*)&src,&len);

连接成功时候,就可以从我们创建的src中获取端口号、ip信息了。

 uint16_t client_port=ntohs(src.sin_port);

 std::string client_ip=inet_ntoa(src.sin_addr);

 接下来就可以开始进行服务了,自定义让做什么。这里让服务端去做这个任务

service(servicesock, client_ip, client_port);

static void service(int sock, const std::string &clientip, 
                const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接被使用!
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0; //将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer 
                        << std::endl;
        }
        else if(s == 0) //对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), 
                            clientport);
            break;
        }
        else{ // 
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

 3.2.1  完整代码——单进程阻塞循环版

     void Start()
    {
        while(true)
        {
            struct sockaddr_in src;
            socklen_t len=sizeof(src);

            //建立连接
            int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port=ntohs(src.sin_port);
            std::string client_ip=inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",
                servicesock, client_ip.c_str(), client_port);

            //版本1————单进程阻塞循环版

            service(servicesock, client_ip, client_port);
    }

问题在于,一次只能够处理一个进程。因为我们调用的函数service是一个死循环函数,如果一个客户端没有终止访问,其他客户端都不能正常来使用。

 3.2.2 完整代码——多进程带信号屏蔽版

那我使用多进程来解决这个问题,可是使用多进程也有问题,我创建了子进程,那我是不是要等待子进程结束啊?如果我使用阻塞等待那和上面有什么本质区别呢?

注:使用非阻塞等待成本很大,可以但不建议。

所以我们还需要用到信号,当子进程结束后,会给父进程发SIGCHLD信号!

 如果我们主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态。

      void Start()
      {        
        signal(SIGCHLD, SIG_IGN); 
        while(true)
        {
            struct sockaddr_in src;
            socklen_t len=sizeof(src);

            //建立连接
            int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port=ntohs(src.sin_port);
            std::string client_ip=inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",
                servicesock, client_ip.c_str(), client_port);
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                 close(listensock);//关闭不需要的文件描述符
                 service(servicesock, client_ip, client_port);
                 exit(0); 
            }
             //关闭父进程不需要的文件描述符 不关闭会导致父进程可用的文件描述符越来越少
            close(servicesock);
          }
       }

 3.3.3 完整代码——多进程版

能不能不屏蔽SIGCHLD信号呢?那我们就需要有人等待子进程,让谁等呢?

让bash领养,让bash等!

    void Start()
    {
        while(true)
        {
           //...跟上面一样

            //版本3————多进程孤儿进程版
            // 利用孤儿进程被系统回收
            pid_t id=fork();
            if(id==0)
            {
                close(listensock);
                if(fork()>0) 
                {
                    //子进程本身 
                    exit(0);
                }
                //子进程的子进程
                service(servicesock, client_ip, client_port);
                exit(0);
            }
            waitpid(id,nullptr,0);//由于子进程创建子进程后立即退出 所以父进程不会阻塞
            close(servicesock);
         }
    }

 3.3.4 完整代码——多线程版

相较于使用多进程,多线程的开销明显小。

class ThreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

class TcpServer
{
private:
    const static int gbacklog = 20;  //后面再说

    //设置的回调函数 必须是static的 不然会多一个this指针
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        service(td->_sock, td->_ip, td->_port);
        delete td;

        return nullptr;
    }
    void Start()
    {
        while(true)
        {
            //...
        
            //版本4————多线程版本
            //因为创建一个进程的代价还是比较大的,创建一个线程相对简便

            //不使用在栈上创建是为了保证线程安全 不会被覆盖 发生拷贝
            ThreadData *td=new ThreadData(); 
            td->_sock=servicesock;
            td->_ip=client_ip;
            td->_port=client_port;
            pthread_t tid ;
            //如果不join 一定会造成内存泄漏 可以在threadRoutine中设置等待
            pthread_create(&tid,nullptr,threadRoutine,td); 
        }
        
    }

};

 3.3.5 完整代码——线程池版本

 线程的创建也是一笔开销,能省就省

线程池版本的服务函数

//线程池版本的服务函数
static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
   
    char buffer[1024];
    while (true)
    {
        // read && write 可以直接被使用!
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        { //
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}
class TcpServer
{
public:
    TcpServer(uint16_t port, std::string ip="0.0.0.0")
        :_port(port)
        ,_ip(ip)
        ,listensock(-1)
        , _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}
    void Start()
    {
        //引入线程池
        _threadpool_ptr->run();
        //signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while(true)
        {
            //...略

            //线程池版本
            Task t(servicesock, client_ip, client_port, service);
            //Task t(servicesock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);
        }
        
    }
    ~TcpServer()
    {}
private:
    uint16_t _port;
    std::string _ip;
    int listensock;//仅用于建立连接
    
    //定义一个线程池先
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

pushTask

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"

//typedef std::function<void (int , const std::string &, const uint16_t &)> func_t;
using func_t = std::function<void (int , const std::string &, const uint16_t &, const std::string &)>;

class Task
{
public:
    Task(){}
    Task(int sock, const std::string ip, uint16_t port, func_t func)
    : _sock(sock), _ip(ip), _port(port), _func(func)
    {}
    void operator ()(const std::string &name)
    {
        _func(_sock, _ip, _port, name);
    }
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
    // int type;
    func_t _func;
};

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

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

相关文章

【数据挖掘】EDA——以2022雪浪算力开发者大赛数据为例

作者简介&#xff1a;重庆大学22级研一&#xff0c;研究方向&#xff1a;时空数据挖掘、图神经网络。目前正在学习大数据、数据挖掘等相关知识&#xff0c;希望毕业后能找到数据相关岗位。 前言 之前写了一个比赛复盘&#xff08;【竞赛复盘】2022雪浪算力开发者大赛——阀体异…

Python脚本之准备测试环境的用户数据

本文为博主原创&#xff0c;未经授权&#xff0c;严禁转载及使用。 本文链接&#xff1a;https://blog.csdn.net/zyooooxie/article/details/127645678 这期是讲述下 我准备测试环境用户数据的经历。 【实际这篇博客推迟发布N个月】 个人博客&#xff1a;https://blog.csdn.…

【读书笔记】《深入浅出数据分析》第三章 寻找最大值

目录 一&#xff0c;Excel却是最基础、最高频、最有机会展示的一款数据分析工具二&#xff0c;作为数据工作者&#xff0c;实际工作中&#xff0c;不管用不用的上&#xff0c;至少到达会的水准1&#xff0c;常用函数2&#xff0c;透视表3&#xff0c;可视化4&#xff0c;数据分…

【RabbitMQ笔记04】消息队列RabbitMQ七种模式之发布订阅模式(Publish/Subscribe)

这篇文章&#xff0c;主要介绍消息队列RabbitMQ七种模式之发布订阅模式&#xff08;Publish/Subscribe&#xff09;。 目录 一、发布订阅模式 1.1、Exchange交换机 &#xff08;1&#xff09;什么是Exchange交换机呢&#xff1f;&#xff1f;&#xff1f; &#xff08;2&am…

数据结构与算法----问答2023

1、什么是哈希表&#xff1f;如何解决碰撞&#xff1f; 哈希表&#xff08;Hash Table&#xff09;&#xff0c;也称为散列表&#xff0c;是一种用于实现字典&#xff08;键值对&#xff09;数据结构的数据结构。它将键映射到哈希表中的一个索引&#xff08;桶&#xff09;来保…

从零开始学习iftop流量监控(找出服务器耗费流量最多的ip和端口)

一、iftop是什么iftop是类似于top的实时流量监控工具。作用&#xff1a;监控网卡的实时流量&#xff08;可以指定网段&#xff09;、反向解析IP、显示端口信息等官网&#xff1a;http://www.ex-parrot.com/~pdw/iftop/二、界面说明>代表发送数据&#xff0c;< 代表接收数…

DHCP服务器的使用以及可能出现的问题(图文详细版)

DHCP服务的使用 开始&#xff0d;管理工具&#xff0d;DHCP,打开DHCP服务器选项窗口 新建作用域 在此处输入名称和描述,单击下一步 随机确定一组IP地址的范围,并指定其子网掩码 , 单击下一步 若想要排除某一个/组特定的IP地址,我们可以在此界面输入该IP地址,若没有,则可…

CTFHub | 前端验证

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…

django项目部署(腾讯云服务器centos)

基本步骤&#xff1a; 购买腾讯云服务器并配配置好 >> 本地项目依赖收集准备 >> 上传项目等文件到服务器 >> 服务器安装部署软件和python环境 >> 开始部署&#xff08;全局来看就这5个步骤&#xff09; 目录 目录 1. 购买腾讯云服务器并配配置好 …

【算法设计技巧】分治算法

分治算法 用于设计算法的另一种常用技巧为分治算法(divide and conquer)。分治算法由两部分组成&#xff1a; 分(divide)&#xff1a;递归解决较小的问题(当然&#xff0c;基准情况除外)治(conquer)&#xff1a;然后&#xff0c;从子问题的解构建原问题的解。 传统上&#x…

升级日记本-课后程序(JAVA基础案例教程-黑马程序员编著-第七章-课后作业)

【实验7-4】 升级版日记本 【任务介绍】 1.任务描述 本案例要求编写一个模拟日记本的程序&#xff0c;通过在控制台输入指令&#xff0c;实现在本地新建日记本、打开日记本和修改日记本等功能。 用户输入指令1代表“新建日记本”&#xff0c;可以从控制台获取用户输入的日记…

物联网MQTT协议简单介绍

物联网曾被认为是继计算机、互联网之后&#xff0c;信息技术行业的第三次浪潮。随着基础通讯设施的不断完善&#xff0c;尤其是 5G 的出现&#xff0c;进一步降低了万物互联的门槛和成本。物联网本身也是 AI 和区块链应用很好的落地场景之一&#xff0c;各大云服务商也在纷纷上…

mysql8.0-日志

目录 错误日志 错误日志主要记录如下几种日志&#xff1a; 查询日志 测试 慢查询日志 二进制日志 日志格式 日志查看 修改日志格式 二进制日志的删除 二进制日志的还原 错误日志 错误日志是MySQL中最重要的日志之一&#xff0c;它记录了当mysql启动和停止时&#xff0c;…

【Python基础】类

面向对象编程 面向对象编程是最有效的软件编写方法之一。面向对象是一种对现实世界理解和抽象的方法&#xff0c;是计算机编程技术发展到一定阶段后的产物。 面向对象和面向过程的区别 比如我想吃西红柿炒蛋&#xff0c;怎么运用面向过程的方法来解决这个问题呢&#xff1f;…

怕上当?来看这份网络钓鱼和诈骗技术趋势

网络钓鱼和诈骗&#xff1a;当前的欺诈类型 网络钓鱼 钓鱼者可以攻击任何在线服务——银行、社交网络、政府门户网站、在线商店、邮件服务、快递公司等——中的证书。但是&#xff0c;顶级品牌的客户往往面临更大风险&#xff0c;因为相比小品牌&#xff0c;人们更喜欢使用和…

12 个适合做外包项目的开源后台管理系统

1.D2admin 开源地址&#xff1a;https://github.com/d2-projects/d2-admin 文档地址&#xff1a;https://d2.pub/zh/doc/d2-admin/ 效果预览&#xff1a;https://d2.pub/d2-admin/preview/#/index 开源协议&#xff1a;MIT 2.vue-element-admin 开源地址&#xff1a;https…

BACnet协议详解————MS/TP物理层,数据链路层和网络层

文章目录写在前面1 物理层2 数据链路层MSTP的流程如下noteMS/TP帧格式3 网络层写在前面 这周加更一篇&#xff0c;来弥补一下之前落下的进度。简单的说两句&#xff0c;之前讲应用层的时候&#xff0c;只是跟官方的手册来同步一下&#xff0c;但是从个人理解来说&#xff0c;自…

Spring拦截器

SpringMVC提供了拦截器机制&#xff0c;允许运行目标方法之前进行一些拦截工作或者目标方法运行之后进行一下其他相关的处理。自定义的拦截器必须实现HandlerInterceptor接口。preHandle()&#xff1a;这个方法在业务处理器处理请求之前被调用&#xff0c;在该方法中对用户请求…

(周末公众号解读系列)2000字-视觉SLAM综述

参考链接&#xff1a;https://mp.weixin.qq.com/s?__bizMzg2NzUxNTU1OA&mid2247528395&idx1&sn6c9290dd7fd926f11cbaca312fbe99a2&chksmceb84202f9cfcb1410353c805b122e8df2e2b79bd4031ddc5d8678f8b11c356a25f55f488907&scene126&sessionid1677323905…

10 分钟把你的 Web 应用转为桌面端应用

在桌面端应用上&#xff0c;Electron 也早已做大做强&#xff0c;GitHub桌面端、VSCode、Figma、Notion、飞书、剪映、得物都基于此。但最近后起之秀的 Tauri 也引人注目&#xff0c;它解决了 Electron 一个大的痛点——打包产物特别大。 我们知道 Electron 基于谷歌内核 Chro…