网络套接字-UDP服务器

一 预备知识

       1 端口号和进程id

        主机间的数据传输本质是两个进程在通信,就像是我们打开抖音刷视频,视频不是都保存在手机上的,而是服务器发送给你的,这里就是用到了网络。

        那如何保证把数据给指定进程呢? 就是用端口号去标识主机中的进程,是个两字节的数据,16个比特位的整数。而ip+端口号表示互联网中唯一的进程,而两个互联网中唯一的进程间通信就是通过套接字通信。

        那端口号不能用进程pid吗? 这样会让进程管理和网络管理耦合度提高,所以就重新设计出了端口号。显然一个端口号只能被一个进程绑定,但一个进程可以关联多个端口号,不过如何理解一个进程绑定多个端口号,暂时没想到使用场景。端口号本质就是一个hash表的下标,下标存着进程pcb指针。

2 初识网络通信

        网络的数据是如何给进程的呢? 首先a主机给b主机发消息,然后消息经过有限和无线等传输到了b主机,此时数据该给谁呢,就是通过数据中的目标端口号找到指定进程,直接给进程?不是的,后面实现我们就知道进程访问网络来的数据实际上是访问文件,所以os不是直接把数据给进程的,而是找到进程后找到文件描述符表,通过套接字找到文件,然后找到缓冲区,将数据拷贝到缓冲区中。显然套接字就是一个文件描述符。

2 初识UDP和TCP协议

        传输层有两个比较重要的协议,就是TCP/UDP。先来认识认识TCP,TCP协议是自带可靠性的,也就是会在数据传输中如果数据丢失了,会采用不同的策略来保证数据能传输给接收者。是面向字节流。

        而UDP协议则是面向用户数据报协议,是个不可靠传输协议,也就是不管数据是否传输成功,直接把数据给下一层就不管了。(什么叫面向数据报和面向字节流,后面我们会理解,反正就是收发单位按一个数据报或者按一个字节来计算) 

UDP的意义:使用简单,而且对服务器压力小,不需要数据百分百送到用户手上就可以用udp。

3 网络字节序

        大小端:大端,将高权值位放低地址,大端,反之小端。首先数据有高地址和低地址,先发送谁,如果不定下来,对方怎么知道先接收的是高地址还是低地址,好吧,那就规定先发出低地址的数据,这样接收主机就可以按接受顺序还原数据,并且由此规定网络中先发的数据是低地址的,后发的数据是高地址的,

        这样还有新问题,当a主机发数据到了b主机上,如果两个主机字节序不一样,在解读网络发来的数据的时候就会出错。因为b主机是按接收顺序,按地址还原的数据,所以字节序不会改变。        例如A主机发出0x123456,会先把低地址上的数据发出去,在网络中排列还是小端,B主机是大端机,B主机在解释这个数据的时候会按大端字节序来解释,就解释反了。而且B主机是无法判断来的数据是大端还是小端,所以我们统一规定网络中的数据序列必须是大端。

        协议没办法解决,因为协议本身也是数据,接收数据的主机甚至都无法区分哪里是报头,哪里是报文。

         转换使用的库函数

二 socket接口

        下面有不少函数,先别急着记忆,后面使用再来理解。

        上述函数都有个参数,好像是个结构体,接下来看看这个sockaddr结构体是什么?

 这是一个通用的结构体,因为pro 6既想提供主机内通信,又想提供跨网络通信,这些用的都是不用的协议,例如网络通信有ipv4和ipv6标准,它们的ip地址就不一样,所以必定是传递不同的参数,可能要实现不同的接口,为了维持接口的一致,就要先保证传入的类型一样,所以就设计出sockaddr这个通用结构体。

        为什么不用void*呢?因为设计时还不支持void*,当支持时已经晚了。

        网络通信就用中间这个这个结构体,本地就用右边的那个结构体,调用socket接口函数时都要强转成sockaddr传入,内部会根据前十六位看看是什么类型,然后判断是网络通信还是本地通信。内部拿到指针,还要把数据接收过去,所以就让我们传大小,他们内部先把数据拿过去,然后再做对应类型的强转,我还想着为什么不是内部通过指针访问前十六位,然后就判断大小,就不用我们外部传了,现在想想这其实是另一种设计思路,我想都可以,接下来就实现一个简易的客户端和服务端通信程序来理解理解udp通信。

