C++ 网络编程学习三

C++ 网络编程学习三

    • 用智能指针延长session的生命周期
    • 处理粘包问题


用智能指针延长session的生命周期

问题:

  • 客户端断开后:会触发服务器对应session的写或读事件,由于是异步编程,需要在回调中对读写事件进行处理。
  • 客户端断开, 则应该析构掉该session。但是此时该session在asio底层回调队列中可能还有很多读写函数对象在排队等着执行 。 如果在某个读写回调对象把这个session析构掉了,那之后执行的读写回调函数可能会再次析构这个session。
  • 所以我们需要保证, 在该session对应asio底层回调队列中,还存在将要执行的读写回调函数时,该session不被析构。通过智能指针来实现伪闭包,延长session的生命周期。
  • 智能指针传给函数对象,函数对象不释放,智能指针也就不会被释放掉。

  • 把智能指针传递给session用的回调函数,函数内部再使用智能指针,这个时候智能指针就不被释放。

  • 假如包含智能指针的函数没有调用怎么办?用lambda表达式和bind强制将智能指针中的shared_ptr加1。

  • 构造一个伪闭包:

    • 利用智能指针被复制或使用引用计数加一的原理保证内存不被回收
    • bind操作可以将值绑定在一个函数对象上生成新的函数对象,如果将智能指针作为参数绑定给函数对象,那么智能指针就以值的方式被新函数对象使用,那么智能指针的生命周期将和新生成的函数对象一致,从而达到延长生命的效果。
// 包含智能指针的Server类。
class CServer
{
public:
    // 构造函数
    CServer(boost::asio::io_context& io_context, short port);
    void ClearSession(std::string uuid);
private:
    void HandleAccept(std::shared_ptr<CSession>, const boost::system::error_code& error);
    void StartAccept();
    boost::asio::io_context& _io_context;// 上下文
    short _port;// 端口
    tcp::acceptor _acceptor;
    // 通过智能指针方式管理Session类,将acceptor接收的连接保存在Session类型的智能指针里。
    // 在Server类中添加成员变量,该变量为一个map类型,key为Session的uid,value为该Session的智能指针。
    std::map<std::string, std::shared_ptr<CSession>> _sessions;
    // 通过Server中的_sessions这个map管理链接,可以增加Session智能指针的引用计数,只有当Session从这个map中移除后,Session才会被释放。
    
};

class CSession :public std::enable_shared_from_this<CSession> {
public:
    // 上下文初始化CSession,socket绑定上下文
    CSession(boost::asio::io_context& io_context, CServer* server);
    tcp::socket& GetSocket() { return _socket; }
    std::string& GetUuid() { return _uuid; }
    void Start();
    void Send(char* msg, int max_length);
private:
    enum { MAX_LENGTH = 1024 };
    void HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> _self_shared);
    void HandleWrite(const boost::system::error_code& error, std::shared_ptr<CSession> _self_shared);
    tcp::socket _socket;
    std::string _uuid;
    char _data[MAX_LENGTH];
    CServer* _server;
    std::queue<std::shared_ptr<MsgNode> > _send_que;
    std::mutex _send_lock;
    
};
// CServer类
CServer::CServer(boost::asio::io_context& io_context, short port): _io_context(io_context), _acceptor(io_context, tcp::endpoint(tcp::v4(), port)) {
	cout << "Server start success, on port: " << port << endl;
	StartAccept();
}

void CServer::StartAccept() {
	// new_session虽然是一个局部变量,但是通过智能指针和bind操作,将new_session作为数值传递给bind函数。
	// bind函数返回的函数对象内部引用了该new_session,所以引用计数加1,这样保证了new_session不会被释放。
	std::shared_ptr<CSession> new_session = make_shared<CSession>(_io_context, this);
	// placeholders::_1 占位符的作用是给HandleAccept函数一个错误码关键字。
	_acceptor.async_accept(new_session->GetSocket(), std::bind(&CServer::HandleAccept, this, new_session, placeholders::_1));
}

void CServer::HandleAccept(std::shared_ptr<CSession> new_session, const boost::system::error_code& error) {
	if (!error) {
		new_session->Start();
		// 在接收连接的逻辑里将Session放入map
		_sessions.insert(make_pair(new_session->GetUuid(), new_session));// make_pair是圆括号,不是尖括号
	}
	else {
		cout << "session accept failed, error is " << error.what() << endl;
	}
	StartAccept();// 继续接收连接就是了
}

