Linux 多路转接 —— select

news2024/11/25 19:51:25

目录

    • 传统艺能😎
    • select😍
    • fd_set 结构😒
      • timeval 结构🤣
    • socket 就绪条件😁
      • 读条件🤣
      • 写就绪😍
      • 异常就绪😒
    • select 工作流程😘
    • select 服务器实现😂
      • socket 类😒
      • SelectServer 类😂
      • 运行服务器😊
    • 事件处理😍
    • 一些问题😁
    • select 优点😍
    • select 缺点😊
    • 适用场景🤣

传统艺能😎

小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
在这里插入图片描述
1319365055

🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我


在这里插入图片描述

select😍

回顾一下上一篇,select 是系统提供的一个多路转接接口

select 系统调用可以让我们的程序同时监视多个文件描述符的事件是否就绪。我们知道select 的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select 才会成功返回并告知调用者

函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds 为需要监视的文件描述符中最大的文件描述符值 +1;readfds 即输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些事件已经就绪;同理 writefds 为写时间对应的参数;exceptfds 为用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些事件已经就绪;timeout 为输入输出型参数,由用户设置 select 等待时间,返回时表示 timeout 的剩余时间。

参数 timeout 的取值:

  1. NULL/nullptr:select 调用后进行阻塞等待,直到被监视的某个事件就绪。
  2. 0:selec 调用后进行非阻塞等待,无论被监视的事件是否就绪,select 检测后都会立即返回。
  3. 特定的时间值:select 在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,过时进行超时返回。

函数调用成功,则返回有事件就绪的文件描述符个数;如果 timeout 时间耗尽,则返回 0。调用失败则返回 -1,同时错误码会被设置。

select 调用失败时,错误码可能被设置为

EBADF:文件描述符为无效的或该文件已关闭
EINTR:此调用被信号所中断
EINVAL:nfds 参数为负值
ENOMEM:核心内存不足

fd_set 结构😒

fd_set 结构与 sigset_t 结构类似,fd_set 本质也是一个位图,用位图中对应的位来表示要监视的文件描述符:

在这里插入图片描述

在这里插入图片描述
调用 select 之前就需要用 fd_set 定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加动作本质是进行位操作,但这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于 fd_set 位图进行各种操作:

void FD_CLR(int fd, fd_set *set); //清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); //测试描述词组set中相关fd的位是否为真在这里插入图片描述

void FD_SET(int fd, fd_set *set); //设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); //清除描述词组set的全部位

timeval 结构🤣

select 函数的最后一个参数是 timeout,这是一个指向 timeval 结构的指针,timeval 用于描述一段时间长度,该结构当中包含两个成员,其中 tv_sec 表示的是秒,tv_usec 表示的是微秒:

在这里插入图片描述

socket 就绪条件😁

读条件🤣

  1. 接收缓冲区中的字节数,≥ 水位标记 SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。
  2. socket TCP通信中,对端关闭连接,此时对该 socket 读,则返回 0。
  3. 监听的socket上有新的连接请求。
  4. socket 上有未处理的错误。

写就绪😍

  1. 发送缓冲区中的可用字节数,大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  2. socket 的写操作被关闭(close或者shutdown),对一个关闭写操作的 socket 进行写操作,会触发SIGPIPE信号。
  3. socket 使用非阻塞connect连接成功或失败之后。
  4. socket 上有未读取的错误。

异常就绪😒

socket 上收到带外数据(带外数据和 TCP 紧急模式相关,TCP报头当中 URG 标志位和 16位 紧急指针搭配使用,就能够发送/接收带外数据)

select 工作流程😘

