网络套接字-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:allall:udp_client udp_server
udp_client:udp_client.ccg++ -o $@ $^ -std=c++11
udp_server:udp_server.ccg++ -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函数内让两个线程跑起来。

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

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

相关文章

Pytorch学习 day13(完整的模型训练步骤)

步骤一&#xff1a;定义神经网络结构 注意&#xff1a;由于一次batch_size的大小为64&#xff0c;表示一次放入64张图片&#xff0c;且Flatten()只会对单张图片的全部通道做拉直操作&#xff0c;也就是不会将batch_size合并&#xff0c;但是一张图片有3个通道&#xff0c;在Ma…

YOLOv9改进项目|关于本周更新计划的说明24/3/12

目前售价售价59.9&#xff0c;改进点30个 专栏地址&#xff1a; 专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 日期&#xff1a;24/3/12 本周更新计划说明&#xff1a; 1. 更新华为Gold YOLO中的…

读西游记第一回:西游记世界格局

天地之数&#xff1a; 元&#xff1a;十二万九千六百岁&#xff08;129600年&#xff09; 1元12会&#xff1a;子、丑、寅、卯、巳、午、未、申、酉、戌、亥。每会18000年。与12地支对应。 亥会期&#xff1a;前5400年混沌期&#xff0c;后5400年&#xff0c;盘古开天辟地&am…

论文阅读——Vision Transformer with Deformable Attention

Vision Transformer with Deformable Attention 多头自注意力公式化为&#xff1a; 第l层transformer模块公式化为&#xff1a; 在Transformer模型中简单地实现DCN是一个non-trivial的问题。在DCN中&#xff0c;特征图上的每个元素都单独学习其偏移&#xff0c;其中HWC特征图上…

Mysql数据库学习笔记——第二篇

DML 添加数据 INSERT INTO 表名(字段1,字段2,……) VALUES(值1,值2,……); # 给指定字段添加数据INSERT INTO 表名 VALUES(值1,值2,……); # 给全部字段添加数据INSERT INTO 表名(字段1,字段2,……) VALUES(值1,值2,……),(值1,值2,……),(值1,值2,……); …

【开发】微服务整合Sentinel

目录 前言 1W&#xff1a;什么是Sentinel&#xff1f; 2W&#xff1a;为什么使用Sentinel&#xff1f; 3W&#xff1a;如何使用Sentinel&#xff1f; 1. 在pom.xml中导入Sentinel依赖坐标 2. 配置控制台 3. 访问API接口的任意端点 流量控制 1. 簇点链路 2. 快速入门…

某医院系统未授权访问

开局还是先测一下登录框&#xff0c;弱密码走一波&#xff0c;无果 通过指纹识别出该站是springboot开发&#xff0c;扫描目录查看是否存在泄露 存在泄露&#xff0c;访问地址 很多接口&#xff0c;不可能一个一个手动测试吧&#xff0c;上工具 工具扫描完会生成文档&#xff0…

算法(结合算法图解)

算法简介简单查找二分查找法 选择排序内存的工作原理数组和链表数组选择排序小结 递归小梗 要想学会递归&#xff0c;首先要学会递归。 递归的基线条件和递归条件递归和栈小结 快速排序分而治之快速排序合并排序时间复杂度的平均情况和最糟情况小结 散列表散列函数缓冲小结性能…

Ubuntu系统下查看安装的CUDA和CUDNN的版本

一、查看 CUDA 版本&#xff1a; #查看cuda版本和显存使用情况nvidia-smi 二、查看 CUDNN 版本&#xff1a; 安装链接&#xff1a;cuDNN Archive | NVIDIA Developer #回到系统主目录 cd ~ #查看cudnn版本 cat /usr/local/cuda/include/cudnn_version.h | grep CUDNN_MAJO…

华为机考:HJ43 迷宫问题

