C++音视频流媒体服务器SRS源码框架解读,协程库state-threads的使用

本章内容解读SRS开源代码框架,无二次开发,以学习交流为目的。

SRS是国人开发的开源流媒体服务器,C++语言开发,本章使用版本:https://github.com/ossrs/srs/tree/5.0release

SRS协程库ST的使用

C语言协程库state-threads(简称ST库):https://sourceforge.net/projects/state-threads/
SRS对state-threads库进行了2次开发 :https://github.com/ossrs/state-threads

1、ST库的编译
在下载的srs-5.0release.zip安装包里有ST源码,直接编译,在什么平台用就在什么平台编译,centos编译的库拿到ubuntu里用是不行的。

cd /srs-5.0release/trunk/3rdparty/st-srs
make linux-debug	#编译

在构建目录生成库文件libst.a,头文件st.h。

2、ST库的使用
SRS封装了协程类SrsSTCoroutine,通过C++类的继承和虚函数回调,实现了在回调函数执行协程处理函数(和linux线程库函数pthread_create用法类似)。

3、SrsContextId上下文ID
SRS上下文ID,由数字和英文字母串组成的随机8位字符串,用于唯一标记一个协程,SRS每个服务端、客户端都在一个单独协程工作,因此有了上下文ID就能找到对应的客户端服务端,日志也会打印出当前协程的上下文ID。
SRS上下文ID是写入ST协程库里的,当协程切换时可以自动获取当前协程上下文ID,使用很方便。

4、SrsAutoFree
这部分代码还包含了SrsAutoFree定义,可以在离开作用域时自动释放指针,也是很有用的一个模块。

源码

源码结构如下:
├── chw_adapt.cpp
├── chw_adapt.h
├── srs_app_st.cpp
├── srs_app_st.hpp
├── srs_kernel_error.cpp
├── srs_kernel_error.hpp
├── srs_protocol_log.cpp
├── srs_protocol_log.hpp
├── srs_protocol_st.cpp
└── srs_protocol_st.hpp

srs_kernel_error源码参考这里:SRS开源代码框架,错误类(SrsCplxError)的使用。日志打印使用printf代替。

chw_adapt.h

#ifndef CHW_ADAPT_H
#define CHW_ADAPT_H

#include <string>
#include <inttypes.h>
#include <unistd.h>

using namespace std;
typedef int64_t srs_utime_t;

#define srs_min(a, b) (((a) < (b))? (a) : (b))
#define srs_max(a, b) (((a) < (b))? (b) : (a))

#define SYS_TIME_RESOLUTION_US 300*1000
#define SRS_UTIME_MILLISECONDS 1000
#define srsu2ms(us) ((us) / SRS_UTIME_MILLISECONDS)
#define srsu2msi(us) int((us) / SRS_UTIME_MILLISECONDS)

// Never timeout.
#define SRS_UTIME_NO_TIMEOUT ((srs_utime_t) -1LL)

// 自动释放对象
// To delete object.
#define SrsAutoFree(className, instance) \
    impl_SrsAutoFree<className> _auto_free_##instance(&instance, false, false, NULL)
// To delete array.
#define SrsAutoFreeA(className, instance) \
    impl_SrsAutoFree<className> _auto_free_array_##instance(&instance, true, false, NULL)
// Use free instead of delete.
#define SrsAutoFreeF(className, instance) \
    impl_SrsAutoFree<className> _auto_free_##instance(&instance, false, true, NULL)
// Use hook instead of delete.
#define SrsAutoFreeH(className, instance, hook) \
    impl_SrsAutoFree<className> _auto_free_##instance(&instance, false, false, hook)
// The template implementation.
template<class T>
class impl_SrsAutoFree
{
private:
    T** ptr;
    bool is_array;
    bool _use_free;
    void (*_hook)(T*);
public:
    // If use_free, use free(void*) to release the p.
    // If specified hook, use hook(p) to release it.
    // Use delete to release p, or delete[] if p is an array.
    impl_SrsAutoFree(T** p, bool array, bool use_free, void (*hook)(T*)) {
        ptr = p;
        is_array = array;
        _use_free = use_free;
        _hook = hook;
    }

    virtual ~impl_SrsAutoFree() {
        if (ptr == NULL || *ptr == NULL) {
            return;
        }

        if (_use_free) {
            free(*ptr);
        } else if (_hook) {
            _hook(*ptr);
        } else {
            if (is_array) {
                delete[] *ptr;
            } else {
                delete *ptr;
            }
        }

        *ptr = NULL;
    }
};
//SRS 上下文ID,就是个字符串
class _SrsContextId
{
private:
    std::string v_;//上下文ID
public:
    _SrsContextId();
    _SrsContextId(const _SrsContextId& cp);
    _SrsContextId& operator=(const _SrsContextId& cp);
    virtual ~_SrsContextId();
public:
    const char* c_str() const;
    bool empty() const;
    // Compare the two context id. @see http://www.cplusplus.com/reference/string/string/compare/
    //      0	They compare equal
    //      <0	Either the value of the first character that does not match is lower in the compared string, or all compared characters match but the compared string is shorter.
    //      >0	Either the value of the first character that does not match is greater in the compared string, or all compared characters match but the compared string is longer.
    int compare(const _SrsContextId& to) const;
    // Set the value of context id.
    _SrsContextId& set_value(const std::string& v);
};
typedef _SrsContextId SrsContextId;

class ISrsContext
{
public:
    ISrsContext();
    virtual ~ISrsContext();
public:
    // Generate a new context id.
    // @remark We do not set to current thread, user should do this.
    virtual SrsContextId generate_id() = 0;//生成上下文ID
    // Get the context id of current thread.
    virtual const SrsContextId& get_id() = 0;//获取上下文ID
    // Set the context id of current thread.
    // @return the current context id.
    virtual const SrsContextId& set_id(const SrsContextId& v) = 0;//设置上下文ID
};

// @global User must implements the LogContext and define a global instance.
extern ISrsContext* _srs_context;

std::string srs_random_str(int len);//生成len个随机数
#endif // CHW_ADAPT_H

chw_adapt.cpp

#include "chw_adapt.h"

#include <sys/time.h>

