计算机网络(三) —— 简单Udp网络程序

news2024/12/24 8:47:47

目录

一,初始化服务器

1.0 辅助文件

1.1 socket函数

1.2 填充sockaddr结构体

1.3 bind绑定函数

1.4 字符串IP和整数IP的转换

二,运行服务器

2.1 接收

2.2 处理

2.3 返回

三,客户端实现

3.1 UdpClient.cc 实现

 3.2 Main.cc 实现

3.3 效果展示

3.4 代码分层

四,两种场景

4.1 发送部分命令给服务器并返回结果

4.2 实现Linux多终端窗口群聊

4.3 实现Windows做客户端,Linux做服务器群聊


一,初始化服务器

1.0 辅助文件

Log.hpp 日志打印文件:

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#ifndef _LOG_H_
#define _LOG_H_

#include <ctime>

#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3

const std::string msg[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"};

std::ostream &Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}

#endif

makefile文件:

.PHONY:all
all:udpserver udpclient
udpserver:Main.cc
	g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
	g++ -o $@ $^ -lpthread -std=c++11

.PHONY:clean
clean:
	rm -f udpserver udpclient 

UdpServer.cc 部分代码:

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <functional>
#include <unordered_map>

#include "Log.hpp"

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0"; // 设置为0,表示任意地址绑定
const int size = 1024;

class Udpserver
{
public:
    Udpserver(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : _sockfd(0), _port(port), _ip(ip), _isrunning(false)
    {
    }

   
    ~Udpserver()
    {
        if (_sockfd > 0)
            close(_sockfd);
    }

private:
    int _sockfd;                                                      // 网络文件描述符
    uint16_t _port;                                                   // 表明服务器进程的端口号
    std::string _ip;                                                  // 地址绑定
    bool _isrunning;                                                  // 表明服务器是否在运行状态
    std::unordered_map<std::string, struct sockaddr_in> _online_user; // 第一个键值是ip,第二个键值是ip对应的套接字结构体信息
};

1.1 socket函数

我们首先会把服务器封装成一个类,然后定义一个服务器对象之后做的第一件事就是初始化服务器,而初始化服务器的第一件事,就是创建套接字,下面介绍以下socket接口:

参数说明:

  • domain:表示创建套接字的类型,该参数相当于struct sockaddr结构体的前16个比特位。在man手册中往下滑有很多很多AF开头的选项,但是我们目前只要关心几个:如果是本地通信,该参数就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:表示创建套接字时所需的服务类型,最常见的就是:①SOCK_DGRAM,基于UDP的用户数据报服务    ②SOCK_STREAM,基于TCP的流式套接字服务
  • protocol:表示创建套接字的协议类别,可以指名为TCP或UDP,但该参数一般直接设置为0即可,表示默认,此时就会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议
  • 当套接字创建成功后,会直接返回一个文件描述符,创建失败返回-1,同时错误码被设置

问题:socket创建套接字时干了什么?

解答:上面说socket创建成功后会返回一个文件描述符,所以最简单的说法就是“socket创建套接字本质就是打开了一个文件”,以前的打开文件对应的一般是磁盘,把磁盘的文件加载到内存中,并且在进程内部构建files_struct,并且包括文件描述符表;而这里的打开“网络文件”,对应的就是网卡了,通过网卡的驱动层,在操作系统中构建“网卡文件”,通过操作“网卡文件”实现对网卡的宏观控制

下面是服务器初始化函数创建套接字代码: 


    void Init()
    {
        // 1,创建Udp套接字,Udp的socket是全双工的,允许被同时读写的
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 表示使用IPv4协议,类型为Udp用户数据报
        if (_sockfd < 0)                          // 创建失败
        {
            Log("socket create error", Error) << "\n";
            exit(SOCKET_ERR);
        }
        Log("socket create success", Debug) << "\n"; // 创建成功,输出日志
        
        // ...
    }

创建成功后,_sockfd会被赋值成3 

1.2 填充sockaddr结构体

在上一篇文章的 4.2 sockaddr结构的时候,讲到过,在代码部分我们会对该结构体进行填充,原因请参照上篇博客:计算机网络(二) —— 网络编程套接字-CSDN博客

创建完套接字后,服务器初始化第一步完成,接着第二步就是构建填充sockaddr结构体了,如下代码:

 // 2,创建和填充sockaddr结构体
 struct sockaddr_in local;
 // 一
 bzero(&local, sizeof(local)); // 把指定类型的指定大小初始化为0,功能类似于memset

 // 二
 // 然后就是将我们自己的服务器的一些信息填充进这个结构体里,方便socket API使用
 local.sin_family = AF_INET; // 表明自身的结构体类型为IPv4   这个family是在宏定义用##来实现的

 // 三
 // local.sin_port = _port; //表明服务器将来要绑定的端口号 -- 需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
 // 除了发正常消息外,我也要把我的端口号发给客户端,这样客户端发给我的时候才能找到我,所以端口号需要发送到网络里的,所以一开始我们把这个东东填充到结构体里时,必须是网络字节序
 local.sin_port = htons(_port); // 把主机序列转化成网络序列,大端不变,小端会转大端

 // 四
 // local.sin_addr = _ip;  //s表示socket,然后in表示inet,addr表示IP地址(ifconfig命令)
 // 我们需要先把string的ip --> uint32_t的ip,并且必须是网络序列的,而这样的各种转化都是有相应的接口的,不需要我们自己写了
 local.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr表示把字符串转为网络字节序列也就是uint32_t
 // 查看sockaddr_in的定义后可以发现,sin_addr其实是一个struct类型,这个类型里的s_addr才是要转化的类型
 //  local.sin_addr.s_addr = htonl(INADDR_ANY); // 这个宏表示任意ip地址,数值为0

1.3 bind绑定函数

上面两步操作完成之后,就是绑定了,下面介绍以下bind函数:

参数解释:

  • sockfd:就是之前socket函数返回的套接字
  • addr:这个就是我们前面填充的sockaddr结构体的指针,里面有绑定的所有必须信息
  • addrlen:传入的sockaddr结构体的长度

下面是初始化服务器的第三步绑定端口的代码:

// 3,绑定套接字
//  local是在地址空间的用户栈上定义的,上面三个参数的所有操作都是在栈上填好,并没有将local与网络套接字socket相关联,所以需要绑定bind函数
int n = bind(_sockfd, (const struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
    Log("port bind error", Error) << "\n";
    exit(BIND_ERR);
}
Log("port bind success", Debug) << "\n";
std::cout << "Waiting user to connect ... " << std::endl;

1.4 字符串IP和整数IP的转换

网络传输的数据是寸土寸金的,如果我们在传输IP时以字符串的点分十进制进行IP传输,那么一个IP就需要15字节,但实际上不需要消耗这么多

IP地址可以划分位四个区域,每个区域取值都是0~255,每个区域是8个比特位,我们就可以只用32比特位表示四个区域来表示IP:

所以完成上面的操作就需要将IP在整数二号字符串之间做转换

首先是数字IP转字符串IP:

然后是字符串IP转数字IP: 

 inet_addr函数与inet_ntoa函数

上面的步骤了解一下即可,而且实际实现起来比较麻烦,所以系统为我们提供了相应的转换函数,我们直接调用即可:

二,运行服务器

2.1 接收

服务器初始化完成后,紧接着就是启动辣,服务器的任务就是周而复始为我们提供服务,所以运行起来一般不会退出,因此服务器的运行代码应该是一个死循环。

服务器运行起来后有三个基础动作

  1. 接收来自客户端的信息
  2. 处理信息
  3. 将结果返回给客户端

第一步就是接收,用到的函数名称为:recvfrom函数,下面来介绍一下这个函数 :

参数有点多,但不复杂,解释一下:

  • sockfd:老朋友了,socket的返回值
  • buf:表示要将读取到的数据放到哪里
  • len:表示要读取数据的字节数
  • flags:表示读取的方式,一般设为0,表示阻塞读取,没数据来的时候就给我等着
  • src_addr输出型参数,会保存发送方的协议结组,IP地址,端口号等,简单来说就是,这个字段会告诉程序“谁发数据过来的”,是为了后面发回去的之后,知道对方是谁在哪(传入结构体地址时需要强转,代码会有)
  • addrlen:表示读取到的src_addr的长度,需要和src_addr的大小一致
  • 返回值:表示读取成功返回实际读取到的字节数,读取失败返回-1,错误码被设置
void Run()
{
    _isrunning = true;
    char inbuffer[size]; //
    while (_isrunning)   // 服务器应该能周而复始一直在运行的,所以是死循环
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 读取数据,从指定套接字里读取消息,然后把读取到的数据放到缓冲区中并指名长度,最后两个参数作为输出型参数,保存对方的IP和port等信息,方便后面我发消息回去
        ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);
        if (n < 0)
        {
            Log("recvfrom error", Warning) << "\n";
            continue;
        }

        // ...
}

2.2 处理

上面拿到数据之后,就是要对数据进行处理辣

但是这个处理,就是根据具体的业务需要,由公司具体实施了,我们这里只用很简单的几行代码模拟处理过程,后面会有几种场景专门针对处理方法做调整,如下代码:

inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string = "server echo# " + info;
//就是简单的字符串拼接,最后的echo_string就是处理完后最终形成的数据