三 实现服务端

1 初始化服务端

        目的:客户端给服务端写数据,然后客户端也能收到服务端发回的数据。

        要一次性实现两个可执行文件,makefile就要用下面的格式。all是有依赖关系,无依赖方法,clean是有依赖方法,没有依赖关系。

.PHONY:all
	all:udp_client udp_server
udp_client:udp_client.cc
	g++ -o $@ $^ -std=c++11
udp_server:udp_server.cc
	g++ -o $@ $^ -std=c++11	
.PHONY:clean
clean:
	rm -rf udp_client udp_server

        server.cc提供接口如下。

#include<functional>
int main()
{

    udp->start();
    return 0;
}

        start内部做初始化动作。

        先来创建套接字来通信,本质是打开一个文件。

        参数介绍;1 :是表明是ip地址类型,因为我们是要网络通信,所以可以传AF_INET或者AF_INET6,有意思的是我们后面bind的时候还要传一次,这是为什么呢?后面提。

        参数2是传套接字类型,有流式套接字和数据报式套接字,参数3为协议,如果是TCP,参数2用下图第一个,udp用第二个,也可以传个零,内部会根据参数2来判断参数3要传什么协议。显然不同的通信方式要用创建不同的套接字。

        那要创建不同的套接字,给的参数肯定是不同的,如下图,这些参数是要保存起来的,所以创建文件前要提前知道大小给下面的数据开空间,例如网络套接字需要ip,ipv4和ipv6地址大小不一。所以在创建文件前就要说明ip的地址类型。面向字节流的文件和面向数据报的文件据我目前理解是不同的,后面看完协议或许能理解区别,所以在创建时要说明文件是面向字节流的还是面向数据报的,以上我对socket参数的理解。

        那什么时候初始化文件传参初始化套接字属性呢? 绑定的时候。

        参数1就是上面调用socket返回的文件描述符,第二个参数就是那个通用结构体,第三个参数就是套接字的大小,为什么要传大小也已经提过啦,比较复杂的就是初始化这个结构体。我们只要初始化前三个成员即可。

        第一个成员比较有意思,这个宏实际上是在定义一个变量,被替换成sa_family_t   sin_family。

        sa_family_t类型恰好就是一个十六位的类型,保存的是就是套接字类型标识。我们知道这个标识是用来分清楚传入的指针是sockadd_in还是sockaddr_un,为什么bind还要再传套接字标识符呢,我们不是已经在创建文件前说好套接字类型了吗,那文件属性应该保存了套接字类型了吧,所以bind的时候就不用再传套接字属性。

        我认为设计者可以获取文件属性,知道绑定的文件需要什么类型的参数,这样对传入参数直接强转,也可以外部指定类型,内部做判断强转,都可以,只是当时用了后者的实现方式。

        而且套接字类型标识怎么不是SOCK_STEREAM和SOCK_DGRAN呢? 为什么我们在初始化传的是AF_INET?

        我的理解就是套接字需要ip+端口,还需要标识是面向数据报还是面向字节流,这些都是套接字的属性,实际上属性就是套接字的类型,所以SOCK_STEREAM和SOCK_DGRAN是套接字类型标识,AF_INET和AF_UNIX也被称为套接字类型标识。

        而在bind的时候,传的参数一般是ip和端口,例如网络通信要传32的ip地址,而ipv6下要传128位,主机通信又不用传ip地址,所以就用AF_INET和AF_UNIX,AF_INET6来区分,而不是用SOCK_STEREAM和SOCK_DGRAN这两个宏。

        sin_port保存的是端口号,有个细节就是我们对这个端口号做了主机转网络序列,显然端口号是要被发送到网络上的。

        sin_addr是sock结构体内的结构体,成员就是一个四字节的ip地址,但是我们没有传一个具体的ip地址,首先我这代码就是在云服务器下跑的,云服务器一般不允许用户bind公网ip地址,貌似是和安全有关系,而且直接绑定也不好,首先云服务器有多个ip地址,如果绑定了具体的ip地址,我们写的程序就只能接收固定目标ip地址的数据,如果我们像上图传个INADDR_ANY(全0没必要转网络字节序),此时这个服务器上收到的消息只要是给我们这个进程对应的端口号,那我们就都能接收。我们服务端代码指定端口号这也是个细节,下面会再谈。

        所以服务器的大致实现如下。

