网络编程(5)——模拟伪闭包实现连接的安全回收

六、day6

今天学习如何利用C11模拟伪闭包实现连接的安全回收,之前的异步服务器为echo模式,但存在安全隐患,在极端情况下客户端关闭可能会导致触发写和读回调函数,二者都进入错误处理逻辑,进而造成二次析构。今天学习如何通过C11智能指针构造伪闭包状态(将智能指针传给函数对象,如果函数对象不消失,该指针也不会消失),延长session的生命周期,不会发生二次析构的风险

1)智能指针管理Server

通过智能指针的方式管理Session类,将acceptor接收的链接保存在Session类型的智能指针里。由于智能指针会在引用计数为0时自动析构,所以为了防止其被自动回收,也方便Server管理Session,因为我们后期会做一些重连踢人等业务逻辑,我们在Server类中添加成员变量,该变量为一个map类型,key为Session的uid,value为该Session的智能指针。

class Server
{
private:void start_accept();  // 启动一个acceptor// 当acceptor接收到连接后启动该函数void handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error);boost::asio::io_context& _ioc;tcp::acceptor _acceptor;std::map<std::string, std::shared_ptr<Session>> _sessions;
public:Server(boost::asio::io_context& ioc, short port);void ClearSession(std::string uuid);
};

通过Server中的_sessions这个map管理链接,可以增加Session智能指针的引用计数,只有当Session从这个map中移除后,Session才会被释放。所以在接收连接的逻辑里将Session放入map。

但这里产生了一个问题,为什么new_session在'}'结束后仍不结束,而是bind后计数加一?这个问题在文章后面回答。

void Server::start_accept() {// make_shared分配并构造一个 std::shared_ptr,_ioc, this是传给Session的参数std::shared_ptr<Session> new_session = std::make_shared<Session>(_ioc, this);// 开始一个异步接受操作,当new_session的socket与客户端连接成功时,调用回调函数handle_accept// 为什么new_session在右括号结束后仍不结束,而是bind后计数加一?_acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept, this, new_session,std::placeholders::_1));
}// 当handle_accept触发时,也就是start_accept的回调函数被触发,当该回调函数结束后从队列中移除后,new_session的引用计数减一
void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error) {// 如果没有错误(error 为 false),调用 new_session->Start() 来启动与旧客户端的会话if (!error) {new_session->Start();_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));}else // 无论当前连接是否成功,都重新调用 start_accept(),以便服务器能够继续接受下一个新客户端的连接请求。// 服务器始终保持在监听状态,随时准备接受新连接start_accept();
}

StartAccept函数中虽然new_session是一个局部变量,但是通过bind操作,将new_session作为数值传递给bind函数,而bind函数返回的函数对象内部引用了该new_session所以引用计数增加1,这样保证了new_session不会被释放。在HandleAccept函数里调用session的start函数监听对端收发数据,并将session放入map中,保证session不被自动释放。

此外,需要封装一个释放函数,将session从map中移除,当其引用计数为0则自动释放

void Server::ClearSession(std::string uuid) {_sessions.erase(uuid);
}

2)Session的uuid

session的uuid可以通过boost提供的生成唯一id的函数获得,也可以自己实现雪花算法

void Session::HandleWrite(const boost::system::error_code& error) {if (!error) {std::lock_guard<std::mutex> lock(_send_lock);_send_que.pop();if (!_send_que.empty()) {auto &msgnode = _send_que.front();boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_max_len),std::bind(&CSession::HandleWrite, this, std::placeholders::_1));}}else {std::cout << "handle write failed, error is " << error.what() << endl;_server->ClearSession(_uuid);}
}
void Session::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred){if (!error) {cout << "read data is " << _data << endl;//发送数据Send(_data, bytes_transferred);memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2));}else {std::cout << "handle read failed, error is " << error.what() << endl;_server->ClearSession(_uuid);}
}

但以上操作仍有造成二次析构的隐患:

正常情况下上述服务器运行不会出现问题,但是当我们像day5一样模拟,在服务器要发送数据前打个断点,此时关闭客户端,在服务器就会先触发写回调函数的错误处理,再触发读回调函数的错误处理,这样session就会两次从map中移除,因为map中key唯一,所以第二次map判断没有session的key就不做移除操作了。

但是这么做还是会有崩溃问题,因为第一次在session写回调函数中移除session,session的引用计数就为0了,调用了session的析构函数,这样在触发session读回调函数时此时session的内存已经被回收了自然会出现崩溃的问题。解决这个问题可以利用智能指针引用计数和bind的特性,实现一个伪闭包的机制延长session的生命周期。

3)构造伪闭包