srs_utime_t _srs_system_time_us_cache = 0;
srs_utime_t _srs_system_time_startup_time = 0;
typedef int (*srs_gettimeofday_t) (struct timeval* tv, struct timezone* tz);
srs_gettimeofday_t _srs_gettimeofday = (srs_gettimeofday_t)::gettimeofday;
srs_utime_t srs_update_system_time()
{
    timeval now;

    if (_srs_gettimeofday(&now, NULL) < 0) {
        printf("gettimeofday failed, ignore");
        return -1;
    }

    // we must convert the tv_sec/tv_usec to int64_t.
    int64_t now_us = ((int64_t)now.tv_sec) * 1000 * 1000 + (int64_t)now.tv_usec;

    // for some ARM os, the starttime maybe invalid,
    // for example, on the cubieboard2, the srs_startup_time is 1262304014640,
    // while now is 1403842979210 in ms, diff is 141538964570 ms, 1638 days
    // it's impossible, and maybe the problem of startup time is invalid.
    // use date +%s to get system time is 1403844851.
    // so we use relative time.
    if (_srs_system_time_us_cache <= 0) {
        _srs_system_time_startup_time = _srs_system_time_us_cache = now_us;
        return _srs_system_time_us_cache;
    }

    // use relative time.
    int64_t diff = now_us - _srs_system_time_us_cache;
    diff = srs_max(0, diff);
    if (diff < 0 || diff > 1000 * SYS_TIME_RESOLUTION_US) {
        printf("clock jump, history=%" PRId64 "us, now=%" PRId64 "us, diff=%" PRId64 "us", _srs_system_time_us_cache, now_us, diff);
        _srs_system_time_startup_time += diff;
    }

    _srs_system_time_us_cache = now_us;
    printf("clock updated, startup=%" PRId64 "us, now=%" PRId64 "us", _srs_system_time_startup_time, _srs_system_time_us_cache);

    return _srs_system_time_us_cache;
}

long srs_random()
{
    static bool _random_initialized = false;
    if (!_random_initialized) {
        _random_initialized = true;
        ::srandom((unsigned long)(srs_update_system_time() | (::getpid()<<13)));
    }

    return random();
}

std::string srs_random_str(int len)
{
    static string random_table = "01234567890123456789012345678901234567890123456789abcdefghijklmnopqrstuvwxyz";

    string ret;
    ret.reserve(len);
    for (int i = 0; i < len; ++i) {
        ret.append(1, random_table[srs_random() % random_table.size()]);
    }

    return ret;
}



_SrsContextId::_SrsContextId()
{
}

_SrsContextId::_SrsContextId(const _SrsContextId& cp)
{
    v_ = cp.v_;
}

_SrsContextId& _SrsContextId::operator=(const _SrsContextId& cp)
{
    v_ = cp.v_;
    return *this;
}

_SrsContextId::~_SrsContextId()
{
}

const char* _SrsContextId::c_str() const
{
    return v_.c_str();
}

bool _SrsContextId::empty() const
{
    return v_.empty();
}

int _SrsContextId::compare(const _SrsContextId& to) const
{
    return v_.compare(to.v_);
}

_SrsContextId& _SrsContextId::set_value(const std::string& v)
{
    v_ = v;
    return *this;
}

ISrsContext::ISrsContext()
{
}

ISrsContext::~ISrsContext()
{
}

srs_app_st.hpp

#ifndef SRS_APP_ST_HPP
#define SRS_APP_ST_HPP

#include <string>
#include "chw_adapt.h"

#include <srs_kernel_error.hpp>
#include <srs_protocol_st.hpp>
#include "srs_protocol_log.hpp"

class SrsFastCoroutine;
// 每个协程都要继承这个类
class ISrsCoroutineHandler
{
public:
    ISrsCoroutineHandler();
    virtual ~ISrsCoroutineHandler();
public:
    // Do the work. The ST-coroutine will terminated normally if it returned.
    // @remark If the cycle has its own loop, it must check the thread pull.
    // 协程处理函数,如果返回则协程结束
    virtual srs_error_t cycle() = 0;
};

// Start the object, generally a croutine.
// 通常是启动一个ST对象
class ISrsStartable
{
public:
    ISrsStartable();
    virtual ~ISrsStartable();
public:
    virtual srs_error_t start() = 0;
};

// The corotine object.
// 协程基类
class SrsCoroutine : public ISrsStartable
{
public:
    SrsCoroutine();
    virtual ~SrsCoroutine();
public:
    virtual void stop() = 0;
    virtual void interrupt() = 0;
    // @return a copy of error, which should be freed by user.
    //      NULL if not terminated and user should pull again.
    virtual srs_error_t pull() = 0;
    // Get and set the context id of coroutine.
    virtual const SrsContextId& cid() = 0;
    virtual void set_cid(const SrsContextId& cid) = 0;
};

// An empty coroutine, user can default to this object before create any real coroutine.
// @see https://github.com/ossrs/srs/pull/908
// 一个空的协程,用户可以在创建任何真正的协程序之前默认为这个对象。
class SrsDummyCoroutine : public SrsCoroutine
{
private:
    SrsContextId cid_;
public:
    SrsDummyCoroutine();
    virtual ~SrsDummyCoroutine();
public:
    virtual srs_error_t start();
    virtual void stop();
    virtual void interrupt();
    virtual srs_error_t pull();
    virtual const SrsContextId& cid();
    virtual void set_cid(const SrsContextId& cid);
};

// A ST-coroutine is a lightweight thread, just like the goroutine.
// But the goroutine maybe run on different thread, while ST-coroutine only
// run in single thread, because it use setjmp and longjmp, so it may cause
// problem in multiple threads. For SRS, we only use single thread module,
// like NGINX to get very high performance, with asynchronous and non-blocking
// sockets.
// @reamrk For multiple processes, please use go-oryx to fork many SRS processes.
//      Please read https://github.com/ossrs/go-oryx
// @remark For debugging of ST-coroutine, read _st_iterate_threads_flag of ST/README
//      https://github.com/ossrs/state-threads/blob/st-1.9/README#L115
// @remark We always create joinable thread, so we must join it or memory leak,
//      Please read https://github.com/ossrs/srs/issues/78