 std::string echo_string = "server echo# " + info;

2.3 返回

当处理完数据后,紧接着就是最后一步辣,就是把结果返回给客户端,返回用到的socket API是sendto函数,下面来介绍下这个函数:

它的参数和recvfrom是一样的,这里就不过多介绍了

// 处理完后要再发送回对方
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len); // 把数据发回给对方

三,客户端实现

3.1 UdpClient.cc 实现

在我们这个简单的Udp网络程序中,客户端的工作其实非常简单,就是“发送数据”,“接收数据”,“打印数据”,所以实现方面比服务器简单许多。

客户端一般是主动发数据给服务器的一方,所以客户端也要有相应的两个过程:

  • 创建套接字
  • 填写sockaddr结构体

问题:为什么客户端不需要我们手动绑定端口?

解答: 

  • 客户端都是最先发出请求的一方,所以服务器的IP地址和端口必须让客户端知道,因为服务器一旦启动,基本情况下不会关闭,所以端口号也不会更改了,所以服务器需要进行端口号绑定
  • 客户端是经常开启和关闭的,因此客户端的端口号是经常变化的,所以每次都绑定会加大成本,所以客户端的端口号只要能标识“唯一性”就可以了,只要客户端首次发送数据的时候,操作系统会自动帮我们绑定一个端口
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 构建服务器信息,因为客户端发给服务端需要知道服务端的ip和port
    struct ThreadData td;
    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);                  // 转成网络序列
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 点分十进制的字符串转化为数字

    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0)
    {
        cout << "socket error" << endl;
        return 1;
    }
    string message;
    char buffer[1024];
    socklen_t len = sizeof(td.server);
    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);
        // std::cout << message << std::endl;
        // 1. 数据 2. 给谁发
        sendto(td.sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&td.server, len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(td.sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }
    close(td.sockfd); // 不用了就和关闭文件描述符一样关闭套接字
    return 0;
}

 3.2 Main.cc 实现

服务器的main函数所在的Main.cc如下:

#include "Udpserver.hpp"
#include "Log.hpp"
#include <memory>
#include <cstdio>
#include <vector>
#include <string>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << "port[1024+]\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2) // argc表示命令行中命令个数
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]); // 字符串转整数
    std::unique_ptr<Udpserver> svr(new Udpserver(port));
    svr->Init(); // 初始化
    svr->Run(); // 服务器启动

    return 0;
}

3.3 效果展示

3.4 代码分层

 在实际开发场景中,其实很少会和 2.2 一样,直接把处理方法内置进服务器头文件中,所以我们可以在服务器启动前构建好处理函数,然后在服务器 Run 的时候,直接把方法传进去,实现代码分用,解耦,要更改的位置如下:

首先在Main.cc 的main函数前面加上处理方法:

// 代码分用
std::string Handler(const std::string &info, const std::string &clientip, uint16_t clientport)
{
    std::cout << "[" << clientip << ": " << clientport << "]#" << info << std::endl;
    std::string res = "Server get a message: ";
    res += info;
    std::cout << res << std::endl;

    return res;
}

然后就是利用C++的包装器,更改服务器的 Run 函数:

 加上包装器,也可以用typedef代替:

// using func_t = std::function<std::string(const std::string&, const std::string &, uint16_t)>; //c++11包装器
typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;

 最后就是更改Run函数,代码如下:

void Run(func_t func)
{
    _isrunning = true;
    char inbuffer[size]; //
    while (_isrunning)   // 服务器应该能周而复始一直在运行的,所以是死循环
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);
        if (n < 0)
        {
            Log("recvfrom error", Warning) << "\n";
            continue;
        }
        // 模拟群聊
        // ①拿到各客户端的ip和端口号
        uint16_t clientport = ntohs(client.sin_port);      // 拿到客户端的端口号,网络序列转主机序列
        std::string clientip = inet_ntoa(client.sin_addr); // 那搭配客户端ip地址,把inet_ntoa四字节ip转化为char*
        
        inbuffer[n] = 0;
        std::string info = inbuffer;

        // 充当了一次数据的处理,下面两条语句被第三条语句代替
        // std::string echo_string = "server echo# " + info;
        // std::cout << echo_string.c_str() << std::endl;
        std::string echo_string = func(info, clientip, clientport);
        // 处理完后要再发送回对方
        sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&client, len); // 把数据发回给对方
    }
}

 完成好后就可以直接实践了:

(可能各位有些混乱,如果遇到了不会了随时评论或私信哦~~,源代码在文章最后会附上 gitee 仓库链接) 

四,两种场景

4.1 发送部分命令给服务器并返回结果

上面的是最简单的客户端服务器的通讯结果,下面我们来搞一点好玩的

出了发送字符串,我们也可以发送部分命令给服务器,服务器处理好命令后再把结果返回给客户端,最后客户端打印

下面我们来更改一下代码,客户端代码不变,要变的就是服务器处理客户端信息的那部分代码,还是和上面代码分用一样,直接在Main.cc里实现处理函数,然后再传给Run函数

下面是Mani.cc 的处理命令的方法,用到的新函数是 popen,作用是直接执行命令并返回结果,有兴趣可以自行了解下:

Main.cc:

// 场景一,实现命令
bool SafeCheck(const std::string &cmd)
{
    std::vector<std::string> key_word = {
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "tcp",
        "while"};
    for (auto &word : key_word)
    {
        auto pos = cmd.find(word);
        if (pos != std::string::npos)
            return false; // 在你的命令中找到上面任意一个的话,就是不合法的,直接返回falsse
    }
    return true;
}
std::string ExcuteCommand(const std::string &cmd)
{
    std::cout << "get a request cmd: " << cmd << std::endl;
    if (!SafeCheck(cmd))
        return "Bad man";

    FILE *fp = popen(cmd.c_str(), "r");
    if (fp == NULL)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while (true)
    {
        char *ok = fgets(buffer, sizeof(buffer), fp);
        if (ok == nullptr)
            break; // 为空了,说明读完了
        result += buffer;
    }
    pclose(fp);
    return result;
}

 然后是UdpServer.hpp 的Run函数的更改:

先更改一下包装器函数的参数数量:

下面是效果展示:

 

4.2 实现Linux多终端窗口群聊

实现群聊我们要做下面几点工作:

  • 能够保存连接服务器的IP和端口,一个人发消息后遍历保存的IP和端口,把消息往所有连接服务器的IP都发送一次
  • 能够让群聊所有人知道是谁发的,也就是消息前要带上IP和端口
  • 利用dup2函数,实现两个窗口,一个窗口只负责发消息,一个窗口只负责收消息