思路:

  1. 利用智能指针被复制或使用引用计数加一的原理保证内存不被回收
  2. bind操作可以将值绑定在一个函数对象上生成新的函数对象,如果将智能指针作为参数绑定给函数对象,那么智能指针就以值的方式被新函数对象使用,那么智能指针的生命周期将和新生成的函数对象一致,从而达到延长生命的效果。
	void headle_read(const boost::system::error_code& error, size_t bytes_transferred,std::shared_ptr<Session> _self_shared);void haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared);

以haddle_write举例,在bind时传递_self_shared指针增加其引用计数,这样_self_shared的生命周期就和async_write的第二个参数(也就是asio要求的回调函数对象headle_read)生命周期一致了。

void Session::haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared) {if (!error) {memset(_data, 0, max_length);_socket.async_read_some(boost::asio::buffer(_data, max_length),std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));}else {cout << "write error" << error.value() << endl;_server->ClearSession(_uuid);}
}

同理,headle_read内部也实现了类似的绑定

void Session::headle_read(const boost::system::error_code& error, size_t bytes_transferred,std::shared_ptr<Session> _self_shared) {if (!error) {cout << "server receive data is " << _data << endl;boost::asio::async_write(_socket, boost::asio::buffer(_data, bytes_transferred),std::bind(&Session::haddle_write, this, std::placeholders::_1, _self_shared));}else {cout << "read error" << endl;_server->ClearSession(_uuid);}
}

除此之外,在第一次绑定读写回调函数的时候要传入智能指针的值,但是要注意传入的方式,不能用两个智能指针管理同一块内存,如下用法是错误的

void Session::Start() {memset(_data, 0, max_length); // 缓冲区清零// 从套接字中读取数据,并绑定回调函数headle_read_socket.async_read_some(boost::asio::buffer(_data, max_length),// 这里可以将shared_ptr<Session>(this)给bind绑定吗?// 不可以,会造成多个智能指针绑定同一块内存的问题std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2,shared_ptr<CSession>(this)));
}

shared_ptr<CSession>(this)生成的新智能指针和this之前绑定的智能指针new_session并不共享引用计数,所以要通过shared_from_this()函数返回智能指针,该智能指针和其他管理这块内存的智能指针共享引用计数。

void Session::Start() {memset(_data, 0, max_length); // 缓冲区清零// 从套接字中读取数据,并绑定回调函数headle_read_socket.async_read_some(boost::asio::buffer(_data, max_length),// 这里可以将shared_ptr<Session>(this)给bind绑定吗?// 不可以,会造成多个智能指针绑定同一块内存的问题std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2,shared_from_this()));
}

shared_from_this()函数并不是session的成员函数,要使用这个函数需要继承std::enable_shared_from_this<Session>