如果想实现一个 select 服务器,那么我们可以大概知道他的工作流程:

  1. 先初始化服务器,创建套接字,绑定和监听套接字
  2. 定义一个 fd_array 数组来保存监听套接字以及已经和客户端建立链接的套接字,监听套接字一开始就要加入数组
  3. 服务器循环进行 select 监测读事件是否就绪,如果就绪就可以执行对应操作
  4. 在 select 之前还应该创建一个 readfds 文件描述符集,将 fd_array 的文件描述符放入 readfds ,select 就会对这些文件描述符对应的读事件进行监视,在最后就会看到 在 readfds 里面会有一条条的记录
  5. select 检测到读事件就绪就会将其对应的文件描述符放进 readfds ,我们就能知道哪些事件已经就绪
  6. 如果是读事件的监听套接字就绪了,就用 accept 从底层全连接队列获取已建立的连接,并将对应连接的套接字添加到 fd_array 里
  7. 如果是读事件和客户端建立连接的套接字,就调用 read 将读取到的信息打印出来
  8. 如果读事件是和客户端建立连接的套接字就绪,也可能是因为客户端关闭了连接,此时服务器应该调用 close 关闭套接字,并将该套接字从 fd_array 数组中清除,因为下一次不需要再监视该读事件了。

因为传入 select 的 readfds、writefds 和 exceptfds都是输入输出型参数,当 select 返回时这些参数中的值已经修改了,因此每次调用 select 都需要重新设置,timeout 也是同理。

进行重新设置,就需要定义一个 fd_array 数组保存与客户端已经建立的若干连接和监听套接字,实际 fd_array 数组当中的文件描述符就是需要让 select 监视读事件的文件描述符。select 服务器只是读取客户端发来的数据,因此只需要让 select 帮我们监视特定文件描述符的读事件,如果要让 select 同时帮我们监视读事件和写事件,则需要分别定义 readfds 和 writefds,并定义两个数组分别保存需要被监视的文件描述符,便于每次调用 select 前对readfds 和 writefds 进行重新设置。

服务器刚开始运行时,fd_array 数组当中只有监听套接字,因此 select 第一次只需要告知监听套接字的读事件是否就绪,但每次调用accept 获取到新连接后,都会将对应的套接字添加到 fd_array 当中,后续 select 就需要监视监听套接字和连接套接字的读事件是否就绪。

由于调用 select 时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历 fd_array 对 readfds 进行重新设置时,还需要记录最大文件描述符值。

select 服务器实现😂

socket 类😒

首先编写一个 Socket 类,对套接字相关的接口进行一定程度的封装,为了能够直接调用 Socket 类中封装的函数,我们将这些函数定义成静态成员函数:

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>