// 将session从map中移除,当其引用计数为0时,自动释放
void CServer::ClearSession(std::string uuid) {
	_sessions.erase(uuid);
}


// CSession类
// 构造函数
CSession::CSession(boost::asio::io_context& ioc, CServer* server) :_socket(ioc), _server(server) {
	boost::uuids::uuid  a_uuid = boost::uuids::random_generator()(); //boost提供的生成唯一id的函数
	_uuid = boost::uuids::to_string(a_uuid);// 将随机数转成string
}

void CSession::Start() {
	memset(_data, 0, MAX_LENGTH);
	// 数据读到_data中,触发HandleRead回调,注意不能再定义一个智能指针。
	// shared_from_this()函数可以还用当前的智能指针。
	_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
		std::bind(&CSession::HandleRead,this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}


void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> _self_shared) {
	if (!error) {
		cout << "read data is " << _data << endl;
		//发送数据
		Send(_data, bytes_transferred);
		// 继续read,重复调用HandleRead
		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, _self_shared));
	}
	else {
		std::cout << "handle write failed, error is " << error.what() << endl;
		_server->ClearSession(_uuid);// 杀死session
	}
}


void CSession::HandleWrite(const boost::system::error_code& error, std::shared_ptr<CSession> _self_shared) {
	if (!error) {
		// 写数据的时候上锁
		std::lock_guard<std::mutex> lock(_send_lock);
		// 调用HandleWrite,说明肯定已经发送完一个数据,这个时候弹出一下,后面只要发送队列不为空,就一直发送。
		_send_que.pop();
		if (!_send_que.empty()) {
			auto& msgnode = _send_que.front();
			boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_msg, msgnode->_total_len),
				std::bind(&CSession::HandleWrite, this, std::placeholders::_1, _self_shared));
		}
	}
	else {
		std::cout << "handle write failed, error is " << error.what() << endl;
		_server->ClearSession(_uuid);// 杀死session
	}
}

// 实现发送接口
void CSession::Send(char* msg, int max_length) {
	bool pending = false; // pending为true表示上一次数据没有发完。
	std::lock_guard<std::mutex> lock(_send_lock);
	if (_send_que.size() > 0) {
		pending = true;
	}
	_send_que.push(make_shared<MsgNode>(msg, max_length)); // 队列里有数据,就不发送了,让队列里面的回调函数发送就行了。
	if (pending) {
		return;
	}
	boost::asio::async_write(_socket, boost::asio::buffer(msg, max_length),
		std::bind(&CSession::HandleWrite,this,std::placeholders::_1,shared_from_this()));
}

处理粘包问题

粘包问题:当客户端发送多个数据包给服务器时,服务器底层的tcp接收缓冲区收到的数据为粘连在一起的,是服务器的问题,不是客户端的问题。
客户端发送: hello world! hello world!
服务器接收:hello world! hello world!
客户端给服务器发送了两个hello world! 服务其TCP缓冲区接收了两次,但是第一次接收的数据粘包了。

粘包原因:TCP发送数据的时候,数据逻辑性出了问题。

  • TCP底层通信是面向字节流的,TCP只保证发送数据的准确性和顺序性,字节流以字节为单位。
  • 客户端每次发送N个字节给服务端,N取决于当前客户端的发送缓冲区是否有数据。比如发送缓冲区总大小为10字节,当前有5字节未发送完,那么此时只有5个字节的空闲时间。
  • 此时调用接口发送hello world!, 就只能发送hello给服务器,那么服务器这次接收到的数据很可能就是连着其他数据的hello,下次才能收到world!。

还有其他产生粘包问题的原因:

  1. 客户端的发送频率远高于服务器的接收频率,服务器接收不过来,就会导致数据在服务器的tcp接收缓冲区滞留形成粘连。
  2. tcp底层的安全和效率机制不允许字节数特别少的小包发送频率过高,tcp会在底层累计数据长度到一定大小才一起发送。
  • 处理粘包的方法主要采用应用层定义收发包格式的方式,这个过程俗称切包处理。用消息id+消息长度+消息内容的tlv协议去切割数据包。

在代码中对粘包进行处理:

  • 定义新的数据结构体,数据包含两部分:消息长度+消息内容,用额外的2字节去存储当前消息的长度。
  • 接收消息数据的CSession类也需要更新。
  • 数据初始化的时候,就要初始化头部信息。

