网络编程(12)——完善粘包处理操作(id字段)

news2024/11/17 4:31:32

十二、day12

之前的粘包处理是基于消息头包含的消息体长度进行对应的切包操作,但并不完整。一般来说,消息头仅包含数据域的长度,但是如果要进行逻辑处理,就需要传递一个id字段表示要处理的消息id,当然可以不在包头传id字段,将id序列化到消息体也是可以的,但是我们为了便于处理也便于回调逻辑层对应的函数,最好是将id写入包头。

之前我们设计的消息结构是这样的

而本节需要加上id字段

在此之前,先完整的复习一下基于boost::asio实现的服务器逻辑层结构

1. 服务器架构设计

1)asio底层通信

前面的asio底层通信过程如下图所示

Asio底层的通信过程

1)首先,在应用层调用async_read时,相当于在io_context中注册了一个读事件,表示程序希望在指定socket上进行异步读取操作,并提供一个读回调函数以在读取完成后做相应的处理;

2)io_context用于管理所有异步操作和相应的回调函数,且当async_read被调用时,asio会将该socket、相应的读事件和回调函数注册到系统内部的模型中(数据结构);当io_context启动时,即io_context.run时,asio根据系统使用对应的模型管理这些事件(windows是iocp,linux是epoll);

3)模型进入一个死循环,会监听所有注册的socket,并监测其状态(可读?可写?),如果socket的状态发生变化(事件被触发),模型将该事件放入就绪事件队列中;

4)io_context::run 在轮询就绪事件队列时,会依次调用每个就绪事件的回调函数(已经放在就绪事件队列中),每个回调函数都包含了处理读操作的逻辑,比如读取数据、处理错误等。

2)逻辑层结构

而服务器架构除了上面的内容之外,一般还有一个逻辑层

一般在解析完对端发送的数据之后,还要对该请求做更进一步地处理,比如根据不同的消息id执行不同的逻辑层函数或不同的操作,比如读数据库、写数据库,还比如游戏中可能需要给玩家叠加不同的buff、增加积分等等,这些都需要交给逻辑层处理,而不仅仅是把消息发给对端。

服务器架构

上图所示的是一个完成的服务器架构,一般需要将逻辑层独立出来,因为如果在解析完对端数据后需要执行一些复杂的操作,比如玩家需要叠加各自buff或者技能,此时可能会耗时1s甚至更多,如果没有独立的逻辑层进行操作,那么系统会一直停留在执行回调函数那一步,造成阻塞,直至操作结束。

而逻辑层是独立的,回调函数只需将数据投递给逻辑队列(回调函数将数据放入队列中之后系统会运行下一步,便不会被阻塞),逻辑系统会自动从队列中取数据并做相应操作,如果需要在执行完操作之后做相应回复,那么逻辑系统会调用写事件并注册写回调给asio网络层,网络层就是asio底层通信的网络层步骤。

以上操作是基于单线程,如果是在多线程的情况下,阻塞的情况会不会好一些?

asio的多线程有两种模式。

1)第一种模式是启动n个线程,每个线程负责一个io_context,每一个io_context负责一部分的socket。比如现在有两个io_context,一个是负责socket的id为奇数的io_context,一个是负责socket的id为偶数的io_context,但同样会造成阻塞的情况。因为不管是多线程还是单线程,只要在线程中有一个io_context中运行,那么它负责的那部分回调函数的处理操作如果比较复杂时,仍会造成阻塞的情况。

2)第二种模式是一个io_context跑在多个线程中,即多个线程共享一个io_context。这种模式下不会造成之前的那种阻塞情况,因为在就绪事件队列中的事件不是一个线程处理了,而是不同的线程共享一个就绪事件队列,不同线程会触发执行不同的回调函数,即使某个回调处理的比较慢,但由于其他事件被不同线程处理了,系统并不需要阻塞等待该回调处理完成之后在执行处理其他回调。

虽然模式二办法会解决系统阻塞、超时的问题,但在现实中需要有一个逻辑层独立于网络层和应用层,这样可以极大地提高网络线程的收发能力,用多线程的方式管理网络层。

2. 完善粘包处理操作

之前的消息结构并不完善,缺少一个消息id,本节进行代码的相应改进。

