Linux网络相关概念和重要知识(2)(UDP套接字编程、聊天室的实现、观察者模式)
目录
1.UDP套接字编程
(1)socket编程
(2)UDP的使用
①socket
②bind
③recvfrom
④sendto
2.聊天室的实现
(1)整体逻辑
(2)对sockaddr_in的封装
(3)客户端设计
①观察者模式
②User设计
③观察者的设计
⑤客户端主程序设计
(4)服务端设计
①服务端主程序的设计
②服务器端成员变量
③利用线程池实现服务端
(5)重定向文件
1.UDP套接字编程
(1)socket编程
套接字编程的种类比较多:网络socket、本地socket(如Unix域间socket)、原始socket。Linux提供了socket的系统调用,保证接口统一,以后实现Linux上的各种socket通信就很简单了。
(2)UDP的使用
使用socket套路非常简单,抓住IP和port,理解通信的大致过程就能很好掌握代码的方法了
①socket


站在用户层,创建socket就是创建了一个特殊的文件,这个文件包含了应该以什么方式,什么协议来通信。但此时,这个文件还应该有自己的网络层面的标识,如IP和port,这就是bind需要做的事了。
②bind

使用过程中,只需要创建对应通信方式的addr,写入对应的网络信息(如IP和port),最后再将addr转为父类指针,传入大小进行bind
总体bind流程:

向addr写入属性:

其中_net_addr是in类型的,注意字节序的调整,修改IP需要到sin_addr里面修改其成员s_addr
测试时常用的IP是127.0.0.1,表示本地机器。把这个IP绑定到socket之后,其它socket就可以通过这个IP向这个socket发消息。上述代码使用INADDR_ANY,意思是其它socket向任何IP发的消息都会被接收。
端口号是2字节16位的,端口号0 - 1023是知名端口号,1023 - 65535是OS动态分配的端口号,是客户端程序的端口号,我们随便选择,只要该端口号没有被使用。将端口号绑定后,对方socket向该socket发送消息,只有匹配IP和端口号才会被socket接收。
注意客户端不需要手动bind,因为客户端每次启动都不能保证相应端口号的空闲。而服务器端几乎不关机,可以指定而且必须指定。因此客户端创建了socket可以直接发消息,发消息时会系统自动bind,将相关IP和port写入socket中。
③recvfrom
没有收到消息时,线程会被阻塞在这个函数里面

④sendto

socket是全双工通信,既可以收的同时也可以发
2.聊天室的实现
(1)整体逻辑
整个聊天室重要的是逻辑,因为它的封装已经比较多了,所以接下来的实现会重点强调每个模块是如何设计,和其它模块耦合的。我们只需要详细理解前面UDP的通信过程即可理解聊天室的实现。
(2)对sockaddr_in的封装
我们可以快速创建管理sockaddr_in的结构体,并单独记录port和IP

服务器端可以快速初始化结构体

接收方也可以使用这个结构体,将接收到的in交给自定义结构体进行IP和port提取