先看效果演示: 

 有点复杂,但是不用担心,我们一步一步来

①首先是我们能够保存发起连接的用户的IP和端口

现在就要用到我们最开始就定义好的一个unordered_map了:

 然后我们在UdpServer.hpp里直接实现一个添加用户的函数:

void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport) // 检查是否为新用户
{
    auto iter = _online_user.find(clientip); // 去哈希表去找对应IP的信息
    if (iter == _online_user.end())          // 如果上面这个查找的迭代器走到了结尾,说明哈希表里还没有这个ip,添加
    {
        _online_user.insert({clientip, client}); // 把ip和对应的套接字结构体插入
        std::cout << "[" << clientip << ": " << clientport << "]add to online user" << std::endl;
    }
    else // 如果存在了,则什么都不做
    {
    }
}

更改Run函数,先将Run函数参数去掉,因为群聊场景不需要传处理方法:

 ②遍历哈希表,对每一个IP都发送

上面的Run函数已经出现了,下面是实现Broadcast发送函数的代码:

void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport)
{
    for (const auto &user : _online_user) // 遍历在线用户,遍历发送
    {
        // 编辑发送形式
        std::string message = "[";
        message += clientip;
        message += ":";
        message += std::to_string(clientport);
        message += "]#";
        message += info;
        socklen_t len = sizeof(user.second);
        sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)(&user.second), len); // sendto发送回给对方
    }
}

 ③最后就是客户端的调整了,我们可以使用dup2重定向函数,实现两个终端,一个终端窗口只发消息,一个窗口接收消息,就和上面的演示一样

 

#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

std::string terminal = "/dev/pts/2";

int OpenTerminal()
{
    int fd = open(terminal.c_str(), O_WRONLY);
    if (fd < 0) // 打开失败
    {
        std::cerr << "open termial error";
        return 1;
    }

    dup2(fd, 2);
    // 测试
    //  printf("hello world\n"); // 把即将打印在当前终端的内容往特定路径的终端打

    // close(fd);
    return 0;
}

(可能有点复杂,如果看到这里的小伙伴有不懂或者有问题的,欢迎随时评论和私聊)

4.3 实现Windows做客户端,Linux做服务器群聊

如标题一样,Linux做服务器,Windows做客户端是非常常见的事情

我们可以在Windows本地实现一个客户端,然后和Linux服务器做通信,因为网络基础入门说过,虽然操作系统不一样,但是网络协议栈是一样的,因为这时规定,生产厂家必须遵守规则

下面是VS2022在Windows环境下的客户端代码:

#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include <iostream>
#include<WinSock2.h> //这两个W开头的头文件顺序必须是这样,反过来编译时就会报错
#include<Windows.h>
#include <cstdlib>
#include <string>
#include<cstdio>
#pragma comment(lib, "ws2_32.lib")

#include<thread>
uint16_t serverport = 8080;
std::string serverip = "58.87.91.241";

struct ThreadData
{
	struct sockaddr_in server;
	SOCKET sockfd;
	std::string serverip;
};

void* send_message(void* args)
{
	ThreadData* td = static_cast<ThreadData*>(args);
	std::string message;
	std::cout << td->serverip << " coming... " << std::endl;

	while (true)
	{
		std::cout << "Please Enter: ";
		std::getline(std::cin, message);
		sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), sizeof(td->server));
	}
}
void* recv_message(void* args)
{
	ThreadData* td = static_cast<ThreadData*>(args);
	char buffer[1024];
	while (true)
	{
		memset(buffer, 0, sizeof(buffer)); //每次接收消息前清空缓冲区
		int len = sizeof(td->server);
		int s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&(td->server), &len);
		if (s > 0)
		{
			buffer[s] = 0;
			std::cout << buffer << std::endl;
		}
	}
}

