Linux网络编程——基于UDP协议的简易聊天室

news2024/11/17 6:42:40

0.关注博主有更多知识

操作系统入门知识合集

目录

1.UDP服务端

1.1消息转发的实现

2.UDP客户端

3.效果展示

1.UDP服务端

使用C、C++混编的方式在Linux环境下实现一个简单的UDP服务端。那么我们先看代码,然后逐步分析:

// udpServer.hpp
#pragma once

#include <string>
#include <iostream>
#include <strings.h>
#include <functional>
/*网络必要的头文件*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

/*其他操作系统接口*/
#include <unistd.h>

namespace server
{
    using namespace std;
    enum {SOCKET_ERROR = 1,START_ERROR,BIND_ERROR};
    typedef function<void(int,struct sockaddr_in &,string &)> func_t;
    class udpServer
    {
    public:
        /*服务器不需要绑定任何IP!
         *绑定默认的0.0.0.0即可
         *绑定了一个指定的IP之后,只能接收到指定IP的客户端的数据*/
        udpServer(const func_t &func,const uint16_t &port,const string &ip = defaultIp)
            :_func(func),_port(port),_ip(ip),_socketFd(-1)
        {
            /*以IP协议、数据报的形式打开文件
             *也就是以UDP协议打开网络文件
             *如果打开失败,说明无法进行网络通信,程序退出*/
            _socketFd = socket(AF_INET,SOCK_DGRAM,0);
            if(_socketFd == -1)
            {
                cerr << "socket fail" << endl;
                exit(SOCKET_ERROR);
            }
            cout << "socket success: " << _socketFd << endl; 

            /*以后对网络的I/O的操作就是对网络文件操作
             *但是还没有将该文件绑定IP和端口号,也就是还没有绑定套接字
             *所以现在要绑定套接字*/
            struct sockaddr_in local = getSockaddr_in();
            int n = bind(_socketFd,(struct sockaddr *)&local,sizeof(local));
            if(n == -1)
            {
                cerr << "bind fail" << endl;
                exit(BIND_ERROR);
            }
            /*至此套接字创建工作完成*/
        }
        ~udpServer()
        {}

        void start()
        {
            while(true)
            {
                /*读取客户端发送的数据
                 *并打印出来,相当于服务器后台日志*/
                char buffer[buffer_num];

                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int n = recvfrom(_socketFd,buffer,sizeof(buffer)-1,0,(struct sockaddr *)&client,&len);
                if(n > 0)
                {
                    buffer[n] = 0;
                    string message = buffer;
                    string clinetIp = inet_ntoa(client.sin_addr);
                    uint16_t clinetPort = ntohs(client.sin_port);

                    cout << clinetIp << "[" << clinetPort << "]# " << message << endl;

                    /*回调*/
                    _func(_socketFd,client,message);
                }
            }
        }
    private:
        /*参与网络通信需要端口号和ip地址
         *端口号是一个2字节的整数
         *ip地址用点分十进制的字符串表示*/
        uint16_t _port;
        string _ip;

        /*以文件的形式打开一个网络文件
         *即使用socket()打开一个网络文件*/
        int _socketFd;

        /*所有对象共享一个默认的IP*/
        static const string defaultIp;
        static const int buffer_num = 1024;

        /*回调函数:服务器要处理的业务逻辑*/
        func_t _func;

        /*获取struct sockaddr结构体*/
        struct sockaddr_in getSockaddr_in()
        {
             struct sockaddr_in local;
            /*初始化sockaddr_in对象
             *并且将端口号、IP地址设置进去
             *我们使用的IP地址是字符串,所以需要转换成整数
             *使用inet_addr()接口,里面自动转换成整数,并且自动大小端字节序*/
            bzero((void *)&local,sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            //local.sin_addr.s_addr = inet_addr(_ip.c_str());
            local.sin_addr.s_addr = INADDR_ANY;
            return local;
        }
    };
    const string udpServer::defaultIp = "0.0.0.0";
} /*namespace server ends here*/

首先我们介绍一下scoket()接口