完善加上粘包处理后的逻辑:

  • 头部未解析:
    • 收到的数据不是满足头部的大小:未处理的数据加上头部当前缓存的数据,如果小于2字节,就说明头部数据没有接收完。
    • 收到的数据比头部多:头部的信息已经接收完,取出头部信息。定义数据节点,取出数据信息。
      • 若数据节点的长度< 头部信息长度:数据还没收完。将数据放到接收节点中,更新信息。
      • 若数据节点的长度大于等于头部信息长度:取出首包全部数据,头部节点清楚一下,轮询切包。
  • 头部已解析:已经处理完头部,消息体没有接收完。
    • 消息体还没有接收全:当前数据拷贝到消息节点里,继续监听对方发送。
    • 消息体长度够了,拷贝信息到消息节点,更新变量,把剩下的数据轮询切包。
      在这里插入图片描述
void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> shared_self) {
	if (!error) {
		
		/** copy_len 已经移动的字符数:
			调用一次HandleRead:会返回总共收到的字节数,会从零开始处理到bytes_transferred这么大,中间会有一些其他的处理,
			copy_len表示处理到哪里了。
		*/ 
		int copy_len = 0; //copy_len表示处理到哪里了
		while (bytes_transferred > 0) {
			if (!_b_head_parse) { // 最开始的时候头部肯定还没有被解析
				// 先判断收到的数据是不是满足头部的大小:未处理的数据加上头部当前缓存的数据,如果小于2字节,就说明头部数据没有接收完
				if (bytes_transferred + _recv_head_node->_cur_len < HEAD_LENGTH) {
					// 将数据全部拷贝到头部节点
					memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, bytes_transferred);
					_recv_head_node->_cur_len += 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, shared_self));
					return;
				}
				else {
					// 收到的数据比头部多
					// 头部剩余未复制的长度
					int head_remain = HEAD_LENGTH - _recv_head_node->_cur_len;
					memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, head_remain);
					// 更新已处理的data长度copy_len 和 剩余未处理的长度bytes_transferred
					copy_len += head_remain;
					bytes_transferred -= head_remain;
					// 获取头部数据 打印数据长度
					short data_len = 0;
					memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);
					cout << "data_len is " << data_len << endl;
					//头部长度非法 断开连接
					if (data_len > MAX_LENGTH) {
						std::cout << "invalid data length is " << data_len << endl;
						_server->ClearSession(_uuid);
						return;
					}

					_recv_msg_node = make_shared<MsgNode>(data_len); //数据节点
					// 消息的长度小于头部规定的长度,说明数据没有收全,则先将部分消息放在接收节点里
					if (bytes_transferred < data_len) {
						// 拷贝到节点里
						memcpy(_recv_msg_node->_data + _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::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
						//头部处理完成
						_b_head_parse = true;
						return;
					}

					// 消息的长度大于等于头部规定的长度,说明这一节数据已经收齐了,可以读取接收了,需要进行切包。
					memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, data_len);
					_recv_msg_node->_cur_len += data_len;
					copy_len += data_len;
					bytes_transferred -= data_len;
					_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0'; //第一个消息包的数据取完了
					cout << "receive data is " << _recv_msg_node->_data << endl;
					//此处可以调用Send发送测试
					Send(_recv_msg_node->_data, _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::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
						return;
					}
					continue;
				}
			}

			else {
				//已经处理完头部,处理上次未接受完的消息数据
				//接收的数据仍不足剩余未处理的
				int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;
				// 这次接收到的消息体,还不满足整合成一个数据结构体。
				if (bytes_transferred < remain_msg) {
					memcpy(_recv_msg_node->_data + _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::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
					return;
				}

				// 接收的消息已经满足形成一个数据包结构体了,
				memcpy(_recv_msg_node->_data + _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->_data[_recv_msg_node->_total_len] = '\0';
				cout << "receive data is " << _recv_msg_node->_data << endl;
				//此处可以调用Send发送测试
				Send(_recv_msg_node->_data, _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::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
					return;
				}
				continue;
			}
		}
	}
	else {
		std::cout << "handle write failed, error is " << error.what() << endl;
		_server->ClearSession(_uuid);// 杀死session
	}
}
  • 对于客户端:发送和接收数据的时候,也要先发送两个字节的数据长度,再发送数据消息的结构。