int main()
{
	WSADATA wsd;
	WSAStartup(MAKEWORD(2, 3), &wsd);
	struct ThreadData td;

	// 构建服务器信息,因为客户端发给服务端需要知道服务端的ip和port
	memset(&td.server, 0, sizeof(td.server));
	td.server.sin_family = AF_INET;
	td.server.sin_port = htons(serverport);                  // 转成网络序列
	td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 点分十进制的字符串转化为数字

	td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (td.sockfd < 0)
	{
		std::cout << "socket error" << std::endl;
		return 1;
	}
	td.serverip = serverip;
	std::thread sender(send_message, &td);
	std::thread recver(recv_message, &td);

	sender.join();
	recver.join()
		;
	WSACleanup();
	return 0;
}

代码和Linux差不多,也是多线程,只是Windows对于库的处理有点不一样,下面是效果演示:

两边打印中文时会乱码,其实是因为两边的编码不一致,我们暂时不考虑,反正能达到Windows和Linux实现网络通信的目的就行了莫

代码gitee链接:计算机网络/网络编程套接字/Udp · 小堃学编程/Linux学习 - 码云 - 开源中国 (gitee.com) 

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

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

相关文章

MongoDB 5.0版本副本集集群

一、MongoDB 5.0版本副本集集群部署 什么是MongoDB的副本集 MongoDB的副本集&#xff08;Replica Set&#xff09;是一种用于提高数据库系统可用性、可靠性和数据冗余性的机制。副本集包含一组相互连接的MongoDB节点&#xff0c;其中包括一个主节点&#xff08;Primary&#…

基于web的赴台展会人员管理系统设计与实现

博主介绍&#xff1a;专注于Java .net php phython 小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不然下次找不到哟 我的博客空间发布了1000毕设题目 方便大家学习使用 感兴趣的可以…

VSC++: 括号对称比较

括号的使用规则&#xff1a;大括号&#xff0c;中括号&#xff0c;小括号{[()]}&#xff1b;中括号&#xff0c;小括号[()]&#xff1b;小括号()&#xff1b;大括号、中括号、小括号、中括号、小括号、大括号{[()][()]}&#xff1b;大括号&#xff0c;中括号&#xff0c;小括号…

Reflection 70B:震撼AI行业的开源模型

随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;开源与闭源模型的竞争变得越来越激烈。近日&#xff0c;Reflection 70B模型的发布在AI行业引发了巨大的震动。这款拥有70亿参数的开源模型不仅在多项基准测试中取得了优异成绩&#xff0c;还在很多情况下超越…

无人机之报警器的工作原理

无人机报警器&#xff08;通常指的是无人机上搭载的某种警报系统或装置&#xff0c;用于在特定条件下触发警报&#xff09;的作用原理可能涉及多个方面&#xff0c;但具体到无人机报警器这一组件&#xff0c;其原理往往与无人机的整体安全监控、电池状态监测或任务执行中的特定…

基于Java+SpringBoot+Vue+MySQL的美发管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 基于SpringBootVue的美发管理系统【附源码文档】、前后端分…

揭秘循环购模式:如何实现消费增值与日常收益

欢迎各位&#xff0c;我是吴军&#xff0c;你们的电商策略顾问。今天&#xff0c;我将向大家介绍一种新颖的商业模式——循环购模式&#xff0c;它如何为商家和消费者创造价值。 你可能会好奇&#xff0c;为何会有“消费1000元&#xff0c;赠送2000元”的优惠&#xff1f;以及…

算法练习小技巧之有序集合--套路详细解析带例题(leetcode)

前言: 本文详细讲解Python中的有序集合SortedList和C中的有序集合multiset的用法&#xff0c;配合leetcode的例题来展示实际的用处。(本人水平不够&#xff0c;还无法讲解有序集合的实现方法&#xff0c;只会用) 觉得有帮助或者写的不错可以点个赞&#xff0c;后面也有几道我找…

[数据集][目标检测]抽烟检测数据集VOC+YOLO格式22559张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;22559 标注数量(xml文件个数)&#xff1a;22559 标注数量(txt文件个数)&#xff1a;22559 标…

本机ip地址怎么看是不是公网