class Socket{
public:
	//创建套接字
	static int SocketCreate()
	{
		int sock = socket(AF_INET, SOCK_STREAM, 0);
		if (sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//设置端口复用
		int opt = 1;
		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
		return sock;
	}
	//绑定
	static void SocketBind(int sock, int port)
	{
		struct sockaddr_in local;
		memset(&local, 0, sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(port);
		local.sin_addr.s_addr = INADDR_ANY;
		
		socklen_t len = sizeof(local);

		if (bind(sock, (struct sockaddr*)&local, len) < 0){
			std::cerr << "bind error" << std::endl;
			exit(3);
		}
	}
	//监听
	static void SocketListen(int sock, int backlog)
	{
		if (listen(sock, backlog) < 0){
			std::cerr << "listen error" << std::endl;
			exit(4);
		}
	}
};

SelectServer 类😂

因为当前使用的是云服务器,所以编写的 select 服务器在绑定时不需要显示绑定IP地址,直接将IP地址设置为 INADDR_ANY 就行了,所以类当中只包含监听套接字和端口号两个成员变量。

在构造 SelectServer 对象时,需要指明 select 服务器的端口号,当然也可以在初始化 select 服务器的时候指明。初始化 select 服务器的时候调用 Socket 类当中的函数,依次进行套接字的创建、绑定和监听即可。在析构函数中可以选择调用 close 函数将监听套接字进行关闭,但实际也可以不用,因为服务器运行后一般是不退出的:

#pragma once

#include "socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5

class SelectServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	SelectServer(int port)
		: _port(port)
	{}
	void InitSelectServer()
	{
		_listen_sock = Socket::SocketCreate();
		Socket::SocketBind(_listen_sock, _port);
		Socket::SocketListen(_listen_sock, BACK_LOG);
	}
	~SelectServer()
	{
		if (_listen_sock >= 0){
			close(_listen_sock);
		}
	}
};

运行服务器😊

服务器初始化完毕就应该周期性的执行某种动作了,而服务器要做的就是不断调用 select ,当事件就绪时执行对应某种动作即可。

首先,在 select 服务器开始死循环调用 select 函数,需要先定义一个 fd_array 数组,先把所有位置初始化为无效,并将监听套接字添加到该数组当中,fd_array 数组中保存的就是需要被select监视读事件是否就绪的文件描述符

此后,服务器就不断调用 select 监视读事件是否就绪,每次调用 select 之前都需要重新设置 readfds,具体设置过程就是遍历 fd_array 数组的文件描述符添加到 readfds 中,并同时记录最大的文件描述符值 maxfd,因为后续调用 select 时需要将maxfd+1作为第一个参数传入。

当 select 函后,如果返回值为0,则说明 timeout 时间耗尽,直接进行下一次 select 调用即可;如果返回值为 -1,则说明 select 调用失败,此时让服务器准备进行下一次 select 调用,但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用 select 。如果 select 返回值大于 0,则说明 select 函数调用成功,此时已经有文件描述符的读事件就绪,接下来就应该对就绪事件进行处理:

#pragma once

#include "socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5
#define NUM 1024
#define DFL_FD - 1

class SelectServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	void Run()
	{
		fd_set readfds; //读文件描述符集
		int fd_array[NUM]; //被监视读事件是否就绪的文件描述符
		ClearFdArray(fd_array, NUM, DFL_FD); //将数组中的所有位置设置为无效
		fd_array[0] = _listen_sock; //将监听套接字添加到fd_array数组中的第0个位置
		for (;;){
			FD_ZERO(&readfds); //清空readfds
			//将fd_array数组当中的文件描述符添加到readfds当中,并记录最大的文件描述符
			int maxfd = DFL_FD;
			for (int i = 0; i < NUM; i++){
				if (fd_array[i] == DFL_FD) //跳过无效的位置
					continue;
				FD_SET(fd_array[i], &readfds); //将有效位置的文件描述符添加到readfds当中
				if (fd_array[i] > maxfd) //更新最大文件描述符
					maxfd = fd_array[i];
			}
			switch (select(maxfd + 1, &readfds, nullptr, nullptr, nullptr)){
				case 0:
					std::cout<<"timeout..."<<std::endl;
					break;
				case -1:
					std::cerr << "select error" << std::endl;
					break;
				default:
					//正常的事件处理
					std::cout<<"有事件发生..."<<std::endl;
					//HandlerEvent(readfds, fd_array, NUM);
					break;
			}//end switch
		}//end for
	}
private:
	void ClearFdArray(int fd_array[], int num, int default_fd)
	{
		for (int i = 0; i < num; i++){
			fd_array[i] = default_fd;
		}
	}
};

事件处理😍

当 select 检测到文件描述符的读事件就绪并成功返回后,就应该对事件进行处理了,这里编写一个 HandlerEvent 函数

在处理时需要遍历 fd_array 数组的文件描述符,依次判断各个文件描述符对应的读事件是否就绪,如果就绪则需要进行事件处理。当一个读事件就绪后,还需要进一步判断该文件描述符是否是监听套接字,如果是监听套接字就应该调用 accept 函数将底层的连接获取上来。但是只调用 accept 将连接获取上来还不够,为了下一次调用 select 时能让 select 帮我们监视新连接的读事件是否就绪,在连接获取上来后还应该将该连接对应的文件描述符添加到 fd_array 数组当中,这样在下一次调用 select 前对 readfds 重新设置时就能将该文件描述符添加进去了。