class Session:public std::enable_shared_from_this<Session>
{
private:tcp::socket _socket; // 处理客户端读写的套接字enum { max_length = 1024 };char _data[max_length]; // headle回调函数void headle_read(const boost::system::error_code& error, size_t bytes_transferred,std::shared_ptr<Session> _self_shared);void haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared);std::string _uuid;Server* _server;
public:Session(boost::asio::io_context& ioc, Server* server) : _socket(ioc), _server(server){// random_generator是函数对象,加()就是函数,再加一个()就是调用该函数boost::uuids::uuid a_uuid = boost::uuids::random_generator()();_uuid = boost::uuids::to_string(a_uuid);}tcp::socket& Socket() { return _socket; }const std::string& GetUuid() const { return _uuid; }void Start();};

再次测试,链接可以安全释放,并不存在二次释放的问题。可以在析构函数内打印析构的信息,发现只析构一次

1. 为什么new_session在'}'结束后仍不结束,而是bind后计数加一?

new_session通过bind绑定时,new_session的计数就会加一,所以在bind后,new_session的生命周期和新构造函数的生命周期相同,因为新生成的函数对象引用了new_session(new_session通过值传递的方式被复制构造函数使用)。所以只要新构造的bind回调函数没有被调用、移除,new_session的生命周期就始终存在,所以new_session不会随着'}'的结束而释放。

bind中的shared_from_this()通过自己生成一个与外界智能指针共享相同引用计数的指针,保证Cession不被释放或者异常释放,因为有可能回调函数还没有调用时,引用Session的应用计数便已经为0,此时如果回调函数被调用,就会发现Session已经被释放。所以为了防止Session被释放,需要生成一个智能指针使得Session的引用计数加一,并传给回调函数,这样能保证回调函数在没有调用之前,一直占用一个Session的引用计数,Session就不会被释放,就达成了一个延时的闭包效果。

总结:使用 std::bind 来绑定 new_session 作为回调参数时,引用计数也会加一,因为 std::bind 内部会将对象拷贝到新生成的函数对象中。这意味着,直到回调函数执行完毕且不再被引用时,new_session 的生命周期才会结束,所以在 } 结束后 new_session 不会被立即销毁。

2. 为什么session执行start函数启动异步操作后会立即将session插入至map种,而不是等异步操作执行完返回后才将session插入map?

new_session->Start();
_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));

代码执行顺序:

1)调用 new_session->Start():

当 Start() 被调用时,它会启动一些异步操作(如异步读取),但是这些异步操作并不会阻塞当前的执行流。它们只是注册了一个回调函数,并告诉操作系统在相关事件发生(如数据到达、连接完成等)时触发回调。所以,new_session->Start() 启动异步读取操作async_read_some后,控制权立即返回到 handle_accept() 中,而不会等待异步操作完成。

2)插入 _sessions:

new_session->Start() 启动异步操作后,下一条语句 _sessions.insert(...) 会立即执行。由于 insert 操作是同步的,服务器将 new_session 立即插入到 _sessions 容器中,这确保 new_session 在异步操作执行期间不会被销毁。

综上,异步操作是非堵塞的,调用 new_session->Start() 后,程序并不会等待异步操作(如 async_read_some)完成。这些异步操作会在某个事件发生后(如客户端发送数据)触发注册的回调函数,但它们本身不会阻塞代码执行。

3.智能指针new_session的计数增减过程?

1)在 Server::start_accept() 中创建 new_session ,引用计数:1

std::shared_ptr<Session> new_session = std::make_shared<Session>(_ioc, this);

2)绑定到异步接受操作(同理,异步读、异步写都会绑定new_session,只有当异步读写结束之后,new_session的引用计数才会减一)引用计数:2

_acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept, this, new_session, std::placeholders::_1));

因为 std::bind 创建了一个闭包,它持有对 new_session 的副本,并将其存储起来,直到异步操作完成并调用回调函数 handle_accept

3)在 Server::handle_accept() 中

void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error) {if (!error) {new_session->Start();  // 启动会话_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));  // 插入到_sessions中}start_accept();  // 继续等待新的客户端连接
}
  • 当 handle_accept() 被调用时,new_session 被传递给该函数,作为一个局部的 std::shared_ptr。此时引用计数增加 1,变为 3
  • 调用 new_session->Start() 后,服务器将 new_session 插入到 _sessions 容器中,引用计数增加 1,变为 4
  • 此时,new_session 的引用计数如下:
    • 异步操作 async_accept 中持有一个副本。
    • 传递给 handle_accept 时有一个副本。
    • 存储在 _sessions 容器中有一个副本。

4)调用 Session::Start() 并启动异步读取操作

void Session::Start() {_socket.async_read_some(boost::asio::buffer(_data, max_length),std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}
  • 在 Start() 函数中,调用 async_read_some() 时,将 shared_from_this() 传递给回调函数 std::bind,创建了另一个 std::shared_ptr<Session>,引用计数增加 1,变为 5。
  • shared_from_this() 确保了回调函数中使用的 Session 对象不会在异步操作完成前被销毁。因为在headle_read()和haddle_write()函数中的_self_shared参数是通过 shared_from_this() 获得的 std::shared_ptr<Session>,并通过 std::bind 传递到 headle_read 回调函数中,这里的new_session引用指针并不会增加,因为_self_shared始终都只是同一个,所以在异步操作完成前session都不会被销毁,因为引用指针是通过shared_from_this增加的,在异步完成前,引用不会减一。

5)异步读取完成后调用 headle_read()

