Linux网络:UDP socket - 简单聊天室
Linux网络:UDP socket - 简单聊天室
- 聊天通信架构
- Server
- InetAddr
- UdpServer
- MessageRouter
- main
- Client
- 测试
聊天通信架构
本博客基于Linux
实现一个简单的聊天通信服务,以熟悉Linux
的网络接口。
总代码地址:[UDPsocket-简单聊天通信]
文件结构如下:
在server
文件夹中,包含三个类,分别写在三个文件中:
InetAddr.hpp
:记录通信主机的ip
和port
,方便进行通信UdpServer.hpp
:完成服务端UDP
套接字的创建,并接收来自客户端的消息MessageRouter
:对收到的消息进行业务处理
两个文件中的main.cpp
是源文件,分别编译得到服务端与客户端的可执行文件。
Server
InetAddr
class InetAddr
{
private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
类成员:
_addr
:套接字地址_ip
:主机地址_port
:主机端口号
其实在_addr
内部已经存储了地址与端口号,这一层封装的意义是提供更加便捷的接口来访问地址与端口。
- 构造函数
InetAddr(const struct sockaddr_in& addr): _addr(addr)
{_ip = inet_ntoa(addr.sin_addr);_port = ntohs(addr.sin_port);
}
构造函数接受一个套接字地址addr
,随后初始化_ip
与_port
。
- 对
_ip
地址来说,要把四字节的序列通过inet_ntoa
转化为字符串形式 - 对
_port
端口来说,则是要把网络字节序转化为主机字节序
- 基本
get
接口:
std::string ip()
{return _ip;
}uint16_t port()
{return _port;
}struct sockaddr_in addr()
{return _addr;
}
这些接口用于外部访问类成员。
- 操作符重载
operator==
bool operator==(const InetAddr& other) const
{return _ip == other._ip && _port == other._port;
}
后续要完成客户的网络地址之间的身份标识,通过_ip + _port
的组合,来确定一个客户,这样就可以区分前后是否是同一个人发消息。因此此处要重载operator==
,辨别两个UDP
报文是否是同一个客户发送的。
UdpServer
- 类架构:
using func_t = std::function<void(int sockfd, std::string message, InetAddr cliAddr)>;class UdpServer
{
private:int _sockfd;uint16_t _port;func_t _callback;
};
类成员:
_sockfd
:创建网络套接字得到的文件描述符,后续通过读写该描述符操作网络_port
:指定服务端监听的端口_callback
:一个回调函数,当服务端收到消息后,调用该回调函数处理信息,这一层操作的意义在于把UDP
套接字与业务逻辑进行解耦
- 枚举错误码:
enum
{SOCKET_ERROR = 1, // 套接字错误BIND_ERROR, // 绑定错误
};
为例方便后续指明错误类型,此处枚举了三个错误码。
- 构造:
UdpServer(uint16_t port, func_t callback): _port(port), _callback(callback)
{_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字if (_sockfd < 0)exit(SOCKET_ERROR);struct sockaddr_in addr; // 初始化套接字信息bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET;addr.sin_port = htons(_port);addr.sin_addr.s_addr = INADDR_ANY;// 绑定套接字int n = bind(_sockfd, (struct sockaddr*)&addr, sizeof(addr)); if (n < 0)exit(BIND_ERROR);
}
在构造函数中,实现UDP
的套接字创建。
参数:
port
:该服务开放的端口callback
:上层处理消息的业务逻辑的回调函数
随后通过socket
函数创建套接字,参数:
AF_INET
:使用ipv4通信SOCK_DGRAM
:使用UDP进行通信0
:不用管,直接填0即可
得到一个文件描述符_sockfd
,后续通过操作该文件描述符进行网络通信。
但是当前套接字还只是一个内存中的变量,操作系统还没有进行真正的网络监听,此时要将套接字绑定起来。
首先初始化套接字地址的信息:
bzero(&addr, sizeof(addr)); // 清空内存原有内容
addr.sin_family = AF_INET; // 使用ipv4通信
addr.sin_port = htons(_port); // 使用指定端口号
addr.sin_addr.s_addr = INADDR_ANY; // 绑定地址
此处addr.sin_addr.s_addr
表示该套接字,接收来自于哪些地址的请求,比如填入127.0.0.1
,那么就只有127.0.0.1
地址可以与该服务通信。而填入0.0.0.0
表示可以接收任意地址的请求,此处INADDR_ANY
就代表0.0.0.0
,只不过被封装为了一个宏。
最后通过bind
进行绑定,此时就创建了一个UDP套接字,基于ipv4进行通信,监听任意地址发送的请求。
- 开始服务:
void start()
{while (true){char buffer[1024];struct sockaddr_in cliAddr;bzero(&cliAddr, sizeof(cliAddr));socklen_t len = sizeof(cliAddr);int n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&cliAddr, &len);if (n > 0){buffer[n] = '\0';_callback(_sockfd, buffer, cliAddr);}}
}
最后写一个start
函数,这个函数用于接收来自客户端的消息。通过recvfrom
接口读取网络中的UDP
报文,读取到buffer
数组中,为了防止字符串没有结尾,最后buffer[n] = '\0'
添加一个字符串的终止符。因为recvfrom
返回接收到的字符个数,所以最后一个字符的下标为n - 1
,在下标n
处补充一个'\0'
。
接收到消息后,通过_callback
把套接字文件描述符_sockfd
,接收到的数据buffer
以及客户端信息cliAddr
发送给业务层处理。
此处注意:
using func_t = std::function<void(int sockfd, std::string message, InetAddr cliAddr)>;
这是_callback
函数的类型,其中std::string message
是一个普通的std::string
类型,他不是引用,也不是指向字符串的指针。因为buffer
是个在栈区的数组,等到下一轮while
循环,这个数组的内容就是未定义的。所以会导致指针越界,访问到错误数据等问题。因此不能使用指针或引用,而是让std::string
对buffer
内的数据进行一次拷贝。cliAddr
同理,不是一个引用或者指针,要进行一次拷贝。
MessageRouter
MessageRouter
是业务层的逻辑,接收到一条消息后,处理消息并发送回给客户端。
如图,当UdpServer
接收到来自客户端的消息后,MessageRouter
要把这个消息转发给其他客户端,也就是说MessageRouter
的任务就是转发消息。而发送消息需要通过套接字文件描述符,这也就是为什么在刚才的_callback
要传一个_sockfd
。
- 类架构:
class MessageRouter
{
private:std::vector<InetAddr> _online_user;pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
};
MessageRouter
要维护所有的用户,所以使用一个数组来存储所有的客户端。每个客户端用一个InetAddr
表示,也就是一个ip + port
确定一个唯一的客户端。
因为后续要引入多线程,会出现并发访问数组的问题,此处使用一把_mutex
锁来进行并发控制。
- 增加用户
bool addUser(const InetAddr& user)
{pthread_mutex_lock(&_mutex);for (auto& o_user : _online_user) // 遍历数组{if (user == o_user) // 用户已存在{pthread_mutex_unlock(&_mutex);return false;}}_online_user.push_back(user); // 新增用户pthread_mutex_unlock(&_mutex);return true;
}
增加一个用户,就要访问数组_online_user
,访问之前要加锁,访问结束后再解锁。
访问前先遍历数组,查看是否当前用户已经存在,如果存在直接返回,返回前别忘了解锁。如果不存在,则尾插新用户到数组中。
- 删除用户
bool delUser(const InetAddr& user)
{pthread_mutex_lock(&_mutex);auto it = find(_online_user.begin(), _online_user.end(), user);if (it == _online_user.end()){pthread_mutex_unlock(&_mutex);return false;}_online_user.erase(it);pthread_mutex_unlock(&_mutex);return true;
}
删除用户与添加用户同理,先遍历数组,如果用户不存在,直接返回。如果存在,那么删掉该用户。
- 消息转发:
struct SendPackage
{SendPackage(MessageRouter* self, int sockfd, std::string message, InetAddr cliAddr): _self(self), _sockfd(sockfd), _message(message), _cliAddr(cliAddr){}MessageRouter* _self;int _sockfd;std::string _message;InetAddr _cliAddr;
};
为了不影响主线程接收消息,提高进行处理消息的效率,此处将消息转发的任务交给一个线程来完成。而Linux
中,线程要使用一个void*(void*)
类型的函数,这样就不好将参数传递给线程了,所以要先用一个结构体将所有参数进行打包。再把指向该结构体的指针转为void*
传给线程。
SendPackage
是一个内部类,用于对线程所需的参数进行打包,让线程可以进行消息的转发。
_self
:指向MessageRouter
的指针,因为线程要访问所有用户,也就是访问_online_user
,所以要一个指针回指来访问_sockfd
:套接字文件描述符,进行消息转发也就是进行网络通信,网络通信依赖于套接字文件描述符_message
:要转发的消息_cliAddr
:发送方客户端的信息
static void* messageSender(void* args)
{SendPackage* sendpkg = (SendPackage*)args;std::string msg = "[" + sendpkg->_cliAddr.ip() + ":"+ std::to_string(sendpkg->_cliAddr.port()) + "]"+ sendpkg->_message;std::cout << "sending..." << msg << std::endl;pthread_mutex_lock(&sendpkg->_self->_mutex);for (auto& usr : sendpkg->_self->_online_user){struct sockaddr_in cliaddr = usr.addr();sendto(sendpkg->_sockfd, msg.c_str(), msg.size(), 0, (sockaddr*)&cliaddr, sizeof(cliaddr));}pthread_mutex_unlock(&sendpkg->_self->_mutex);delete sendpkg;return nullptr;
}
该函数是线程执行的函数,用于对消息进行转发,首先将参数void*
转回SendPackage*
,也就是刚刚的参数包结构体。
随后拼接字符串msg
,这是要转发消息内容。格式为:
[ip:port] 消息
前面的[]
表明这是哪一个用户发送的消息,后面是具体的消息内容。
随后服务端输出一条日志std::cout << "sending..." << msg << std::endl;
,表示自己转发了这条消息。
随后访问_online_user
数组,遍历所有成员,并且对通过sendto
函数,进行消息转发。
转发完消息后,进行解锁,并且delete
释放sendpkg
,这是一个堆区上的对象,后续会讲解原因。
- 回调主逻辑:
void router(int sockfd, std::string message, InetAddr cliAddr)
{// 首次发消息 -> 注册addUser(cliAddr);// 用户退出if (message == "/quit"){delUser(cliAddr);return;}// 线程转发消息SendPackage* sendpkg = new SendPackage(this, sockfd, message, cliAddr);pthread_t tid;pthread_create(&tid, 0, messageSender, messageSender);pthread_detach(tid);
}
这个函数,就是UdpSerever
中回调的函数,当用户要发消息时,首先添加该用户addUser(cliAddr)
,如果用户已经存在,addUser
函数内部也不会重复添加。
如果用户想要退出,输入"/quit"
,此时会进行删除delUser
。
如果前面已经添加好了用户,随后就开始进行消息转发,此时对参数进行打包new SendPackage
。这里要用new
创建,把这个参数包创建在堆区,因为router
创建完线程,就直接退出了,此时栈区中的所有数据都会销毁。那么线程就无法读取到栈区中的参数包,所以要把参数包创建在堆区,随后让线程自己释放。
创建线程时,给线程指定函数messageSender
,参数messageSender
,这样线程就会去调用函数,然后完成数据的转发。
最后router
退出之前,先把创建的线程detach
,让其自己回收。
main
在main.cpp
中,完成所有逻辑的拼接,启动整个服务。
void useage(char* argv[])
{std::cout << "useage:" << std::endl;std::cout << "\t" << argv[0] << " + port" << std::endl;
}int main(int argc, char* argv[], char* env[])
{if (argc != 2){useage(argv);return 0;}// 主逻辑return 0;
}
首先判断用户执行该程序的指令,用户需要指定一个端口,表示该服务使用的端口。如果没有指定,则useage
输出提示用户输入一个端口号。
- 主逻辑
uint16_t port = std::stoi(argv[1]);MessageRouter msgRouter;
auto func = std::bind(&MessageRouter::router, &msgRouter, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);UdpServer udpSvr(port, func);
udpSvr.start();
当确定用户输入了一个端口后,首先用port
接收这个端口,从字符串转为数字。
随后把MessageRouter
的router
函数作为回调函数,传给udpSvr
对象。由于该函数是一个类内的函数,第一个参数为this
指针。因此使用bind
,把第一个参数绑定为&msgRouter
,也就是一个具体对象的指针。这样新函数func
的类型就与UdpServer
的回调函数一致。
最后传入端口号port
与回调函数func
,启动服务。
Client
客户端的任务很简单,只需要完成数据的发送与接收即可。此处把发送消息和接收消息交给两个不同的线程去完成。
- 线程参数:
struct SockInfo
{SockInfo(int sockfd, const struct sockaddr_in& sockaddr): _sockfd(sockfd), _sockaddr(sockaddr){}int _sockfd;struct sockaddr_in _sockaddr;
};
由于要使用多线程,和之前也一样要把所有参数放到一个结构体一起传参。此处只需要把服务端的信息,以及通信的套接字文件描述符传送给线程。
_sockfd
:与服务端通信的文件描述符_sockaddr
:服务端的套接字地址信息
- 接收消息:
void* recvMessage(void* args)
{SockInfo* sockInfo = (SockInfo*)args;while(true){char buffer[1024];int n = recvfrom(sockInfo->_sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);if (n > 0){buffer[n] = '\0';std::cout << buffer << std::endl;}}return nullptr;
}
首先把收到的void*
参数包,转回SockInfo*
。
随后进入死循环,接收来自服务端的消息。此处recvfrom
的最后两个参数设为nullptr
,表示不关心谁发送的消息,忽略消息发送方的地址与端口信息。因为通过sockInfo->_sockfd
通信,而这个套接字就是在和服务端通信,无需再确认身份了。
收到消息后,直接cout << buffer << endl
,输出接收到的消息。
- 发送消息:
void* sendMessage(void* args)
{SockInfo* sockInfo = (SockInfo*)args;std::string message;while(true){std::getline(std::cin, message);sendto(sockInfo->_sockfd, message.c_str(), message.size(), 0, (sockaddr*)&sockInfo->_sockaddr, sizeof(sockInfo->_sockaddr));}return nullptr;
}
同理,解析出参数包后,进入一个死循环。每轮循环等待用户输入一个消息,随后把这个消息发送给服务端,服务端会进行消息转发。
- 主函数:
void usage(char* argv[])
{std::cout << "Usage:\n\t";std::cout << argv[0] << " server_ip server_port" << std::endl;
}int main(int argc, char* argv[], char* env[])
{if (argc != 3){usage(argv);return 0;}// 主逻辑return 0;
}
主函数中,需要用户输入一个ip
和一个port
,表示客户端的地址和端口,如果输入错误,调用usage
提示用户。
- 主逻辑:
// 解析地址与端口
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);// 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);// 初始化服务端信息
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);// 创建线程
SockInfo sockInfo(sockfd, server);
pthread_t sender;
pthread_t recver;
pthread_create(&sender, 0, sendMessage, &sockInfo);
pthread_create(&recver, 0, recvMessage, &sockInfo);pthread_join(sender, nullptr);
pthread_join(recver, nullptr);
首先创建套接字,然后初始化服务端的信息。在sockaddr_in server
中就填充了服务端的地址与端口号。
客户端无需进行bind
绑定,在第一次通过sendto
发送消息时,操作系统会自动为其分配一个端口号。
随后创建两个线程,分别执行sendMessage
和recvMessage
,进行消息的接收与发送。
此处有一个小细节,sockInfo
不是new
出来的,而是直接存储在栈区的变量。这个情况与之前有所不同,之前是因为router
函数创建完线程后,就直接退出了。而此处的主函数不能退出,主函数退出整个进程都终止了,所以栈区中的数据会一直存在,不需要new
。
最后在main
函数中通过join
等待两个线程。
测试
左上角是服务端,剩余三个终端是客户端。首先右上角的终端启动,发送了一个hello
,随后服务端把hello
返送回给了右上角的终端。因为之前写逻辑时,只有客户端发送一次消息,服务端才会把客户端加入到_online_users
中,下面两个终端没有发消息,所以服务端不知道这两个客户端存在,也就没有转发消息。
随后左下角终端发送了iammike
,这个消息被转发给了右上角的终端,因为右上角的终端已经在_online_users
中。
发送一段时间消息后,右上角终端输入/quit
,此时服务端将其删除。最后左下角终端发送iamlisa
,此时右上角终端收不到该消息了,说明右上角终端已经成功退出。
相关文章:

Linux网络:UDP socket - 简单聊天室
Linux网络:UDP socket - 简单聊天室 聊天通信架构ServerInetAddrUdpServerMessageRoutermain Client测试 聊天通信架构 本博客基于Linux实现一个简单的聊天通信服务,以熟悉Linux的网络接口。 总代码地址:[UDPsocket-简单聊天通信] 文件结构…...

Codeforces Round 646 (Div. 2) E. Tree Shuffling(树,贪心)
题目链接 Codeforces Round 646 (Div. 2) E. Tree Shuffling 思路 考虑一个节点 u u u,显然它子树中的操作可以由它本身和祖先来进行。如果它的祖先有比它花费更小的,直接跳过节点 u u u。 我们分别记录每一个子树中位置不对的 0 0 0和 1 1 1的个数&…...

HCIE-Datacom题库_11_IPsecVPN【17道题】
一、单选题 1.IPsecSA(SecurityAssociation,安全联盟)有两种生成方式,分别是手工方式和IKE自动协商方式,以下关于这两种方式的描述中,错误的是哪一项? 手工方式和IKE方式建立的SA都支持动态刷新 IKE方式建立的SA,其生存周期由…...

Dongle Sentinal在Jenkins下访问不了的问题
背景: 工作站部署的jenkins的脚本无法正常打包,定位后发现是本地获取不了license,但是使用usb over network的远程license都能获取并正常打包 分析: 获取不了license的原因是本地无法识别dongle。根据提供信息,之前…...