如果是与客户端建立连接的读事件就绪,那么就应该调用 read 函数读取客户端发来的数据,读取成功则进行打印。如果读取失败或者客户端关闭连接,那么 select 服务器应该调用 close 函数关闭对应连接,但此时只关闭连接也是不够的,还应该将该连接对应的文件描述符从 fd_array 数组当中清除,否则后续调用的 select 还会继续监视该连接的读事件是否就绪。

代码如下:

#pragma once

#include "socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5
#define NUM 1024
#define DFL_FD - 1

class SelectServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	void HandlerEvent(const fd_set& readfds, int fd_array[], int num)
	{
		for (int i = 0; i < num; i++){
			if (fd_array[i] == DFL_FD){ //跳过无效的位置
				continue;
			}
			if (fd_array[i] == _listen_sock&&FD_ISSET(fd_array[i], &readfds)){ //连接事件就绪
				//获取连接
				struct sockaddr_in peer;
				memset(&peer, 0, sizeof(peer));
				socklen_t len = sizeof(peer);
				int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
				if (sock < 0){ //获取连接失败
					std::cerr << "accept error" << std::endl;
					continue;
				}
				std::string peer_ip = inet_ntoa(peer.sin_addr);
				int peer_port = ntohs(peer.sin_port);
				std::cout << "get a new link[" << peer_ip << ":" << peer_port << "]" << std::endl;

				if (!SetFdArray(fd_array, num, sock)){ //将获取到的套接字添加到fd_array当中
					close(sock);
					std::cout << "select server is full, close fd: " << sock << std::endl;
				}
			}
			else if (FD_ISSET(fd_array[i], &readfds)){ //读事件就绪
				char buffer[1024];
				ssize_t size = read(fd_array[i], buffer, sizeof(buffer)-1);
				if (size > 0){ //读取成功
					buffer[size] = '\0';
					std::cout << "echo# " << buffer << std::endl;
				}
				else if (size == 0){ //对端连接关闭
					std::cout << "client quit" << std::endl;
					close(fd_array[i]);
					fd_array[i] = DFL_FD; //将该文件描述符从fd_array中清除
				}
				else{
					std::cerr << "read error" << std::endl;
					close(fd_array[i]);
					fd_array[i] = DFL_FD;
				}
			}
		}
	}
private:
	bool SetFdArray(int fd_array[], int num, int fd)
	{
		for (int i = 0; i <num; i++){
			if (fd_array[i] == DFL_FD){ //该位置没有被使用
				fd_array[i] = fd;
				return true;
			}
		}
		return false; //fd_array数组已满
	}
};

当调用 accept 从底层获取上来连接后,不能立即调用 read 读取连接当中的数据,因为此时新连接中的数据可能并没有就绪,如果直接调用read 可能需要进行阻塞等待,我们应该将等待过程交给 select 来完成,因此获取完连接后应该直接将该连接对应的文件描述符添加到 fd_array 数组当中就行了,当该连接的读事件就绪时 select 会告知我们,再进行数据读取就不会被阻塞了。

添加文件描述符到 fd_array 数组当中,本质就是遍历 fd_array 数组,找到一个没有被使用的位置将该文件描述符添加进去即可。但有可能 fd_array 数组中全部的位置都已经被占用了,那么文件描述符就会添加失败,此时就只能将刚刚获取上来的连接对应的套接字进行关闭,因为此时服务器没有能力处理这个连接。

至此 select 服务器编写完毕,用 telnet 工具连接我们的服务器,此时通过 telnet 向服务器发送的数据就能够被服务器读到并且打印输出了。
此外,虽然当前 select 服务器是一个单进程的服务器,但它却可以同时为多个客户端提供服务,根本原因就是因为 select 函数调用后会告知 select 服务器哪个客户端对应的连接事件就绪了,此时 select 服务器就可以读取发来的数据,读取完后又会调用 select 等待某个客户端连接的读事件就绪。

一些问题😁

当前的 select 服务器还存在一些问题:

select 服务器如果要向客户端发送数据,不能直接调用write函数,因为调用write函数时实际也分为 “等” 和“拷贝” 两步,我们也应该将 “等” 的这个过程交给 select ,因此在每次调用 select 之前,除了需要重新设置 readfds 还需要重新设置writefds,并且还需要一个数组来保存需要被监视写事件是否就绪的文件描述符,就绪时我们才能够调用 write 向客户端发送数据。

其次没有定制协议。代码中读取数据时并没有按照某种规则进行读取,此时就可能造成粘包问题,根本原因就是因为我们没有定制协议,比如HTTP协议规定在读取底层数据时读取到空行就表明读完了一个HTTP报头,此时再根据HTTP报头当中的 Content-Length 属性得知正文的长度,最终就能够读取到一个完整的HTTP报文。

没有对应的输入输出缓冲区。代码中直接将读取的数据存储到了字符数组 buffer 当中,这是不严谨的,因为本次数据读取可能并没有读取到一个完整的报文,此时服务器就不能进行数据的分析处理,应该将读取到的数据存储到一个输入缓冲区当中,当读取到一个完整的报文后再让服务器进行处理。此外服务器的响应数据也不应该直接调用 write 发送给客户端,应该先存储到一个输出缓冲区当中,因为数据可能很庞大,无法一次发送完毕,可能需要进行分批发送。

select 优点😍

当然,这也是所有多路转接接口的优点:

  1. 可以同时等待多个文件描述符,并且只负责等待,实际的IO操作由 accept、read、write 等接口来完成,这些接口在进行IO操作时不会被阻塞。
  2. select 可同时等待多个文件描述符,将“等”的时间重叠提高了IO的效率。

select 缺点😊

  1. 每次调用 select 都需要手动设置 fd 集合,非常不便。
  2. 每次调用 select 都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。
  3. 每次调用 select 都需要遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
  4. select 可监控的文件描述符数量太少。

这里我细嗦一下 select 的文件描述符数量,调用 select 时的 readfds、writefds以及exceptfds 都是fd_set结构,fd_set 结构本质是一个位图,它用每一个比特位来标记一个文件描述符,因此 select 可监控的文件描述符个数是取决于 fd_set 的比特位个数。

我们可以计算一下 fd_size 的比特位大小:

#include <iostream>
#include <sys/types.h>

int main()
{
	std::cout << sizeof(fd_set)* 8 << std::endl;
	return 0;
}

结果很明显,可监控的文件描述符数量为 1024 个 \color{red} {结果很明显,可监控的文件描述符数量为 1024 个} 结果很明显,可监控的文件描述符数量为1024

在这里插入图片描述

适用场景🤣

多路转接接口的 select,poll epoll 都有自己适用的场景,在不恰当的场合使用某个接口只会适得其反。

多路转接接口一般适用于多连接,且多连接中只有少部分连接比较活跃。因为少量连接比较活跃,也就意味着几乎所有的连接在进行IO操作时,都需要花费大量时间来等待事件就绪,此时使用多路转接接口就可以将这些等的事件进行重叠,提高IO效率。对于多连接中大部分连接都很活跃的场景,就不适合多路转接了。因为每个连接都很活跃,也就意味着任何时刻事件基本都是就绪的,此时根本不需要动用多路转接接口来帮我们进行等待,毕竟使用多路转接接口需要花费系统时间和空间资源。

多连接中只有少量连接是比较活跃的,比如聊天工具,我们登录QQ后大部分时间其实是没有聊天的,此时服务端不可能调用一个 read 来阻塞等待读事件就绪。

多连接中大部分连接都很活跃,比如企业当中进行数据备份时,两台服务器之间不断在交互数据,这时的连接是特别活跃的,几乎不需要等的过程,也就没必要使用多路转接接口了。

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

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

相关文章

极致呈现系列之:Echarts饼图的千变万化