  第一个名为"domain"的参数代表域,即我们想要使用网络套接字、原始套接字还是Unix域间套接字,很显然,这里我们应该选择网络套接字,所以该参数填AF_INET,它是一个宏,代表以IPV4网络协议

  第二个名为"type"的参数代表网络通信的种类。我们前面介绍过UDP协议是面向数据报传输的、TCP协议是面向字节流传输的,那么我们现在要实现UDP服务器,所以该参数填SOCK_DGRAM,它也是一个宏,代表数据报

  第三个名为"prorocal"的参数代表使用的网络协议,但是因为我们前两个参数已经确定了我们使用UDP协议,所以该参数填0

那么socket()的作用是什么?我们观察它的返回值:

  可以发现socket()的返回值是一个文件描述符,这就说明了socket()会打开一个文件,并且我们不难猜测出打开的文件是一个网络文件,这个网络文件就是我们收发数据的入口

那么在udpServer类的构造函数当中,调用了socket()之后,我们又调用了一个名为getScokaddr_in()的成员函数,该函数返回一个类型为strcut sockaddr_in的结构体,前面我们介绍过,该结构体用于网络通信,我们在getScokaddr_in()函数内部设定好结构体的属性,然后返回,最后在外部接收。需要注意的是,我们需要设置struct sockaddr_in结构体的协议家族、端口号和IP地址,同时也需要大小端的问题,所以我们使用一些大小端转换的接口进行字节序转换,最重要的是,我们的IP地址设置为了"0.0.0.0"或者使用宏INADDR_ANY,这是因为服务器不需要绑定任何IP,如果服务器绑定了一个指定的IP,那么这就说明该服务器只能接收到该IP地址对应的数据。INADDR_ANY是一个宏

  这里就会有一个奇怪的问题,如果我们不用INADDR_ANY设置属性,而是使用我们默认提供的"0.0.0.0"字符串,可以看到注释部分使用了一个名为inet_addr()的接口:

  这是因为我们指定的IP是一个点分十进制表示的字符串,但是操作系统使用的IP地址是一个整数:

  所以我们必须将字符串转化为整数,当然,这个工作不需要程序员来做,因为我们无法得知我们机器的大小端字节序究竟是什么。

接下来我们就调用了bind()接口,即绑定套接字。那么何为bind()?或者说bind()的作用是什么?bind()的作用就是绑定套接字到使用socket()打开的网络文件当中,套接字就是我们之前所说的IP地址和端口号,将套接字绑定到了网络文件之后,才可以收发网络消息(不然别人怎么知道我在哪?)。那么它的声明如下:

  参数的目的非常明显,就是让我们将设置好属性的结构体绑定到socket()打开的网络文件当中。

此时构造函数的功能全部完成,目的就是让udpServer类在实例化对象的时候就初始化好服务器,然后只需调用start()成员即可启动服务器。那么在start()成员函数当中,使用recvfrom()接口阻塞式的读取从网络文件当中得到的数据,它的后两个参数是输入输出型参数,目的是知道是哪个客户端给服务器发送的数据,方便进行后续的工作。注意我们有一个名为_func的成员,它的类型是一个包装器,它能够回调外部的函数,也就是说可以处理其他的业务逻辑。聊天室的功能就在该回调函数当中实现。

1.1消息转发的实现

当服务端接收到来自客户端的消息时,通过_func()回调到外部逻辑,然后实现一次对所有链接到服务端的客户端的消息转发,达到群聊的效果。那么为了方便管理用户,可以单独定义一个类,并提供诸多成员方法:

// onlineUsers.hpp
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <unordered_map>
using namespace std;

/*网络必要头文件*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

/*用户具有的属性*/
class Users
{
public:
    Users(const string &ip, const uint16_t &port)
        : _ip(ip), _port(port)
    {}

public:
    string _ip;
    uint16_t _port;
};

/*管理用户的类*/
class onlineUsers
{
public:
    /*添加用户,即将ip+port作为key,Users对象作为value*/
    void addUser(const string &ip, const uint16_t &port)
    {
        string id = ip + to_string(port);
        _users.insert(make_pair(id, Users(ip, port)));
    }

