tars开源框架地址:https://github.com/Tencent/Tars

系列文章:

鹅厂开源框架tars之日志服务

鹅厂开源框架tars之运营监控服务

鹅厂开源框架tars之基础组件

鹅厂开源框架tars之网络层实现

简介:Tars是腾讯从2008年到今天一直在使用的后台逻辑层的统一应用框架TAF(Total Application Framework),目前支持C++,Java,PHP,Nodejs语言。该框架为用户提供了涉及到开发、运维、以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。 它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。目前该框架在腾讯内部,各大核心业务都在使用,颇受欢迎,基于该框架部署运行的服务节点规模达到上万个。

tars开源框架库里面用cpp实现了比较多的公用组件,这些组件一般统一放在util文件夹,在应用层也可以自由使用,工欲善其事必先利其器,所以有必要把这些工具组件做了解,更好的使用,提高效率

一、线程安全队列TC_ThreadQueue

先看下框架对TC_ThreadQueue类的使用如下:

typedef TC_ThreadQueue<tagRecvData*, deque<tagRecvData*> > recv_queue;  接收队列
    typedef TC_ThreadQueue<tagSendData*, deque<tagSendData*> > send_queue; 发送队列

TC_ThreadQueue的实现比较简单继承了TC_ThreadLock,从之前文章

《鹅厂开源框架tars之网络层实现》实现的介绍可以看到这个类比较重要,因为从框架中收到的网络包都会加入到这个缓存队列里面,然后多业务线程ServantHandle会调用waitForRecvQueue从该队列里面取网络数据包,然后调用dispatch调用协议消息对应的处理函数,先看下框架对TC_ThreadQueue的实现:

TC_ThreadQueue继承于TC_ThreadLock用于实现线程锁和wait如下(TC_ThreadLock 为普通线程锁,在下面介绍),看下队列的成员函数:push_front在队列前面加入数据,

如上图调用push_front函数的时候调用Lock lock(*this)加锁 ,避免网络层接收数据和业务层取同一队列的数据冲突,notify()通知等待在该锁上某一个线程醒过来 ,调用该函数之前必须加锁,  ,因为有数据过来了,例如网络层有线程需要取包并进行分发处理

再看一个成员函数pop_front,从头部获取数据, 没有数据则等待.millsecond   阻塞等待时间(ms) 
     *                    0 表示不阻塞 
     *                      -1 永久等待

template<typename T, typename D> bool TC_ThreadQueue<T, D>::pop_front(T& t, size_t millsecond)
{
    Lock lock(*this);

if (_queue.empty())
    {
        if(millsecond == 0)
        {
            return false;
        }
        if(millsecond == (size_t)-1)
        {
            wait();
        }
        else
        {
            //超时了
            if(!timedWait(millsecond))
            {
                return false;
            }
        }
    }

if (_queue.empty())
    {
        return false;
    }

t = _queue.front();
    _queue.pop_front();
    assert(_size > 0);
    --_size;

return true;
}

BindAdapter::waitForRecvQueue的函数就是调用了pop_front函数,用于等待接收队列,函数原型如下:

这里BindAdapter::waitForRecvQueue调用waitForRecvQueue用于业务线程在等待服务器监听的适配器收到网络包后进行业务包的处理,这里传入的iWaitTime是0表示不阻塞等待数据,立即返回

二、TC_ThreadLock普通线程锁

第一点TC_ThreadQueue继承的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 包括以下成员变量:

T               _mutex; //互斥锁

mutable P       _cond;//条件变量

typedef TC_LockT<TC_Monitor<T, P> > Lock;
typedef TC_TryLockT<TC_Monitor<T, P> > TryLock;

第一个参数TC_ThreadMutex代表线程锁:同一个线程不可以重复加锁 ,包含成员变量mutable pthread_mutex_t _mutex;互斥锁(延伸阅读,这里TC_ThreadMutex.h还包括另外一个类:TC_ThreadRecMutex: *循环锁(一个线程可以加多次锁),使用和定义场景如下:)