​
  const static uint16_t default_port = 8080;
    class UdpServer
    {
    public:
       UdpServer(u_int16_t port = default_port)//缺省参数要从左往右给
        :_port(port)
        {
            ;
        }
        ~UdpServer()
        {
 
        }
        void start()
        {            
            //打开网络文件
            socket_ = socket(AF_INET,SOCK_DGRAM,0);
            if(socket_ < 0)
            {
                std::cout<<"create socket error"<<strerror(errno)<<std::endl;
                exit(SOCKET_ERR);
            }
            std::cout<<"create socket successs"<<std::endl;
            
            //绑定端口号和ip地址
            struct sockaddr_in sock;//头文件<netinet/in.h>
            bzero(&sock,sizeof(sock));
            sock.sin_addr.s_addr = INADDR_ANY;//设置ip地址 表示所有的ip的地址
            sock.sin_port = htons(_port);
            sock.sin_family = AF_INET; 
            if(bind(socket_,(sockaddr*)(&sock),sizeof(sock)) < 0)
            {
                std::cout<<"bind error "<<strerror(errno)<<std::endl;
                exit(BIND_ERR);
            }
            std::cout<<"bind successs"<<std::endl;
        }
       
    private:
        int socket_;
        uint16_t _port;
    };

​

        前面说了我们写的服务端ip地址由云服务器上的os来绑定,那端口号为什么也是自己指定呢?为什么不能让os随机应变,看到哪个端口号空了就把那个端口号给我的服务端,因为我们服务端这个进程的端口号就像是报警电话,是不能经常变的,所以需要公司对服务器的端口号做统一划分,这样客户端通信的时候就一直都知道服务端的端口号是什么。

        注意:exit返回的错误宏,统一封装在err.hpp中。

2 服务端工作函数

        前面已经说了服务端如何打开文件,显然网络中来的数据要被放到这个文件内,所以我们接下来就要让服务端从这个文件中读取数据,就是用到下面的recvfrom函数。

        sockfd读数据到buf中,buf长度为len,flag表示读取方式,有阻塞和非阻塞。返回值表示读取的字节。还有两个输入输出型参数意义:因为要记录是谁发过来的数据,所以需要保存对方的ip+端口号,下面会有用的。

        当我们可以收数据了,就可以再把数据发给客户端,sendto函数就是发消息的函数,后面两个参数就是表示要给谁发数据,这就是为什么前面recvfrom函数中要传入一个sock结构体的原因,此时就会把我们bind的ip和端口发给客户端。

        有意思的是,我们这个sock内部保存的客户端的ip和端口,是我们先前从网络中读取的,此时这个数据是什么序列呢?有兴趣的可以尝试一下,就会发现recvfrom函数没帮我们转成主机序列,所以我们在将sock内的数据要发送到网络时也就不用再转成网络序列了。

        所以,按照当前设计就是服务端创建好文件后就死循环地从文件读和写。

四 客户端实现

        也要创建套接字来通信

        客户端要不要绑定呢? 不用,要os自己指定ip和端口号。为什么不要自己绑定呢?因为我们手机上会多个客户端,如果每个软件设计者都自己指定端口号,那冲突了怎么办?

        那服务端的端口号为什么要自己指定呢?免得服务端挂了后端口号就变了,那我们的软件难道都要更新服务端的端口号吗? 所以软件服务端的端口号是固定的,那服务器上不会有很多个进程,多个服务端吗,那会不会别的进程在抢端口号导致某个进程起不来,不会的,这些端口号都会被公司统一管理分配,你有见过哪家微信的服务端部署在阿里上吗,所以不会出现竞争。

        可是要发消息用sendto函数的时候,就有点犯难了,如何知道服务器的IP和端口,显然服务器的ip和端口也是设计者一开始传入的。

        客户端如果不给服务端发消息,服务端就无法获取客户端的ip和端口,也无法给客户端发消息,客户端也无法获取服务端ip和端口,所以在编写客户端时会传入ip和端口。