首先,之前的消息节点被收发共用,只不过收数据用的是第一种构造函数,发数据用的是第二种构造函数。为了减少耦合和歧义,需要重新设计消息节点。

1) 消息节点

重新构建一个MsgNode类,并派生出RecvNode 和SendNode

  • MsgNode 表示消息节点的基类,头部的消息用该结构存储

  • RecvNode 表示接收消息的节点

  • SendNode 表示发送消息的节点

#pragma once
#include <iostream>
#include <string>
#include <boost/asio.hpp>

using std::cout;
using std::cin;
using std::endl;

class MsgNode
{
public:
    short _cur_len;
    short _total_len;
    char* _msg;

    MsgNode(short max_len) :_total_len(max_len), _cur_len(0) {
        _msg = new char[_total_len + 1](); // 加()会将分配内存的每个元素初始化为0,不加不会初始化
        _msg[_total_len] = '\0';
    }
    ~MsgNode() {
        std::cout << "destruct MsgNode" << endl;
        delete[] _msg;
    }
    void Clear() {
        ::memset(_msg, 0, _total_len);
        _cur_len = 0;
    }
};
// 构造收节点
class RecvNode :public MsgNode {
private:
    short _msg_id;
public:
    RecvNode(short max_len, short msg_id);
};
// 构造发节点
class SendNode :public MsgNode {
private:
    short _msg_id;
public:
    SendNode(const char* msg, short max_len, short msg_id);
};

具体实现为:

#include "MsgNode.h"
#include "Const.h"

RecvNode::RecvNode(short max_len, short msg_id) :MsgNode(max_len),
_msg_id(msg_id) {}

// 发送的数据首地址、数据长度、消息id,发送节点总长度为消息体长度+头节点长度
SendNode::SendNode(const char* msg, short max_len, short msg_id) : MsgNode(max_len + HEAD_TOTAL_LEN)
, _msg_id(msg_id) {
    // 将消息id转换为网络序,并存储至至发送节点内
    short msg_id_host = boost::asio::detail::socket_ops::host_to_network_short(msg_id);
    memcpy(_msg, &msg_id_host, HEAD_ID_LEN);
    // 将消息体长度转换为网络序,并存储至至发送节点内
    short max_len_host = boost::asio::detail::socket_ops::host_to_network_short(max_len);
    memcpy(_msg + HEAD_ID_LEN, &max_len_host, HEAD_DATA_LEN);
    // 将消息内容存储至发送节点内
    memcpy(_msg + HEAD_ID_LEN + HEAD_DATA_LEN, msg, max_len);
}

Const.h 定义为

#pragma once
const size_t MAX_LENGTH = 1024 * 2;
const short MAX_RECVQUE = 10000;
const short MAX_SENDQUE = 1000;
const size_t HEAD_TOTAL_LEN = 4;
const size_t HEAD_ID_LEN = 2;
const size_t HEAD_DATA_LEN = 2;

构建SendNode节点时,需要将消息id和消息长度转换为网络序,然后写入数据域_msg ,前2字节存储id,id后为消息长度,偏移4字节后为消息体内容。

2)Session类

Session类和前面差不多,不过需要把收发的逻辑做相应的修改

首先,队列_send_que、消息头结构、消息体结构需要重新声明,分别使用SendNode,RecvNode,MsgNode作为元素类型。

	std::queue<std::shared_ptr<SendNode> > _send_que;
	std::mutex _send_lock;
	std::shared_ptr<RecvNode> _recv_msg_node; // 收到的消息结构
	bool _b_head_parse; // 表示是否处理完头部信息
	std::shared_ptr<MsgNode> _recv_head_node; // 收到的头部结构

Session的构造函数也需要做相应变化,消息头结构的大小更改为4字节,包括id和消息体长度

	CSession(boost::asio::io_context& ioc, CServer* server) : _socket(ioc), _server(server), _b_close(false),
		_b_head_parse(false) {
		// random_generator是函数对象,加()就是函数,再加一个()就是调用该函数
		boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
		_uuid = boost::uuids::to_string(a_uuid);
		_recv_head_node = std::make_shared<MsgNode>(HEAD_TOTAL_LEN);
	}

重新定义Send函数,两个Send的重载都需要重新定义

参数列表增加msgid,构造发送节点时需输入三个参数msg, max_length, msgid(发送内容,内容长度,消息id)

