iis服务器助手广告
返回顶部
首页 > 资讯 > 操作系统 >【Linux】手把手教你实现udp服务器
  • 413
分享到

【Linux】手把手教你实现udp服务器

c++后端linuxudp服务器网络协议运维 2023-08-31 14:08:39 413人浏览 薄情痞子
摘要

网络套接字~ 文章目录 前言一、udp服务器的实现总结 前言 上一篇文章中我们讲到了很多的网络名词以及相关知识,下面我们就直接进入udp服务器的实现。 一、udp服务器的实现 首先我们需要创建五个文件(文件名可以自己命

网络套接字~

文章目录


前言

上一篇文章中我们讲到了很多的网络名词以及相关知识,下面我们就直接进入udp服务器的实现。


一、udp服务器的实现

首先我们需要创建五个文件(文件名可以自己命名也可以和我一样),分别是makefile,udpclient.cc,udpclient.hpp,udpserver.cc,udpserver.hpp,下面我们先进行makefile的编写,在makefile中我们要一次创建两个可执行程序:

cc=g++.PHONY:allall:udpClient udpServerudpClient:udpClient.cc$(cc) -o $@ $^ -std=c++11udpServer:udpServer.cc$(cc) -o $@ $^ -std=c++11.PHONY:cleanclean:rm -f udpClient udpServer

我们通过all就可以创建多个可执行程序了,对于cc这个变量我们设置为g++,以后如果想换其他的编译器就可以直接替换了。

在udpserver.hpp这个文件中我们先写出整体框架

namespace Server{    class udpServer    {    public:        udpServer()         {        }        void InitServer()        {                  }        void start()        {                   }        ~udpServer()        {        }    private:        //服务器一定要有自己的服务端口号(注意端口号是16位的)        uint16_t _port;     //端口号        //实际上一款服务器不建议指明一个IP        string _ip;    //ip    };}

那么我们现在服务器的ip填多少呢?实际上我们只是完成测试,所以ip就填0.0.0.0就好了,这样的话任意的ip都能访问我们的服务器,所以我们定义一个static变量来保存ip:

 static const string defaultIp = "0.0.0.0";

有了ip和端口号后,我们就可以用构造函数初始化了:

 udpServer(const uint16_t& port,const string ip = defaultIp)            :_port(port)            ,_ip(ip)         {        }

 我们的服务器未来要启动的话就必须先初始化然后再启动,所以我们写了init和start接口,那么该如何初始化呢?实际上不管是udp还是tcp,我们初始化都是需要套接字的,下面我们看看套接字的接口:

 如何理解套接字呢,我们都知道linux一切皆文件,所以未来的网络通信一定是在同一个文件中只要和网卡设备关联起来就实现了网络通信,所以套接字的目的实际上是创建一个文件,可以看到我们的套接字有三个参数,第一个参数的解释是域,实际上就是让我们选择是进行网络通信还是本地通信,这里我们一般选择AF_INET选项,代表使用IPV4协议的网络通信。第二个参数是type,表面套接字要向我们提供服务的类型,怎么理解呢,如下图:

我们现在所写的UDP服务器的特点是不可靠传输无连接,而这正是与SOCK_DGRAM这个选项所匹配的,我们查看这个选项的解释可以看到:DGRAM适用于不可靠传输,连接少

我们下一篇要实现的TCP服务器,就会用到SOCK_STREAM这个选项,因为这个选项的解释是面向流式服务,而我们TCP的特点就是面向字节流。

第三个参数我们一般缺省为0,因为这个参数代表我们未来要采用什么协议,如果我们写为0,那么这个接口会根据我们填的前两个参数来帮我们确定第三个参数是选择TCP协议还是UDP协议。

这个接口的返回值相信大家也看到了,没错!一旦创建套接字成功,那么就会给我们返回一个文件描述符,如果失败则会给我们返回-1并且提供错误码。

了解了Socket这个接口,那么我们下一步就是增加一个私有变量来接收socket返回的文件描述符(注意:这个文件描述符会被后面的接口多次用到):

 然后我们在构造函数中将这个文件描述符初始化为-1:

udpServer(const uint16_t& port,const string ip = defaultIp)            :_port(port)            ,_ip(ip)            ,_sockfd(-1)        {        }

然后我们初始化第一步:使用套接字

 void InitServer()        {            //UDP第一步:创建了一个套接字            _sockfd = socket(AF_INET,SOCK_DGRAM,0);            if (_sockfd==-1)            {                cerr<<"socket error: "<

如果套接字创建失败,就算没有给我们的文件描述符返回-1,由于我们初始化的时候就初始化为-1,所以还是会报错,注意:一旦连套接字都没创建成功,那么就没有继续的必要了直接退出即可,这里我们直接用枚举列出所有的退出码然后在退出的时候使用:

    enum     {       SOCKET_ERROR = 2    };

创建成功我们就直接打印一下文件描述符即可。

下面进入初始化第二步:绑定端口和ip

 首先第一个参数就是我们使用socket接口给我们返回的文件描述符,第二个参数是什么呢?大家看到这个参数名struct sockaddr*是否感到熟悉呢?没错就是我们上一篇讲到的sockaddr结构:

 注意我们用的IPV4协议要用sockaddr_in这个结构,但是接口参数是sockaddr*这个结构,所以我们用的时候要做一下强制类型转换。可以看到我们的这个结构有4个位置需要我们填充,第一个AF_INET代表协议家族,第二个是端口号,第三个是IP地址,第四个是这个结构体的大小。

第三个参数是这个结构体的长度。

对于bind这个接口,如果成功则返回0,如果失败则返回-1.

由于bind的第二个参数是结构体指针,所以我们需要先创建一个新的结构体,然后对这个结构体进行填充,填充后传入参数:

            struct sockaddr_in local;  //在栈(用户)上定义了一个结构体变量            bzero(&local,sizeof(local));            local.sin_family = AF_INET;            local.sin_port = htons(_port);  //给别人发消息要将port和ip发送给对方 htons主机转网络序列(port是short类型)            local.sin_addr.s_addr = inet_addr(_ip.c_str());     //1.string->uint32_t 2.主机转网络,ip是四字节htonl

bzero这个接口可以将我们的结构体里面的内容初始化为0,然后我们进行填充首先协议家族填写AF_INET这里是固定写法,然后就是填写端口号和ip地址,对于端口号,在结构体中的类型是16字节的short短整型,而htons这个接口可以将主机字节序转化为网络字节序(还记得我们上一篇讲的内容吗?网络中所有字节序必须是大端存储,而主机中有可能大端有可能小端,所以hton这个接口就是将任意的主机字节序转换为网络字节序的接口),htons后面的s代表要转化为16字节的,如果你的port是32字节的,那么你就需要用htonl转换为long类型。

对于ip的填充,首先结构体中的ip的类型是32位的,而我们刚刚在类内定义的是一个字符串,所以我们需要先将字符串转换为32位整形,然后再将这个32位整形由主机字节序转化为网络字节序,所以正常的步骤是:1.string->uint32_t  2.htonl(uint32_t)          但是现在我们有一个很好用的接口,这个接口是inet_addr,下面我们看看这个接口:

 我们可以看到inet_addr的参数是一个const char*类型,这是什么呢?实际上这个类型就是我们ip常用的点分十进制类型,这个函数的返回值是in_addr_t,也就是说这个函数可以直接将点分十进制类型转化为我们结构体中所需要的ip类型。

我们将这个结构体填充完毕后,下面就直接绑定端口号和ip:

            int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));            if (n==-1)            {                cerr<<"bind error:  "<

前面我们说过,bind的参数与我们ipv4协议使用的结构体类型不一样需要强制转化。当我们绑定失败,我们就打印错误信息,然后加一个bind接口的错误码用于返回:

    enum     {       SOCKET_ERROR = 2       ,BIND_ERR    };

绑定结束后我们的服务器初始化接口就结束了,下面我们进入服务器启动的接口,在这里我们要注意,服务器启动的本质就是一个死循环,就比如我们的手机系统,如果不是主动的退出,我们的手机是不会关机的。

对于udp服务器的启动,我们先大概的思考一下:./udpserver ip port也就是说需要三个参数,所以我们可以先设计一下udpserver.cc:

首先对于不懂如何启动服务器的用户我们需要加一个使用手册,保证用户可以正常启动服务器:

static void Usage(string proc){    cout<<"Usage:\n\t"<ain(int arGC,char* argv[]){    if (argc!=3)    {        Usage(argv[0]);        exit(USAGE_ERR);    }    uint16_t port = atoi(argv[2]);    string ip = argv[1];    unique_ptr usvr(new udpServer(port,ip));    usvr->InitServer();    usvr->start();    return 0;}

对于main函数的参数我们之前已经讲过,argc代表你传了几个参数,argv这个数组对应的下标就是我们的参数。我们的目的是:./udpserver ip port这样使用,所以一共有三个参数,如果用户没有传3个参数,那么我们就直接提示如何使用并且退出程序,这里我们也可以弄一个错误码写到枚举中:

    enum     {       USAGE_ERR = 1       ,SOCKET_ERROR       ,BIND_ERR    };

如果用户输入成功,那么我们先获取用户输入的端口号,因为用户输入的是字符串,所以需要将字符串转化为整形,我们用uint16_t的类型来接收端口号,因为我们的server类中的ip是string的,所以可以直接用string变量获取ip地址。然后我们用一个智能指针来管理服务器,在服务器中使用端口号和ip构造服务器,然后对服务器进行初始化和启动即可。

下面我们讲解一个在绑定前填充结构体中ip地址的问题:实际上我们在正在做项目的时候,是不会直接像下面这样指明一个IP的:

真实的写法应该是下面这样:

local.sin_addr.s_addr = INADDR_ANY; //任意地址绑定才是服务器的真实写法

 什么意思呢?实际上就是当我们将服务器的IP设为ANY(本质其实是0),就代表未来发给我的数据只要是绑定了我的端口那么就能与我通信,这样就不会漏掉没有我IP地址的服务器给我发的消息了。还记得我们刚开始写的IP是什么吗?没错就是全0,也就是说我们现在写的这个服务器是不需要我们具体的IP只需要通过端口号就可以启动台服务器,并且未来客户端访问我们的服务器的时候是不需要指明IP的,任意一个IP+特定的端口号都能访问我们这台服务器。既然不需要IP,下面我们就修改一下代码:

static void Usage(string proc){    cout<<"Usage:\n\t"< usvr(new udpServer(handerMessage,port));    usvr->InitServer();    usvr->start();    return 0;}

所以实际上一个服务器的IP不重要,只要我们有端口号就能启动这台服务器,并且客户端用任意的IP和我们服务器特定的端口号就可以和我们的服务器通信。

下面我们编写start接口的代码,一旦启动我们就要接受数据,所以我们先认识一个接口:

 这个接口的第一个参数是我们创建套接字返回的文件描述符,意思就是我们从哪个套接字里读数据。第二个参数是一个缓冲区,第三个参数是这个缓冲区的长度,2和3这两个参数代表的是你读到的数据要放在哪个缓冲区里,第四个参数是读取方式,这里我们默认填0代表阻塞式读取,也就是说客户端不给我们服务端发消息时,我们就一直等待客户端发消息,这就叫阻塞式读取。第五个参数和第六个参数非常重要,这两个参数是输出型参数,也就是说未来客户端给我们发消息时,会将数据放到缓冲区中,然后会将客户端的端口号和IP放到struct sockaddr*这个结构体当中,第六个参数就是这个结构体的长度,我们可以理解为:我们只需要创建一个空的结构体,然后客户端发消息后这个接口就会将客户端的端口号和IP放到我们自己创建的结构体中。

 对于这个接口的返回值,如果成功则会给我们返回读到数据的字节数,如果失败返回-1.

static const int gnum = 1024;void start()        {            //服务器的本质实际上就是一个死循环            char buffer[gnum];            for (;;)            {                struct sockaddr_in peer;                socklen_t len = sizeof(peer);   //必填                ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(structsockaddr*)&peer,&len); //成功返回字节数}        }

我们在使用recvfrom接口的时候,对于缓冲区是不用考虑\0的存在的,所以长度是1024-1.然后我们的结构体类型在参数中需要做强制类型转换,理由与上面同理。下面我们思考读到数据该干什么?我们的目的是实现一个udp服务器用来进行简单的聊天,聊天的时候要显示出客户端的ip和端口号,所以我们这样设计:

void start()        {            //服务器的本质实际上就是一个死循环            char buffer[gnum];            for (;;)            {                struct sockaddr_in peer;                socklen_t len = sizeof(peer);   //必填                ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len); //成功返回字节数                //1.数据是什么? 2.谁发的                if (s>0)                {                    buffer[s] = 0;                    string clientip = inet_ntoa(peer.sin_addr);   //1.网络序列 2.int->点分十进制IP                    uint16_t clientport = ntohs(peer.sin_port);                    string message = buffer;                    cout<

如果读取数据成功,我们先将缓冲区中最后一个位置填上\0,这样我们就可以用string来接收这个缓冲区中的字符了,然后我们获取用户的IP,由于结构体中的类型是网络的,所以我们需要将网络字节序转回主机字节序,而这里有一个接口与我们那会用的inet_addr正好相反,那就是inet_ntoa接口:

 这个接口可以为完成两步:1.ntol(struct in_addr) 2.ntol(struct in_addr)->char*

ntol就是hton相反的转换接口。

获取到string类型的ip后,我们再接收端口号,同样需要转换,然后我们就打印用户端ip[端口号]+用户端发的消息即可。这样服务端的代码就实现完成了。

下面我们开始完成客户端代码:

首先客户端必须要有的是服务端的IP和服务端的port,所以我们先写一个框架:

namespace Client{    class udpClient    {    public:       udpClient(const string& serverip,const uint16_t &serverport)          :_serverip(serverip)          ,_serverport(serverport)       {       }       void InitClient()       {                 }       void run()       {}       ~udpClient()       {       }    private:       string _serverip;       uint16_t _serverport;    };}

前面我们说过,对于服务器而言,ip地址是不重要的,只需要端口号就可以启动服务器,因为一般服务器的IP都是全0,代表任意IP都可以访问,所以我们的客户端只需要随便填一个IP加上特殊的端口号就可以通信了,那么客户端内部ip和port肯定是必须要有的,明白了这个知识我们就先实现一下client.cc的框架:

#include "udpClient.hpp"#include using namespace Client;static void Usage(string proc){    cout<<"Usage:\n\t"< ucli(new udpClient(serverip,serverport));    ucli->InitClient();    ucli->run();    return 0;}

这里的原理和我们服务器写的一模一样,我们就直接编写客户端代码:

首先我们客户端的初始化一定也是需要创建套接字的,既然要创建套接字就必须要有一个变量接收套接字返回的文件描述符:

udpClient(const string& serverip,const uint16_t &serverport)          :_sockfd(-1)          ,_serverip(serverip)          ,_serverport(serverport)        {       }

然后我们编写初始化函数:

void InitClient()       {           // 1.创建socket           _sockfd = socket(AF_INET,SOCK_DGRAM,0);           if (_sockfd==-1)           {               cerr<<"socket error: "<程序员自己bind(由OS自动形成端口绑定))                  }

我们客户端的代码很简单,相比服务端客户端是不需要明确的去bind的,这是因为服务端必须要有指定的不能随意改变的端口,这样我们的客户端才能找到服务端,就像110一样,110这个电话是不能随意更改的,但是对于用户端自己来讲,我自己是什么样的端口不重要,我只需要通过服务端的端口访问服务端。所以我们一定要注意:客户端需要bind,但是不需要程序员明确的bind,这里我们自己不bind,操作系统察觉到我们没有绑定后会自动帮我们绑定,并且每次绑定的端口号都是随机的。

下面我们编写客户端运行的函数,客户端运行很简单,我们只需要让客户端输入数据,这样的话我们服务端就可以接受到数据,因为我们的目的就是简单的网络通信。

对于客户端发消息,我们需要认识一个接口:

 第一个参数是我们创建套接字返回的文件描述符,第二个参数和第三个参数是一起的,buf是我们发送的数据所在的缓冲区,第三个参数是缓冲区的长度,第四个参数是发送方式,我们还是默认填0表示阻塞发送,有数据就发,没数据就等。第五个参数和第六个参数同样是输入型参数,我们客户端需要提前创建一个结构体,向里面填充我们客户端的ip和端口号,然后通过sendto接口发送到服务端,然后服务端就会接收到我们的数据和ip和端口号。

void run()       {           struct sockaddr_in server;           memset(&server,0,sizeof(server));           server.sin_family = AF_INET;           server.sin_addr.s_addr = inet_addr(_serverip.c_str());           server.sin_port = htons(_serverport);           string message;           while (!_quit)           {              cout<<"Please Enter# ";              cin>>message;              sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));           }       }

这里我们的客户端要持续的输入所以设为死循环,quit是我们新增的一个成员变量:

 以上就是我们客户端的代码了,实际上客户端的代码非常简单,下面我们运行起来:

 运行起来后我们可以看到是没问题的,这里也解释了为什么说客户端端口不需要程序员绑定,我们可以看到每次客户端重新登录在服务端显示的端口号都是不一样的,因为这是操作系统自动指定的端口号,而我们的服务端的端口号是唯一的,我们客户端必须输入服务端正确的端口号才能访问服务端,当然小伙伴们也一样将服务端的可执行程序直接发给你们的小伙伴,然后让他们直接通过任意ip+ 你的服务器端口号来和你进行聊天,下面是多人通过网络聊天的界面:


总结

以上就是我们udp服务器的所有内容了,下一篇文章我们将会把这个服务器改造称为英汉互译,大型聊天室等好玩的工具

来源地址:https://blog.csdn.net/Sxy_wspsby/article/details/131384658

--结束END--

本文标题: 【Linux】手把手教你实现udp服务器

本文链接: https://www.lsjlt.com/news/385983.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 【Linux】手把手教你实现udp服务器
    网络套接字~ 文章目录 前言一、udp服务器的实现总结 前言 上一篇文章中我们讲到了很多的网络名词以及相关知识,下面我们就直接进入udp服务器的实现。 一、udp服务器的实现 首先我们需要创建五个文件(文件名可以自己命...
    99+
    2023-08-31
    c++ 后端 linux udp 服务器 网络协议 运维
  • 手把手教你使用TensorFlow2实现RNN
    目录概述权重共享计算过程:案例数据集RNN 层获取数据完整代码概述 RNN (Recurrent Netural Network) 是用于处理序列数据的神经网络. 所谓序列数据, 即...
    99+
    2024-04-02
  • 手把手教你实现一个 Python 计时器
    为了更好地掌握 Python 计时器的应用,我们后面还补充了有关Python类、上下文管理器和装饰器的背景知识。因篇幅限制,其中利用上下文管理器和装饰器优化 Python 计时器,将在后续文章学习,不在本篇文章范围内。Python 计时器首...
    99+
    2023-05-14
    Python 编程语言 计时器
  • 手把手教你Vue3实现路由跳转
    目录一、安装 vue-router二、新建 vue 页面2.1 login.vue2.2 register.vue三、新建路由文件3.1 新建 index.js3.2 新建 rout...
    99+
    2024-04-02
  • 手把手教你vue实现动态路由
    目录1、什么是动态路由?2、动态路由的好处3、动态路由如何实现总结1、什么是动态路由? 动态路由,动态即不是写死的,是可变的。我们可以根据自己不同的需求加载不同的路由,做到不同的实现...
    99+
    2024-04-02
  • 手把手教你Linux的网络配置
    目录 网络连接测试 测试Linux虚拟机是否与主机连接 测试主机是否与虚拟机连接 网络连接模式 桥接模式 NAT模式 仅主机模式 修改静态IP 修改 IP 地址后可能会遇到的问题 配置主机名 网络连接测试 测试Linux虚拟机是...
    99+
    2023-08-31
    linux 运维 服务器
  • 手把手教你轻松建立Ftp服务器
    要建立FTP服务器,你需要按照以下步骤进行操作:1. 选择一个合适的操作系统:FTP服务器可以在多种操作系统上运行,包括Window...
    99+
    2023-08-23
    Ftp服务器
  • 手把手教你实现Android编译期注解
    详细阐述了实现一个Android编译期注解sdk的步骤以及注意事项,并简要分析了运行时注解以及字节码技术在生成代码上与编译期注解的不同与优劣 一、编译期注解在开发中的重要性 从早期令...
    99+
    2024-04-02
  • 手把手教你实现PyTorch的MNIST数据集
    目录概述 获取数据 网络模型 train 函数 test 函数 main 函数 完整代码:概述 MNIST 包含 0~9 的手写数字, 共有 60000 个训练集和 10000 个...
    99+
    2024-04-02
  • 手把手教你实现Python重试超时装饰器
    目录一、前言二、简单分析三、代码模拟实现重试装饰器-初版重试装饰器-改进版重试装饰器-加强版重试装饰器-最终版修改记录一、前言 在写业务代码时候,有许多场景需要重试某块业务逻辑,例如...
    99+
    2023-05-18
    Python重试超时装饰器 Python超时重试
  • 手把手教你将项目部署到服务器!
    一、导入centos7虚拟机: 打开VMWare,点击“打开虚拟机”,选择centos7.ova之后,选择存储路径: 点击导入: 选择“不再显示此消息”,点击“重试”按钮: 点击“编辑虚拟机设置”,修改处理器、内存、硬盘等信息后,启...
    99+
    2023-09-10
    运维 java 服务器
  • 手把手教你用Java实现一套简单的鉴权服务
    目录前言一、何为鉴权服务二、利用servlet+jdbc实现简单的用户登录程序1.明确思路2.手把手教你实现一个简单的web登录程序三、回顾1.密码未加密裸奔2.登录信息未存储3.对...
    99+
    2024-04-02
  • CSS动画教程:手把手教你实现脉冲特效
    引言:CSS动画是网页设计中常用的一种效果,它可以为网页增添活力和视觉吸引力。本篇文章将带您深入了解如何利用CSS实现脉冲特效,并提供具体的代码示例教您一步步完成。一、了解脉冲特效脉冲特效是一种循环变化的动画效果,通常用在按钮、图标或其他元...
    99+
    2023-10-21
    CSS动画 教程 脉冲
  • pytorch实战7:手把手教你基于pytorch实现VGG16
    手把手教你基于pytorch实现VGG16(长文) 前言 ​ 最近在看经典的卷积网络架构,打算自己尝试复现一下,在此系列文章中,会参考很多文章,有些已经忘记了出处,所以就不贴链接了,希望大家理解...
    99+
    2023-09-06
    pytorch 深度学习 python
  • 手把手教你用js实现瀑布流布局
    它可以有效的降低页面的复杂度,节省很多的空间;并且,瀑布流的参差不齐的排列方式,可以通过界面展示给用户多条数据,并且让用户可以有向下浏览的冲动,提供了很好的用户体验!例如淘宝的页面就采用了这种布局方式,给大家看看淘宝的瀑布流布局的效果图(手...
    99+
    2023-05-14
    JavaScript
  • 手把手教你用C语言实现三子棋
    目录1.设计简单菜单2.创建棋盘3.下棋过程的实现 3.1玩家下棋 3.2电脑下棋3.3判断输赢4.游戏源码总结1.设计简单菜单 相信大家在玩游戏时会发现,进入游...
    99+
    2024-04-02
  • CSS动画教程:手把手教你实现翻页特效
    CSS动画教程:手把手教你实现翻页特效,需要具体代码示例CSS动画是现代网站设计中必不可少的一部分。它可以为网页增添生动感,吸引用户的注意力,并且提高用户体验。其中一种常见的CSS动画效果就是翻页特效。在这篇教程中,我将带领大家一步一步实现...
    99+
    2023-10-24
    CSS动画 翻页特效 手把手教程
  • CSS动画教程:手把手教你实现震动特效
    引言:在现代Web开发中,动画效果的应用越来越广泛。CSS动画是一种简单而强大的实现动画效果的方法。本文将带您一起学习如何使用CSS动画实现震动特效,并提供具体的代码示例。一、了解CSS动画基础知识在使用CSS动画之前,我们需要了解一些基础...
    99+
    2023-10-21
    CSS动画 手把手教程 震动特效
  • CSS动画教程:手把手教你实现旋转特效
    引言:CSS动画是现代网页设计的重要组成部分之一,通过CSS动画可以为网页增加交互性和视觉吸引力。本文将教你如何使用CSS实现一个简单而漂亮的旋转特效,通过简单的代码示例,让你轻松掌握该技巧。创建HTML结构:首先,我们需要创建一个HTML...
    99+
    2023-10-21
    CSS动画 手把手教 旋转特效
  • 手把手教你用Matplotlib实现数据可视化
    目录介绍简单图形绘制快速上手自定义X/Y轴图表实现汇总正弦曲线图柱状图散点图饼图量场图等高线图图形样式折线图散点图饼图组合图形样式图形位置figure对象subplots对象规范绘图...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作