./client 服务端ip + port
int main(int argc,char*argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        std::cout<<"stage error"<<strerror(errno)<<std::endl;
        exit(USAGE_ERR);
    }
    const std::string ip_ = argv[1];
    u_int16_t port = atoi(argv[2]);
    
    int socket_ = socket(AF_INET,SOCK_DGRAM,0);
    if(socket_ < 0)
    {
        std::cout<<"create socket error"<<strerror(errno)<<std::endl;
        exit(SOCKET_ERR);
    }
    std::cout<<"create socket successs"<<std::endl;        
    //打开网络文件
           
    //发消息
    struct sockaddr_in sock;
    sock.sin_addr.s_addr = inet_addr(ip_.c_str());

将字符串类型的地址转为四字节地址,而且是网络字节序了

    sock.sin_port = htons(port);
    sock.sin_family = AF_INET;
    pthread_t id;
    pthread_create(&id,nullptr,recv,&socket_);
    while(true)
    {
        std::string msg;
        std::cout<<"客户端# "<<std::endl;
        getline(std::cin,msg);
        sendto(socket_,msg.c_str(),msg.size(),0,(sockaddr*)&sock,sizeof(sock));

        struct sockaddr_in tmp;
        char buffer[1024] = {0};
        socklen_t len = sizeof(tmp);
        int ret = recvfrom(socket_,buffer,sizeof(buffer)-1,0,(sockaddr*)&tmp,&len);
        buffer[ret] = '\0';
        if(ret)
            std::cout<<buffer<<std::endl;
    }   
    return 0;
}

        收消息,并且保存服务端的ip和端口,虽然我们已经知道了服务端的ip和端口,但是总不能让内部做判断,不用接收服务端的ip和端口,就为了省几个字节,没必要,本代码比较简单,我们只是先搭个架子,能让服务端收到消息,客户端也能发消息并且收到回信就好了,tmp内容会和msg变量内容一样。

那客户端什么时候绑定的呢,显然是在sendto函数时,使用样例如下。

        127.0.0.1是本地环回ip,表示当前主机,填这个ip就是告诉os,数据不用发到网络,你直接按照往网络发的步骤做,在发给网络前停住发回给自己,因为网络通信中一般是不会出错的,所以只要我们的数据能走到往网络发的哪一步,我们的代码就没问题。毕竟我们也不好弄出两台主机。

五 应用1

        如何将服务器改成群聊模式,也就是任意一个人发送的消息,结果要给每一个人。这里我们就用到了多线程模式,一个线程收消息,一个线程发消息给所有的用户,这其实就是一个生产消费者模型,而缓冲区应该放什么呢?收到的消息。而我们先前就实现过了一个环形队列CirQueue,现在可以直接拿来用了。所以我们在服务端增加了两个线程,这两个线程也是我先前封装的小组件,我们后面讲怎么用,并且把代码贴出来,大家看看内部实现即可。

        online_用来保存用户的套接字信息,方便发消息给所有用户。为什么要保存构建用户名,后面方便输出提示显示是谁发的。

        添加用户

        void adduser(std::string name, struct sockaddr_in sock)
        {
            {
                LockGuard lock(&mutex_);
                if(!online_.count(name))//返回1:存在,返回0:不存在
                    online_[name] = sock;
            }
        }

        线程接收数据。

         //开始接受数据
        void Recv()
        {
           while(true)
           {
                struct sockaddr_in sock;
                char buffer[1024] = {0};
                socklen_t len = sizeof(sock);
                int n = recvfrom(socket_,buffer,sizeof(buffer)-1,0,(sockaddr*)&sock,&len);
                buffer[n] = '\0';

                std::cout<<"port_ "<<sock.sin_port<<" ip "<<sock.sin_addr.s_addr;
                std::string clientip = inet_ntoa(sock.sin_addr);
                u_int16_t clientport = ntohs(sock.sin_port);
                std::cout<<"port_ "<<clientport<<" ip "<<clientip;

                std::string name;
                name += clientip;
                name += "-";
                name += to_string(clientport);
                name += "#";
                adduser(name,sock);
                name += buffer;

               推送到环形队列
                rp_.push(name);

                std::cout<<clientip<<"-"<<clientport<<"# "<<buffer<<std::endl;//输出处理前的字符
                //上面是在接收客户端的ip和端口号
                // sendto(socket_, name.c_str(),name.size(),0,(sockaddr*)&sock,len);//不用转网络序列,是因为我们接收的时候就没转成主机序列
           }
        }

        开始广播,负责调用sendto函数发消息。

          void broadcast()//负责给所有的客户端发消息
        {
            while(true)
            {
                std::string msg;
                rp_.pop(msg);//获取一条消息
                std::vector<sockaddr_in> tmp;
                {
                    LockGuard lock(&mutex_);
                    for(auto e : online_)
                    {
                        tmp.push_back(e.second);
                    }
                }

                for(auto e : tmp)
                {
                    sendto(socket_, msg.c_str(),msg.size(),0,(sockaddr*)&e,sizeof(e));
//不用转网络序列,是因为我们接收的时候就没转成主机序列

                }
            }
        }

        不用给队列加锁,环形队列内部会用信号量和锁做保护,访问online_要加锁保护,所以我在类内又增加一个锁成员mutex_。

        在加锁时又用到了之前封装的加锁接口,此时直接把锁传入,内部会自行加锁,这对{}一点都不多余,是为了给锁划分作用域。

        可是为什么要拷贝一份再sendto呢,这里就非常巧妙了,因为我们如果是直接访问online_,那就要先加锁,那sendto也在加锁内,sendto又不会有线程安全的问题,为了符合加锁区域越小越好,所以就设计了这样的代码,在获取用户ip时加锁,免得另一个线程来添加,获取完后就可以直接发消息了。

        环形队列实现。