// ST-coroutine是一个轻量级的线程,就像goroutine一样。
// 但是goroutine可能在不同的线程上运行,而ST-coroutine只在单个线程中运行,因为它使用了setjmp和longjmp,所以它可能会在多个线程中导致问题。
// 对于SRS,我们只使用单线程模块,类似NGINX,来获得非常高的性能,具有异步和非阻塞套接字。
// 对于多个进程,请使用go-oryx来fork多个SRS进程。
//SrsSTCoroutine 是对协程的封装
class SrsSTCoroutine : public SrsCoroutine
{
private:
    SrsFastCoroutine* impl_;
public:
    // Create a thread with name n and handler h.
    // @remark User can specify a cid for thread to use, or we will allocate a new one.
    SrsSTCoroutine(std::string n, ISrsCoroutineHandler* h);
    SrsSTCoroutine(std::string n, ISrsCoroutineHandler* h, SrsContextId cid);
    virtual ~SrsSTCoroutine();
public:
    // Set the stack size of coroutine, default to 0(64KB).
    void set_stack_size(int v);
public:
    // Start the thread.
    // @remark Should never start it when stopped or terminated.
    virtual srs_error_t start();//启动协程
    // Interrupt the thread then wait to terminated.
    // @remark If user want to notify thread to quit async, for example if there are
    //      many threads to stop like the encoder, use the interrupt to notify all threads
    //      to terminate then use stop to wait for each to terminate.
    virtual void stop();//停止协程
    // Interrupt the thread and notify it to terminate, it will be wakeup if it's blocked
    // in some IO operations, such as st_read or st_write, then it will found should quit,
    // finally the thread should terminated normally, user can use the stop to join it.
    //中断线程并通知它终止,如果它在一些IO操作中被阻止,如st_read或st_write,它就会被唤醒,然后它就会发现应该退出,最后线程会正常终止,用户可以使用stop加入它。
    virtual void interrupt();
    // Check whether thread is terminated normally or error(stopped or termianted with error),
    // and the thread should be running if it return ERROR_SUCCESS.
    // @remark Return specified error when thread terminated normally with error.
    // @remark Return ERROR_THREAD_TERMINATED when thread terminated normally without error.
    // @remark Return ERROR_THREAD_INTERRUPED when thread is interrupted.
    virtual srs_error_t pull();//检查线程是否正常终止或错误(停止或出现错误),如果返回ERROR_SUCCESS,线程应该运行。
    // Get and set the context id of thread.
    virtual const SrsContextId& cid();//获取上下文ID
    virtual void set_cid(const SrsContextId& cid);//设置上下文ID
};

// High performance coroutine.
// 高性能协程,真正调用ST库启动协成
class SrsFastCoroutine
{
private:
    std::string name;
    int stack_size;
    ISrsCoroutineHandler* handler;//基类指针,指向要启动协程的那个对象
private:
    srs_thread_t trd;
    SrsContextId cid_;//当前协程的上下文ID
    srs_error_t trd_err;
private:
    bool started;
    bool interrupted;
    bool disposed;
    // Cycle done, no need to interrupt it.
    bool cycle_done;
private:
    // Sub state in disposed, we need to wait for thread to quit.
    // 子状态被处理后,我们需要等待线程退出。
    bool stopping_;
    SrsContextId stopping_cid_;
public:
    SrsFastCoroutine(std::string n, ISrsCoroutineHandler* h);
    SrsFastCoroutine(std::string n, ISrsCoroutineHandler* h, SrsContextId cid);
    virtual ~SrsFastCoroutine();
public:
    void set_stack_size(int v);
public:
    srs_error_t start();//创建协程
    void stop();
    void interrupt();
    inline srs_error_t pull() {
        if (trd_err == srs_success) {
            return srs_success;
        }
        return srs_error_copy(trd_err);
    }
    const SrsContextId& cid();//获取上下文ID
    virtual void set_cid(const SrsContextId& cid);//设置上下文ID
private:
    srs_error_t cycle();//启动协程处理函数
    static void* pfn(void* arg);
};

// Like goroutine sync.WaitGroup.
// 类似go语言的sync.WaitGroup
class SrsWaitGroup
{
private:
    int nn_;
    srs_cond_t done_;
public:
    SrsWaitGroup();
    virtual ~SrsWaitGroup();
public:
    // When start for n coroutines.
    void add(int n);
    // When coroutine is done.
    void done();
    // Wait for all corotine to be done.
    void wait();
};

#endif


srs_app_st.cpp

#include <srs_app_st.hpp>

#include <string>
using namespace std;

#include <srs_kernel_error.hpp>

ISrsCoroutineHandler::ISrsCoroutineHandler()
{
}

ISrsCoroutineHandler::~ISrsCoroutineHandler()
{
}

ISrsStartable::ISrsStartable()
{
}

ISrsStartable::~ISrsStartable()
{
}

SrsCoroutine::SrsCoroutine()
{
}

SrsCoroutine::~SrsCoroutine()
{
}

SrsDummyCoroutine::SrsDummyCoroutine()
{
}

SrsDummyCoroutine::~SrsDummyCoroutine()
{
}

srs_error_t SrsDummyCoroutine::start()
{
    return srs_error_new(ERROR_THREAD, "dummy coroutine");
}

void SrsDummyCoroutine::stop()
{
}

void SrsDummyCoroutine::interrupt()
{
}

srs_error_t SrsDummyCoroutine::pull()
{
    return srs_error_new(ERROR_THREAD, "dummy pull");
}

const SrsContextId& SrsDummyCoroutine::cid()
{
    return cid_;
}

void SrsDummyCoroutine::set_cid(const SrsContextId& cid)
{
    cid_ = cid;
}

SrsSTCoroutine::SrsSTCoroutine(string n, ISrsCoroutineHandler* h)
{
    impl_ = new SrsFastCoroutine(n, h);
}

SrsSTCoroutine::SrsSTCoroutine(string n, ISrsCoroutineHandler* h, SrsContextId cid)
{
    impl_ = new SrsFastCoroutine(n, h, cid);
}

SrsSTCoroutine::~SrsSTCoroutine()
{
    srs_freep(impl_);
}

void SrsSTCoroutine::set_stack_size(int v)
{
    impl_->set_stack_size(v);
}

