微服务开源框架TARS 之 基础组件
作者 herman
导语
本文源自herman的系列文章之一《鹅厂开源框架TARS之基础组件》。相关代码已按TARS开源社区最新版本更新。
TARS开源框架库里面用C++实现了比较多的公用组件,这些组件一般统一放在 util
文件夹,在应用层也可以自由使用,工欲善其事必先利其器,所以有必要把这些工具组件做了解,更好的使用,提高效率。接下来,本文将对如下TarsCpp组件进行分析:
- 线程操作
- 线程安全队列: TC_ThreadQueue
- 普通线程锁: TC_ThreadLock
- 线程基类: TC_Thread
- 智能指针
- 智能指针类: TC_AutoPtr
- DB操作
- MySQL操作类: TC_Mysql
- 网络操作
- 网络组件
- 服务配置
- 命令解析类: TC_Option
- 配置文件类: TC_Config
- 仿函数
- 通用仿函数类: TC_Functor
- Hash
- hash算法
- 异常处理
- 异常类: TC_Exception
线程安全队列: TC_ThreadQueue
typedef TC_ThreadQueue<tagRecvData*, deque<tagRecvData*> > recv_queue; // 接收队列
typedef TC_ThreadQueue<tagSendData*, deque<tagSendData*> > send_queue; // 发送队列
/*** @brief 线程安全队列*/
template<typename T, typename D = deque<T> >
class TC_ThreadQueue
{public:TC_ThreadQueue():_size(0){};public:typedef D queue_type;/*** @brief 从头部获取数据, 没有数据抛异常** @param t* @return bool: true, 获取了数据, false, 无数据*/T front();/*** @brief 从头部获取数据, 没有数据则等待.** @param t * @param millsecond(wait = true时才生效) 阻塞等待时间(ms)* 0 表示不阻塞 * -1 永久等待* @param wait, 是否wait* @return bool: true, 获取了数据, false, 无数据*/bool pop_front(T& t, size_t millsecond = 0, bool wait = true);......
}
template<typename T, typename D> void TC_ThreadQueue<T, D>::push_front(const T& t, bool notify)
{if(notify) {std::unique_lock<std::mutex> lock(_mutex);_cond.notify_one();_queue.push_front(t);++_size;}else{std::lock_guard<std::mutex> lock (_mutex);_queue.push_front(t);++_size;}
}
再看一个成员函数pop_front
,从头部获取数据,没有数据则等待。millisecond
阻塞等待时间(ms)
template<typename T, typename D> bool TC_ThreadQueue<T, D>::pop_front(T& t, size_t millsecond, bool wait)
{if(wait) {std::unique_lock<std::mutex> lock(_mutex);if (_queue.empty()) {if (millsecond == 0) {return false;}if (millsecond == (size_t) -1) {_cond.wait(lock);}else {//超时了if (_cond.wait_for(lock, std::chrono::milliseconds(millsecond)) == std::cv_status::timeout) {return false;}}}if (_queue.empty()) {return false;}t = _queue.front();_queue.pop_front();assert(_size > 0);--_size;return true;}else{std::lock_guard<std::mutex> lock (_mutex);if (_queue.empty()){return false;}t = _queue.front();_queue.pop_front();assert(_size > 0);--_size;return true;}
}
BindAdapter::waitForRecvQueue
的函数就是调用了pop_front
函数,用于等待接收队列,函数原型如下:
bool TC_EpollServer::BindAdapter::waitForRecvQueue(uint32_t handleIndex, shared_ptr<RecvContext> &data)
{bool bRet = getRecvQueue(handleIndex).pop_front(data);if (!bRet){return bRet;}--_iRecvBufferSize;return bRet;
}
普通线程锁: TC_ThreadLock
typedef TC_Monitor<TC_ThreadMutex, TC_ThreadCond> TC_ThreadLock;
TC_Monitor
线程锁监控模板类。通常线程锁,都通过该类来使用,而不是直接用TC_ThreadMutex
、TC_ThreadRecMutex
。
类的定义template <class T, class P> class TC_Monitor
需要传入两个模板参数,TC_Monitor
包括以下成员变量:
mutable int _nnotify; // 上锁的次数
mutable P _cond; // 条件变量
T _mutex; // 互斥锁
/*** @brief 定义锁控制对象*/
typedef TC_LockT<TC_Monitor<T, P> > Lock;
typedef TC_TryLockT<TC_Monitor<T, P> > TryLock;
第一个参数 TC_ThreadMutex
代表线程锁:同一个线程不可以重复加锁 ,包含成员变量
mutable std::mutex _mutex
延伸阅读,这里
tc_thread_mutex.h
还包括另外一个循环锁类TC_ThreadRecMutex
,即一个线程可以加多次锁,定义如下:
// 定义于tc_monitor.h中
typedef TC_Monitor<TC_ThreadRecMutex, TC_ThreadCond> TC_ThreadRecLock;
第二个参数 TC_ThreadCond
代表线程信号条件类:所有锁可以在上面等待信号发生,包含线程条件成员变量:
mutable std::condition_variable_any _cond
结合实际的使用场景,TC_Monitor::timedWait()
会调用 TC_ThreadCond
对象的 timedWait
函数,下一步调用 chrono
库的 milliseconds
;TC_ThreadCond::signal()
实现发送信号,等待在该条件上的一个线程会醒。
TC_LockT
类定义: template <typename T> class TC_LockT
锁模板类,与其他具体锁配合使用,构造时候加锁,析够的时候解锁。
TC_LockT
构造函数,传入互斥量初始化成员变量 _mutex
,TC_LockT
构造函数实现:
TC_LockT(const T& mutex) : _mutex(mutex) {_mutex.lock();_acquired = true;
}
到这里就可以看出 TC_Monitor
定义的 typedef TC_LockT<TC_Monitor<T, P> > Lock
,这里 Lock
类型的模板参数用的是 TC_Monitor
类。
实际使用场景如下:
Lock lock(*this);
TC_LockT
的构造函数,传入参数 this
为 TC_Monitor
的子类对象,TC_LockT
的构造函数调用_mutex.lock()
;实际就是调用了 TC_Monitor
对象的 lock
函数,TC_Monitor
的 lock
函数实现:
void lock() const
{_mutex.lock();_nnotify = 0;
}
这里 _mutex
为 TC_ThreadMutex
对象,进一步调用了 TC_ThreadRecMutex::lock()
成员函数,实现如下:
void TC_ThreadMutex::lock() const
{_mutex.lock();
}
然后上面定义的lock栈变量退出函数的时候调用 TC_LockT
的析构函数:实现如下:
virtual ~TC_LockT()
{if (_acquired){_mutex.unlock(); //这里会调用TC_Monitor的unlock函数}
}
TC_Monitor
的 unlock
函数实现:
void unlock() const
{notifyImpl(_nnotify);_mutex.unlock(); //这里会调用C++标准库<mutex>中的unlock
}
这里调用 notifyImpl
函数是因为 TC_Monitor
类不只可以实现简单的互斥锁功能,还可以实现条件变量Condition功能,其中 notifyImpl
的实现为
void notifyImpl(int nnotify) const
{if(nnotify != 0){if(nnotify == -1){_cond.broadcast();return;}else{while(nnotify > 0){_cond.signal();--nnotify;}}}
}
线程基类: TC_Thread
还是老样子,先看下项目实际对线程基类的使用。实际项目使用中,我们对 TC_Thread
又封装了一下,实现了一个BasicThread
类,下面看下 BasicThread
的定义:
class BasicThread : public tars::TC_Thread, public tars::TC_ThreadLock
{...void terminate(){_bTerm = true;{Lock lock(*this);notifyAll();}getThreadControl().join();}
}
class TC_Thread : public TC_Runable
{.../*** 使用了C++11标准线程库std::thread, 构造函数传参数threadEntry线程函数,* 返回 TC_ThreadControl(_th),其中_th为std::thread对象*/TC_ThreadControl start();static void threadEntry(TC_Thread *pThread); //静态函数, 线程入口virtual void run() = 0;...
}
下一步看下线程控制类 TC_ThreadControl
的定义:
class TC_ThreadControl
{...
explicit TC_ThreadControl(std::thread *th); // 构造,传入std::thread对象void join(); // 调用std::thread的join()阻塞当前的线程,直到另外一个线程运行结束static void sleep(); // 调用std::this_thread::sleep函数线程将暂停执行
...
}
class TC_Runable
{public:virtual ~TC_Runable(){};virtual void run() = 0; //定义了run纯虚函数
};
class AntiSdkSyncThread : public BasicThread //这里等于多继承了TC_Thread和TC_ThreadLock两个类
{void run() //实现基类的纯虚函数{Lock lock(*this);timedWait(10 * 1000); (间隔执行时间,实现了线程的定时执行功能)if(NULL != g_busi_interf){Int32 ret = g_busi_interf->proc_(); //需要定期执行的函数}}
}
智能指针类: TC_AutoPtr
TC_HandleBase
智能指针基类的定义如下,所有需要智能指针的类都需要从该对象继承,其中使用了C++11标准库中的<atomic>
进行原子计数。
class UTIL_DLL_API TC_HandleBase
{public:/*** @brief 复制** @return TC_HandleBase&*/TC_HandleBase& operator=(const TC_HandleBase&){return *this;}/*** @brief 增加计数*/void incRef() { ++_atomic; }/*** @brief 减少计数*/void decRef(){if((--_atomic) == 0 && !_bNoDelete){_bNoDelete = true;delete this;}}/*** @brief 获取计数.** @return int 计数值*/int getRef() const { return _atomic; }/*** @brief 设置不自动释放. * * @param b 是否自动删除,true or false*/void setNoDelete(bool b) { _bNoDelete = b; }protected:/*** @brief 构造函数 */TC_HandleBase() : _atomic(0), _bNoDelete(false){}/*** @brief 拷贝构造*/TC_HandleBase(const TC_HandleBase&) : _atomic(0), _bNoDelete(false){}/*** @brief 析构*/virtual ~TC_HandleBase(){}protected:std::atomic<int> _atomic; // 引用计数bool _bNoDelete; // 是否自动删除
};
下一步看 TC_AutoPtr
智能指针模板类,可以放在容器中,且线程安全的智能指针,该智能指针通过引用计数实现,其构造函数和析构函数定义如下:
template<typename T>
class TC_AutoPtr
{ TC_AutoPtr(T* p = 0){_ptr = p;if(_ptr){_ptr->incRef(); //构造函数 引用计算加1}}...~TC_AutoPtr(){if(_ptr){_ptr->decRef(); //析构函数 引用计算减1}}
}
例子:实战项目使用
struct ConnStruct : public TC_HandleBase{...}typedef TC_AutoPtr<ConnStruct> ConnStructPtr;
TC_AutoPtr
拷贝构造调用 _ptr->incRef();
这里 ptr
为 ConnStruct
,ConnStruct
继承于TC_HandleBase
,等于调用了TC_HandleBaseT<int>::incRef() {++_atomic;}
引用计数原子操作加1、析构引用计数原子操作减1,当引用计数减少到0时根据设置的开关是否要进行删除来决定是否触发delete。
例子:这是TARS使用异步rpc回调的典型例子,这里回调类使用了智能指针
// 定义回调函数智能指针,其中SessionCallback父类继承于TC_HandleBase
typedef TC_AutoPtr<SessionCallback> SessionCallbackPtr; //创建回调类SessionCallbackPtr,并传入初始化参数uin gameid等;
SessionCallbackPtr cb = new SessionCallback(iUin, iGameId, iSeqID, iCmd,sSessionID, theServant, current, cs, this);
//异步调用sessionserver远程接口
getSessionPrx()->async_getSession(cb, iUin, iGameId);
接口返回完成,回调SessionCallback::callback_getSession(tars::Int32 ret, const MGComm::SessionValue& retValue)
函数,接收sessionserver
接口的返回的SessionValue
结构。
因为 SessionCallbackPtr
使用了智能指针,所以业务不需要去手动释放前面 new
出来的 SessionCallbackPtr
,还是比较方便的。
MySQL操作类: TC_Mysql
TC_Mysql封装好的mysql操作类,非线程安全,对于 insert/update 可以有更好的函数封装,防止SQL注入
TC_Mysql mysql;
//初始化mysql,init时不链接,请求时自动建立链接;
//数据库可以为空;
//端口默认为3306
mysql.init("192.168.1.2", "pc", "pc@sn", "db_tars_demo");
通常用:void init(const TC_DBConf& tcDBConf);
直接初始化数据库。例如:stDirectMysql.init(_stZoneDirectDBConf);
struct TC_DBConf
{string _host;string _user;string _password;string _database;string _charset;int _port;int _flag; //客户端标识TC_DBConf(): _port(0), _flag(0){}/*** @brief 读取数据库配置. * * @param mpParam 存放数据库配置的map * dbhost: 主机地址* dbuser:用户名* dbpass:密码* dbname:数据库名称* dbport:端口*/void loadFromMap(const map<string, string> &mpParam){map<string, string> mpTmp = mpParam;_host = mpTmp["dbhost"];_user = mpTmp["dbuser"];_password = mpTmp["dbpass"];_database = mpTmp["dbname"];_charset = mpTmp["charset"];_port = atoi(mpTmp["dbport"].c_str());_flag = 0;if(mpTmp["dbport"] == ""){_port = 3306;}}
};
//进一步看下获取数据的使用
TC_Mysql::MysqlData data;
data = mysql.queryRecord("select * from t_app_users");
for(size_t i = 0; i < data.size(); i++)
{//如果不存在ID字段,则抛出异常cout << data[i]["ID"] << endl;
}
class MysqlData
{ ...vector<map<string, string> >& data(); ...
}
//插入数据,指定数据的类型:数值 或 字符串,对于字符串会自动转义
map<string, pair<TC_Mysql::FT, string> > m;
m["ID"] = make_pair(TC_Mysql::DB_INT, "2334");
m["USERID"] = make_pair(TC_Mysql::DB_STR, "abcttt");
m["APP"] = make_pair(TC_Mysql::DB_STR, "abcapbbp");
m["LASTTIME"] = make_pair(TC_Mysql::DB_INT, "now()");mysql.replaceRecord("t_user_logs", m);
网络组件
整个TARS核心就提供一个很完善的网络框架,包括RPC功能,这里只介绍几个常用的网络组件。
TC_Socket : 封装了socket的基本方法
提供socket的操作类;支持tcp/udp socket;支持本地域套接字。
再下一层TARS封装了TC_TCPClient
和TC_UDPClient
两个类用于实际操作tcp和udp应用。
使用方式:
例如:tcp客户端
TC_TCPClient stRouterClient;stRouterClient.init(sIP, iPort, iTimeOut); // 这里传入ip和端口然后调用sendRecv进行消息的收发Int32 ret = stRouterClient.sendRecv(request.c_str(), request.length(), recvBuf, iRecvLen);
注意多线程使用的时候,不能多线程同时send/recv,小心串包。
TC_Epoller
提供网络epoll的操作类,默认是ET模式,当状态发生变化的时候才获得通知,提供add、mod、del、wait等基础操作。
TC_ClientSocket : 客户端socket相关操作基类
提供关键成员函数init(const string &sIp, int iPort, int iTimeout)
,传入 IP 端口 和 超时时间
TC_TCPClient
继承于 TC_ClientSocket
提供成员函数:
例子:
stRouterClient.init(sIP, iPort, iTimeOut);size_t iRecvLen = sizeof(recvBuf)-1;
Int32 ret = stRouterClient.sendRecv(request.c_str(), request.length(), recvBuf, iRecvLen);
同理还有TC_UDPClient
实现UDP客户端。
命令解析类: TC_Option
./main.exe --name=value --param1 param2 param3
TC_Option op;
//解析命令行
op.decode(argc, argv);
//获取成对的参数,即获取 - - 表示的所有参数对
map<string, string> mp = op.getMulti();
//表示非 – 的参数:即 param2, param3
vector<string> d = op.getSingle();
如果value,param有空格或者 --
,用引号括起来就可以了。
配置文件类: TC_Config
- 配置文件解析类(兼容wbl模式);
- 支持从string中解析配置文件;
- 支持生成配置文件;
- 解析出错抛出异常;
- 采用[]获取配置,如果无配置则抛出异常;
- 采用get获取配置,不存在则返回空;
- 读取配置文件是线程安全的,insert域等函数非线程安全
例子:
TC_Config config;
config.parseFile(ServerConfig::BasePath + ServerConfig::ServerName + ".conf");
stTmpGameServerConfig.iGameId = TC_Common::strto<UInt32>(config["/Main/<GameId>"]);
配置文件样例
<Main>GameId = 3001ZoneId = 102AsyncThreadCheckInterval = 1000...
</Main>
使用get
方法例子:如果读不到该配置,则返回默认值 sDefault
,即下面例子中的 20000000
stTmpGameServerConfig.iMaxRegNum = TC_Common::strto<Int32>(config.get("/Main/<MaxRegNum>", "20000000"));
通用仿函数类: TC_Functor
仿函数对象调用方式, 即对上述的几种方式都可以在右侧添加一对圆括号,并在括号内部放一组合适的参数来调用,例如
a(p1,p2);
把整个调用(包括参数)封装一个函数对象, 调用对象建立时就传入了参数,调用的时候不用传入参数,例如
A a(p1, p2); a();
简单又好用的封装,具体见下面使用例子自然明白:
C函数调用
void TestFunction3(const string &s, int i){cout << "TestFunction3('" << s << "', '" << i << "')" << endl;
}
//采用函数指针构造对象
TC_Functor<void, TL::TLMaker<const string&, int>::Result > cmd3(TestFunction3);
string s3("s3");
cmd3(s3, 10);
C函数调用用wrapper封装:
//调用封装,构造的时候传入参数TC_Functor<void,TL::TLMaker<const string&, int>::Result>::wrapper_type fwrapper3(cmd3, s3, 10);
fwrapper3(); //参数已经在构造的时候传入,调用的时候不用传参数了
说明:
void
: 函数的返回值TL::TLMaker<const string&, int>::Result
: 代表参数类型
对于调用的封装,注意对于传引用类型,具体的调用时候要保证引用的对象存在。
C++指向类成员函数的调用
struct TestMember
{void mem3(const string &s, int i){cout << "TestMember::mem3(" << s << "," << i << ") called" << endl;}
}
TC_Functor<void, TL::TLMaker<const string&, int>::Result > cmd3(&tm, &TestMember::mem3);
cmd3("a", 33);
指向类成员函数的调用用wrapper封装:
TC_Functor<void, TL::TLMaker<const string&, int>::Result >::wrapper_type fwrapper3(cmd3, "a", 10);
fwrapper3();
实际例子:注册协议解析器
服务初始化initialize的时候,一般会调用
addServantProtocol(sRouterObj, AppProtocol::parseStream<0, uint16_t, false>,iHeaderLen);
这里设置BindAdapter
的协议解析函数 protocol_functor _pf
为 parseStream
函数,如下:
/*** @param T* @param offset* @param netorder* @param in* @param out* @return int*/
template<size_t offset, typename T, bool netorder>
static TC_NetWorkBuffer::PACKET_TYPE parseStream(TC_NetWorkBuffer& in,vector<char>& out)
{size_t len = offset + sizeof(T);if (in.getBufferLength() < len){return TC_NetWorkBuffer::PACKET_LESS;}string header;in.getHeader(len, header);assert(header.size() == len);T iHeaderLen = 0;::memcpy(&iHeaderLen, header.c_str() + offset, sizeof(T));if (netorder){iHeaderLen = net2host<T>(iHeaderLen);}//长度保护一下if (iHeaderLen < (T)(len) || (uint32_t)iHeaderLen > TARS_NET_MAX_PACKAGE_SIZE){return TC_NetWorkBuffer::PACKET_ERR;}if (in.getBufferLength() < (uint32_t)iHeaderLen){return TC_NetWorkBuffer::PACKET_LESS;}in.getHeader(iHeaderLen, out);assert(out.size() == iHeaderLen);in.moveHeader(iHeaderLen);return TC_NetWorkBuffer::PACKET_FULL;
}
注册好解析函数之后,网络层收包调用parseProtocol
函数
int TC_EpollServer::Connection::parseProtocol(TC_NetWorkBuffer &rbuf)
{...TC_NetWorkBuffer::PACKET_TYPE b = _pBindAdapter->getProtocol()(rbuf, ro); //这里回调前面设置好的协议解析函数,从而实现协议解析...
}
hash算法
util/tc_hash_fun.h
中包含了对hash算法的实现,使用 hash_new
,可以对输入的字节流进行hash得到相当均匀的hash值,使用方式如下
#include "util/tc_hash_fun.h"
#include <iterator>
#include <iostream>
#include <sys/time.h>using namespace tars;
using namespace std;int main(int argc, char* *argv[])
{unsigned int i = tars::hash_new<string>()("abcd");cout << i << endl;return 0;
}
异常类: TC_Exception
class TC_Exception : public exception
{ /*** @brief 构造函数,提供了一个可以传入errno的构造函数, * 异常抛出时直接获取的错误信息* * @param buffer 异常的告警信息 * @param err 错误码, 可用strerror获取错误信息*/TC_Exception(const string &buffer, int err);
}
总结
本文介绍分析了TARS框架中用C++实现的公用基础组件,加深对这些工具类基础组件的理解,减少在使用这些组件过程中产生的问题,提高开发效率。
TARS源码:https://github.com/TarsCloud
获取《TARS官方培训电子书》:https://wj.qq.com/s2/6570357/3adb/
微服务开源框架TARS 之 基础组件相关推荐
- 微服务开源框架TARS的RPC源码解析 之 初识TARS C++服务端
作者:Cony 导语:微服务开源框架TARS的RPC调用包含客户端与服务端,<微服务开源框架TARS的RPC源码解析>系列文章将从初识客户端.客户端的同步及异步调用.初识服务端.服务端的工 ...
- 微服务开源框架TARS 之 框架服务解析
作者 herman 简介 本文源自herman的系列文章之一<鹅厂开源框架TARS之运营服务监控>.相关代码已按TARS开源社区最新版本更新. TARS框架为用户提供了涉及到开发.运维.以 ...
- Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单。
Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单. 前方: 开源地址:GitHub - cyq1162/Taurus.MVC: Taurus.mvc i ...
- 浅谈阿里巴巴蚂蚁金服SOFA微服务开源框架
前言: 提到分布式.微服务,大部分人首先想到的可能是SpringCloud.Duubo等主流框架,但事实上在SpringCloud还未出现之际,蚂蚁金服内部就已经有了一套比较完整的金融级分布式架构So ...
- java开源框架有哪些_常用的Java微服务开源框架有哪些呢
随着程序规模的扩大以及复杂性增大,越来越多的Java程序员选择使用微服务进行项目设计研发.微服务的出现有助于开发人员用更低的成本和更少的错误来开发程序,因此也成为Java开发人员最重要的技能之一.但常 ...
- TARS 微服务开源生态
TARS 微服务开源生态 基础层:基础架构(不同架构芯片.云.容器).基础开发语言 存储层和协议层:存储(Cache.数据库.大数据.文件系统).协议 逻辑平台层:框架.服务发现.Service Me ...
- guns 最新开源框架企业版下载_国内比较火的5款Java微服务开源项目
本文介绍国内比较火的5款Java微服务开源项目,pig是基于Spring Cloud.OAuth2.0.Vue的前后端分离的系统. 通用RBAC权限设计及其数据权限和分库分表 支持服务限流.动态路由. ...
- 美团大规模微服务通信框架及治理体系OCTO核心组件开源
来源:美团技术团队 数据猿官网 | www.datayuan.cn 今日头条丨一点资讯丨腾讯丨搜狐丨网易丨凤凰丨阿里UC大鱼丨新浪微博丨新浪看点丨百度百家丨博客中国丨趣头条丨腾讯云·云+社区 微服务通 ...
- 十款优质企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)
Java微服务开源项目 前言 一.pig 二.zheng 三.SpringBlade 四.SOP 五.matecloud 六.mall 七.jeecg-boot 八.Cloud-Platform 九. ...
最新文章
- 恶意软件每天至少30万个变种 杀毒软件捉襟见肘 来看4种恶意软件反查杀技术...
- IOS中Key-Value Coding (KVC)的使用详解
- echarts 弹出放大_echarts 如何让饼图hover放大效果一直显示
- nginx介绍(三) - 虚拟主机
- Linux学习总结(32)——Shell脚本高效编写技巧
- 电子围栏原理解析---原理
- python数据生成可视化_Python数据分析:手把手教你用Pandas生成可视化图表
- vue-elementui制作表格(二)
- 关于数字化营销,最通俗的讲解在这里
- chrome浏览器安装包点击无反应
- 免费PPT模板下载??
- dell笔记本重装系统
- numpy手写NLP模型(四)———— RNN
- JAVA读取Excel表格,建数据库建表,并生成java实体实例
- 扁平化风格博客——后续
- Python之OpenCV截取视频段
- 51单片机密码锁(含确认键、清零键、删除键)
- 华为4月将开始升级鸿蒙系统,暂定4月,华为鸿蒙系统登场!升级标准已确定,淘汰名单也出炉...
- 用代码向 90 后逝去的青春致敬
- Java反射破坏了封装性?
热门文章
- 李莉娜非诚勿扰三拒男嘉宾 网友表痛惜
- StreamX学习笔记之StreamAPI开发应用
- 笔记本硬盘删除的数据怎么恢复丨顶尖数据恢复丨
- 在线算命程序 源码 用python语言写的 算命的网络化发展趋势 你怎么理解的?
- 牧场物语移植java_【牧场物语1德版】牧场物语1移植原版游戏下载-街机中国
- M-value-The 2019 ICPC Asia-East Continent Final
- 如何利用深度学习技术处理图像水印?
- php3D动画,html5的canvas实现几何模型3D运动动画效果
- openGauss5 企业版之SQL语法和数据结构
- 高精地图在互联网行业的应用与发展