try
	{
		// 创建上下文服务
		boost::asio::io_context   ioc;
		//构造endpoint
		tcp::endpoint  remote_ep(asio::ip::address::from_string("127.0.0.1"), 10086);
		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;
		}

		std::cout << "Enter message: ";
		char request[MAX_LENGTH];
		std::cin.getline(request, MAX_LENGTH); //输入数据
		size_t request_length = strlen(request);

		char send_data[MAX_LENGTH] = { 0 };
		memcpy(send_data, &request_length, 2); // 先首部2字节,构造数据长度
		memcpy(send_data + 2, request, request_length); // 再构造数据体
		boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2));

		char reply_head[HEAD_LENGTH];
		size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_LENGTH)); // 先接收头部,获取信息长度
		short msglen = 0;
		memcpy(&msglen, reply_head, HEAD_LENGTH);
		char msg[MAX_LENGTH] = { 0 };
		size_t  msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen)); // 再接收尾部

		std::cout << "Reply is: ";
		std::cout.write(msg, msglen) << endl;
		std::cout << "Reply len is " << msglen;
		std::cout << "\n";

	}
	catch (const std::exception& e)
	{
		std::cerr << "Exception: " << e.what() << "\n";
	}

参考列表
https://www.bilibili.com/video/BV1ys4y1D7Mu

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Kubernetes】K3S

目录 前言一、原理单体架构高可用架构 二、初始化1.配置yum源2.关掉防火墙3.关掉selinux4. 修改内核参数5.关掉swap交换分区 三、安装master节点1. 安装container2.启动master服务 四、安装node节点五、卸载六、总结 前言 各位小伙伴们&#xff0c;大家好&#xff0c;小涛又来…

力扣每日一题 使二叉树所有路径值相等的最小代价 满二叉树 贪心

Problem: 2673. 使二叉树所有路径值相等的最小代价 文章目录 思路复杂度Code 思路 &#x1f468;‍&#x1f3eb; 灵神题解 复杂度 ⏰ 时间复杂度: O ( n ) O(n) O(n) &#x1f30e; 空间复杂度: O ( 1 ) O(1) O(1) Code class Solution {public int minIncrements(int …

InnoDB锁介绍

本文主要介绍MySQL InnoDB引擎中的各种锁策略和锁类别&#xff0c;并针对记录锁做演示以便于理解。 以下内容适用于MySQL 8.0版本。 读写锁 处理并发读/写访问的系统通常实现一个由两种锁类型组成的锁系统。这两种锁通常被称为共享锁(shared lock)和排他锁(exclusive lock)&…

Java玩转《啊哈算法》暴力枚举之坑爹奥数

每个笨蛋都会随时准备杀了自己&#xff0c;这是最怯懦&#xff0c;也是最简单的出路。 路 缘起代码地址枚举题1题2题2 - Plus完整代码 缘起 各位小伙伴们好呀&#xff01;本人最近看了下《啊哈算法》&#xff0c;写的确实不错。 但稍显遗憾的是&#xff0c;书籍示例代码是c语…

算法修炼-动态规划之斐波那契数列模型

一、动态规划的算法原理 这是本人动态规划的第一篇文章&#xff0c;所以先阐述一下动态规划的算法原理以及做题步骤。动态规划本人的理解就是通过题目所给的条件正确地填满dp表&#xff08;一段数组&#xff09;。首先要先确定好dp表每个位置的值所代表的含义是什么&#xff0c…

二叉树的增删查改

本节复习二叉树的增删查改&#xff0c; 二叉树的知识相对于前面的循序表&#xff0c; 链表&#xff0c; 以及栈和队列都要多一些。 同时二叉树的增删查改理解起来相对来说要困难一些。 本节来好好复习一下二叉树的增删查改。 目录 准备文件 创建结构体蓝图 二叉树的前序遍历…

Windows PowerShell 命令行历史记录补全

Windows 命令行历史记录补全 使用 powershell 安装PSReadLine 2.1.0 Install-Module PSReadLine -RequiredVersion 2.1.0检查是否存在配置文件 Test-path $profile # 为 false 则执行命令创建 New-item –type file –force $profile编辑配置文件 notepad $profile# 输入如下…

数据结构------栈(Stack)和队列(Queue)

也是好久没写博客了&#xff0c;那今天就回归一下&#xff0c;写一篇数据结构的博客吧。今天要写的是栈和队列&#xff0c;也是数据结构中比较基础的知识。那么下面开始今天要写的博客了。 目录 栈&#xff08;Stack&#xff09; 队列&#xff08;Queue&#xff09; 喜欢就点…

从C到C++

二、从C到C 本章介绍一些C拓展的非面向对象功能。 引用&#xff08;掌握&#xff09; 1.1 概念 引用从一定程度上讲是一个指针的平替&#xff0c;几乎被所有面向对象编程语言所使用。引用相当于对某一个目标变量起”别名“。 操作引用与操作原变量完全一样。 #include <iost…