    /*删除用户,通过id作为key删除用户*/
    void delUser(const string &ip, const uint16_t &port)
    {
        string id = ip + to_string(port);
        _users.erase(id);
    }

    /*判断是否在线*/
    bool isOnline(const string &ip, const uint16_t &port)
    {
        string id = ip + to_string(port);
        return _users.find(id) == _users.end() ? false : true;
    }

    /*向在线的所有用户转发消息*/
    void routingMessage(int _socketFd,const string &ip,const uint16_t &port,string &message)
    {
        for(auto &user : _users)
        {
            struct sockaddr_in client;
            bzero((void *)&client,sizeof(client));
            client.sin_family = AF_INET;
            client.sin_port = htons(user.second._port);
            client.sin_addr.s_addr = inet_addr(user.second._ip.c_str());
            
            string s = ip + "-" + to_string(port) + "# " + message;
            sendto(_socketFd,s.c_str(),s.size(),0,(struct sockaddr *)&client,sizeof(client));
        }
    }

private:
    /*哈希表存储用户*/
    unordered_map<string, Users> _users;
};

那么服务端的业务逻辑就是这样的:

#include "udpServer.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <signal.h>
using namespace std;
using namespace server;

#include "onlineUsers.hpp"

onlineUsers ous;
void chatDemo(int _socketFd,struct sockaddr_in &client,string &message)
{
    string ip = inet_ntoa(client.sin_addr);
    uint16_t port = ntohs(client.sin_port);
    if(message == "online") ous.addUser(ip,port);
    if(message == "offline") ous.delUser(ip,port);

    if(ous.isOnline(ip,port))
    {
        ous.routingMessage(_socketFd,ip,port,message);/*路由消息(转发消息)*/
    }
    else 
    {
        string response = "please online...";
        sendto(_socketFd,response.c_str(),response.size(),0,(struct sockaddr *)&client,sizeof(client));
    }
}

void Usage(char *command)
{
    cout << "\nUsage:\n\t" << command << "\t" << "server_port\n" << endl; 
}

/*服务器启动时只需要绑定端口号*/
int main(int argc,char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(START_ERROR);
    }
    uint16_t port = atoi(argv[1]);
    
    unique_ptr<udpServer> upus(new udpServer(chatDemo,port));
    upus->start();
    return 0;
}

至此,服务端的工作完成。

2.UDP客户端

客户端的代码与服务端的代码十分类似,我们先看上层的调用逻辑:

// udpClient.cpp

#include "udpClient.hpp"
#include <iostream>
#include <string>
#include <memory>
using namespace std;
using namespace client;

void Usage(char *command)
{
    cout << "\nUsage:\n\t" << command << "\t" << "server_ip\t" << "server_port\n" << endl; 
}

/*客户端启动时必须绑定两个选项
 *意在指明与哪个服务器建立通信*/
int main(int argc,char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(START_ERROR);
    }
    string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);
    unique_ptr<udpClinet> upuc(new udpClinet(serverIp,serverPort));
    upuc->start();
    return 0;
}

可以发现客户端与服务端的区别。服务端启动时只需要绑定端口号,目的就是为了确定唯一的进程,方便建立通信。但是客户端在启动的时候需要IP地址和端口号,也就是说启动时需要套接字,那么这个套接字是谁的?客户端启动时需要的套接字不是客户端的套接字,而是服务端的套接字。因为客户端要与服务端建立通信,那么它就需要知道服务端在哪,并且我们在编写客户端时,我们不需要关心客户端自己的套接字,因为这个部分的工作由操作系统帮忙完成,具体什么意思,我们看客户端的实现:

// udpClient.hpp

#pragma once

#include <string>
#include <iostream>
#include <strings.h>
/*网络必要的头文件*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

/*其他操作系统接口*/
#include <unistd.h>
#include <pthread.h>
#include "blockQueue.hpp"
#include <vector>
using namespace blockqueue;

namespace client
{
    using namespace std;
    enum
    {
        SOCKET_ERROR = 1,
        START_ERROR
    };
    class udpClinet
    {
    public:
        udpClinet(const string &serverIp, const uint16_t &serverPort)
            : _socketFd(-1), _serverIp(serverIp), _serverPort(serverPort)
        {
            _socketFd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_socketFd == -1)
            {
                cerr << "socket fail" << endl;
                exit(SOCKET_ERROR);
            }
            cout << "socket success: " << _socketFd << endl;
            /*客户端不关心自己的IP和端口号,所以不需要我们自己动手bind
             *但这并不代表客户端不需要bind,只是这个工作操作系统给做了*/
        }

        static void *productorDemo(void *args)
        {
            pthread_detach(pthread_self());
            udpClinet *_this = static_cast<udpClinet *>(args);
            blockQueue<string> *bq = _this->bq;
            while (true)
            {
                int socketFd = (static_cast<udpClinet *>(args))->_socketFd;
                char buffer[1024];
                struct sockaddr_in server;
                socklen_t len = sizeof(server);
                size_t n = recvfrom(socketFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&server, &len);
                if(n > 0) buffer[n] = 0;
                string message = buffer;
                bq->push(message);
            }
        }

        void start()
        {
            /*主线程充当消费者,新线程充当生产者
             *生产者从网络当中获取消息,并将该消息写到阻塞队列当中
             *消费者直接从阻塞队列当中获取消息*/
            pthread_t consumer, productor;
            pthread_create(&productor, nullptr, productorDemo, this);

            while (true)
            {
                string message;
                cerr << "Please Say[回车刷新聊天记录]# ";
                getline(cin, message);
                struct sockaddr_in server = getSockaddr_in();
                sendto(_socketFd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
                
                /*消费者从阻塞队列当中获取消息,一次全部拿完
                 *强制让主线程休眠一小会,目的是起到每次输入完后,强制调度生产者线程*/
                usleep(1234);
                while(!bq->isEmpty())
                {
                    string ret;
                    bq->pop(&ret);
                    cout << ret << endl;
                }
            }
        }

    private:
        /*网络通信三个条件:
         *网络文件、IP、端口号
         *作为客户端不需要关心自己的IP和端口号
         *要关心服务器的IP和端口号*/
        int _socketFd;
        string _serverIp;
        uint16_t _serverPort;

        static blockQueue<string> *bq;/*阻塞队列*/

        /*获取struct sockaddr结构体*/
        struct sockaddr_in getSockaddr_in()
        {
            struct sockaddr_in server;
            /*初始化sockaddr_in对象
             *并且将端口号、IP地址设置进去
             *我们使用的IP地址是字符串,所以需要转换成整数
             *使用inet_addr()接口,里面自动转换成整数,并且自动大小端字节序*/
            bzero((void *)&server, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(_serverPort);
            server.sin_addr.s_addr = inet_addr(_serverIp.c_str());
            return server;
        }
    };
    blockQueue<string> *udpClinet::bq = new blockQueue<string>();
} /*namespace client ends here*/

可以发现,在构造函数当中,我们并没有调用bind()绑定套接字,在注释当中也说到了该任务由操作系统完成。原因还是上面说过的老话,客户端最关心的就是服务端的IP地址和端口号,客户端本身的IP地址和端口号的最大功能就是让服务端知道是哪个客户端发送的数据,所以说这个工作不需要我们来完成。所以我们在getSockaddr_in()成员函数中,将结构体的IP地址和端口号,都设置成了服务端的IP地址和端口号。

那么start()接口就是客户端的启动入口了,为了方便网络通信,我们新创建了一个线程,这个线程充当生产者,它从网络当中获取消息并将消息放入阻塞队列当中;那么主线程就相当于消费者,每次完成输入之后都会从阻塞队列当中获取消息并打印出来。注意中间使用了一条usleep(1234)语句,无论它休眠多久,我们的目的就是在每次输入完成后,强行调度生产者线程从网络当中获取最近的消息,主线程醒来后再从阻塞队列当中获取消息并输出。

当然,我们没有显式地使用bind()绑定的一个重要原因就是,bind()会将固定的套接字绑定到socket当中,并且因为客户端的套接字通常都是固定的(服务器的套接字通常是固定的),并且由于客户端是一个经常加载、退出的程序,所以程序启动时都会绑定一个固定的套接字,这就会造成一个偶然错误,即万一程序启动时该套接字被其他进程占用了,那么该客户端就起不来了。所以我们bind()的任务交给操作系统,操作系统会选择空闲的端口号来绑定,具体绑定的过程就发生在recvfrom()或者sendto()接口当中,操作系统会检测当前进程有没有绑定套接字,如果没有操作系统就会自动完成这个任务。客户端的绑定套接字的作用就是让服务端知道是哪个客户端发来的数据,将来发送的数据要到哪个客户端去

在这里,附上阻塞队列的生产消费模型的代码:

#pragma once

#include <queue>
#include <pthread.h>
namespace blockqueue
{
    using namespace std;