目录 创建一个最简单的饼图美化饼图修改颜色修改饼图的边框线条样式修改饼图的标签样式添加饼图的阴影效果添加修改饼图的图例样式 添加交互饼图的变化环形图动画装饰仪表盘 创建一个最简单的饼图 这个没什么好说的&#xff0c;懂的都懂&#xff0c;直接上代码 //安装 Echart…

LlamaIndex 简介:LLM 应用程序的数据框架

LlamaIndex 是一个非凡的工具&#xff0c;创建为一个全面的“数据框架”&#xff0c;以促进 LLM&#xff08;大型语言模型&#xff09;应用程序的开发。该框架与 ChatGPT 集成&#xff0c;充当大型语言模型和用户私人数据之间的桥梁。 借助 LlamaIndex&#xff0c;用户可以轻松…

Apache Zeppelin系列教程第九篇——Zeppelin NoteBook数据缓存

背景 在使用Zeppelin JDBC Intercepter 对于Hive 数据进行查询过程中&#xff0c;如果遇到非常复杂的sql&#xff0c;查询效率是非常慢 比如&#xff1a; select dt,count(*) from table group by dt做过数据开发的同学都知道&#xff0c;在hive sql查询过程中&#xff0c;hive…

MySQL - 第1节 - MySQL数据库基础

1.数据库的概念 数据库是按照数据结构来组织、存储和管理数据的仓库&#xff0c;是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。 虽然单纯的使用文件也可以存储数据&#xff0c;但会存在如下缺点&#xff1a; • 安全性问题&#xff1a;数据误操…

深搜-选数类问题

目录 1.问题引入 2.知识讲解 3.例题解析 【例题1】全排列。 【例题2】素数环Ⅱ。 【样例3】素数分解。 1.问题引入 上一节探讨了迷宫类问题&#xff0c;和平时遇到的迷宫小游戏类似&#xff0c;可以使用搜索程序求得迷宫的路径和最短路。本小节继续研究深搜的另一类问…

MySQL数据库的认识及基础命令操作

目录 一、数据库的基本概念 1、数据库定义 &#xff08;1&#xff09; 数据 &#xff08;2&#xff09;表 &#xff08;3&#xff09; 数据库 2、 数据库管理系统&#xff08;DBMS&#xff09; 3、 数据库系统&#xff08;DBS&#xff09; 二、数据库系统发展史 1、 第一…

【RabbitMQ教程】第五章 —— RabbitMQ - 死信队列

&#x1f4a7; 【 R a b b i t M Q 教程】第五章—— R a b b i t M Q − 死信队列 \color{#FF1493}{【RabbitMQ教程】第五章 —— RabbitMQ - 死信队列} 【RabbitMQ教程】第五章——RabbitMQ−死信队列&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我亦是行人…

SpringCloud:分布式事务Seata

1.什么是分布式事务 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上&#xff0c;简单的说&#xff0c;就是一次大的操作由不同的小操作组成&#xff0c;这些小的操作分布在不同的服务器上&#xff0c;且属…

【Flutter】Flutter 创建每个页面公用的底部框

文章目录 一、 前言二、 创建公用底部框的步骤1. 创建一个公用的底部框 Widget2. 在页面中使用公用的底部框 Widget 三、 示例&#xff1a;电商应用中的公用底部框1. 创建电商应用的底部框 Widget2. 在电商应用的各个页面中使用底部框 Widget 四、 完整代码示例五、 一些注意事…

第一次ubuntu wsl ssh远程登录各种报错+解决

第一次ubuntu wsl ssh远程登录各种报错+解决 最新推荐文章于 2023-04-13 20:23:43 发布 kh3064 于 2020-11-03 15:12:16 发布 1869 收藏 5 文章标签: ubuntu

计算理论导引实验三:构造图灵机

计算理论导引实验三&#xff1a;构造图灵机 实验描述形式化定义图灵机M的状态图 算法设计与描述状态转移关系类键盘输入及逻辑处理类 编码实现测试运行 实验描述 要求构造一个能够识别语言L的图灵机。语言L的描述和实验内容如下图所示 形式化定义 根据实验描述&#xff0c;可…