void CSession::Send(char* msg, int max_length, short msgid) {
	bool pending = false; // 发送标志,true时有未完成的发送操作,false为空
	// 使用lock_guard锁住_send_lock,确保_send_lock(发送队列)访问的线程安全的
	// 锁的存在确保了多个线程不会同时修改发送队列
	std::lock_guard<std::mutex> lock(_send_lock);
	int send_que_size = _send_que.size();
	if (send_que_size > MAX_SENDQUE) {
		cout << "session: " << _uuid << " send que fulled, size is " << MAX_SENDQUE << endl;
		return;
	}

	// 判断队列是否有未完成的发送操作
	if (_send_que.size() > 0) {
		pending = true;
	}
	_send_que.push(std::make_shared<SendNode>(msg, max_length, msgid)); // 将发送消息存储至队列
	if (pending) { // 如果有未完成的发送,直接返回
		return;
	}
	// 异步发送
	auto& msgnode = _send_que.front();
	boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_msg, msgnode->_total_len),
		std::bind(&CSession::haddle_write, this, std::placeholders::_1, shared_from_this()));
} // 当'}'结束后,_send_lock解锁,发送队列解锁


void CSession::Send(std::string msg, short msgid) {
	bool pending = false; // 发送标志,true时有未完成的发送操作,false为空
	// 使用lock_guard锁住_send_lock,确保_send_lock(发送队列)访问的线程安全的
	// 锁的存在确保了多个线程不会同时修改发送队列
	std::lock_guard<std::mutex> lock(_send_lock);
	int send_que_size = _send_que.size();
	if (send_que_size > MAX_SENDQUE) {
		cout << "session: " << _uuid << " send que fulled, size is " << MAX_SENDQUE << endl;
		return;
	}

	// 判断队列是否有未完成的发送操作
	if (_send_que.size() > 0) {
		pending = true;
	}
	_send_que.push(std::make_shared<SendNode>(msg.c_str(), msg.length(),msgid)); // 将发送消息存储至队列
	if (pending) { // 如果有未完成的发送,直接返回
		return;
	}
	// 异步发送
	auto& msgnode = _send_que.front();
	boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_msg, msgnode->_total_len),
		std::bind(&CSession::haddle_write, this, std::placeholders::_1, shared_from_this()));
} // 当'}'结束后,_send_lock解锁,发送队列解锁

读回调也需更改,在文章10中haddle_write函数的基础上做修改,可参考该文章

https://zhuanlan.zhihu.com/p/722233898