    template <class T>
    class blockQueue
    {
    private:
#define MAXCAP 100 /*缓冲区上限大小,随时可变*/
    public:
        blockQueue(const size_t &cap = MAXCAP) : _cap(cap)
        {
            pthread_mutex_init(&_mutex, nullptr);
            pthread_cond_init(&_productorCond, nullptr);
            pthread_cond_init(&_consumerCond, nullptr);
        }
        ~blockQueue()
        {
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_productorCond);
            pthread_cond_destroy(&_consumerCond);
        }

        /*向阻塞队列(缓冲区)生产数据*/
        void push(const T &in)
        {
            pthread_mutex_lock(&_mutex);

            /*如果阻塞队列为满,生产者不能生产,进入自己的条件变量等待*/
            while (isFull())
            { /*这里必须使用while而不是if*/
                /*进入条件变量相当于发生一次线程切换
                 *然是要将锁释放,让其他线程拥有锁
                 *这就是为什么需要传入锁的原因*/
                pthread_cond_wait(&_productorCond, &_mutex);
            }
            _q.push(in);

            /*生产者生产一个,说明阻塞队列就多一个数据
             *所以此时可以唤醒消费者消费*/
            pthread_cond_signal(&_consumerCond);
            pthread_mutex_unlock(&_mutex);
        }

        /*从阻塞队列(缓冲区)拿数据
         *使用输出型参数*/
        void pop(T *out)
        {
            pthread_mutex_lock(&_mutex);
            while (isEmpty())
            {
                pthread_cond_wait(&_consumerCond, &_mutex);
            }
            *out = _q.front();
            _q.pop();
            pthread_cond_signal(&_productorCond);
            pthread_mutex_unlock(&_mutex);
        }

        bool isFull()
        {
            return _q.size() == _cap;
        }
        bool isEmpty()
        {
            return _q.size() == 0;
        }

    private:
        /*以一个队列作为缓冲区*/
        queue<T> _q;

        /*生产者与消费者之间存在互斥与同步关系
         *故定义一把锁、生产者条件变量、消费者条件变量*/
        pthread_mutex_t _mutex;
        pthread_cond_t _productorCond;
        pthread_cond_t _consumerCond;

        /*缓冲区的大小*/
        size_t _cap;
    };
} /*namespace blockqueue ends here*/

3.效果展示

服务器启动时,需要绑定端口号(云服务器下):