template<class T>
class CirQueue
{
public:
    CirQueue(int n = SIZE)
    :vt_(n)
    ,head_(0)
    ,tail_(0)
    {
        sem_init(&Consumer_,0,0);
        sem_init(&Productor_,0,n);
        pthread_mutex_init(&Con_mutex,nullptr);
        pthread_mutex_init(&Pro_mutex,nullptr);
    }
    ~CirQueue()
    {
        sem_destroy(&Consumer_);
        sem_destroy(&Productor_);
        pthread_mutex_destroy(&Con_mutex);
        pthread_mutex_destroy(&Pro_mutex);
    }
     void Lock(pthread_mutex_t& mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    void Unlock(pthread_mutex_t& mutex)
    {
        pthread_mutex_unlock(&mutex);
    }
    void push(const T& data)
    {
        sem_wait(&Productor_);//申请信号量
        tail_ = tail_ % vt_.size();
        Lock(Con_mutex);//加锁
        vt_[tail_++] = data;
        Unlock(Con_mutex);//解锁
        sem_post(&Consumer_);//释放信号量
    }
    void pop(T& data)
    {
        sem_wait(&Consumer_);
        head_ = head_ % vt_.size();
        Lock(Pro_mutex);
        data = vt_[head_++];//多线程访问会出问题,这里还要加锁,因为信号量只是起到限制执行流数量
        Unlock(Pro_mutex);
        sem_post(&Productor_);
    }
public:
    std::vector<T> vt_;
    int head_;
    int tail_;
    sem_t Consumer_;
    sem_t Productor_;
    pthread_mutex_t Con_mutex;
    pthread_mutex_t Pro_mutex;
};

        线程创建封装

class Thread
{
public:
    typedef enum
    {
        NEW = 1,
        RUNING,
        EXIT
    }status;
    // typedef void* (*fun_t)(void*);
    using fun_t = std::function<void()>;
    Thread()
    {
        ;
    }
    Thread(int num,fun_t fun)
    :id_(0),status_(NEW),fun_(fun)
    {
        name_ = "thread->" + std::to_string(num);
    }
    ~Thread()
    {
        ;
    }
    static void * threadRun(void*arg)
    {
        Thread* th = (Thread*) arg;
        // th->fun_(th->arg_);
        th->fun_();
        return nullptr;
    }
    void Run()
    {
        int n = pthread_create(&id_,nullptr,threadRun,(void*)this);
        if(n != 0)//成功返回0,不成功返回错误码
            exit(4);
        status_ = RUNING;    
    }
    void join()
    {
        pthread_join(id_,nullptr);
        status_ = EXIT;
    }
    std::string getname()
    {
        return name_;
    }
    int getstatus()
    {
        return status_;
    }
    pthread_t  getid()
    {
        if(status_ == RUNING)
            return id_;
        else
        {
            std::cout<<name_<<" not create ";
            return 1;
        }    
    }
    pthread_t id_;
    std::string name_;//线程名
    status status_;//线程状态
    fun_t fun_;//线程执行函数
    void* arg_;//线程参数
};