全部代码:
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <stdint.h>
#include <strings.h>
using namespace std;class myInetAddr
{
private:void Set_Port() // 端口号的网络字节序转换为主机字节序{_port = ntohs(_net_addr.sin_port);}void Set_Ip() // IP的网络字节序转换为主机字节序{char ipbuffer[100] = {0};_ip = inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));// 相较于inet_ntoa,这个函数更安全,不会被覆盖}public:myInetAddr(const sockaddr_in &addr) : _net_addr(addr) // 传入一个sockaddr_in结构体,将其赋值给_net_addr{Set_Port(); // 调用Set_Port函数,将sockaddr_in结构体里面的端口号提取出来Set_Ip(); // 调用Set_Ip函数,将sockaddr_in结构体里面的IP地址提取出来}// 只需要传个端口号,就可以实现IP、端口号的自动初始化,其中IP默认为空,意味着局域网内任意IP都可以通信myInetAddr(uint16_t port, string ip = ""): _port(port), _ip(ip) // 传入端口号,IP默认为空,可自动初始化{// 初始化文件的属性bzero(&_net_addr, sizeof(_net_addr)); // 对_net_addr里面的空间清0,类似memset_net_addr.sin_family = AF_INET; // 网络通信,该属性必须要和socket的一致,后续才能绑定_net_addr.sin_port = htons(_port); // 写入端口号,htons保证字节序,主机字节序转网络字节序// sockaddr_in里面有struct in_addr sin_addr,这个结构体里面有s_addr,这个就是IP地址_net_addr.sin_addr.s_addr = INADDR_ANY; // 局域网内任意IP,只要端口号一致就能通信,也可以inet_addr(指定IP的c_str)}const sockaddr *Get_Const_Sockaddr_ptr(){return (const sockaddr *)&_net_addr;}socklen_t Get_Socklen(){return sizeof(_net_addr);}const string Get_Ip() const // 获取sockaddr_in结构体里面的IP地址,加const是为了保证不会修改_ip{return _ip;}const uint16_t Get_Port() const // 获取sockaddr_in结构体里面的端口号,加const是为了保证不会修改_ip{return _port;}private:// 使用sockaddr_in结构而不是sockaddrsockaddr_in _net_addr; // 管理属性的结构体,包括IP和端口号string _ip; // 单独记录IP地址uint16_t _port; // 单独记录端口号
};
(3)客户端设计
①观察者模式

在管理用户的UserManager中有一个链表,这个链表的实例化类型是BaseUser的指针,链表中的指针就是观察者,这些观察者可以通过多态实现管理User。当有什么操作需要完成时,会遍历链表中的指针,调用User里面的方法完成任务。整个过程中,UserManager都充当管理者、观察者,因此叫做观察者模式。
②User设计

这个User里面保存着用户的属性和方法,当服务器遍历list执行里面的方法时就会到这个类里面调用函数,每个观察者都知道观察对象的IP和port。
③观察者的设计
路由设计,服务器接收消息后就会在这里路由,遍历方法向别人发送信息。