void CSession::HandleRead(const boost::system::error_code& error, size_t bytes_transferred,
	std::shared_ptr<CSession> _self_shared) {
	if (!error) {

		// 打印缓存区的数据并将该线程暂停2s
		//PrintRecvData(_data, bytes_transferred);
		//std::chrono::milliseconds dura(2000);
		//std::this_thread::sleep_for(dura);

		// 每触发一次handale_read,它会返回实际读取的字节数bytes_transferred,copy_len表示已处理的长度,每处理一字节,copy_len便加一
		int copy_len = 0; // 已经处理的字符数
		while (bytes_transferred > 0) { // 只要读取到数据就对其处理
			if (!_b_head_parse) { // 判断消息头部是否已处理,_b_head_parse默认为false
				// 异步读取到的字节数 + 已接收到的头部长度 < 头部总长度
				if (bytes_transferred + _recv_head_node->_cur_len < HEAD_TOTAL_LEN) { // 收到的数据长度小于头部长度,说明头部还未全部读取
					// 如果未完全接收消息头,则将接收到的数据复制到头部缓冲区
					// _recv_head_node->_msg,更新当前头部的接收长度,并继续异步读取剩余数据。
					memcpy(_recv_head_node->_msg + _recv_head_node->_cur_len, _data + copy_len, bytes_transferred);
					_recv_head_node->_cur_len += bytes_transferred;
					// 缓冲区清零,无需更新copy_len追踪已处理的字符数,因为之前读取的数据已经全部写入头部节点,下一个
					// 读入的消息从头开始(copy_len=0)往头节点写
					::memset(_data, 0, MAX_LENGTH);
					// 继续读消息
					_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::headle_read, this,
						std::placeholders::_1, std::placeholders::_2, _self_shared));
					return;
				}

				// 如果接收到的数据量足够处理消息头部,则计算头部剩余的未接收字节,
				// 并将其从 _data 缓冲区复制到头部消息缓冲区 _recv_head_node->_msg
				int head_remain = HEAD_TOTAL_LEN - _recv_head_node->_cur_len; // 头部剩余未复制的长度
				// 填充头部节点
				memcpy(_recv_head_node->_msg + _recv_head_node->_cur_len, _data + copy_len, head_remain);
				copy_len += head_remain; // 更新已处理的data长度
				bytes_transferred -= head_remain; // 更新剩余未处理的长度

				short msg_id = 0; // 获取消息id
				memcpy(&msg_id, _recv_head_node->_msg, HEAD_ID_LEN);
				//网络字节序转化为本地字节序
				msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);
				cout << "msg_id is " << msg_id << endl;
				// 判断id是否合法
				if (msg_id > MAX_LENGTH) {
					std::cout << "invaild msg_id is " << msg_id << endl;
					_server->ClearSession(_uuid);
					return;
				}
				
				short msg_len = 0; // 获取头部数据(消息长度)
				memcpy(&msg_len, _recv_head_node->_msg + HEAD_ID_LEN, HEAD_DATA_LEN);
				//网络字节序转化为本地字节序
				msg_len = boost::asio::detail::socket_ops::network_to_host_short(msg_len);
				cout << "msg_len is " << msg_len << endl;
				
				if (msg_len > MAX_LENGTH) { // 判断头部长度是否非法
					std::cout << "invalid data length is " << msg_len << endl;
					_server->ClearSession(_uuid);
					return;
				}

				_recv_msg_node = std::make_shared<RecvNode>(msg_len, msg_id); // 已知数据长度msg_len,构建消息内容载体
				//消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里
				if (bytes_transferred < msg_len) {
					memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);
					_recv_msg_node->_cur_len += bytes_transferred;
					// copy_len不用更新,缓冲区会清零,下一个读入data的数据从头开始写入,copy_len也会被初始化为0
					::memset(_data, 0, MAX_LENGTH);
					_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
						std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
					
					_b_head_parse = true; //头部处理完成
					return;
				}

				// 接收的长度多于消息内容长度
				memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, msg_len);
				_recv_msg_node->_cur_len += msg_len;
				copy_len += msg_len;
				bytes_transferred -= msg_len;
				_recv_msg_node->_msg[_recv_msg_node->_total_len] = '\0';
				// cout << "receive data is " << _recv_msg_node->_msg << endl;

				// protobuf序列化
				//MsgData msgdata;
				//std::string receive_data;
				//msgdata.ParseFromString(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len));
				//std::cout << "receive msg id is " << msgdata.id () << " msg data is  " << msgdata.data() << endl;
				//std::string return_str = "Server has received msg, msg data is " + msgdata.data();
				//MsgData msgreturn;
				//msgreturn.set_id(msgdata.id());
				//msgreturn.set_data(return_str);
				//msgreturn.SerializeToString(&return_str);
				//Send(return_str);

				// jsoncpp序列化
				Json::Reader reader;
				Json::Value root;
				reader.parse(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len), root);
				std::cout << "recevie msg id  is " << root["id"].asInt() << " msg data is "
					<< root["data"].asString() << endl;
				root["data"] = "Server has received msg, msg data is " + root["data"].asString();
				std::string return_str = root.toStyledString();
				Send(return_str, root["id"].asInt());

				//Send(_recv_msg_node->_msg, _recv_msg_node->_total_len); // 回传
				// 清理已处理的头部消息并重置,准备解析下一条消息
				_b_head_parse = false;
				_recv_head_node->Clear();

				// 如果当前数据已经全部处理完,重置缓冲区 _data,并继续异步读取新的数据
				if (bytes_transferred <= 0) {
					::memset(_data, 0, MAX_LENGTH);
					_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
						std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
					return;
				}

				continue; // 异步读取的消息未处理完,继续填充头节点乃至新的消息节点
			}

			//已经处理完头部,处理上次未接受完的消息数据
			int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;
			if (bytes_transferred < remain_msg) { //接收的数据仍不足剩余未处理的
				memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);
				_recv_msg_node->_cur_len += bytes_transferred;
				::memset(_data, 0, MAX_LENGTH);
				_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
					std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
				return;
			}
			// 接收的数据多于剩余未处理的长度
			memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, remain_msg);
			_recv_msg_node->_cur_len += remain_msg;
			bytes_transferred -= remain_msg;
			copy_len += remain_msg;
			_recv_msg_node->_msg[_recv_msg_node->_total_len] = '\0';
			//cout << "receive data is " << _recv_msg_node->_msg << endl;

			// protobuf序列化
			//MsgData msgdata;
			//std::string receive_data;
			//msgdata.ParseFromString(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len));
			//std::cout << "receive msg id is " << msgdata.id() << " msg data is  " << msgdata.data() << endl;
			//std::string return_str = "Server has received msg, msg data is " + msgdata.data();
			//MsgData msgreturn;
			//msgreturn.set_id(msgdata.id());
			//msgreturn.set_data(return_str);
			//msgreturn.SerializeToString(&return_str);
			//Send(return_str);

			//jsoncpp序列化
			Json::Reader reader;
			Json::Value root;
			reader.parse(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len), root);
			std::cout << "recevie msg id  is " << root["id"].asInt() << " msg data is "
				<< root["data"].asString() << endl;
			root["data"] = "Server has received msg, msg data is " + root["data"].asString();
			std::string return_str = root.toStyledString();
			Send(return_str, root["id"].asInt());

			//此处可以调用Send发送测试
			//Send(_recv_msg_node->_msg, _recv_msg_node->_total_len);
			//继续轮询剩余未处理数据
			_b_head_parse = false;
			_recv_head_node->Clear();
			if (bytes_transferred <= 0) {
				::memset(_data, 0, MAX_LENGTH);
				_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
					std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
				return;
			}
			continue;
		}
	}
	else {
		std::cout << "handle read failed, error is " << error.what() << endl;
		Close();
		_server->ClearSession(_uuid);
	}
}