        我们前面添加了不少成员,构造和析构函数也有不少变化,构造函数,Thread的构造函数很简单,一个数字用来给内部做线程名,还有个可调用对象。

        释放锁和回收线程。

start函数内让两个线程跑起来。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[实战]API防护破解之签名验签

前言&#xff1a; 传统的接口在传输的过程中&#xff0c;是非常容易被抓包进行篡改&#xff0c;从而进行中间人攻击。这时候我们可以通过对参数进行签名验证&#xff0c;如果参数与签名值不匹配&#xff0c;则请求不通过&#xff0c;直接返回错误信息&#xff0c;从而防止黑客…

Java错误:微服务报错Cannot execute request on any known serve

&#x1f414;问题内容 报Cannot execute request on any known server 这个错&#xff1a;连接Eureka服务端地址不对。 &#x1f414;解决方式 检查.yml文件或者.properties文件配置 下划线下划线后面的小写字母等同于去掉下划线大写下划线后面的字母&#xff08;驼峰原则&am…

网络安全从业人员何去何从

从2024年1月1日开始到今天&#xff0c;基本没有真正放下自己休息过一天。可能很多人会说是卷&#xff0c;其实真正的原因是压力。不仅仅是生活压力还有行业压力。 今年这个行业让很多人开始感到了迷茫&#xff0c;不仅是股市的低迷&#xff0c;更多的来自于各大公司不断的因为…

Linux学习:基础开发工具的使用(1)

目录 1. Linux软件包管理器&#xff1a;yum工具1.1 yum是什么&#xff08;软件商城&#xff09;1.2 yum的使用1.3 yum的背景生态 2. 项目开发与集成开发环境3. vim编辑器3.1 vim编辑器的常见模式与模式切换3.3 vim编辑器的使用3.3.1 命令模式下的常见命令&#xff1a;3.3.2 vim…

【安装mysql】centos7 安装mysql

文章目录 1.卸载不用的环境2.获取mysql官方yum源3.开始安装4.常规登录4.1方法一&#xff1a;【博主可以】4.2方法二&#xff1a;直接用client登录4.3方法三&#xff1a;修改配置文件 5.设置配置文件5.1配置my .cnf5.2开机自启动 1.卸载不用的环境 查看有无mysql/mariadb ps ax…

优雅的记录日志,拒绝打印模糊信息导致bug定位难

想必大家都有过这样的经历&#xff1a;在项目中遇到报错需要紧急修复时&#xff0c;却因为日志信息模糊不清&#xff0c;无法迅速准确地定位到错误源头&#xff0c;这确实让人感到十分苦恼和无奈。 在新入职一家公司并着手修改遗留bug时&#xff0c;经常发现之前的开发者并未记…

Java Day9 Stream流

Stream流 1、认识2、Stream流使用步骤3、如何获取Stream流4.Stream流的中间方法5、 Stream流终结方法 1、认识 2、Stream流使用步骤 3、如何获取Stream流 //list获取stream流List<String> listnew ArrayList<>();Collections.addAll(list,"崔十一","…

NUMA简介

NUMA 1 什么是NUMA 早期的计算机&#xff0c;内存控制器还没有整合进 CPU&#xff0c;所有的内存访问都需要经过北桥芯片来完成。如下图所示&#xff0c;CPU 通过前端总线&#xff08;FSB&#xff0c;Front Side Bus&#xff09;连接到北桥芯片&#xff0c;然后北桥芯片连接到…

小型校园网配置笔记

1&#xff0c;搭建网络拓扑图 LSW1:三层交换机命令&#xff1a; <Huawei>sys [Huawei]undo info-center enable Info: Information center is disabled. [Huawei]vlan batch 10 20 30 40 100 101 [Huawei]int vlan 10 [Huawei-Vlanif10]ip add 192.168.10.254 24 …

vivado FIFO IP核的使用

FIFO是先进先出的数据缓存器。起到跨时钟域的数据缓冲作用&#xff0c;一般在实际应用过程当中采用异步读写的方式&#xff0c; 选择的配置如下 封装IP核 module clk_wiz(input resetn ,input clk_in1 ,output clk_out1 ,output clk_out2 …

VScode Error Lens插件

安装完成之后&#xff0c;当我们输入一些错误的语法格式的时候&#xff0c;它都会有一些提示&#xff01; 一开始是英文提示 修改为中文提示 设置搜索 typescript.local

力扣串题:字符串中的第二大数字

此题的精妙之处在于char类型到int类型的转化&#xff0c;需要运算来解决 int secondHighest(char * s) {int max1-1;int max2-1;int szstrlen(s);int i 0 ;for(i0;i<sz;i){if(s[i]>0&&s[i]<9){if((s[i]-0)>max1){max2max1;max1s[i]-0;}else if((s[i]-0)&l…

VUE/HTML网页在线编辑AutoCAD DWG文档

猿大师办公助手作为一款专业的网页在线编辑Office插件&#xff0c;不仅支持微软Office、金山WPS及永中Office完整嵌入到最新版Chrome、Ddge、Firefox等浏览器中使用&#xff0c;猿大师还可以把Autodesk的AutoCAD、DWG TrueView、Design Review等软件嵌入到浏览器网页中&#xf…

写给新手的单元测试框架unittest运行的简单问题

当使用unittest框架编写和运行单元测试时&#xff0c;需要遵循以下步骤&#xff1a; 1、导入unittest模块&#xff1a;在代码中首先导入unittest模块。 import unittest 2、创建测试类&#xff1a;创建一个继承自unittest.TestCase的测试类。该类将包含一系列测试方法。 clas…

《ElementPlus 与 ElementUI 差异集合》icon 图标使用(包含:el-button,el-input和el-dropdown 差异对比)

安装 注意 ElementPlus 的 Icon 图标 要额外安装插件 element-plus/icons-vue. npm install element-plus/icons-vue注册 全局注册 定义一个文件 element-icon.js &#xff0c;注意代码第 6 行。加上了前缀 ElIcon &#xff0c;避免组件命名重复&#xff0c;且易于理解为 e…

深入解析C++树形关联式容器:map、set及其衍生容器的使用与原理

文章目录 一、引言二、关联式容器的中的 paira.pair 的创建及使用b.pair 间的比较 三、 map 与 set 详解1. map 的基本操作2. set 的基本操作3.关联式容器的迭代器 四、 multimap 与 multiset 的特性五、关联式容器的使用技巧与注意事项1. 键值类型的选择与设计2. 自定义比较函…

openGauss学习笔记-241 openGauss性能调优-SQL调优-审视和修改表定义

文章目录 openGauss学习笔记-241 openGauss性能调优-SQL调优-审视和修改表定义241.1 审视和修改表定义概述241.2 选择存储模型241.3 使用局部聚簇241.4 使用分区表241.5 选择数据类型 openGauss学习笔记-241 openGauss性能调优-SQL调优-审视和修改表定义 241.1 审视和修改表定…

R语言复现:如何利用logistic逐步回归进行影响因素分析?

Logistic回归在医学科研、特别是观察性研究领域&#xff0c;无论是现况调查、病例对照研究、还是队列研究中都是大家经常用到的统计方法&#xff0c;而在影响因素研究筛选自变量时&#xff0c;大家习惯性用的比较多的还是先单后多&#xff0c;P&#xff1c;0.05纳入多因素研究&…

【深度学习笔记】8_2 异步计算

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;部分标注了个人理解&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 8.2 异步计算 此节内容对应的内容有兴趣的可以去看看原文。 今天的计算机是高度并行的系统&#xff0c;由多个CPU核、多个GPU、多个处…

yolov5模型压缩-torch_pruning

参考论文:DepGraph: Towards Any Structural Pruning(https://arxiv.org/abs/2301.12900) 主要原理:物理的移除参数,并自动找出层与层以及层之间的依赖,完成模型的自动裁剪 模型压缩效果:yolov5剪枝流程如下: pip install torch_pruning 新建prune.py: import torch_…