srs_error_t SrsSTCoroutine::start()
{
    return impl_->start();
}

void SrsSTCoroutine::stop()
{
    impl_->stop();
}

void SrsSTCoroutine::interrupt()
{
    impl_->interrupt();
}

srs_error_t SrsSTCoroutine::pull()
{
    return impl_->pull();
}

const SrsContextId& SrsSTCoroutine::cid()
{
    return impl_->cid();
}

void SrsSTCoroutine::set_cid(const SrsContextId& cid)
{
    impl_->set_cid(cid);
}

SrsFastCoroutine::SrsFastCoroutine(string n, ISrsCoroutineHandler* h)
{
    // TODO: FIXME: Reduce duplicated code.
    name = n;
    handler = h;
    trd = NULL;
    trd_err = srs_success;
    started = interrupted = disposed = cycle_done = false;
    stopping_ = false;

    //  0 use default, default is 64K.
    stack_size = 0;
}

SrsFastCoroutine::SrsFastCoroutine(string n, ISrsCoroutineHandler* h, SrsContextId cid)
{
    name = n;
    handler = h;
    cid_ = cid;
    trd = NULL;
    trd_err = srs_success;
    started = interrupted = disposed = cycle_done = false;
    stopping_ = false;

    //  0 use default, default is 64K.
    stack_size = 0;
}

SrsFastCoroutine::~SrsFastCoroutine()
{
    stop();

    // TODO: FIXME: We must assert the cycle is done.

    srs_freep(trd_err);
}

void SrsFastCoroutine::set_stack_size(int v)
{
    stack_size = v;
}

srs_error_t SrsFastCoroutine::start()
{
    srs_error_t err = srs_success;

    if (started || disposed) {
        if (disposed) {
            err = srs_error_new(ERROR_THREAD, "disposed");
        } else {
            err = srs_error_new(ERROR_THREAD, "started");
        }

        if (trd_err == srs_success) {
            trd_err = srs_error_copy(err);
        }

        return err;
    }

    if ((trd = (srs_thread_t)_pfn_st_thread_create(pfn, this, 1, stack_size)) == NULL) {
        err = srs_error_new(ERROR_THREAD, "create failed");

        srs_freep(trd_err);
        trd_err = srs_error_copy(err);

        return err;
    }

    started = true;

    return err;
}

void SrsFastCoroutine::stop()
{
    if (disposed) {
        if (stopping_) {
            /*srs_error*/printf("thread is stopping by %s\n", stopping_cid_.c_str());
            srs_assert(!stopping_);
        }
        return;
    }
    disposed = true;
    stopping_ = true;

    interrupt();

    // When not started, the trd is NULL.
    if (trd) {
        void* res = NULL;
        int r0 = srs_thread_join(trd, &res);
        if (r0) {
            // By st_thread_join
            if (errno == EINVAL) srs_assert(!r0);
            if (errno == EDEADLK) srs_assert(!r0);
            // By st_cond_timedwait
            if (errno == EINTR) srs_assert(!r0);
            if (errno == ETIME) srs_assert(!r0);
            // Others
            srs_assert(!r0);
        }

        srs_error_t err_res = (srs_error_t)res;
        if (err_res != srs_success) {
            // When worker cycle done, the error has already been overrided,
            // so the trd_err should be equal to err_res.
            srs_assert(trd_err == err_res);
        }
    }

    // If there's no error occur from worker, try to set to terminated error.
    if (trd_err == srs_success && !cycle_done) {
        trd_err = srs_error_new(ERROR_THREAD, "terminated");
    }

    // Now, we'are stopped.
    stopping_ = false;

    return;
}

void SrsFastCoroutine::interrupt()
{
    if (!started || interrupted || cycle_done) {
        return;
    }
    interrupted = true;

    if (trd_err == srs_success) {
        trd_err = srs_error_new(ERROR_THREAD, "interrupted");
    }

    // Note that if another thread is stopping thread and waiting in st_thread_join,
    // the interrupt will make the st_thread_join fail.
    srs_thread_interrupt(trd);
}

const SrsContextId& SrsFastCoroutine::cid()
{
    return cid_;
}

void SrsFastCoroutine::set_cid(const SrsContextId& cid)
{
    cid_ = cid;
    srs_context_set_cid_of(trd, cid);
}

srs_error_t SrsFastCoroutine::cycle()
{
    if (_srs_context) {
        if (cid_.empty()) {
            cid_ = _srs_context->generate_id();
        }
        _srs_context->set_id(cid_);
    }

    srs_error_t err = handler->cycle();
    if (err != srs_success) {
        return srs_error_wrap(err, "coroutine cycle");
    }

    // Set cycle done, no need to interrupt it.
    cycle_done = true;

    return err;
}

void* SrsFastCoroutine::pfn(void* arg)
{
    SrsFastCoroutine* p = (SrsFastCoroutine*)arg;

    srs_error_t err = p->cycle();

    // Set the err for function pull to fetch it.
    // @see https://github.com/ossrs/srs/pull/1304#issuecomment-480484151
    if (err != srs_success) {
        srs_freep(p->trd_err);
        // It's ok to directly use it, because it's returned by st_thread_join.
        p->trd_err = err;
    }

    return (void*)err;
}

SrsWaitGroup::SrsWaitGroup()
{
    nn_ = 0;
    done_ = srs_cond_new();
}

SrsWaitGroup::~SrsWaitGroup()
{
    wait();
    srs_cond_destroy(done_);
}

void SrsWaitGroup::add(int n)
{
    nn_ += n;
}

void SrsWaitGroup::done()
{
    nn_--;
    if (nn_ <= 0) {
        srs_cond_signal(done_);
    }
}

void SrsWaitGroup::wait()
{
    if (nn_ > 0) {
        srs_cond_wait(done_);
    }
}

srs_protocol_log.hpp

#ifndef SRS_PROTOCOL_LOG_HPP
#define SRS_PROTOCOL_LOG_HPP

#include <map>
#include <string>

#include <srs_protocol_st.hpp>
#include "chw_adapt.h"