X射线衍射(X-ray Diffraction,XRD)小白版
文章目录 实验过程原理晶体构成X射线波长diffraction 干涉效应 Braggs Law晶体间距d散射角度θ半波长λ/2公式 公式名称由来应用设备 实验过程 In the X-ray experiment , a sample is placed into the center of an instrument and illuminated with a beam of X-rays. 在X射…...

Nordic 定时器系统app timer[获取时间戳]
获取时间戳 想要在Nordic 定时器系统中获取时间戳,也就是是在调用app_timer的时候时间戳要有效,我们可以看看定时器系统初始化: ret_code_t app_timer_init(void) {ret_code_t err_code;drv_rtc_config_t config {.prescaler APP_TIMER_CONFIG_RTC_FREQUENCY,.int…...

【Linux】实验:mkdir 命令 、 tee 命令
#1024程序员节|征文# 1.命令说明 本文主要实验 linux 的两个命令:mkdir -p 路径、 tee 创建文件。 命令:mkdir -p 路径 说明:该命令将自动创建路径下的目录及子目录,结尾可以/ 也可以不带/,默认都是建文…...

asp.net core mvc发布时输出视图文件Views
var builder WebApplication.CreateBuilder(args); builder.Services.AddRazorPages();builder.Services.AddControllersWithViews(ops > {//全局异常过滤器,注册ops.Filters.Add<ExceptionFilter>(); })// Views视图文件输出到发布目录,视图文…...

服务器模块测试
目录 测试逻辑 测试工具 测试 测试逻辑 我们可以使用一个简单的业务处理逻辑来进行测试。 最简单的,我们业务逻辑就直接返回一个固定的字符串 void Message(const PtrConnection&con,Buffer* inbuffer) //模拟用户新数据回调 {inbuffer->MoveReadOf…...

ATTCK 框架讲解
摘要 ATT&CK框架作为MITRE公司开发的网络攻击行为知识库,自2015年发布以来,已成为信息安全领域的重要工具。该框架通过提炼和归纳真实世界中的网络威胁事件,以攻击者的视角构建了一套系统化的战术和技术分类体系。本文详细阐述了ATT&…...

ADC在STM32F1系列的使用详解
目录 1. ADC简介 2. 逐次逼近型ADC(ADC0809) 3. ADC框图(STM32) 4. ADC基本结构 5. 输入通道 6. 转换模式 6.1 单次转换 6.1.1 非扫描模式 6.1.2 扫描模式 6.2 连续转换 6.2.1 非扫描模式 6.2.2 扫描模式…...

网络空间安全之一个WH的超前沿全栈技术深入学习之路(一:渗透测试行业术语扫盲)作者——LJS
欢迎各位彦祖与热巴畅游本人专栏与博客 你的三连是我最大的动力 以下图片仅代表专栏特色 [点击箭头指向的专栏名即可闪现] 专栏跑道一 ➡️网络空间安全——全栈前沿技术持续深入学习 专栏跑道二➡️ 24 Network Security -LJS 专栏跑道三 ➡️ MYSQL REDIS Advanc…...

中间件-概念
什么是中间件? 中间件(Middleware)是位于 Web 服务器和应用程序之间的组件,它可以处理每个请求和响应。中间件的主要作用是在请求到达应用程序之前或响应返回客户端之前对其进行处理。中间件可以执行各种任务,如日志记…...

vscode离线状态ssh连接不断输入密码登不上:配置commit_id
如题,vscode在一个离线服务器上,通过remote-ssh登录远程服务器,不断弹出密码框,总是进不去,后来了解到主要是不同vscode版本需要下载对应抑制commit-id的vscode-server-linux-x64.tar.gz包。 1)vscode, 点…...

Vim使用与进阶
1. Vim 技巧 撤销 U 反撤销 Ctrl U 历史命令 history 2.要在Vim中进行多行缩进,可以按以下步骤操作: 进入Vim编辑器并进入命令模式。使用 v 键或 Shift v 键选择多行需要缩进的文本。按下 > 键进行向右缩进,或按下 < 键进行向左…...

python中frida的安装+frida-server(雷电模拟器)保姆级安装教程
一.安装雷电模拟器 雷电模拟器官网 直接下载安装即可 (1)打开必要权限 雷电模拟器的设置已完毕 二.安装adb工具 本文以autox.js来实现adb操作 (1)vscode中下载auto.js插件 (2)雷电模拟器下载autox.j…...

Java线程安全集合之COW
概述 java.util.concurrent.CopyOnWriteArrayList写时复制顺序表,一种采用写时复制技术(COW)实现的线程安全的顺序表,可代替java.util.ArrayList用于并发环境中。写时复制,在写入时,会复制顺序表的新副本&…...

智能汽车制造:海康NVR管理平台/工具EasyNVR多品牌NVR管理工具/设备实现无插件视频监控直播方案
一、背景介绍 近年来,随着网络在我国的普及和深化发展,企业的信息化建设不断深入,各行各业都加快了信息网络平台的建设,大多数单位已经或者正在铺设企业内部的计算机局域网。与此同时,网络也成为先进的新兴应用提供了…...

[渗透]前端源码Chrome浏览器修改并运行
文章目录 简述本项目所使用的代码[Fir](https://so.csdn.net/so/search?qFir&spm1001.2101.3001.7020) Cloud 完整项目 原始页面修改源码本地运行前端源码修改页面布局修改请求接口 本项目请求方式 简述 好久之前,就已经看到,_无论什么样的加密&am…...

SAP揭秘者-怎么查看SAP 版本及S4 HANA的版本
文章摘要: 在给客户实施SAP项目或部署SAP服务器及SAP跟外部系统集成时,经常客户或第三方软件公司会问SAP版本或SAP HANA的版本。那么到底怎么来看这个SAP的版本呢?这个问题其实很多SAP模块顾问都不知道怎么看,你可以想象一下&…...

UE4 材质学习笔记13(格斯特纳波)
一.格斯特纳波 要让水面动起来,必须要保证平面有足够的三角面。我们可以在材质里的细节面板打开曲面细分,可以分裂三角面且使之数量更多,选择“扁平曲面细分,其作用是切割我的三角面,然后给我做一大堆三角面出来。 这…...

简述 C# 二维数据集合 List 的创建、遍历、修改、输出
简述 C# 二维数据集合 List 的创建、遍历、修改、输出 1、为什么要使用列表 List2、引入命名空间3、声明一维列表 List4、声明创建一个二维列表 List,数据类型 int5、 简单访问元素6、遍历二维列表,控制台输出7、遍历二维列表,修改数据&#…...

ps2024 一键安装教程 永久使用!
下载后,直接解压打开exe文件就能安装了 下载: https://pan.baidu.com/s/1uDSug00prwRw5igF0N-Xhw?pwd8888 【软件名称】:ps2024 【软件大小】:4.7g 【软件版本】:25.12.0.806 【软件简介】:Photoshop,简称“PS”,是由美国Adobe公司推出…...

ScrollView 真机微信小程序无法隐藏滚动条
问题描述 根据官方文档,使用:show-scrollbar"false",隐藏滚动条无效 解决方法 添加一段样式在 scroll-view 上或者父级节点上下 ::-webkit-scrollbar {width: 0;height: 0;color: transparent;display: none;} eg. .inforDetails_app {p…...

【日志】编辑器开发——修复根据Excel表格数据生成Json文件和配置表代码报错
2024.10.15 又是蕉绿且摆烂的一天,不仅需要克制网瘾,还要努力学习,不然真的会被抛弃啊。但是我还是不想卷,给我的时间大概还有半年,突然好奇半年时间到底能学点什么或者做点什么。 【力扣刷题】 暂无 【数据结构】 …...

C#线性查找算法
前言 线性查找算法是一种简单的查找算法,用于在一个数组或列表中查找一个特定的元素。它从数组的第一个元素开始,逐个检查每个元素,直到找到所需的元素或搜索完整个数组。线性查找的时间复杂度为O(n),其中n是数组中的元素数量。 …...

GPT+Python)近红外光谱数据分析与定性/定量建模技巧
2022年11月30日,可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT3.5,将人工智能的发展推向了一个新的高度。2023年4月,更强版本的ChatGPT4.0上线,文本、语音、图像等多模态交互方式使其在…...

Spark动态资源释放机制 详解
Apache Spark 是一个分布式数据处理框架,其动态资源分配(或称为动态资源释放)机制,是为了更高效地利用集群资源,尤其是在执行具有不同工作负载的作业时。Spark 的动态资源释放机制允许它根据作业的需求自动分配和释放集…...

基于径向基神经网络(RBF)的构网型VSG自适应惯量控制MATLAB仿真模型
微❤关注“电气仔推送”获得资料(专享优惠) 模型简介 逆变器虚拟同步发电机控制和核心控制参数就是虚拟惯量与虚拟阻尼,目前的文献中已有众多论文对VSG的虚拟参数展开了研究,但是百分之90都是采用构造函数的方法,使用…...

简单汇编教程9 字符串与字符串指令
目录 字符串的指令 movs 字符串传送 lods, stos使用 cmpsb的使用 SCASB的使用 字符串你很熟悉了,我们定义了无数次了! %macro ANNOUNCE_STRING 2%1 db %2%1_LEN equ $ - %1 %endmacro 当然,我们现在来学习一个比较新的定义方式…...