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

news2024/11/15 13:37:22

网络套接字~

文章目录

  • 前言
  • 一、udp服务器的实现
  • 总结


前言

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


一、udp服务器的实现

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

cc=g++
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
	$(cc) -o $@ $^ -std=c++11
udpServer:udpServer.cc
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	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: "<<errno<<" : "<<strerror(errno)<<endl;
                exit(SOCKET_ERROR);
            }
            cout<<"server socket success: "<<" : "<<_sockfd<<endl;
        }

如果套接字创建失败,就算没有给我们的文件描述符返回-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:  "<<errno<<" : "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }

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

    enum 
    {
       SOCKET_ERROR = 2
       ,BIND_ERR
    };

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

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

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

static void Usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" local_ip local_port\n\n";
}

int main(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<udpServer> 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"<<proc<<" local_port\n\n";
}

int main(int argc,char* argv[])
{
    if (argc!=2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    } 
    uint16_t port = atoi(argv[1]); 
    unique_ptr<udpServer> 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<<clientip<<"["<<clientport<<"]#"<<message<<endl;
                }
            }
        }

如果读取数据成功,我们先将缓冲区中最后一个位置填上\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 <memory>
using namespace Client;
static void Usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" server_ip server_port\n\n";
}

//./udpClient server_ip server_port
int main(int argc, char* argv[])
{
    if (argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    unique_ptr<udpClient> 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: "<<errno<<" : "<<strerror(errno)<<endl;
               exit(2);
           }
           cout<<"client socket success: "<<" : "<<_sockfd<<endl;
           //2.  client要不要bind(必须要),client要不要明确的bind(不需要,不需要程序员自己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服务器的所有内容了,下一篇文章我们将会把这个服务器改造称为英汉互译,大型聊天室等好玩的工具。

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

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

相关文章

Unity3d C#实现调取网络时间限制程序的体验时长的功能

前言 如题的需求应该经常在开发被提到&#xff0c;例如给客户体验3–5天的程序&#xff0c;到期后使其不可使用&#xff0c;或者几年的使用期限。这个功能常常需要使用到usb加密狗来限制&#xff0c;当然这也的话就需要一定的硬件投入。很多临时提供的版本基本是要求软件来实现…

Java如何将jar包上传到Maven中央仓库(超详细2023最新版)

文章目录 Java如何将jar包上传到Maven中央仓库引言Step1&#xff1a;注册 JIRA 账号Step2&#xff1a;发布申请Step3&#xff1a;下载并安装GPGStep4&#xff1a;配置maven的setting.xmlStep5&#xff1a;配置pom.xmlStep6&#xff1a;上传 jar 包Step7&#xff1a;引入 jar 包…

SwiftUI 中限制任意视图为指定的屏幕旋转方向

功能需求 在 SwiftUI 开发中,我们有时需要限制 App 中某些视图为特定的屏幕旋转方向,而另一些视图不做限制(或做其它限制),这可以做到吗? 如上图所示:我们成功的限制了 SwiftUI 中不同视图对应于不同的屏幕旋转方向(Interface Orientations)。 在本篇博文中,您将学到…

OpenCV - C++实战(01) — OpenCV简介

目录 第1章 OpenCV简介 1.1 简介 1.1.1 OpencV 库简介 1.1.2 命名空间 1.2 OpenCV模块 1.3 装载、显示和存储图像 1.3.1 创建图像 1.3.2 读取图像 1.3.3 定义窗口与显示图像 1.3.4 图像翻转 1.3.5 保存图像 1.3.6 图像的复制 1.3.7 创建数组和向量 1.…

01、前端使用 thymeleaf 后,视图页面找不到---Cannot resolve MVC View ‘xxxxx前端页面‘

Cannot resolve MVC View ‘xxxxx前端页面’ 没有找到对应的mvc的前端页面。 代码&#xff1a;前端这里引入了 thymeleaf 模板 解决&#xff1a; 需要添加 thymeleaf 的依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>s…

APP调用bindService的跨进程调用过程

app执行bindService时会经过如下8次跨系统进程调用过程&#xff1a; 第1步&#xff1a;通过AMS.getService跨进程调用 第2步&#xff1a;AMS返回它的IBinder 第3步&#xff1a;通过AMS的IBinder调用AMS的bindService方法 第4步&#xff1a;而AMS存放有Server端的IBinder&…

华为数通方向HCIP-DataCom H12-821题库(单选题:101-120)

第101题 可用于多种路由协议,由 ​​if-match​​​和 ​​apply​​子句组成的路由选择工具是 A、​​route-policy​​ B、​​IP-Prefix​​ C、​​commnityfilter​​ D、​​as-path-filter​​ 答案&#xff1a;A 解析&#xff1a; Route-policy&#xff08;路由策…

扫雷小游戏

目录 一.扫雷小游戏 二.游戏主体一览 ​编辑 三.模块化设计扫雷游戏 3.1打印欢迎菜单 3.2创建两个二维数组 3.3棋盘稍加修改 3.4布置雷 3.5排查雷 四.游戏总体代码 4.1game.h头文件 4.2game.c函数实现源文件 4.3游戏main函数主体 五.游戏效果图 一.扫雷小游戏 这是…

EB Tresos第一个项目报13026

EB项目创建 前期的准备工作见以上这篇文章&#xff0c;不做过多叙述&#xff1b;但是点击Generate Project会报错&#xff0c;报错如下&#xff1a; Code generator finished. Errors “1” Warnings “0” 然后点击 Problems View 查看提示&#xff0c;提示如下&#xff1a; …

大数据:AI大模型对数据分析领域的颠覆(文末送书)

随着数字化时代的到来&#xff0c;大数据已经成为了各行各业中不可或缺的资源。然而&#xff0c;有效地分析和利用大数据仍然是一个挑战。在这个背景下&#xff0c;OpenAI推出的Code Interpreter正在对数据分析领域进行颠覆性的影响。 如何颠覆数据分析领域&#xff1f;带着这…

为什么Python列表和字典前面会加星号(**)?

目录标题 前言一、列表&#xff08;list&#xff09;、元组&#xff08;tuple&#xff09;前面加星号*二、字典&#xff08;dict&#xff09;前面加两星号**尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! Python 中&#xff0c;单星号*和双星号**除了作为“乘”和“…

视频汇聚/云存储/安防监控AI视频智能分析平台——明厨亮灶解决方案

人工智能技术已经越来越多地融入到视频监控领域中&#xff0c;近期我们也发布了基于AI智能视频云存储/安防监控视频AI智能分析平台的众多新功能&#xff0c;该平台内置多种AI算法&#xff0c;可对实时视频中的人脸、人体、物体等进行检测、跟踪与抓拍&#xff0c;支持人脸检测、…

算法 for GAMES

栈 #include <iostream> #include <stack>int main() {std::stack<int> intStack;// 压入元素到堆栈intStack.push(5);intStack.push(10);intStack.push(15);// 查看堆栈顶部元素std::cout << "Top element: " << intStack.top() <…

虚虚实实,让敌人难以琢磨

与敌作战&#xff0c;虚虚实实&#xff0c;难以琢磨 【安志强趣讲《孙子兵法》第20讲】 第六篇&#xff1a;虚实篇 【全篇趣讲白话】 打仗就是要虚虚实实&#xff0c;让敌人难以琢磨。 【原文】 孙子曰&#xff1a;凡先处战地而待敌者佚&#xff0c;后处战地而趋战者劳。故善…

Locked勒索病毒:最新变种locked袭击了您的计算机?

导言&#xff1a; 在数字时代&#xff0c;一场隐秘的威胁正悄然而至&#xff0c;它的名字是.locked勒索病毒。这个黑暗的存在以其狡猾的攻击方式和致命的数据封锁能力&#xff0c;威胁着我们的数字生活。本文91数据恢复将带您深入了解.locked勒索病毒的本质&#xff0c;探索恢…

【Day-20慢就是快】代码随想录-栈与队列-有效的括号

给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&#xff0c;‘[’&#xff0c;‘]’ 的字符串&#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被…

Redis7安装

1. 使用什么系统安装redis 由于企业里面做Redis开发&#xff0c;99%都是Linux版的运用和安装&#xff0c;几乎不会涉及到Windows版&#xff0c;上一步的讲解只是为了知识的完整性&#xff0c;Windows版不作为重点&#xff0c;同学可以下去自己玩&#xff0c;企业实战就认一个版…

会声会影2023全新中文专业版下载安装教程

熟练使用会声会影视频编辑工具&#xff0c;对视频创作过程的帮助是极大的。大家可以放心大胆地去研究会声会影的视频编辑技巧&#xff0c;会声会影2023与以往版本会声会影版本最大的区别是&#xff1a;账户制管理。可以通过账户添加或移除设备&#xff0c;非常便捷。该软件一直…

Vue2向Vue3过度Vuex核心概念mutations

目录 1 核心概念-mutations1.定义mutations2.格式说明3.组件中提交 mutations4.练习5.总结 2 带参数的 mutations1.目标&#xff1a;2.语法2.1 提供mutation函数&#xff08;带参数&#xff09;2.2 提交mutation 3 练习-mutations的减法功能1.步骤2.代码实现 4 练习-Vuex中的值…

05.Image Captioning with Semantic Attention

目录 前言泛读摘要Introduction创新/贡献点Related Work 精读输入的注意力模型输出的注意力模型模型学习视觉属性/概念预测实验MS-COCO的性能实验分析 Conclusion 代码略 前言 本课程来自深度之眼《多模态》训练营&#xff0c;部分截图来自课程视频。 文章标题&#xff1a;Ima…