// The st thread context, get_id will get the st-thread id,
// which identify the client.
// st线程上下文,get_id将得到标识客户端的st线程id。
class SrsThreadContext : public ISrsContext
{
private:
    std::map<srs_thread_t, SrsContextId> cache;
public:
    SrsThreadContext();
    virtual ~SrsThreadContext();
public:
    virtual SrsContextId generate_id();
    virtual const SrsContextId& get_id();
    virtual const SrsContextId& set_id(const SrsContextId& v);
private:
    virtual void clear_cid();
};

// Set the context id of specified thread, not self.
// 设置指定线程的上下文id,而不是设置自身。
extern const SrsContextId& srs_context_set_cid_of(srs_thread_t trd, const SrsContextId& v);

// The context restore stores the context and restore it when done.
// 上下文恢复存储上下文,并在完成时进行恢复。
// Usage:
//      SrsContextRestore(_srs_context->get_id());
#define SrsContextRestore(cid) impl_SrsContextRestore _context_restore_instance(cid)
class impl_SrsContextRestore
{
private:
    SrsContextId cid_;
public:
    impl_SrsContextRestore(SrsContextId cid);
    virtual ~impl_SrsContextRestore();
};

#endif

srs_protocol_log.cpp

#include <srs_protocol_log.hpp>

#include <stdarg.h>
#include <sys/time.h>
#include <unistd.h>
#include <sstream>
using namespace std;

#include <srs_kernel_error.hpp>

//SrsPps* _srs_pps_cids_get = NULL;
//SrsPps* _srs_pps_cids_set = NULL;

#define SRS_BASIC_LOG_SIZE 8192

SrsThreadContext::SrsThreadContext()
{
}

SrsThreadContext::~SrsThreadContext()
{
}

SrsContextId SrsThreadContext::generate_id()
{
    SrsContextId cid = SrsContextId();
    return cid.set_value(srs_random_str(8));
}

static SrsContextId _srs_context_default;
static int _srs_context_key = -1;
void _srs_context_destructor(void* arg)
{
    SrsContextId* cid = (SrsContextId*)arg;
    srs_freep(cid);
}

const SrsContextId& SrsThreadContext::get_id()
{
//    ++_srs_pps_cids_get->sugar;

    if (!srs_thread_self()) {
        return _srs_context_default;
    }

    void* cid = srs_thread_getspecific(_srs_context_key);
    if (!cid) {
        return _srs_context_default;
    }

    return *(SrsContextId*)cid;
}

const SrsContextId& SrsThreadContext::set_id(const SrsContextId& v)
{
    return srs_context_set_cid_of(srs_thread_self(), v);
}

void SrsThreadContext::clear_cid()
{
}

const SrsContextId& srs_context_set_cid_of(srs_thread_t trd, const SrsContextId& v)
{
//    ++_srs_pps_cids_set->sugar;

    if (!trd) {
        _srs_context_default = v;
        return v;
    }

    SrsContextId* cid = new SrsContextId();
    *cid = v;

    if (_srs_context_key < 0) {
        int r0 = srs_key_create(&_srs_context_key, _srs_context_destructor);
        srs_assert(r0 == 0);
    }

    int r0 = srs_thread_setspecific2(trd, _srs_context_key, cid);
    srs_assert(r0 == 0);

    return v;
}

impl_SrsContextRestore::impl_SrsContextRestore(SrsContextId cid)
{
    cid_ = cid;
}

impl_SrsContextRestore::~impl_SrsContextRestore()
{
    _srs_context->set_id(cid_);
}

srs_protocol_st.hpp

#ifndef SRS_PROTOCOL_ST_HPP
#define SRS_PROTOCOL_ST_HPP

#include "chw_adapt.h"
#include <string>

#include <srs_kernel_error.hpp>
// Wrap for coroutine.
// 对ST库的封装,提供接口
typedef void* srs_netfd_t;
typedef void* srs_thread_t;
typedef void* srs_cond_t;
typedef void* srs_mutex_t;

// Initialize ST, requires epoll for linux.
extern srs_error_t srs_st_init();
// Destroy ST, free resources for asan detecting.
extern void srs_st_destroy(void);

// Close the netfd, and close the underlayer fd.
// @remark when close, user must ensure io completed.
extern void srs_close_stfd(srs_netfd_t& stfd);

// Set the FD_CLOEXEC of FD.
extern srs_error_t srs_fd_closeexec(int fd);

// Set the SO_REUSEADDR of fd.
extern srs_error_t srs_fd_reuseaddr(int fd);

// Set the SO_REUSEPORT of fd.
extern srs_error_t srs_fd_reuseport(int fd);

// Set the SO_KEEPALIVE of fd.
extern srs_error_t srs_fd_keepalive(int fd);

// Get current coroutine/thread.
extern srs_thread_t srs_thread_self();
extern void srs_thread_exit(void* retval);
extern int srs_thread_join(srs_thread_t thread, void **retvalp);
extern void srs_thread_interrupt(srs_thread_t thread);
extern void srs_thread_yield();

// For utest to mock the thread create.
typedef void* (*_ST_THREAD_CREATE_PFN)(void *(*start)(void *arg), void *arg, int joinable, int stack_size);
extern _ST_THREAD_CREATE_PFN _pfn_st_thread_create;

// For client, to open socket and connect to server.
// @param tm The timeout in srs_utime_t.
extern srs_error_t srs_tcp_connect(std::string server, int port, srs_utime_t tm, srs_netfd_t* pstfd);

// For server, listen at TCP endpoint.
extern srs_error_t srs_tcp_listen(std::string ip, int port, srs_netfd_t* pfd);

// For server, listen at UDP endpoint.
extern srs_error_t srs_udp_listen(std::string ip, int port, srs_netfd_t* pfd);

// Wrap for coroutine.
extern srs_cond_t srs_cond_new();
extern int srs_cond_destroy(srs_cond_t cond);
extern int srs_cond_wait(srs_cond_t cond);
extern int srs_cond_timedwait(srs_cond_t cond, srs_utime_t timeout);
extern int srs_cond_signal(srs_cond_t cond);
extern int srs_cond_broadcast(srs_cond_t cond);

extern srs_mutex_t srs_mutex_new();
extern int srs_mutex_destroy(srs_mutex_t mutex);
extern int srs_mutex_lock(srs_mutex_t mutex);
extern int srs_mutex_unlock(srs_mutex_t mutex);