【批量修改后缀名】如何批量去修改文件后缀名(亲测图文结合)

【写在前面】前段时间&#xff0c;因为素材需要&#xff0c;就去之前我制作相册的一个网站上下载了一批照片&#xff0c;但是照片下载下来的格式居然是.png!600*0&#xff0c;这种格式的也打不开&#xff0c;于是乎我自己就吭哧吭哧的去一个个的修改&#xff0c;然后我一想他娘…

EBU5476 Microprocessor System Design 知识点总结_5 GPIO

GPIO General Purpose Input Output, Memory-Mapped IO 把设备&#xff0c;控制等寄存器映射到内存里。好处就是访问设备方式和内存一样&#xff0c;也不用设计复杂的IO电路&#xff0c;便捷&#xff1b;缺点在于占用了内存空间。 Peripheral-Mapped IO IO有一块专门的存储…

Vue|单文件组件与脚手架安装

一、单文件组件1.1 介绍1.2 文件组成1.3 加深认知 二、脚手架安装2.1 什么是脚手架?2.2 使用镜像2.3 全局安装vue/cli2.4 创建并启动项目 一、单文件组件 1.1 介绍 [.vue]文件&#xff0c;称为单文件组件&#xff0c;是Vue.js自定义的一种文件格式&#xff0c;一个.vue文件就…

AST使用(二)

//在此之前&#xff0c;先了解下path和node/*path指的是路径 其常用的方法当前路径所对应的源代码 : path.toString判断path是什么type&#xff0c;使用path.isXXX 这个方法 : if(path.isStringLiteral()){}获取path的上一级路径 : let parent path.parentPath;获取path的子…

nginx的安装及代理和负载均衡设置

一、通过yum方式进行安装 官网参考地址&#xff1a;https://nginx.org/en/linux_packages.html#RHEL 1.1 安装好依赖 执行下面的命令安装 sudo yum install yum-utils1.2、 先配置好yum源 新建文件/etc/yum.repos.d/nginx.repo&#xff0c;文件内容&#xff1a; [nginx-s…

一个成熟的软件测试工程师应该具备那些“技能”

1、良好的沟通 相信大家都在网上看到过各种吐槽程序员不解风情的段子&#xff0c;开怀大笑之余深思&#xff0c;作为一个测试工程师又何尝不是如此&#xff1f;通常沟通技能成为横亘在测试工程师与其他合作部门之间的万丈鸿沟&#xff0c;也成为测试工程师成长的最大瓶颈。下面…

Vector-常用CAN工具 - 以太网报文收发方向

目录 Rx 和 Tx 标记 Example&#xff1a;从 CANoe 向 ECU 发送以太网数据包 用例 2&#xff1a;从 ECU 接收以太网数据包 如何仅显示物理或虚拟通信 如何仅显示 Rx 或 Tx 以太网数据包 VN5000以太网包过滤 1、什么是硬件过滤&#xff1f; 2、什么时候使用硬件过滤&…

useEffect,useLayoutEffect的基础知识和底层机制

useEffect 是 React 中一个重要的 Hook&#xff0c;用来处理组件的副作用操作。它的基础知识包括两个方面&#xff1a;执行时机和参数。 执行时机&#xff1a; useEff ect 的执行时机包括两种情况&#xff1a; 组件挂载时&#xff0c;即第一次渲染之后。组件更新时&#xff…

智能垃圾分类小程序,流量主变现,外卖cps权益变现,uniCloud云开发无需购买服务器和域名,助力每一位创业者。

技术优势 基于 uniapp uniCloud 研发&#xff0c;无需购买服务器和域名&#xff0c;uniCloud 是 DCloud 联合阿里云、腾讯云 serverless 构建。从此不用关心服务器运维、弹性扩容、大并发承载、防DDoS攻击等&#xff0c;轻松应对高并发应用&#xff0c; 传统小程序开通流量主…