注意这个sockfd是服务器的socket
全部代码
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "myLog.hpp"
#include "myMutex.hpp"
#include "myInetAddr.hpp"using namespace std;
using namespace myLogModule;
using namespace myMutexModule;class BaseUser // 多态,管理用户的基类
{
public:// 虚函数说明销毁的时候会调用子类的析构函数,default说明是默认的析构函数virtual ~BaseUser() = default;// 纯虚函数,第一个参数作用是socket的fd,发送消息时会根据fd发送消息和接收消息,顺便还会发送主机IP和端口号virtual void SendTo(int sockfd, const string &message) = 0;virtual bool CompareAddr(const myInetAddr &addr) = 0;
};class User : public BaseUser // 服务器用来保存用户的IP和端口号
{
public:User(const myInetAddr &addr): _addr(addr){}// 这个sockfd是服务器的socket,用于转发消息,_addr是接收转发消息的客户端的地址void SendTo(int sockfd, const string &message) override // override说明这个函数是重写的,用于规范化代码{LOG(DEBUG) << "服务器转发一条消息至" << _addr.Get_Ip() << ":" << _addr.Get_Port();// 将服务器收到的message发给客户端sendto(sockfd, message.c_str(), message.size(), 0, _addr.Get_Const_Sockaddr_ptr(), _addr.Get_Socklen());}bool CompareAddr(const myInetAddr &addr) override{return _addr.Get_Ip() == addr.Get_Ip() && _addr.Get_Port() == addr.Get_Port();}private:myInetAddr _addr;
};// 观察者,这里面会管理所有的用户
class UsersManager
{
public: // 添加和删除用户都只需要传入管理用户IP和端口号的结构体即可void AddUser(const myInetAddr &addr) // 只管添加用户,查重由该函数自己完成{myLockGuard lockguard(_mutex); // 一次性只允许一个进程访问公共资源,即用户链表for (auto &user : _online_users){if (user->CompareAddr(addr)){return;}}_online_users.push_back(make_shared<User>(addr)); // 多态,父类指针指向子类对象// 服务器新添加了一个用户,这个用户的管理的结构体是myInetAddr,但实际上是多态,所以可以调用子类的函数,这个结构体里面也有IP和端口号的信息LOG(INFO) << "新增用户:" << addr.Get_Ip() << ":" << to_string(addr.Get_Port());}void DeleteUser(const myInetAddr &addr){myLockGuard lockguard(_mutex); // 一次性只允许一个进程访问公共资源,即用户链表for (auto &user : _online_users){if (user->CompareAddr(addr)){_online_users.remove(user); // 这个函数会到链表里面找到这个user,然后删除,前面if已经找到了,所以这里不会有问题LOG(INFO) << addr.Get_Ip() << ":" << to_string(addr.Get_Port()) << "退出聊天室,服务器已删除用户";return; // 找到了就删除,不需要再继续找了}}}void Router(int sockfd, const string &message) // 路由函数,将服务器收到的消息转发给所有的用户{myLockGuard lockguard(_mutex); // 一次性只允许一个进程访问公共资源,即用户链表for (auto &user : _online_users) // 遍历所有用户{// 每一次获取数据都会调用一次这个函数,这个函数会将数据转发给所有的用户user->SendTo(sockfd, message); // 某一个用户发送过来信息,遍历所有用户,将信息转发给所有用户,这个socket是发信息的socket,这样其它用户才知道是他发的}}private:// 每个用户都是观察者list<shared_ptr<BaseUser>> _online_users; // 这是多态的体现,基类指针指向派生类对象,可以调用子类的函数myMutex _mutex; // 一把锁,保证线程安全
};
⑤客户端主程序设计
为了实现全双工,客户端主程序需要双线程执行,一端负责写消息,一端负责收信息(收不到信息时会一直阻塞在recvfrom中)