void Session::headle_read(const boost::system::error_code& error, size_t bytes_transferred,std::shared_ptr<Session> _self_shared) {if (!error) {boost::asio::async_write(_socket, boost::asio::buffer(_data, bytes_transferred),std::bind(&Session::haddle_write, this, std::placeholders::_1, _self_shared));} else {_server->ClearSession(_uuid);}
}

引用计数:仍然是 5

在 headle_read 函数中,_self_shared 是 shared_from_this() 传递过来的 std::shared_ptr<Session>,此时 new_session 的引用计数保持不变。如果读取操作成功,继续绑定 async_write 操作,并传递 std::shared_ptr<Session> 给回调函数 haddle_write。如果发生错误,调用 _server->ClearSession(_uuid),从 _sessions 中删除 new_session,引用计数减少 1,变为 4。

6)异步写入完成后调用 haddle_write()

void Session::haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared) {if (!error) {_socket.async_read_some(boost::asio::buffer(_data, max_length),std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));} else {_server->ClearSession(_uuid);}
}

引用计数:保持不变

  • 写入操作完成后,再次启动新的异步读取操作,依然使用 _self_shared 传递给 async_read_some,保持 new_session 的引用计数不变。
  • 如果写入操作失败,调用 _server->ClearSession(_uuid),从 _sessions 中移除 new_session,引用计数减少 1。

7)客户端断开连接或发生错误时

void Server::ClearSession(std::string uuid) {_sessions.erase(uuid);
}

引用计数减少:

  • 当客户端断开连接或发生错误时,ClearSession() 被调用,从 _sessions 容器中删除 new_session。这导致引用计数减少 1。
  • 此时,如果没有其他异步操作绑定 new_session,并且所有与 new_session 相关的回调函数都已经执行完毕,那么引用计数最终会减少到 0。
  • 当所有的异步操作完成,new_session 不再被任何地方持有,引用计数归零,此时 std::shared_ptr 自动销毁 Session 对象,释放其占用的内存。

4.为什么“当 handle_accept() 被调用时,new_session 被传递给该函数,作为一个局部的 std::shared_ptr。此时引用计数增加 1,变为 3”但是“在 headle_read 函数中,_self_shared 是 shared_from_this() 传递过来的 std::shared_ptr<Session>,此时 new_session 的引用计数保持不变。”new_session 同样被传递给该函数,但为什么这里的计数不加一?

第一种情况:handle_accept 中传递 new_session