在数字化时代&#xff0c;‌了解自己的网络连接属性&#xff0c;‌尤其是本机IP地址是否为公网IP&#xff0c;‌对于网络安全、‌远程访问、‌在线服务配置等方面都至关重要。‌公网IP&#xff0c;‌即互联网上的唯一地址&#xff0c;‌能让任何连接互联网的设备访问到你的设备…

java框架第五课(终极版本)SpringBoot

一.关于SpringBoot (1)回忆Spring 传统的Spring由Spring 框架(ioc,aop)加mybatis加Springweb组成&#xff0c;虽然相比原生的java程序Spring框架帮我们大大减少了代码量&#xff0c;减少了冗余&#xff0c;提高了开发效率但是由于Spring框架下的配置和相关的jar包依赖过多&am…

图像去噪:使用DAMRmF算法

在数字图像处理领域&#xff0c;噪声是不可避免的&#xff0c;它会影响图像的质量和可读性。为了提高图像的质量&#xff0c;去噪算法是必不可少的工具。在这篇文章中&#xff0c;我们将介绍一种名为DAMRmF的去噪算法&#xff0c;并展示如何使用MATLAB实现和应用它。 一、什么…

1-8 图像腐蚀 opencv树莓派4B 入门系列笔记

目录 一、提前准备 二、代码详解 kernelnp.ones((2,2),np.uint8) _, binary_image cv2.threshold(image, 127, 255, cv2.THRESH_BINARY) eroded_imagecv2.erode(binary_image,kernel,iterations1) eroded_image2cv2.erode(image2,kernel,iterations1) 三、运行现象 四…

如何处理忘记实现接口所有方法

在Java编程中&#xff0c;当你声明一个类实现了某个接口时&#xff0c;但没有实现接口中的所有方法&#xff0c;编译器会报错。这是因为接口规定了必须由实现类提供的功能&#xff0c;任何没有实现的接口方法&#xff0c;都会被认为是实现不完整的&#xff0c;导致编译失败。 …

[含视频和源码]CRUD的最佳实践,联动前后端,包含微信小程序,API,HTML等(三)

关说不练假把式&#xff0c;在上一&#xff0c;二篇中介绍了我心目中的CRUD的样子 基于之前的理念&#xff0c;我开发了一个命名为PasteTemplate的项目&#xff0c;这个项目呢后续会转化成项目模板&#xff0c;转化成项目模板后&#xff0c;后续需要开发新的项目就可以基于这…

【时时三省】(C语言基础)指针进阶 例题3

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 例题&#xff1a; 这个arr里面是放了&#xff3b;a b c d e f \0&#xff3d; 第一个arr 这个sizeof是计算这个数组的大小 这个里面加上\0一共是7个元素 所以打印7 第二个arr0 这个指数组…

OGRE 3D----创建第一个OGRE 3D示例

目录 1. OGRE 3D概述 2. OGRE 3D vs VTK 3. 编译OGRE 3D 源码 4. 创建示例和配置其编译环境 5. 配置示例程序的执行环境 1. OGRE 3D概述 OGRE (Object-Oriented Graphics Rendering Engine) 是一个开源的、高级的 3D 图形渲染引擎&#xff0c;它提供了一个抽象层&#xf…

Mybatis---代理设计模式(超详细)

Mybatis—代理设计模式 文章目录 Mybatis---代理设计模式一、什么是代理设计模式二、静态代理1、定义2、结构3、示例 三、动态代理1、定义2、newProxyInstance &#xff08;&#xff09;方法3、示例 四、CGLIB代理1、引入2、定义3、工作原理4、示例 一、什么是代理设计模式 首…

大数据-121 - Flink Time Watermark 详解 附带示例详解

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Unity(2022.3.41LTS) - UI详细介绍- Toggle(切换)

目录 零.简介 一、基本功能 二、属性和设置 三、使用方法 四、优化和注意事项 零.简介 在 Unity 中&#xff0c;Toggle 是一种常用的 UI 组件&#xff0c;用于表示一个布尔值的状态&#xff0c;类似于复选框。 一、基本功能 状态切换&#xff1a;Toggle 有两种状态&…