华为机考&#xff1a;HJ43 迷宫问题 描述 DFS 从迷宫入口开始进行dfs搜索&#xff0c;每次进入一个点&#xff0c;将其加入临时路径数组中&#xff0c;把该位改成0表示不能进入&#xff0c;然后依次搜索该位下、右、上、左四个方向的点&#xff0c;如果搜索的这个点可以进入则…

3d渲染的模型仿佛有一层雾是怎么回事?---模大狮模型网

当在3D渲染的模型上出现仿佛有一层雾的效果时&#xff0c;可能是由于以下几个原因导致的&#xff1a; 环境光设置过高&#xff1a; 如果环境光设置过高&#xff0c;会使整个场景看起来像是笼罩在一层薄雾中。尝试降低环境光的强度&#xff0c;让场景更清晰明亮。 材质透明度设…

图【数据结构】

文章目录 图的基本概念邻接矩阵邻接表图的遍历BFSDFS 图的基本概念 图是由顶点集合及顶点间的关系组成的一种数据结构 顶点和边&#xff1a;图中结点称为顶点 权值:边附带的数据信息 路径 &#xff1a; 简单路径 和 回路&#xff1a; 子图&#xff1a;设图G {V, E}和图G1…

MySQL8 设置大小写敏感

问题描述 今天对我本地的数据库迁移服务器上&#xff0c;完成之后启动项目报错 说数据库中不存在 quartz_LOCKS 这张表 我打开服务器上面的数据上面展示的表名是 quartz_LOCKS&#xff0c;然后通过查询 lower_case_table_names 配置可知 show variables like lower_case_tabl…

Kamailio的SIP服务的性能

官方的性能报告&#xff1a; Kamailio (OpenSER) 1.2.0 - Transaction Module and User Location Performance Tests 如下的提取的性能参数也是基于官方的性能报告&#xff0c;信令走的UDP&#xff0c;作为做系统方案的参照&#xff0c;Kamailio的性能还是非常&#xff0c;非常…

leetcode代码记录(盛最多水的容器

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以…

【Hibernate-Validate】常用注解

常用注解: NotNull:被注释的元素(任何元素)必须不为 nul, 集合为空也是可以的。NotEmpty:用来校验字符串、集合、map、数组不能为null或也不能为空(字符串传入空格也不可以)(集合需至少包含一个元素)NotBlank:被注释的字符串的必须非空&#xff0c;空格也不行&#xff0c;空字…

来吧伙计们,让AI教我们怎么说海盗语

“如果想伺机而动&#xff0c;就是这样。”——杰克船长提到海盗&#xff0c;我们往往联想到约翰尼德普在《加勒比海盗》中饰演的杰克船长。我们有什么理由不喜欢海盗呢&#xff1f;他们航行在海上&#xff0c;寻找埋藏的宝藏&#xff0c;痛饮朗姆酒&#xff0c;用自己独特的海…

FreMIM:傅里叶变换与遮罩的图像建模在医学图像分割中的应用

代码链接&#xff1a;GitHub - Rubics-Xuan/FreMIM: This repo holds the official code for the paper "FreMIM: Fourier Transform Meets Masked Image Modeling for Medical Image Segmentation". 论文链接&#xff1a;https://arxiv.org/abs/2304.10864 收录于…

差旅补助解决方案|数字化差补赋能业务提效

长期以来&#xff0c;差旅补助一直是企业为了激励员工出差并表达对员工的关怀而采取的一种方式&#xff0c;以经济和福利支持来鼓励员工积极投入工作。然而&#xff0c;由于传统差旅补助的核算、发放和管理方式存在诸多问题&#xff0c;往往适得其反&#xff0c;无法实现企业的…

RocketMQ 面试题及答案整理,最新面试题

RocketMQ的消息存储机制是如何设计的&#xff1f; RocketMQ消息存储机制的设计原理&#xff1a; 1、CommitLog文件&#xff1a; 所有的消息都存储在一个连续的CommitLog文件中&#xff0c;保证了消息的顺序写入&#xff0c;提高写入性能。 2、消费队列&#xff1a; 为每个主…