HandleRead函数中新增一段读取消息id的代码

首先,当消息头节点_recv_head_node填充完毕后,获取头节点中存储的消息id并转换为本地字节序,并判断id的合法性;然后,解析消息长度,并构建消息体节点_recv_msg_node,读取剩下的消息体内容

	            short msg_id = 0; // 获取消息id
				memcpy(&msg_id, _recv_head_node->_msg, HEAD_ID_LEN);
				//网络字节序转化为本地字节序
				msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);
				cout << "msg_id is " << msg_id << endl;
				// 判断id是否合法
				if (msg_id > MAX_LENGTH) {
					std::cout << "invaild msg_id is " << msg_id << endl;
					_server->ClearSession(_uuid);
					return;
				}

3)客户端

客户端也需额外收发消息id

#include <boost/asio.hpp>
#include <iostream>
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>

using namespace boost::asio::ip;
using std::cout;
using std::endl;
const int MAX_LENGTH = 1024 * 2; // 发送和接收的长度为1024 * 2字节
const int HEAD_LENGTH = 2;
const int HEAD_TOTAL = 4;

int main()
{
    try {
        boost::asio::io_context ioc; // 创建上下文服务
        // 127.0.0.1是本机的回路地址,也就是服务器和客户端在一个机器上
        tcp::endpoint remote_ep(address::from_string("127.0.0.1"), 10086); // 构造endpoint
        tcp::socket sock(ioc);
        boost::system::error_code error = boost::asio::error::host_not_found; // 错误:主机未找到
        sock.connect(remote_ep, error);
        if (error) {
            cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
            return 0;
        }

        Json::Value root;
        root["id"] = 1001;
        root["data"] = "hello world";
        std::string request = root.toStyledString();
        size_t request_length = request.length();
        char send_data[MAX_LENGTH] = { 0 };
        int msgid = 1001;
        int msgid_host = boost::asio::detail::socket_ops::host_to_network_short(msgid);
        memcpy(send_data, &msgid_host, 2);

        //转为网络字节序
        int request_host_length = boost::asio::detail::socket_ops::host_to_network_short(request_length);
        memcpy(send_data + 2, &request_host_length, 2);
        memcpy(send_data + 4, request.c_str(), request_length);
        boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 4));

        char reply_head[HEAD_TOTAL]; // 首先读取对端发送消息的总长度
        size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_TOTAL));
        msgid = 0;
        memcpy(&msgid, reply_head, HEAD_LENGTH);
        short msglen = 0; // 消息总长度
        memcpy(&msglen, reply_head + 2, HEAD_LENGTH); // 将消息总长度赋值给msglen
        //转为本地字节序
        msglen = boost::asio::detail::socket_ops::network_to_host_short(msglen);
        char msg[MAX_LENGTH] = { 0 }; // 构建消息体(不含消息总长度)
        size_t msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen));

        Json::Reader reader;
        reader.parse(std::string(msg, msg_length), root);
        std::cout << "msg id is " << root["id"] << " msg is " << root["data"] << endl;
        getchar();
    }
    catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << endl;
    }
    return 0;
}