typedef TC_Monitor<TC_ThreadRecMutex, TC_ThreadCond> TC_ThreadRecLock;定义于TC_Monitor.h文件中

第二个参数TC_ThreadCond代表线程信号条件类:所有锁可以在上面等待信号发生成功变量mutable pthread_cond_t _cond;控制条件变量wait.结合实际的使用场景,TC_Monitor::timedWait()会调用TC_ThreadCond对象的timedWait函数,下一步调用posix线程库的pthread_cond_wait;TC_ThreadCond::signal()实现发送信号, 等待在该条件上的一个线程会醒

TC_LockT类定义:template <typename T> class TC_LockT锁模板类其他具体锁配合使用,构造时候加锁,析够的时候解锁.

TC_LockT构造函数,传入互斥量初始化成员变量_mutex,TC_LockT构造函数实现:TC_LockT(const T& mutex) : _mutex(mutex)。分析到这里就可以推导出TC_Monitor 定义的typedef TC_LockT<TC_Monitor<T, P> > Lock;这里Lock类型,这里Lock类型的模板参数用的是TC_Monitor类。然后由第一点的线程安全队列TC_ThreadQueue的实际使用场景如下:

Lock lock(*this);

因为TC_ThreadQueue继承于TC_Monitor类,所以这里等于定义了TC_LockT<TC_Monitor<T, P> > lock栈变量,改造时候调用

TC_LockT的构造函数,传入参数this为TC_Monitor的子类对象,TC_LockT的构造函数调用_mutex.lock();实际就是调用了TC_Monitor对象的lock函数,TC_Monitor的lock函数实现:{_mutex.lock(); _nnotify = 0;} 由上文的分析可知这里_mutex为TC_ThreadRecMutex对象,进一步调用了TC_ThreadRecMutex::lock()成员函数:核心实现:{ int rc = pthread_mutex_lock(&_mutex);} 调用了POSIX线程的pthread_mutex_lock函数,函数参数定义为posix互斥锁pthread_mutex_t _mutex。然后上面定义的lock栈变量退出函数的时候调用~TC_LockT的析构函数:实现如下:

virtual ~TC_LockT()
    {
        if (_acquired)
        {
            _mutex.unlock(); //这里会调用
TC_Monitor的unlock函数
        }
    }

下一步TC_Monitor的unlock函数实现:

void unlock() const
    {
        notifyImpl(_nnotify);
        _mutex.unlock(); //这里会调用posix线程库函数pthread_mutex_unlock(&_mutex);
    }

下一步notifyImpl实现,这里为什么要调用  notifyImpl(_nnotify);函数是因为TC_Monitor类不只可以实现简单的互斥锁功能,还可以实现条件变量Condition功能,这里解锁了需要通知其他等待锁资源的线程,至于notifyImpl函数的实现见下文,例如从第一点的线程安全队列TC_ThreadQueue类,调用pop_front函数可以传入第二个参数millsecond等待时间参数:实现如下

template<typename T, typename D> bool TC_ThreadQueue<T, D>::pop_front(T& t, size_t millsecond)

{ ....

Lock lock(*this); //这里加互斥锁

if(!timedWait(millsecond)) //如果缓存队列为空,则根据millsecond决定是否阻塞等待
            {
                return false;
            }

....

}

下一步timedWait实现

bool timedWait(int millsecond) const

{

notifyImpl(_nnotify);

bool rc;

try
        {
            rc = _cond.timedWait(_mutex, millsecond); //这里就实现了线程的等待指定时间
        }
        catch(...)
        {
            _nnotify = 0;
            throw;
        }

_nnotify = 0;
        return rc;

}

分析了pop_front知道线程如果等待数据没到,根据传入的millsecond参数有可能阻塞等待数据,所以缓存队列TC_ThreadQueue必然在收到数据的时候,有一个通知的机制,看下push数据的函数实现:

template<typename T, typename D> void TC_ThreadQueue<T, D>::push_back(const T& t)
{
    Lock lock(*this);

    notify();  //这里调用TC_ThreadQueue父类TC_Monitor的成员函数通知等待在该锁上某一个线程醒过来:++_nnotify

_queue.push_back(t);
    ++_size;
}

互斥锁TC_Monitor::unlock的时候,会调用notifyImpl函数,通知其他线程解锁

void notifyImpl(int nnotify) const
    {
        if(nnotify != 0)   //nnotify上锁的次数
        {
            if(nnotify == -1)
            {
                _cond.broadcast();
                return;
            }
            else
            {
                while(nnotify > 0)
                {
                    _cond.signal();
                    --nnotify;
                }
            }
        }
    }

三、TC_Thread线程基类

还是老样子,先看下项目实际对BasicThread类的使用,实际项目使用中,我们对TC_Thread又封装了一下,实现了一个BasicThread 类,下面看下BasicThread 的定义:

class BasicThread : public taf::TC_Thread, public taf::TC_ThreadLock

{

   ...

    void terminate()
    {
        _bTerm = true;
        {
            Lock lock(*this);
            notifyAll();
        }
        getThreadControl().join();
    }

}

BasicThread类,继承了TC_Thread和TC_ThreadLock,其中TC_ThreadLock第二点已经说明过了,所以这里重点看下TC_Thread类的使用,TC_Thread的定义

class TC_Thread : public TC_Runable

{

...

TC_ThreadControl start();//调用posix的pthread_create 参数threadEntry线程函数,返回TC_ThreadControl(_tid)

static void threadEntry(TC_Thread *pThread); //静态函数, 线程入口

virtual void run() = 0;

....

}

下一步看下TC_ThreadControl的定义: 线程控制类

class TC_ThreadControl

{

TC_ThreadControl::TC_ThreadControl() : _thread(pthread_self()) //构造,传入pthread_self(): 查询线程自身线程标识号

join() //调用posix的pthread_join阻塞当前的线程,直到另外一个线程运行结束

sleep() 调用nanosleep函数线程将暂停执行

}

下一步看下TC_Runable的定义

class TC_Runable
{
public:
    virtual ~TC_Runable(){};
    virtual void run() = 0; //定义了run纯虚函数
};

最后看下实际项目中对线程类的使用

class AntiSdkSyncThread : public BasicThread //这里等于多继承了C_Thread和TC_ThreadLock两个类

{

void run()  //实现基类的纯虚函数
    {

Lock lock(*this);

timedWait(10 * 1000); (间隔执行时间,实现了线程的定时执行功能)

if(NULL != g_busi_interf)
                {
                    Int32 ret = g_busi_interf->proc_();  //需要定期执行的函数

}

}

}

定义好了AntiSdkSyncThread g_antiSdkSyncThread;类,那么需要启动线程的时候执行g_antiSdkSyncThread.start();就会自然创建线程,并且hreadEntry线程函数会调用pThread->run()多态函数,进程退出的时候调用g_antiSdkSyncThread.terminate();

四、智能指针

这里的智能指针可以放在容器中,且线程安全的智能指针,CPP11标准库的auto_ptr是不能放在容器中的,貌似已经被淘汰了,目前多数使用CPP11标准库的shared_ptr,不过需要编译器支持cpp11.

TC_HandleBaseT 智能指针基类.所有需要智能指针支持的类都需要从该对象继承

template<class  T>
class TC_HandleBaseT
{

...

atomic_type   _atomic; 用于计数

void decRef()
    {
        if(_atomic.dec_and_test() && !_bNoDelete)
        {
            _bNoDelete = true;
            delete this;
        }
    }

...

}

typedef TC_HandleBaseT<TC_Atomic> TC_HandleBase;

下一步看 TC_Atomic实现原子操作类,对int做原子操作
class TC_Atomic
{

...TC_Atomic& operator++()

TC_Atomic& operator--()

}