extern int srs_key_create(int* keyp, void (*destructor)(void*));
extern int srs_thread_setspecific(int key, void* value);
extern int srs_thread_setspecific2(srs_thread_t thread, int key, void* value);
extern void* srs_thread_getspecific(int key);

extern int srs_netfd_fileno(srs_netfd_t stfd);

extern int srs_usleep(srs_utime_t usecs);

extern srs_netfd_t srs_netfd_open_socket(int osfd);
extern srs_netfd_t srs_netfd_open(int osfd);

extern int srs_recvfrom(srs_netfd_t stfd, void *buf, int len, struct sockaddr *from, int *fromlen, srs_utime_t timeout);
extern int srs_sendto(srs_netfd_t stfd, void *buf, int len, const struct sockaddr *to, int tolen, srs_utime_t timeout);
extern int srs_recvmsg(srs_netfd_t stfd, struct msghdr *msg, int flags, srs_utime_t timeout);
extern int srs_sendmsg(srs_netfd_t stfd, const struct msghdr *msg, int flags, srs_utime_t timeout);

extern srs_netfd_t srs_accept(srs_netfd_t stfd, struct sockaddr *addr, int *addrlen, srs_utime_t timeout);

extern ssize_t srs_read(srs_netfd_t stfd, void *buf, size_t nbyte, srs_utime_t timeout);

extern bool srs_is_never_timeout(srs_utime_t tm);

#endif

srs_protocol_st.cpp

#include <srs_protocol_st.hpp>

#include <st.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
using namespace std;
#include <srs_kernel_error.hpp>

// nginx also set to 512
#define SERVER_LISTEN_BACKLOG 512

#ifdef __linux__
#include <sys/epoll.h>

bool srs_st_epoll_is_supported(void)
{
    struct epoll_event ev;

    ev.events = EPOLLIN;
    ev.data.ptr = NULL;
    /* Guaranteed to fail */
    epoll_ctl(-1, EPOLL_CTL_ADD, -1, &ev);

    return (errno != ENOSYS);
}
#endif

srs_error_t srs_st_init()
{
#ifdef __linux__
    // check epoll, some old linux donot support epoll.
    if (!srs_st_epoll_is_supported()) {
        return srs_error_new(ERROR_THREAD, "linux epoll disabled");
    }
#endif

    // Select the best event system available on the OS. In Linux this is
    // epoll(). On BSD it will be kqueue.
#if defined(SRS_CYGWIN64)
    if (st_set_eventsys(ST_EVENTSYS_SELECT) == -1) {
        return srs_error_new(ERROR_ST_SET_SELECT, "st enable st failed, current is %s", st_get_eventsys_name());
    }
#else
    if (st_set_eventsys(ST_EVENTSYS_ALT) == -1) {
        return srs_error_new(ERROR_THREAD, "st enable st failed, current is %s", st_get_eventsys_name());
    }
#endif

    // Before ST init, we might have already initialized the background cid.
//    SrsContextId cid = _srs_context->get_id();
//    if (cid.empty()) {
//        cid = _srs_context->generate_id();
//    }

    int r0 = 0;
    if((r0 = st_init()) != 0){
        return srs_error_new(ERROR_THREAD, "st initialize failed, r0=%d", r0);
    }

    // Switch to the background cid.
//    _srs_context->set_id(cid);
    printf("st_init success, use %s", st_get_eventsys_name());

    return srs_success;
}

void srs_st_destroy(void)
{
    st_destroy();
}

void srs_close_stfd(srs_netfd_t& stfd)
{
    if (stfd) {
        // we must ensure the close is ok.
        int r0 = st_netfd_close((st_netfd_t)stfd);
        if (r0) {
            // By _st_epoll_fd_close or _st_kq_fd_close
            if (errno == EBUSY) srs_assert(!r0);
            // By close
            if (errno == EBADF) srs_assert(!r0);
            if (errno == EINTR) srs_assert(!r0);
            if (errno == EIO) srs_assert(!r0);
            // Others
            srs_assert(!r0);
        }
        stfd = NULL;
    }
}

srs_error_t srs_fd_closeexec(int fd)
{
    int flags = fcntl(fd, F_GETFD);
    flags |= FD_CLOEXEC;
    if (fcntl(fd, F_SETFD, flags) == -1) {
        return srs_error_new(ERROR_THREAD, "FD_CLOEXEC fd=%d", fd);
    }

    return srs_success;
}

srs_error_t srs_fd_reuseaddr(int fd)
{
    int v = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &v, sizeof(int)) == -1) {
        return srs_error_new(ERROR_THREAD, "SO_REUSEADDR fd=%d", fd);
    }

    return srs_success;
}

srs_error_t srs_fd_reuseport(int fd)
{
#if defined(SO_REUSEPORT)
    int v = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &v, sizeof(int)) == -1) {
        printf("SO_REUSEPORT failed for fd=%d", fd);
    }
#else
    #warning "SO_REUSEPORT is not supported by your OS"
    srs_warn("SO_REUSEPORT is not supported util Linux kernel 3.9");
#endif

    return srs_success;
}

srs_error_t srs_fd_keepalive(int fd)
{
#ifdef SO_KEEPALIVE
    int v = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &v, sizeof(int)) == -1) {
        return srs_error_new(ERROR_THREAD, "SO_KEEPALIVE fd=%d", fd);
    }
#endif

    return srs_success;
}

srs_thread_t srs_thread_self()
{
    return (srs_thread_t)st_thread_self();
}

void srs_thread_exit(void* retval)
{
    st_thread_exit(retval);
}

int srs_thread_join(srs_thread_t thread, void **retvalp)
{
    return st_thread_join((st_thread_t)thread, retvalp);
}

void srs_thread_interrupt(srs_thread_t thread)
{
    st_thread_interrupt((st_thread_t)thread);
}

void srs_thread_yield()
{
    st_thread_yield();
}

_ST_THREAD_CREATE_PFN _pfn_st_thread_create = (_ST_THREAD_CREATE_PFN)st_thread_create;