4)测试

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

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

相关文章

【Godot4.3】简单物理模拟之圆粒子碰撞检测

概述 最近开始研究游戏物理的内容&#xff0c;研究运动、速度、加速度之类的内容。也开始模仿一些简单的粒子模拟。这些都是一些基础、简单且古老的算法&#xff0c;但是对于理解游戏内的物理模拟很有帮助。甚至你可以在js、Python或者其他程序语言中实现它们。 图形的碰撞检…

Linux操作系统中SpringGateway

1、SpringGateway简介 核心功能有三个&#xff1a; 路由&#xff1a;用于设置转发地址的 断言&#xff1a;用来判断真实应该请求什么地址 过滤器&#xff1a;可以过滤地址和处理参数 1、什么是网关 网关是后台服务的统一入口&#xff0c;类似于平时网络里提到的网关。 2…

ppt压缩有什么简单方法?压缩PPT文件的几种方法

ppt压缩有什么简单方法&#xff1f;许多用户常常面临文件过大的问题&#xff0c;尤其在需要通过电子邮件发送或上传至网络平台时&#xff0c;大文件会带来诸多麻烦。此外&#xff0c;较大的文件可能导致软件响应缓慢&#xff0c;从而影响整体的演示体验。因此&#xff0c;寻找有…

ESP8266/01s模块烧录MQTT AT固件篇

&#xff08;代码完美实现&#xff09;stm32 新版 onenet mqtt物联网(保姆级教程) 地址&#xff1a; &#xff08;代码完美实现&#xff09;stm32 新版 onenet mqtt物联网(保姆级教程)https://blog.csdn.net/Wang2869902214/article/details/142501323 乐鑫ESP8266/安信可…

Python项目Flask框架整合Mysql