void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error) {if (!error) {new_session->Start();  // 启动会话_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));  // 插入到_sessions中}start_accept();  // 继续等待新的客户端连接
}
  • 传递过程:handle_accept 函数接收的是 std::shared_ptr<Session>,也就是说,在调用 handle_accept 时,会将 new_session 作为参数按值传递
  • 为什么计数增加?:在按值传递时,new_session 被拷贝了一份,这份拷贝的 std::shared_ptr 在函数内部存在,它持有同一个 Session 对象。这种拷贝操作会增加引用计数,因此当 handle_accept 被调用时,new_session 的引用计数增加 1。
  • 总结:因为 new_session 是按值传递的,std::shared_ptr 的拷贝操作导致引用计数增加。

第二种情况:headle_read 中传递 _self_shared

void Session::headle_read(const boost::system::error_code& error, size_t bytes_transferred,std::shared_ptr<Session> _self_shared) {if (!error) {}
}
  • 传递过程:_self_shared 是通过 shared_from_this() 获得的 std::shared_ptr<Session>,并通过 std::bind 传递到 headle_read 回调函数中。
  • 为什么计数不增加?:这里的关键在于 _self_shared 并不是按值传递一个新的 std::shared_ptr,而是通过 std::bind 传递的已经存在的 std::shared_ptr(即 shared_from_this() 返回的那个 shared_ptr)。
  • 传递的是同一个 std::shared_ptr 对象:由于 std::bind 是按引用或按值传递该 shared_ptr 对象(取决于如何绑定),回调函数接收到的是一个已经存在的 std::shared_ptr,而不是新拷贝的 std::shared_ptr。因此,这个传递操作并不会创建新的 shared_ptr 对象,因此不会增加引用计数。_self_shared 是通过 参数传递 进入 headle_read 函数的,而不是像 new_session 那样通过 std::bind 进行传递(按值传递)。因此,在这个函数的参数传递过程中,不再进一步增加引用计数。换句话说,_self_shared 的引用计数已经通过之前的 shared_from_this() 加一,所以这里不再加一。
  • 总结:在 headle_read 中,传递的是 shared_from_this() 已经创建的 std::shared_ptr 的引用,传递过程中没有创建新的 shared_ptr 对象,因此引用计数保持不变

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

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

相关文章

Java 类加载委托机制

1. 引言 Java 中的类加载机制是 JVM 的核心之一&#xff0c;它通过将字节码加载到内存中&#xff0c;使得程序能够正常运行。而在这个过程中&#xff0c;Java 引入了一种独特的“类加载委托机制”&#xff08;也称双亲委派机制&#xff09;&#xff0c;以确保类加载的稳定性与…

【ZYNQ 开发】填坑!双核数据采集系统LWIP TCP发送,运行一段时间不再发送且无法ping通的问题解决

问题描述 之所以说是填坑&#xff0c;是因为之前写了一篇关于这个双核数据采集系统的调试记录&#xff0c;问题的具体表现是系统会在运行一段时间后&#xff08;随机不定时&#xff0c;长了可能将近两小时&#xff0c;短则几分钟&#xff09;&#xff0c;突然间就不向电脑发送数…

windows下安装rabbitMQ并开通管理界面和允许远程访问

如题&#xff0c;在windows下安装一个rabbitMQ server&#xff1b;然后用浏览器访问其管理界面&#xff1b;由于rabbitMQ的默认账号guest默认只能本机访问&#xff0c;因此需要设置允许其他机器远程访问。这跟mysql的思路很像&#xff0c;默认只能本地访问&#xff0c;要远程访…

Web和UE5像素流送、通信教程

一、web端配置 首先打开Github地址&#xff1a;https://github.com/EpicGamesExt/PixelStreamingInfrastructure 找到自己虚幻引擎对应版本的项目并下载下来&#xff0c;我这里用的是5.3。 打开项目找到PixelStreamingInfrastructure-master > Frontend > implementat…

Redis介绍及整合Spring

目录 Redis介绍 Spring与Redis集成 Redis介绍 Redis是内存数据库&#xff0c;Key-value型NOSQL数据库&#xff0c;项目上经常将一些不经常变化并且反复查询的数据放入Redis缓存&#xff0c;由于数据放在内存中&#xff0c;所以查询、维护的速度远远快于硬盘方式操作数据&#…

启动服务并登录MySQL9数据库

【图书推荐】《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;》-CSDN博客 《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;&#xff08;数据库技术丛书&#xff09;》(王英英)【摘要 书评 试读】- 京东图书 (jd.com) Windows平台下安装与配置MyS…

解密自闭症康复方法:帮助孩子走出困境

在人生的长河中&#xff0c;每一个孩子都是家庭的希望&#xff0c;是父母心中最柔软的那一部分。然而&#xff0c;当自闭症这一沉重的阴霾笼罩在某些家庭之上时&#xff0c;那份希望似乎变得遥不可及。但请相信&#xff0c;无论前路多么艰难&#xff0c;总有一束光&#xff0c;…

Llama3.2开源:Meta发布1B和3B端侧模型、11B和90B多模态模型

最近这一两周不少互联网公司都已经开始秋招提前批面试了。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球友…

用python裁切PDF文件中的图片

想把所有pdf文件的图片下边裁切掉一块&#xff0c;用Adobe Acrobat只能一页页处理&#xff0c;于是想到了用python进行批处理。 代码如下&#xff1a; """ Title: cutPdfImage Author: JackieZheng Date: 2024-09-26 20:51:24 LastEditTime: 2024-09-26 22:14…

大数据毕业设计选题推荐-民族服饰数据分析系统-Python数据可视化-Hive-Hadoop-Spark

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

栏目二:Echart绘制动态折线图+柱状图

栏目二&#xff1a;Echart绘制动态折线图柱状图 配置了一个ECharts图表&#xff0c;该图表集成了数据区域缩放、双Y轴显示及多种图表类型&#xff08;折线图、柱状图、象形柱图&#xff09;。图表通过X轴数据展示&#xff0c;支持平滑折线展示比率数据并自动添加百分比标识&…

Docker-2.如何保存数据退出

在使用Docker时&#xff0c;我们常常需要修改容器中的文件&#xff0c;并且希望在容器重启后这些修改能够得到保留。 0.简介 使用Docker时有一个需要注意的问题&#xff1a;当你修改了容器中的文件后&#xff0c;重启容器后这些修改将会被重置&#xff0c;深入研究这个问题。 …

Java类设计模式

1、单例模式 核心&#xff1a;保证一个类只有一个对象&#xff0c;并且提供一个访问该实例的全局访问点 五种单例模式&#xff1a;主要&#xff1a;饿汉式&#xff1a;线程安全&#xff0c;调用效率高&#xff0c;不能延时加载懒汉式&#xff1a;线程安全&#xff0c;调用效率…

从零开始Ubuntu24.04上Docker构建自动化部署(三)Docker安装Nginx

安装nginx sudo docker pull nginx 启动nginx 宿主机创建目录 sudo mkdir -p /home/nginx/{conf,conf.d,html,logs} 先启动nginx sudo docker run -d --name mynginx -p 80:80 nginx 宿主机上拷贝docker上nginx服务上文件到本地目录 sudo docker cp mynginx:/etc/nginx/ngin…

企业间图文档发放:如何在保障安全的同时提升效率?

不管是大型企业&#xff0c;还是小型创业公司&#xff0c;不论企业规模大小&#xff0c;每天都会有大量的图文档发放&#xff0c;对内传输协作和对外发送使用&#xff0c;数据的生产也是企业业务生产力的体现之一。 伴随着业务范围的不断扩大&#xff0c;企业与客户、合作伙伴之…

五子棋双人对战项目(2)——登录模块

目录 一、数据库模块 1、创建数据库 2、使用MyBatis连接并操作数据库 编写后端数据库代码 二、约定前后端交互接口 三、后端代码编写 文件路径如下&#xff1a; UserAPI&#xff1a; UserMapper&#xff1a; 四、前端代码 登录页面 login.html&#xff1a; 注册页面…

鸿蒙harmonyos next flutter通信之EventChannel获取ohos系统时间

建立通道 flutter代码&#xff1a; EventChannel eventChannel EventChannel("com.xmg.eventChannel"); ohos代码&#xff1a; //定义eventChannelprivate eventChannel: EventChannel | null null//定义eventSinkprivate eventSink: EventSink | null null//建…

SQL常用语法

SQL&#xff08;Structured Query Language&#xff09;是一种用于存储、操作和检索数据库中数据的标准编程语言。以下是一些常用的 SQL 语法&#xff1a; 数据库操作 创建数据库&#xff1a;CREATE DATABASE database_name;删除数据库&#xff1a;DROP DATABASE database_name…

linux dbus介绍,彻底懂linux bluez dbus

零. 前言 由于Bluez的介绍文档有限,以及对Linux 系统/驱动概念、D-Bus 通信和蓝牙协议都有要求,加上网络上其实没有一个完整的介绍Bluez系列的文档,所以不管是蓝牙初学者还是蓝牙从业人员,都有不小的难度,学习曲线也相对较陡,所以我有了这个想法,专门对Bluez做一个系统…

什么是大语言模型的上下文窗口

在大语言模型的使用中&#xff0c;“支持 32k 上下文”的意思是该模型可以处理并记住最多 32,000 个标记&#xff08;tokens&#xff09;的输入。这些标记通常是文本的最小组成部分&#xff0c;可以是一个字符、一个单词&#xff0c;或一个词组的部分。大多数自然语言处理模型并…