下一步看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;}  因为_atomic为typedef TC_HandleBaseT<TC_Atomic> TC_HandleBase;

所以等于调用了TC_Atomic类的++操作符重载函数,下面看下taf_atomic_add_return的实现

static __inline__ int taf_atomic_add_return(int i, taf_atomic_t *v)
{
    /* Modern 486+ processor */
    int __i = i;
    __asm__ __volatile__(
        TAF_LOCK "xaddl %0, %1;"
        :"=r"(i)
        :"m"(v->counter), "0"(i));
    return i + __i;
}

引用计数原子操作加1、析构引用计数原子操作减1,当引用计数减少到0时根据设置的开关是否要进行删除来决定是否触发delete

例子:    这是tar使用异步rpc回调的典型例子,这里回调类使用了智能指针

typedef TC_AutoPtr<SessionCallback> SessionCallbackPtr;  定义回调函数智能指针,其中SessionCallback父类继承于TC_HandleBase

//创建回调类SessionCallbackPtr,并传入初始化参数uin gameid等;

SessionCallbackPtr cb = new SessionCallback(iUin, iGameId, iSeqID, iCmd, sSessionID, theServant, current, cs, this);
            getSessionPrx()->async_getSession(cb, iUin, iGameId); //异步调用sessionserver远程接口

接口返回完成,回调SessionCallback::callback_getSession(taf::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("10.1.36.39", "pc", "pc@sn", "db_dmqq_system");

通常用:void init(const TC_DBConf& tcDBConf);直接初始化数据库:例如:stDirectMysql.init(_stZoneDirectDBConf);

看下TC_DBConf的定义

       //进一步看下获取数据的使用

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;

}

查询出来的mysql数据用MysqlData封装

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功能,这里只介绍几个常用的网络组件,详细的网络层实现在另外一篇文章单独介绍:鹅厂开源框架tars之网络层实现

 6.1 TC_Socket   封装了socket的基本方法 

  1. 提供socket的操作类;
  2. 支持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,小心串包

6.2TC_Epoller :

提供网络epoll的操作类

默认是ET模式,当状态发生变化的时候才获得通知

提供add、mod、del、wait等基础操作