srs_error_t srs_tcp_connect(string server, int port, srs_utime_t tm, srs_netfd_t* pstfd)
{
    st_utime_t timeout = ST_UTIME_NO_TIMEOUT;
    if (tm != SRS_UTIME_NO_TIMEOUT) {
        timeout = tm;
    }

    *pstfd = NULL;
    srs_netfd_t stfd = NULL;

    char sport[8];
    int r0 = snprintf(sport, sizeof(sport), "%d", port);
    srs_assert(r0 > 0 && r0 < (int)sizeof(sport));

    addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    addrinfo* r  = NULL;
    SrsAutoFreeH(addrinfo, r, freeaddrinfo);
    if(getaddrinfo(server.c_str(), sport, (const addrinfo*)&hints, &r)) {
        return srs_error_new(ERROR_THREAD, "get address info");
    }

    int sock = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
    if(sock == -1){
        return srs_error_new(ERROR_SOCKET_CREATE, "create socket");
    }

    srs_assert(!stfd);
    stfd = st_netfd_open_socket(sock);
    if(stfd == NULL){
        ::close(sock);
        return srs_error_new(ERROR_THREAD, "open socket");
    }

    if (st_connect((st_netfd_t)stfd, r->ai_addr, r->ai_addrlen, timeout) == -1){
        srs_close_stfd(stfd);
        return srs_error_new(ERROR_THREAD, "connect to %s:%d", server.c_str(), port);
    }

    *pstfd = stfd;
    return srs_success;
}

srs_error_t do_srs_tcp_listen(int fd, addrinfo* r, srs_netfd_t* pfd)
{
    srs_error_t err = srs_success;

    // Detect alive for TCP connection.
    // @see https://github.com/ossrs/srs/issues/1044
    if ((err = srs_fd_keepalive(fd)) != srs_success) {
        return srs_error_wrap(err, "set keepalive");
    }

    if ((err = srs_fd_closeexec(fd)) != srs_success) {
        return srs_error_wrap(err, "set closeexec");
    }

    if ((err = srs_fd_reuseaddr(fd)) != srs_success) {
        return srs_error_wrap(err, "set reuseaddr");
    }

    if ((err = srs_fd_reuseport(fd)) != srs_success) {
        return srs_error_wrap(err, "set reuseport");
    }

    if (::bind(fd, r->ai_addr, r->ai_addrlen) == -1) {
        return srs_error_new(ERROR_THREAD, "bind");
    }

    if (::listen(fd, SERVER_LISTEN_BACKLOG) == -1) {
        return srs_error_new(ERROR_THREAD, "listen");
    }

    if ((*pfd = srs_netfd_open_socket(fd)) == NULL){
        return srs_error_new(ERROR_THREAD, "st open");
    }

    return err;
}

srs_error_t srs_tcp_listen(std::string ip, int port, srs_netfd_t* pfd)
{
    srs_error_t err = srs_success;

    char sport[8];
    int r0 = snprintf(sport, sizeof(sport), "%d", port);
    srs_assert(r0 > 0 && r0 < (int)sizeof(sport));

    addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags    = AI_NUMERICHOST;

    addrinfo* r = NULL;
    SrsAutoFreeH(addrinfo, r, freeaddrinfo);
    if(getaddrinfo(ip.c_str(), sport, (const addrinfo*)&hints, &r)) {
        return srs_error_new(ERROR_THREAD, "getaddrinfo hints=(%d,%d,%d)",
            hints.ai_family, hints.ai_socktype, hints.ai_flags);
    }

    int fd = 0;
    if ((fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol)) == -1) {
        return srs_error_new(ERROR_SOCKET_CREATE, "socket domain=%d, type=%d, protocol=%d",
            r->ai_family, r->ai_socktype, r->ai_protocol);
    }

    if ((err = do_srs_tcp_listen(fd, r, pfd)) != srs_success) {
        ::close(fd);
        return srs_error_wrap(err, "fd=%d", fd);
    }

    return err;
}

srs_error_t do_srs_udp_listen(int fd, addrinfo* r, srs_netfd_t* pfd)
{
    srs_error_t err = srs_success;

    if ((err = srs_fd_closeexec(fd)) != srs_success) {
        return srs_error_wrap(err, "set closeexec");
    }

    if ((err = srs_fd_reuseaddr(fd)) != srs_success) {
        return srs_error_wrap(err, "set reuseaddr");
    }

    if ((err = srs_fd_reuseport(fd)) != srs_success) {
        return srs_error_wrap(err, "set reuseport");
    }

    if (::bind(fd, r->ai_addr, r->ai_addrlen) == -1) {
        return srs_error_new(ERROR_THREAD, "bind");
    }

    if ((*pfd = srs_netfd_open_socket(fd)) == NULL){
        return srs_error_new(ERROR_THREAD, "st open");
    }

    return err;
}

srs_error_t srs_udp_listen(std::string ip, int port, srs_netfd_t* pfd)
{
    srs_error_t err = srs_success;

    char sport[8];
    int r0 = snprintf(sport, sizeof(sport), "%d", port);
    srs_assert(r0 > 0 && r0 < (int)sizeof(sport));

    addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_flags    = AI_NUMERICHOST;

    addrinfo* r  = NULL;
    SrsAutoFreeH(addrinfo, r, freeaddrinfo);
    if(getaddrinfo(ip.c_str(), sport, (const addrinfo*)&hints, &r)) {
        return srs_error_new(ERROR_THREAD, "getaddrinfo hints=(%d,%d,%d)",
            hints.ai_family, hints.ai_socktype, hints.ai_flags);
    }

    int fd = 0;
    if ((fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol)) == -1) {
        return srs_error_new(ERROR_SOCKET_CREATE, "socket domain=%d, type=%d, protocol=%d",
            r->ai_family, r->ai_socktype, r->ai_protocol);
    }

    if ((err = do_srs_udp_listen(fd, r, pfd)) != srs_success) {
        ::close(fd);
        return srs_error_wrap(err, "fd=%d", fd);
    }

    return err;
}

srs_cond_t srs_cond_new()
{
    return (srs_cond_t)st_cond_new();
}

int srs_cond_destroy(srs_cond_t cond)
{
    return st_cond_destroy((st_cond_t)cond);
}

int srs_cond_wait(srs_cond_t cond)
{
    return st_cond_wait((st_cond_t)cond);
}

int srs_cond_timedwait(srs_cond_t cond, srs_utime_t timeout)
{
    return st_cond_timedwait((st_cond_t)cond, (st_utime_t)timeout);
}