客户端启动时,需要绑定服务器的IP地址和服务器进程的端口号(虚拟机下):

再在本地环境中启动一个客户端,以检测群发功能是否正常(云服务器下):

现在两个客户端登陆,并且先让虚拟机客户端先发送消息,测试云服务器上的客户端能否收到消息:

云服务器客户端发送消息,测试虚拟机客户端能否收到消息:

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

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

相关文章

sql 性能优化基于explain调优(二)

文章目录 Explain问题描述解决方案 Explain 关于Explain具体怎么用以及有哪些优点&#xff0c;我就不过多的跟大家去讲解了&#xff0c;从我最初的文章: explain是什么&#xff1f;explain优缺点及如何使用explain优化SQL&#xff0c;大家可以点击这个链接看一下&#xff0c;对…

Midjourney基础教程

本教程收集于:AIGC从入门到精通教程 Midjourney基础教程 目录 新手快速入门知识汇总:

Facebook 广告效果越来越差,怎么办?

在如今的数字营销领域中&#xff0c;Facebook作为独立站卖家首选的推广引流平台&#xff0c;具备了许多优势。 一方面&#xff0c;Facebook拥有庞大的用户数量&#xff0c;是全球最大的社交媒体平台之一。另一方面&#xff0c;Facebook的广告算法可以将广告推送给更加精准的受…

创建.Net MAUI工程

下载Visual Studio或Visual Studio for Mac或Rider&#xff08;收费&#xff09; Visual Studio for Mac 2022Visual StudioRider 工具推荐 如果使用的是Windows电脑&#xff0c;建议使用Visual Studio 2022并安装ReSharper插件。如果使用的是Mac电脑&#xff0c;建议使用Ri…

【教程】用GraphSAGE和UnsupervisedSampler进行节点表示学习

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 无监督的GraphSAGE 加载 CORA 网络数据 按需采样的无监督GraphSAGE 无监督取样器&#xff08;UnsupervisedSampler&#xff09; 提取节点嵌入 节点嵌入的可视化 下游任务 数据拆分 分类器训练 无监…

8.1.0版本ELK搭建,不开启xpack认证机制

8.1.0版本ELK搭建&#xff0c;不开启xpack认证机制 部署环境安排下载安装包服务器环境配置部署elasticsearch部署kibana部署logstash部署httpd&#xff0c;filebeat配置kibana页面 部署环境安排 ip部署服务192.168.0.121kibana,elasticsearch192.168.0.83elasticsearch,logsta…

Spring boot 注解@Async不生效 无效 不起作用

今天在做公司项目时&#xff0c;有一个发邮件的需求。所以写了一个发送邮件的方法后来发现发邮件很慢&#xff0c;导致接口响应也很慢。于是我便想到要使用异步调用去处理这个方法。于是我把注解Async 加到了自己service类下的一个发邮件的一个方法&#xff0c;后来发现并没有生…

Push rejected,用Git修改已提交的注释

问题&#xff1a;有时候因注释与git规定的模板不匹配&#xff0c;会导致远程提交被拒绝 Push rejected 解决&#xff1a;修改不符合规范的注释再push即可 1、打开命令窗口 在项目根目录下右键点击出 Git批处理命令窗口。 2、查看已提交的commit 运行命令&#xff1a;git reba…

dataease源码阅读

源码&#xff1a;https://gitee.com/fit2cloud-feizhiyun/DataEase.git 文件夹目录 1.仪表盘主路由&#xff1a;frontend/src/views/panel |-- penel |-- index.vue |-- panel.js |-- appTemplate | |-- AppTemplateContent.vue | |-- index.vue | |-- component | |-- AppT…

华为OD机试真题 Java 实现【机器人活动区域】【2023Q1 200分】

一、题目描述 现有一个机器人&#xff0c;可放置于 M N的网格中任意位置&#xff0c;每个网格包含一个非负整数编号。当相邻网格的数字编号差值的绝对值小于等于 1 时&#xff0c;机器人可在网格间移动 问题&#xff1a;求机器人可活动的最大范围对应的网格点数目。 说明&a…