工厂模式 详解 设计模式

工厂模式 其主要目的是封装对象的创建过程&#xff0c;使客户端代码和具体的对象实现解耦。这样子就不用每次都new对象&#xff0c;更换对象的话&#xff0c;所有new对象的地方也要修改&#xff0c;违背了开闭原则&#xff08;对扩展开放&#xff0c;对修改关闭&#xff09;。…

Unity UI适配规则和对热门游戏适配策略的拆解

前言 本文会介绍一些关于UI适配的基础概念&#xff0c;并且统计了市面上常见的设备的分辨率的情况。同时通过拆解目前市面上较为成功的两款休闲游戏Royal Match和Monopoly GO(两款均为近期游戏付费榜前几的游戏)&#xff0c;大致推断出他们的适配策略&#xff0c;以供学习和参…

go并发模式之----阻塞/屏障模式

常见模式之一&#xff1a;阻塞/屏障模式 定义 顾名思义&#xff0c;就是阻塞等待所有goroutine&#xff0c;直到所有goroutine完成&#xff0c;聚合所有结果 使用场景 多个网络请求&#xff0c;聚合结果 大任务拆分成多个子任务&#xff0c;聚合结果 示例 package main ​…

Delegate动画案例(P30 5.6delegate动画)

一、ListElement&#xff0c;ListModel&#xff0c;ListView 1. ListElement ListElement 是 QML 中用于定义列表项的元素。它可以包含多个属性&#xff0c;每个属性对应列表项中的一个数据字段。通过在 ListModel 中使用 ListElement&#xff0c;可以定义一个列表的数据模型…

USB-C接口:办公新宠,一线连接笔记本与显示器

USB-C接口如今已成为笔记本与显示器连接的优选方案。无需担心正反插错&#xff0c;支持雷电4和DP视频信号输出&#xff0c;高速数据传输&#xff0c;还有最高100W的快充功能&#xff0c;真是方便又实用&#xff01; 一线连接&#xff0c;多功能融合 通过这个接口&#xff0c;你…

面试笔记系列三之spring基础知识点整理及常见面试题

目录 如何实现一个IOC容器? 说说你对Spring 的理解&#xff1f; 你觉得Spring的核心是什么&#xff1f; 说一下使用spring的优势&#xff1f; Spring是如何简化开发的&#xff1f; IOC 运行时序 prepareRefresh() 初始化上下文环境 obtainFreshBeanFactory() 创建并…

瑞_23种设计模式_外观模式

文章目录 1 外观模式&#xff08;Facade Pattern&#xff09;1.1 介绍1.2 概述1.3 外观模式的结构 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 jdk源码解析 &#x1f64a; 前言&#xff1a;本文章为瑞_系列专栏之《23种设计模式》的外观模式篇。本文中的部分…

如何在Windows部署TortoiseSVN客户端并实现公网连接内网VisualSVN服务端

文章目录 前言1. TortoiseSVN 客户端下载安装2. 创建检出文件夹3. 创建与提交文件4. 公网访问测试 前言 TortoiseSVN是一个开源的版本控制系统&#xff0c;它与Apache Subversion&#xff08;SVN&#xff09;集成在一起&#xff0c;提供了一个用户友好的界面&#xff0c;方便用…

Flutter开发之Slider

Flutter开发之Slider 本文是关于介绍Slider相关属性的含义。 class SliderThemeData {/// slider轨道的高度 final double? trackHeight; /// 滑块滑过的轨道颜色 final Color? activeTrackColor; /// 滑块未滑过的轨道颜色 final Color? inactiveTrackColor; /// 滑块滑过…

JavaEE——简单认识JavaScript

文章目录 一、简单认识 JavaScript 的组成二、基本的输入输出和简单语法三、变量的使用四、JS 中的动态类型图示解释常见语言的类型形式 五、JS中的数组六、JS 中的函数七、JS 中的对象 一、简单认识 JavaScript 的组成 对于 JavaScript &#xff0c;其中的组成大致分为下面的…

多线程如何设计?一对多/多对一/多对多

二、14个多线程设计模式 参考原文&#xff1a;https://www.cnblogs.com/rainbowbridge/p/17443503.html single Thread 模式 一座桥只能通过一个人 Single Thread模式是一种单线程设计模式&#xff0c;即在一个应用程序中只有一个主线程、一个事件循环&#xff0c;对外只提…
最新文章