使用方式:(见tars之网络层实现CommunicatorEpoll部分

6.3 TC_ClientSocket 客户端socket相关操作基类:

提供关键成员函数init(const string &sIp, int iPort, int iTimeout); 传入ip 端口 和 超时时间

TC_TCPClient继承于TC_ClientSocket 提供成员函数:

sendRecv(发送到服务器, 从服务器返回不超过iRecvLen的字节)

sendRecvBySep( 发送倒服务器, 并等待服务器直到结尾字符, 包含结尾字符)

例子:

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客户端

七、 命令解析、配置文件

8.1TC_Config

  1. 配置文件解析类(兼容wbl模式);
  2. 支持从string中解析配置文件;
  3. 支持生成配置文件
  4. 解析出错抛出异常;
  5. 采用[]获取配置,如果无配置则抛出异常;
  6. 采用get获取配置,不存在则返回空;

读取配置文件是线程安全的,insert域等函数非线程安全

例子:

TC_Config config;
    config.parseFile(ServerConfig::BasePath + ServerConfig::ServerName + ".conf");
    stTmpGameServerConfig.iGameId = TC_Common::strto<UInt32>(config["/Main/<GameId>"]);

配置文件样例

使用get方法例子:如果读不到该配置,则返回默认值:sDefault

stTmpGameServerConfig.iMaxRegNum = TC_Common::strto<Int32>(config.get("/Main/<MaxRegNum>", "20000000"));

8.2 TC_Option 

  1. 命令解析类;
  2. 通常用于解析命令行参数;
  3. 只支持双—的参数形式

分析main的输入参数,支持以下形式的参数:

./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_Atomic

  1. 原子计数类;
  2. 4字节整形数的加/减/都是原子的;

例子:

TC_Atomic g_loginSeqNo;

pc.ServiceSeq     = g_loginSeqNo.inc();

pc.SeqNo = g_loginSeqNo.inc();

十、通用仿函数类

TC_Functor 参考loki库的设计

1.仿函数对象调用方式, 即对上述的几种方式都可以在右侧添加一对圆括号,并在括号内部放一组合适的参数来调用,例如a(p1,p2);

2.把整个调用(包括参数)封装一个函数对象, 调用对象建立时就传入了参数,调用的时候不用传入参数,例如A a(p1, p2); a();

简单又好用的封装,具体见下面使用例子自然明白:

10.1 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();  //参数已经在构造的时候传入,调用的时候不用传参数了

说明:

  1. void : 函数的返回值
  2. TL::TLMaker<const string&, int>::Result : 代表参数类型

对于调用的封装,注意对于传引用类型,具体的调用时候要保证引用的对象存在

10.2  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函数,如下:

注册好解析函数之后,网络层收包调用parseProtocol函数

TC_EpollServer::Connection::parseProtocol(recv_queue::queue_type &o)

{

int b = _pBindAdapter->getProtocol()(_recvbuffer, ro); //这里回调前面设置好的协议解析函数,从而实现协议解析

}

一十一、强大的loki库

11.1 TypeTraits

在编译期间对类型的特性进行提取和判断

例子:

bool b;

b = TL::TypeTraits<vector<int>::iterator>::isPointer;

cout << "vector<int>::iterator is " << b << endl;

b = TL::TypeTraits<char*>::isPointer;

cout << "char* is " << b << endl;

b = TL::TypeTraits<char&>::isReference;

cout << "char& is " << b << endl;

11.2.TypeSelect 类型选择器 在编译期从两个类型中选择适当类型

例子:字符串转换模板

template<typename T>
T TC_Common::strto(const string &sStr)
{
    typedef typename TL::TypeSelect<TL::TypeTraits<T>::isStdArith, p::strto1<T>, p::strto2<T> >::Result strto_type;

return strto_type()(sStr);
}

模板使用例子:字符串转整形

stTmpGameServerConfig.iGameId = TC_Common::strto<UInt32>(config["/Main/<GameId>"]);

一十二、util/tc_hash_fun.h 

*可以对输入

的字节流进行hash得到相当均匀的hash值 ;使用例子:

一十三、TC_Exception 异常类

class TC_Exception : public exception

{

* @brief 构造函数,提供了一个可以传入errno的构造函数, 
     *  
     *        异常抛出时直接获取的错误信息
     *  
     * @param buffer 异常的告警信息 
     * @param err    错误码, 可用strerror获取错误信息
     */
    TC_Exception(const string &buffer, int err);

}

鹅厂开源框架tars之基础组件相关推荐

  1. 微服务开源框架TARS 之 基础组件

    作者 herman 导语 本文源自herman的系列文章之一<鹅厂开源框架TARS之基础组件>.相关代码已按TARS开源社区最新版本更新. TARS开源框架库里面用C++实现了比较多的公用 ...

  2. 微服务开源框架TARS 之 框架服务解析

    作者 herman 简介 本文源自herman的系列文章之一<鹅厂开源框架TARS之运营服务监控>.相关代码已按TARS开源社区最新版本更新. TARS框架为用户提供了涉及到开发.运维.以 ...

  3. 腾讯大数据回答2019:鹅厂开源先锋,日均计算量超30万亿,全力打破数据墙

    乾明 发自 腾讯汇  量子位 报道 | 公众号 QbitAI 开源,开源,开源. 这就是腾讯2019年技术领域最直观的变化. 最新代表事件,来自于腾讯首个开源的AI项目Angel,完成3.0版本进化后 ...

  4. 鹅厂开源先锋,日均计算量超30万亿,全力打破数据墙

    开源,开源,开源. 这就是腾讯2019年技术领域最直观的变化. 最新代表事件,来自于腾讯首个开源的AI项目Angel,完成3.0版本进化后,得到全球技术专家认可,从开源基金会LF AI毕业,成为业内顶 ...

  5. 微服务开源框架TARS的RPC源码解析 之 初识TARS C++服务端

    作者:Cony 导语:微服务开源框架TARS的RPC调用包含客户端与服务端,<微服务开源框架TARS的RPC源码解析>系列文章将从初识客户端.客户端的同步及异步调用.初识服务端.服务端的工 ...

  6. 【设计思想解读开源框架】Java基础入门清华大学出版社课后答案

    Java基础 JDK 和 JRE 有什么区别? == 和 equals 的区别是什么? 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗? final 在 java ...

  7. 腾讯开源框架Tars学习 (1、搭建平台,输出hello world)

    1.使用docker 搭建平台环境 ##拉取最新的镜像 docker pull tarscloud/framework:latest #拉取节点镜像 docker pull tarscloud/tar ...

  8. 深度学习开源框架系列:基础算法之傅立叶变换:1:概要介绍

    傅立叶变换时数字信号处理的重要方法之一,是法国数学家傅立叶在1807年在法国科学学会上发表的一篇文章中所提出的,在文章中使用了正弦函数描述温度分布,而且提出了一个著名的论断:任何连续性的周期信号都可以 ...

  9. 马化腾首谈腾讯开源时,鹅厂已在Github上放出82个项目,标星24万+ | 附AI项目链接...

    乾明 发自 凹非寺  量子位 报道 | 公众号 QbitAI "腾讯希望在科研领域投入更多力量,把'科技向善'纳入公司新的使命与愿景.我们将通过内外部开放源代码等方式,积极参与'全球科技共同 ...

  10. 基础组件完善的今天,如何通过业务组件提效?

    简介: 无论是在前端刀耕火种的 jQuery/YUI 时代,还是到现在基于数据驱动 UI 的 React/Vue 时代,物料/组件一直是前端永恒的话题.基于大量重复逻辑的封装可以很显而易见地提升前端 ...

最新文章

  1. Java项目:校园外卖点餐系统(java+SSM+JSP+maven+mysql)
  2. NeHe教程Qt实现——lesson11
  3. 一个完整的canvas画图
  4. 使用OpenGL实现翻书动画
  5. cesium three性能比较_mapboxgl + three 动画 — 网格热图
  6. sdut 2074 区间覆盖问题(贪心)
  7. python定位元素在列表中的位置_python定位列表元素
  8. 牵引力教育推荐最先进的5大敏捷PHP开发框架
  9. IntelliJ IDEA-遇见的问题总结
  10. [ZJOI2010]排列计数
  11. 备案号链接工信部_网站主页底部网站备案号的悬挂和链接的工作通知
  12. java中使用阻塞队列实现生产这与消费这之间的关系
  13. 百元百鸡 //构造结构体变量
  14. 明日方舟 长夜临光side story
  15. Redis下载与安装教程
  16. 局域网如何设置服务器
  17. reactjs前端实现文件新窗口下载
  18. 基于msm8916移植lcd流程
  19. Visual Studio 2017 C# 对 AutoCad2014 的二次开发设置
  20. win10 64位安装32位office

热门文章

  1. 基于STM32的物联网健康监测系统设计(附源码)
  2. 愚见《Nighttime Visibility Enhancement by Increasing the Dynamic Range andSuppression of Light Effects》
  3. 职业生涯必备——程序员“黑话”指南
  4. mysql知识系列:报错right syntax to use near IDENTIFIED BY
  5. 人人都在谈的 “数据驱动” 到底是什么?你确认自己做的是数据驱动吗?
  6. 虚幻官方文档使用UMG(虚幻运动图像)的用户界面笔记[VS2019]
  7. 矩阵分解在推荐系统中的应用及实践
  8. c语言26字母排序,C语言,26个字母的冒泡排序
  9. numpy_flatten函数
  10. Label的常用属性设置和使用