ESP32-C2开发板Homekit例程

准备 1.1硬件ESP32 C2开发板&#xff0c;如图1-1所示 图1-1 ESP32 C2开发板 1.2软件 CozyLife APP可以在各大应用市场搜索下载&#xff0c;也可以扫描二维码下载如图1-2所示 HomeKit flash download tool 烧录工具 esp32c2 homkit演示固件 烧录教程 打开flash_download_to…

每日一题161——对角线遍历

给你一个大小为 m x n 的矩阵 mat &#xff0c;请以对角线遍历的顺序&#xff0c;用一个数组返回这个矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;mat [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,4,7,5,3,6,8,9] 示例 2&#xff1a; 输入&#xff1a;mat …

【大数据学习篇8】 热门品类Top10分析

在HBase命令行工具中执行“list”命令&#xff0c;查看HBase数据库中的所有数据表。学习目标/Target 掌握热门品类Top10分析实现思路 掌握如何创建Spark连接并读取数据集 掌握利用Spark获取业务数据 掌握利用Spark统计品类的行为类型 掌握利用Spark过滤品类的行为类型 掌握利用…

【嵌入式烧录刷写文件】-1.4-移动Motorola S-record(S19/SREC/mot/SX)中指定地址范围内的数据

案例背景&#xff08;共5页精讲&#xff09;&#xff1a; 有如下一段S19文件&#xff0c;将源地址范围0x9100-0x9104中数据&#xff0c;移动至一个“空的&#xff0c;未填充的”目标地址范围0xA000-0xA004。 S0110000486578766965772056312E30352EA6 S123910058595A5B5C5D5E5…

调用返回风格

主程序子程序 面向过程 单线程控制&#xff0c;把问题划分为若干个处理步骤&#xff0c;构件即为主程序和子程序&#xff0c;子程序通常可合成为模块。过程调用作为交互机制&#xff0c;即充当连接件的角色。调用关系具有层次性&#xff0c;其语义逻辑表现为主程序的正确性取…

nodejs微信小程序 vue+uniapp停车场车位管理系统sringboot+python

使用微信小程序进行应用开发&#xff0c;使用My SQL软件搭建数据库&#xff0c;管理后台数据并使用Java语言进行程序设计&#xff0c;借鉴国内现有的停车场管理系统&#xff0c;在他们的基础上进行增减和创新&#xff0c;使用Photoshop完成升降式停车场管理系统的界面部件设计&…

Python学习30:存款买房(C)

描述‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬ 你刚刚大学毕业&#xff0c;…

龙蜥开发者说:构建软件包?不,是构建开源每一个角落!| 第 20 期

「龙蜥开发者说」第 20 期来了&#xff01;开发者与开源社区相辅相成&#xff0c;相互成就&#xff0c;这些个人在龙蜥社区的使用心得、实践总结和技术成长经历都是宝贵的&#xff0c;我们希望在这里让更多人看见技术的力量。本期故事&#xff0c;我们邀请了龙蜥社区开发者任博…

JavaWeb-Ajax的学习

Ajax 今日目标&#xff1a; 能够使用 axios 发送 ajax 请求熟悉 json 格式&#xff0c;并能使用 Fastjson 完成 java 对象和 json 串的相互转换使用 axios json 完成综合案例 概述 AJAX (Asynchronous JavaScript And XML)&#xff1a;异步的 JavaScript 和 XML。 我们先来…

LED显示屏的部件组成

LED显示屏通常由以下几个主要部件组成&#xff1a; LED模块&#xff1a;LED模块是构成LED显示屏的基本单元&#xff0c;包含多个LED发光元件以及相应的电路和连接器。LED模块通常以方形或长方形的形式存在&#xff0c;可以根据需要组合成各种尺寸和形状的显示屏。免费送你Led模…