全部代码
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include "myCommon.hpp"using namespace std;int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// sockaddr_in写入要连接的服务器的IP和端口号
sockaddr_in server; // 可以向指定IP和端口号发送信息void ClientQuit(int signal)
{string message = "QUIT";sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr *)(&server), sizeof(server));cout << "你已退出聊天室" << endl;exit(0);
}void *ReceiveMessage(void *args)
{while (1){sockaddr_in temp;socklen_t len = sizeof(temp);char read_buffer[1024];int n = recvfrom(sockfd, read_buffer, sizeof(read_buffer) - 1, 0,(sockaddr *)(&temp), &len); // 接收对方服务器的IP和端口号read_buffer[n] = '\0';cerr << read_buffer << endl;// 输出接收到的信息,用错误流接收,后面专门用重定向设置一个聊天界面}
}// CS模式,client和server,client发送消息,server接收消息,服务器端永远不会主动发送消息,都是被动的
int main(int argc, char *argv[]) // 第二个参数是IP,第三个参数是端口号
{if (argc != 3){cerr << "三个参数,一个IP,一个端口号" << endl;exit(CLIENT_ERROR); // 2表示客户端错误}string server_ip = argv[1];uint16_t server_port = stoi(argv[2]);if (sockfd < 0){cerr << "客户端启动失败" << endl;exit(CLIENT_ERROR);}cout << "客户端启动成功" << endl;memset(&server, 0, sizeof(server)); // 用0初始化,0是char类型的0,即0x00server.sin_family = AF_INET;server.sin_port = htons(server_port); // 保证字节序server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 保证字节序signal(2, ClientQuit); // 捕获ctrl+c信号,退出程序,触发信号会执行ClientQuit函数,这个函数会向服务器发送QUIT信息,服务器会删除用户信息pthread_t tid;pthread_create(&tid, nullptr, ReceiveMessage, nullptr);while (true){cout << "请输入信息:";string message;getline(cin, message); // 获取信息// 不需要绑定socket,直接向文件写信息即可,client也有自己的属性,但IP和端口号不需要显式调用bind,客户端指定端口号可能会冲突,首次sendto会自动绑定// 客户端自动bind,一个端口号只能bind一次,一个进程可以绑定多个端口号sendto(sockfd, message.c_str(), message.size(), 0, // 根据打开的socket发送信息,向socket文件发送消息(const sockaddr *)(&server), sizeof(server)); // 要访问的服务端的IP和端口号// 发送信息的条件:向socket中写数据,socket和本机的IP和端口号绑定(显式绑定和自动绑定)// const sockaddr *指向目的的IP和端口号,发送出去后自己socket里面的IP和端口号也会发出去,对方就能找到你}return 0;
}
(4)服务端设计
①服务端主程序的设计
服务端最主要的就是利用回调函数将User的函数方法传过去,当要进行用户管理或者转发消息时,下层就能通过回调函数跑回顶层,通过这个唯一的um进行管理。
全部代码
#include "UdpServer.hpp"
#include "Users.hpp"int main()
{shared_ptr<UsersManager> um = make_shared<UsersManager>(); // 用户管理模块,服务器启动,这用来管理用户unique_ptr<UdpServer> server(make_unique<UdpServer>()); // 创建一个服务端对象server->RegisterService(//这是回调函数,服务端启动创建管理用户的对象,再用lambda将这个对象传入服务端,这样服务端就能回调上层的um的函数[&um](const myInetAddr &addr){um->AddUser(addr);},[&um](int sockfd, const string &message){um->Router(sockfd, message);},[&um](const myInetAddr &addr){um->DeleteUser(addr);});// 服务端启动,注册服务,这里注册了三个服务,分别是添加用户、路由、删除用户,完成RegisterService后,回调函数保证server里面访问的UsersManager的对象都是um的server->start(); // 启动服务端,之后会一直循环等待接收数据return 0;
}
②服务器端成员变量

三个函数指针是回调函数的,用于调用上层的对象的函数,转发消息
③利用线程池实现服务端
后面的代码逻辑都比较简单,就是让服务器陷入start()循环中,一直接收消息并通过回调函数转发消息。这个转发消息的任务被推到线程池中完成(线程池的实现不再展示)。唯一需要注意的是推送任务中要匹配任务参数,可以通过bind或者lambda调整,后面可以多注意一下。
全部代码
#pragma once#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdint.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <functional>
#include "myLog.hpp"
#include "myInetAddr.hpp"
#include "myCommon.hpp"
#include "myThreadPool.hpp"using namespace std;
using namespace myLogModule; // 使用日志模块
using namespace myThreadPoolModule;const uint16_t default_port = 8888; // 默认端口号,无需指定IP
const int max_size = 1024; // 存储信息的最大字节数using adduser_t = function<void(const myInetAddr &addr)>;
using route_t = function<void(int sockfd, const string &message)>;
using delete_t = function<void(const myInetAddr &addr)>;class UdpServer
{
public:void RegisterService(adduser_t adduser, route_t route, delete_t del){_adduser = adduser;_route = route;_deluser = del;}UdpServer(uint16_t port = default_port): _socket_fd(-1), // 服务端的socket文件描述符_addr(port), // 服务端的端口号,默认为default_port,用于构造myInetAddr对象,自动初始化IP和端口号_isrunning(false) // 服务端的运行状态{// socket创建网络通信的文件_socket_fd = socket(AF_INET, SOCK_DGRAM, 0); // 网络通信,UDP通信方式,0默认if (_socket_fd == -1) // 创建失败返回-1{LOG(FATAL) << "服务端启动失败"; // 输出错误信息exit(SERVER_ERROR); // 直接退出程序,1表示服务端的异常退出}// 到这里创建了文件并写好了属性,接下来要bindif (bind(_socket_fd, _addr.Get_Const_Sockaddr_ptr(), _addr.Get_Socklen()) != 0) // 将属性和文件绑定,绑定成功返回0// 这个文件被绑定后,其它进程才能通过相应的IP和端口号找到这个文件{LOG(FATAL) << "绑定失败";exit(SERVER_ERROR); // 直接退出程序,1表示服务端的异常退出}// socket打开文件,bind将和文件通信的条件写入,至此其它进程才能找到这个进程LOG(INFO) << "服务端已绑定完毕,正在等待启动";}void start(){myThreadPool<task_t>::GetInstance()->StartAddTask(); // 启动线程池LOG(INFO) << "服务端(端口8888)启动成功";sockaddr_in client_addr; // sockaddr_in类型,这是用于获取第一次接收的用户端的数据所附带的IP、端口号信息socklen_t client_addr_len = sizeof(client_addr);_isrunning = true; // 启动服务器while (_isrunning) // 服务器运行时一直循环{bzero(_read_buffer, sizeof(_read_buffer)); // 对读写数组清0,数组名就是数组的首地址_write_buffer.clear(); // 对写数组清空// 会被阻塞在这个函数,直到收到数据,这个函数会将收到的数据写入_read_buffer,还会将客户端的IP和端口号信息写入client_addr和client_addr_lenssize_t n = recvfrom(_socket_fd, _read_buffer, sizeof(_read_buffer) - 1, 0, // 读到的数据存到_read_buffer里面(struct sockaddr *)&client_addr, &client_addr_len); // 读到属性并写入client_addr和client_addr_len// n不包含'\0',所以要手动加上if (n > 0){_read_buffer[n] = '\0'; // 读到的最后一个字符后面加上'\0',保证字符串安全// 处理收到的属性,client_addr是sockaddr_in类型,需要转换为myInetAddr类型,这样就可以有了客户端的myInetAddrmyInetAddr client_inet_addr(client_addr); // 直接将客户端的IP和端口号信息传入myInetAddr对象,自动初始化IP和端口号// 某一个用户开始向服务器发送数据,服务器开始接收数据,但先要添加用户_adduser(client_inet_addr); // 添加用户,调用回调函数,服务器的main函数定义的um里面的list添加用户,函数内部查重,这里不操作string _write_buffer = client_inet_addr.Get_Ip() + ":" + to_string(client_inet_addr.Get_Port()) + "发出消息:" + _read_buffer;if (strcmp(_read_buffer, "QUIT") == 0){// 移除该UsersManager里面保存的用户信息task_t task_deluser = bind(UdpServer::_deluser, client_addr);myThreadPool<task_t>::GetInstance()->AddTask(move(task_deluser));// 这里不能return,服务器一直在start函数里循环,return会直接退出函数//QUIT之后通知其它人_write_buffer = client_inet_addr.Get_Ip() + ":" + to_string(client_inet_addr.Get_Port()) + "已退出聊天室";}// 将回调函数_route绑定到新的task_t中,这个类型没有参数,将这个专门封装的类型推送到线程池中task_t execute_task = bind(UdpServer::_route, _socket_fd, _write_buffer);myThreadPool<task_t>::GetInstance()->AddTask(move(execute_task)); // 将这个任务推送到线程池中,这个线程池会执行um的函数}}}void stop(){_isrunning = false; // 服务器停止,自动根据while退出循环}~UdpServer(){if (_socket_fd != -1){close(_socket_fd);LOG(INFO) << "已关闭服务器端";}}private:int _socket_fd; // 调用socket之后创建文件后返回的文件fdbool _isrunning; // 记录当前服务端的运行状态myInetAddr _addr;string _write_buffer; // 服务器准备写出的信息char _read_buffer[max_size]; // 服务器读到的信息adduser_t _adduser; // 函数指针,用于添加用户delete_t _deluser; // 函数指针,用于删除用户route_t _route; // 函数指针,用于转发消息
};
(5)重定向文件
./test 1>test.txt可将输出重定向到文件中,标准输入、标准错误可以打印到不同文件中,方便我们进行debug,在这里我们也可以借此将聊天消息统一传到一个界面里面
注意:
1>log.txt 2>log.txt不可行,两个打开一个文件,其中一个流输出的内容会被清空,1>log.txt 2>>log.txt就可以了。
1>log.txt 2>&1也可以实现,把1的内容拷贝给2,让2也指向log.txt,这就意味着文件不是2打开的,只算做1打开文件,只是1单独将描述符私发给2,2不会清空文件
相关文章:
Linux网络相关概念和重要知识(2)(UDP套接字编程、聊天室的实现、观察者模式)
目录 1.UDP套接字编程 (1)socket编程 (2)UDP的使用 ①socket ②bind ③recvfrom ④sendto 2.聊天室的实现 (1)整体逻辑 (2)对sockaddr_in的封装 (3)…...
机器学习之条件概率
1. 引言 概率模型在机器学习中广泛应用于数据分析、模式识别和推理任务。本文将调研几种重要的概率模型,包括EM算法、MCMC、朴素贝叶斯、贝叶斯网络、概率图模型(CRF、HMM)以及最大熵模型,介绍其基本原理、算法流程、应用场景及优势。 2. EM算法(Expectation-Maximizati…...
国科云:浅谈DNS在IPv6改造过程中的重要性
在IPv6改造过程中,DNS(域名系统)起着至关重要的作用,主要体现在以下几个方面: 地址解析与映射 IPv6地址采用128位的地址空间,与IPv4的32位地址相比,地址长度大大增加,这使得手动记…...
JVM 03
今天是2025/03/24 15:21 day 11 总路线请移步主页Java大纲相关文章 今天进行JVM 5,6 个模块的归纳 首先是JVM的相关内容概括的思维导图 5. 优化技术 JVM通过多种优化技术提升程序执行效率,核心围绕热点代码检测和编译优化实现动态性能提升。 热点代码检测 JVM…...
【VUE】day07 路由
【VUE】day07 路由 1. 路由2. 前端路由的工作方式3. 实现简易的前端路由4. 安装和配置路由4.1 安装vue-router包4.2 创建路由模块4.3 导入并挂在路由模块 5. 在路由模块中声明路由的对应关系5.1 router-view 1. 路由 在 Vue.js 中,路由(Routing…...
内网穿透的应用-本地部署ChatTTS教程:Windows搭建AI语音合成服务的全流程配置
文章目录 前言1. 下载运行ChatTTS模型2. 安装Cpolar工具3. 实现公网访问4. 配置ChatTTS固定公网地址 前言 各位开发者小伙伴们!今天我要给大家推荐一个超级火的AI项目——ChatTTS。这个开源文本转语音(TTS)项目的火爆程度简直让人难以置信&a…...
2025-03-21 Unity 网络基础3——TCP网络通信准备知识
文章目录 1 IP/端口类1.1 IPAddress1.2 IPEndPoint 2 域名解析2.1 IPHostEntry2.2 Dns 3 序列化与反序列化3.1 序列化3.1.1 内置类型 -> 字节数组3.1.2 字符串 -> 字节数组3.1.3 类对象 -> 字节数组 3.2 反序列化3.2.1 字节数组 -> 内置类型3.2.2 字节数组 -> 字…...
静态时序分析:SDC约束命令set_min_pulse_width详解
相关阅读 静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html?spm1001.2014.3001.5482 最小脉冲宽度检查用于确保一个单元的时钟引脚和异步置位/复位引脚的的脉冲宽度满足最小要求,如果违反该要求,则可能出现功能错误。严格意…...
推荐1款简洁、小巧的实用收音机软件,支持手机和电脑
聊一聊 没想到现在还有人喜欢听广播。 我一直以为听广播必须要用那种小广播机才可以。 原来手机或电脑上也是可以的。 今天给大家分享一款可以在电脑和手机上听广播的软件。 软件介绍 龙卷风收音机 电台广播收音机分电脑和手机两个版本。 电脑端无需安装,下载…...
HO与OH差异之Navigation
在上一篇的内容中我们进一步的了解了Navigation的用法,但是既然写到这里了我就再来扩充一下有关Navigation的内容。 HarmonyOS与OpenHarmony之间有些写法与内容是有差异的,就比如Navigation的跳转。以下内容中HarmonyOS我都简称为HO,OpenHar…...
盖泽 寻边器 帮助类
EA系列 Aligner晶圆校准器 晶圆校准器是一种应用于晶圆加工中的晶圆预对准装置,通过利用晶圆上的缺口(notch)将晶圆调整至预设位置,以确保晶圆的位置及方向,方便后续工艺的进行。产品广泛应用于半导体制造过程中的各个阶段,可集成至各类半导体设备中使用。 通讯方式 串口 …...
模糊规则激活方法详解(python实例对比)
前文我们已经了解了多种隶属函数,如三角形、梯形、高斯型、S型和Z型,并且讨论了模糊推理的基本过程,包括模糊化、规则评估、聚合和解模糊化。我们还了解了如何生成模糊规则的方法,比如专家经验、聚类分析、决策树、遗传算法和ANFI…...
value-key 的作用
在 el-autocomplete 组件中,value-key 是一个非常重要的属性,它用于指定选项对象中作为值的字段名。当选项列表是一个包含多个属性的对象数组时,value-key 能帮助组件明确哪个属性是实际要使用的值。比如,选项列表为 [{id: 01, na…...
venv 和 conda 哪个更适合管理python虚拟环境
在 Python 开发中,管理虚拟环境是避免依赖冲突和提高项目可复现性的关键。venv(Python 内置)和 conda(第三方工具)各有优劣,选择取决于你的具体需求。以下是详细对比和推荐场景: 1. venv&#x…...
论文阅读:2023 arxiv Provable Robust Watermarking for AI-Generated Text
总目录 大模型安全相关研究:https://blog.csdn.net/WhiffeYF/article/details/142132328 Provable Robust Watermarking for AI-Generated Text https://arxiv.org/pdf/2306.17439 https://github.com/XuandongZhao/Unigram-Watermark https://www.doubao.com/chat/211092…...
kube-vip实践
kube-vip 是一款专为 Kubernetes 设计的轻量级高可用(HA)和负载均衡工具,通过虚拟 IP(VIP)机制实现控制平面和服务的高可用性。以下从核心原理、部署实践到高级配置进行全面解析。 一、核心原理与模式 kube-vip 通过…...
python 中match...case 和 C switch case区别
文章目录 语法结构匹配模式的灵活性穿透特性缺省情况处理 在 Python 3.10 及以后的版本中引入了 match...case 语句,它和其他编程语言里的 switch...case 语句有相似之处,但也存在不少区别, 语法结构 match...case(Python&…...
Web前端考核 JavaScript知识点详解
一、JavaScript 基础语法 1.1 变量声明 关键字作用域提升重复声明暂时性死区var函数级✅✅❌let块级❌❌✅const块级❌❌✅ 1.1.1变量提升的例子 在 JavaScript 中,var 声明的变量会存在变量提升的现象,而 let 和 const 则不会。变量提升是指变量的声…...
Spring Boot3 配置文件
统一配置文件管理 SpringBoot工程下,进行统一的配置管理,你想设置的任何参数(端口号、项目根路径、数据库连接信息等等)都集中到一个固定位置和命名的配置文件(application.properties或application.yml)中࿰…...
消防设施操作员考试:巧用时间高效备考攻略
合理规划时间是备考消防设施操作员考试的关键,能让学习事半功倍。 一、制定详细时间表 根据备考时间和考试内容,制定每日、每周的学习计划。将学习时间合理分配给理论知识学习、技能实操练习和模拟考试。例如,每天安排 3 - 4 小时学习理…...
机器学习之KMeans算法
文章目录 引言1. KMeans算法简介2. KMeans算法的数学原理3. KMeans算法的步骤3.1 初始化簇中心3.2 分配数据点3.3 更新簇中心3.4 停止条件 4. KMeans算法的优缺点4.1 优点4.2 缺点 5. KMeans算法的应用场景5.1 图像分割5.2 市场细分5.3 文档聚类5.4 异常检测 6. Python实现KMea…...
深度学习技术与应用的未来展望:从基础理论到实际实现
深度学习作为人工智能领域的核心技术之一,近年来引起了极大的关注。它不仅在学术界带来了革命性的进展,也在工业界展现出了广泛的应用前景。从图像识别到自然语言处理,再到强化学习和生成对抗网络(GAN),深度…...
FastStoneCapture下载安装教程(附安装包)专业截图工具
文章目录 前言FastStoneCapture下载FastStoneCapture安装步骤FastStoneCapture使用步骤 前言 在日常工作与学习里,高效截图工具至关重要。本教程将为你呈现FastStoneCapture下载安装教程,助你轻松拥有。 FastStoneCapture下载 FastStone Capture 是一款…...
基于AWS Endpoint Security的合规性保障
设计AWS云架构方案实现基于AWS Endpoint Security(EPS)的合规性保障,使用EPS持续收集终端设备的安全状态数据(如补丁版本、密码策略),并通过CloudWatch生成合规性报告。企业可利用这些数据满足GDPR、HIPAA等法规对终端设备的安全审…...
26考研——图_图的遍历(6)
408答疑 文章目录 三、图的遍历图的遍历概述图的遍历算法的重要性图的遍历与树的遍历的区别图的遍历过程中的注意事项避免重复访问遍历算法的分类遍历结果的不唯一性 广度优先搜索广度优先搜索(BFS)概述BFS 的特点广度优先遍历的过程示例图遍历过程 BFS …...
C++类与对象的第一个简单的实战练习-3.24笔记
在哔哩哔哩学习的这个老师的C面向对象高级语言程序设计教程(118集全)讲的真的很不错 实战一: 情况一:将所有代码写到一个文件main.cpp中 #include<iostream> //不知道包含strcpy的头文件名称是什么,问ai可知 #include<…...
4.1 C#获取目录的3个方法的区别
C#中常用有如下3个获取目录的方式如下 1.Directory.GetCurrentDirectory():获取当前工作目录,工作目录可能被用户或其他代码修改。尽量少用。(似乎只要在运行中使用另存为或者打开某个文件夹,当前工作目录就修改) 2.Application…...
架构设计之自定义延迟双删缓存注解(上)
架构设计之自定义延迟双删缓存注解(上) 小薛博客官方架构设计之自定义延迟双删缓存注解(上)地址 1、业务场景问题 在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,修改请求的实现中需要修改数据库后&#…...
oracle数据库(数据库启动关闭/sqlplus登录及基本操作/设置字符集/distinct去重)
目录 1. Oracle数据库启动 2. Oracle数据库关闭 3. sqlplus登录Oracle数据库 3.1 使用sqlplus登录Oracle数据库 3.2 使用sqlplus登录Oracle数据库 3.3 远程登录 3.4 解锁用户 3.5 修改用户密码 3.6 查看当前语言环境 4. sqlplus基本操作 4.1 显示当前用户 4.2 查看当前用户…...
深入理解智能家居领域中RS485、Modbus、KNX 和 Zigbee协议概念
首先详细介绍一下 RS485 和 Modbus 这两个在工业自动化和数据通讯领域中非常重要的概念。 RS485 1. 定义与特点 RS485 标准:RS485 是一种串行通信标准,也称为TIA-485标准,主要用于数据传输。它规定了物理层的电气特性,与数据格式…...