int srs_cond_signal(srs_cond_t cond)
{
    return st_cond_signal((st_cond_t)cond);
}

int srs_cond_broadcast(srs_cond_t cond)
{
    return st_cond_broadcast((st_cond_t)cond);
}

srs_mutex_t srs_mutex_new()
{
    return (srs_mutex_t)st_mutex_new();
}

int srs_mutex_destroy(srs_mutex_t mutex)
{
    if (!mutex) {
        return 0;
    }
    return st_mutex_destroy((st_mutex_t)mutex);
}

int srs_mutex_lock(srs_mutex_t mutex)
{
    return st_mutex_lock((st_mutex_t)mutex);
}

int srs_mutex_unlock(srs_mutex_t mutex)
{
    return st_mutex_unlock((st_mutex_t)mutex);
}

int srs_key_create(int *keyp, void (*destructor)(void *))
{
    return st_key_create(keyp, destructor);
}

int srs_thread_setspecific(int key, void *value)
{
    return st_thread_setspecific(key, value);
}

void *srs_thread_getspecific(int key)
{
    return st_thread_getspecific(key);
}

int srs_thread_setspecific2(srs_thread_t thread, int key, void* value)
{
    return st_thread_setspecific2((st_thread_t)thread, key, value);
}

int srs_netfd_fileno(srs_netfd_t stfd)
{
    return st_netfd_fileno((st_netfd_t)stfd);
}

int srs_usleep(srs_utime_t usecs)
{
    return st_usleep((st_utime_t)usecs);
}

srs_netfd_t srs_netfd_open_socket(int osfd)
{
    return (srs_netfd_t)st_netfd_open_socket(osfd);
}

srs_netfd_t srs_netfd_open(int osfd)
{
    return (srs_netfd_t)st_netfd_open(osfd);
}

int srs_recvfrom(srs_netfd_t stfd, void *buf, int len, struct sockaddr *from, int *fromlen, srs_utime_t timeout)
{
    return st_recvfrom((st_netfd_t)stfd, buf, len, from, fromlen, (st_utime_t)timeout);
}

int srs_sendto(srs_netfd_t stfd, void *buf, int len, const struct sockaddr * to, int tolen, srs_utime_t timeout)
{
    return st_sendto((st_netfd_t)stfd, buf, len, to, tolen, (st_utime_t)timeout);
}

int srs_recvmsg(srs_netfd_t stfd, struct msghdr *msg, int flags, srs_utime_t timeout)
{
    return st_recvmsg((st_netfd_t)stfd, msg, flags, (st_utime_t)timeout);
}

int srs_sendmsg(srs_netfd_t stfd, const struct msghdr *msg, int flags, srs_utime_t timeout)
{
    return st_sendmsg((st_netfd_t)stfd, msg, flags, (st_utime_t)timeout);
}

srs_netfd_t srs_accept(srs_netfd_t stfd, struct sockaddr *addr, int *addrlen, srs_utime_t timeout)
{
    return (srs_netfd_t)st_accept((st_netfd_t)stfd, addr, addrlen, (st_utime_t)timeout);
}

ssize_t srs_read(srs_netfd_t stfd, void *buf, size_t nbyte, srs_utime_t timeout)
{
    return st_read((st_netfd_t)stfd, buf, nbyte, (st_utime_t)timeout);
}

bool srs_is_never_timeout(srs_utime_t tm)
{
    return tm == SRS_UTIME_NO_TIMEOUT;
}


ST协程库测试

#include "chw_adapt.h"
#include "srs_protocol_log.hpp"
#include "srs_app_st.hpp"
#include <st.h>
ISrsContext* _srs_context = NULL;

//0.在ST_TEST对象里启动一个协程
class ST_TEST : public ISrsCoroutineHandler{
public:
    ST_TEST(std::string flag) :_flag(flag)
    {
        _srs_context->set_id(_srs_context->generate_id());
        trd = new SrsSTCoroutine(flag, this, _srs_context->get_id());//2.new一个ST对象
    }
    srs_error_t startST(){
        srs_error_t err = srs_success;
        if ((err = trd->start()) != srs_success) {//3.start()创建协程
            return srs_error_wrap(err, "start timer");
        }
        printf("startST:%s\n",_flag.c_str());
        return err;
    }
public: virtual srs_error_t cycle() {//4.协程处理函数,回调cycle()
        srs_error_t err = srs_success;
        printf("cycle:%s\n",_flag.c_str());
        while(1)
        {
            srs_usleep(1000*1000);//协程睡眠,1秒
            //打印协程上下文ID
            printf("flag=%s,cid=%s\n",_flag.c_str(), _srs_context->get_id().c_str());
        }
        return err;
    }
private:
    SrsCoroutine* trd;
    std::string _flag;
};

    _srs_context = new SrsThreadContext();
    srs_st_init();//1.初始化ST

    _srs_context->set_id(_srs_context->generate_id());
    printf("\nmain cid=%s\n", _srs_context->get_id().c_str());//打印主协程上下文ID
    ST_TEST *pST_TEST1 = new ST_TEST("hello");
    pST_TEST1->startST();
    srs_usleep(1000);
    ST_TEST *pST_TEST2 = new ST_TEST("world");
    pST_TEST2->startST();

    st_thread_exit(NULL);

打印

st_init success, use epoll
main cid=380h90p8
startST:hello
cycle:hello
startST:world
cycle:world
flag=hello,cid=125u917t
flag=world,cid=i9wko515
flag=hello,cid=125u917t
flag=world,cid=i9wko515

SrsAutoFree测试

class SrsAutoFree_TEST{
public:
    SrsAutoFree_TEST(){printf("SrsAutoFree_TEST\n");}
    ~SrsAutoFree_TEST(){printf("~SrsAutoFree_TEST\n");}
};

void testAutoFree()
{
    SrsAutoFree_TEST *pSrsAutoFree_TEST = nullptr;
    SrsAutoFree(SrsAutoFree_TEST,pSrsAutoFree_TEST);
    pSrsAutoFree_TEST = new SrsAutoFree_TEST;
}

testAutoFree();

打印

SrsAutoFree_TEST
~SrsAutoFree_TEST
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值