一、在配置类中编写Mysql配置信息 二、实现Mysql配置类 import pymysql from config.config import MYSQL_HOST, MYSQL_USER, MYSQL_PASSWD, MYSQL_PROT, MYSQL_DB, MYSQL_CHARSETclass MysqlDB():def __init__(self, MYSQL_HOST, MYSQL_USER, MYSQL_PASSWD, MYSQL_PROT, MYS…

time命令:轻松测量Linux命令执行时间!

一、命令简介 用途&#xff1a; 用于测量 Linux 命令执行的时间&#xff0c;包括实际时间、用户 CPU 时间和系统 CPU 时间。刚开始以为是用来“看现在几点钟”的 &#x1f972;。标签&#xff1a; 实用工具&#xff0c;性能分析。 ‍ 二、命令参数 2.1 命令格式 time [选项…

COSCon'24 第九届中国开源年会议题征集正式启动

一年一度的开源盛会&#xff0c;COSCon24 第九届中国开源年会暨开源社十周年嘉年华将于2024年11月2-3日在中关村国家自主创新示范区会议中心举办。在为期2天的大会中&#xff0c;我们将为大家带来精彩纷呈的 Keynote 主题演讲&#xff08;上午&#xff09;&#xff0c;和百花齐…

【初阶数据结构】排序——选择排序

目录 前言选择排序堆排序 前言 对于常见的排序算法有以下几种&#xff1a; 下面这节我们来看选择排序算法。 选择排序 基本思想&#xff1a;   每一次从待排序的数据元素中遍历选出最大&#xff08;或最小&#xff09;的元素放在序列的起始位置&#xff0c;直到全部待排序…

2024前端技术发展概况

当前前端技术呈现出多方面的发展态势&#xff0c;以下是详细介绍&#xff1a; 前端框架不断演进&#xff1a; React&#xff1a;作为流行的前端框架之一&#xff0c;React 依旧保持着强大的生命力。它具有高效的虚拟 DOM 机制&#xff0c;能够快速更新和渲染页面&#xff0c;极…

如何创建一个docker,给它命名,且下次重新打开它

1.创建一个新的docker并同时命名 docker run -it --name one ubuntu:18.04 /bin/bash 这时候我们已经创建了一个docker,并且命名为"one" 2.关闭当前docker exit 3.这时docker已经终止了&#xff0c;我们需要使用它要重新启动 docker start one 4.现在可以重新打…

多线程篇八

多线程篇八 如笔者理解有误欢迎指正交流&#x1f338;&#x1f338;&#x1f338; 线程池 什么是线程池&#xff1f; 顾名思义&#xff0c;线程池是一个存放了很多线程的池子.既然有很多线程&#xff0c;那一定很方便调用对吧&#xff0c;有很多线程那大家一定喜欢一起玩吧&…

【计算机网络】Tcp报文的组成,Tcp是如何实现可靠传输的?

Tcp的报文组成 TCP&#xff08;传输控制协议&#xff09;是计算机网络中一种重要的传输协议&#xff0c;其报文组成包括多个字段&#xff0c;每个字段具有特定的含义。以下是TCP报文头的主要组成部分&#xff1a; 源端口号&#xff08;Source Port&#xff09;&#xff1a;占用…

c语言中例题:打印出杨辉三角

杨辉三角是一个经典的数学模型 从顶部的单个1开始&#xff0c;下面每一行的数字都是其正上方的两个数之和 每行数字左右对称&#xff0c;且由1开始逐渐变大 1 1 1 1 2 1 1 3 3 1 由以上规律可以看出arr[i][j] arr[i-1][j] ar…

Android使用RecyclerView仿美团分类界面

RecyclerView目前来说对大家可能不陌生了。由于在公司的项目中&#xff0c;我们一直用的listview和gridview。某天产品设计仿照美团的分类界面设计了一个界面&#xff0c;我发现用gridview不能实现这样的效果&#xff0c;所以就想到了RecyclerView&#xff0c;确实是一个很好的…

对话总结:Scale AI的创始人兼CEO Alex Wang

AI的三大支柱 计算:主要由大公司如NVIDIA推动。算法:顶尖实验室如OpenAI主导。数据:Scale致力于推动数据进展。前沿数据的重要性 与人类智能相比较,前沿数据是AI发展的关键。互联网数据是机器与人类合作的结果。语言模型的发展 第一阶段:原始的Transformer论文和GPT的小规…

PHP爬虫淘宝商品SKU详细信息获取指南

在电子商务领域&#xff0c;获取商品的SKU&#xff08;Stock Keeping Unit&#xff0c;库存单位&#xff09;详细信息对于商家进行库存管理、订单处理和客户服务至关重要。淘宝作为中国最大的电商平台之一&#xff0c;提供了丰富的API接口&#xff0c;使得开发者能够通过PHP爬虫…

AI在教育行业应用的启发和未来的方向

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 shelly已经给大家分享了很多AI的工具&#…

ThinkPHP一对多的关联模型运用

一、序言 最近在写ThinkPHP关联模型的时候一些用法总忘&#xff0c;我就想通过写博客的方式复习和整理下一些用法。 具体版本&#xff1a; topthink/framework&#xff1a;6.1.4topthink/think-orm&#xff1a;2.0.61 二、实例应用 1、一对多的关联 本文案例&#xff1a;一个用…

MySQL - 单表查询

DQL (数据查询语言)是用来查询数据库表中的记录的操作。在实际的业务系统中&#xff0c;查询操作的频率远远高于增删改。常见的查询操作包括条件查询、排序、分组等。 1. DQL 语法 SELECT 字段列表 FROM 表名列表 [WHERE 条件列表] [GROUP BY 分组字段] [HAVING 分组后条件]…

玩转图像处理:Python与OpenCV实现高效绿幕背景替换

文章目录 前言色度抠图技术&#xff08;Chroma Keying&#xff09;基本原理 数据准备代码实现性能分析代码优化优化后的速度 前言 现阶段绿幕抠图有很多种方式&#xff0c;比如色度抠图&#xff08;Chroma Keying&#xff09;、亮度抠图&#xff08;Luma Keying&#xff09;、色…