海思3518E RTSP实时图传源码分析

海思3518E RTSP实时图传源码分析 blakmi 2024-02-16 17:37:00 1009

文章目录

源码框架分析

主函数中只有两部分,初始化rtsp服务和视频编码程序

RtspServer_init

我们在编写代码前的设想是让开发板作为服务器,windows作为客户端。于是服务器一定是先运行,然后像socket那样初始化完成后阻塞监听,等待客户端连接。

S(server)C(client)模式,基本都是这样的结构。

初始化部分代码如下

void RtspServer_init(void)
{
    int i;
    pthread_t threadId = 0;

    memset(&g_rtp_playload,0,sizeof(g_rtp_playload));
    strcpy(&g_rtp_playload,"G726-32");
    g_audio_rate = 8000;
    pthread_mutex_init(&g_sendmutex,NULL);
    pthread_mutex_init(&g_mutex,NULL);
    pthread_cond_init(&g_cond,NULL);
    memset(&g_rtspClients,0,sizeof(RTSP_CLIENT)*MAX_RTSP_CLIENT);

    //pthread_create(&g_SendDataThreadId, NULL, SendDataThread, NULL);

    struct sched_param thdsched;
    thdsched.sched_priority = 2;
    //to listen visiting
    pthread_create(&threadId, NULL, RtspServerListen, NULL);
    //pthread_setschedparam(threadId,SCHED_RR,&thdsched);
    printf("RTSP:-----Init Rtsp server\n");

    pthread_create(&gs_RtpPid, 0, vdRTPSendThread, NULL);

    //exitok++;
}

一上来先申请一个线程,盲猜就是用来监听

接着为payload的存放申请空间,以RTP开头,是应为rtsp与rtp之间有联系,rtsp实际上是以rtp包来发送的,所以以rtp开头
g_rtp_playload是一个20个字节的数组,后面将G726-32放入数组,这表示是一种音频标准的payload

下面的g_audio_rate = 8000;就是音频的采样率,后续没有用到,如果做音频相关的东西就可以进行使用

接着初始化全局的mutex和cond

再为g_rtspClients申请空间,用于存放连接的客户端的信息,数据结构如下

typedef struct
{
    int index;
    int socket;
    int reqchn;
    int seqnum;
    int seqnum2;
    unsigned int tsvid;
    unsigned int tsaud;
    int status;
    int sessionid;
    int rtpport[2];
    int rtcpport;
    char IP[20];
    char urlPre[PARAM_STRING_MAX];
}RTSP_CLIENT;

接着配置sched_param结构体,看起来是和线程调度的优先级有关。如果线程比较多,对CPU资源进行争抢的时候,就需要设置,可以调用系统API进行线程的调度。
这里作为预留,实际代码中没有必要使用
pthread_setschedparam(threadId,SCHED_RR,&thdsched);注释掉了

RtspServer_init中真正被调用的函数是RtspServerListen,开启了一个线程进行监听。socket的转网络字节序、servaddr的配置、bind、listen都放在了这个线程里面。

vdRTPSendThread用于发送,也开启了一个线程进行

SAMPLE_VENC_720P_CLASSIC

这个函数就是标准的海思sample,但是加入了rtsp的内容

我们将sample修改为只用一路,码率控制模式改为固定fixQP

其他内容没有什么变动,具体参考专栏海思3518E开发笔记中2开头的内容

修改的部分是第六步,从venc编码通道(vb)中获取视频流并保存,这里改为获取视频流后不保存,直接放到rtsp通道中传输

于是在原来保存264码流的地方,换成通过rtsp发送出去

HI_S32 SAMPLE_COMM_VENC_Sentjin(VENC_STREAM_S *pstStream)
{
    HI_S32 i,flag=0;

    for(i=0;i<MAX_RTSP_CLIENT;i++)//have atleast a connect
    {
        if(g_rtspClients[i].status == RTSP_SENDING)
        {
            flag = 1;
            break;
        }
    }
    if(flag)
    {
        for (i = 0; i < pstStream->u32PackCount; i++)
        {
        HI_S32 lens=0,j,lastadd=0,newadd=0,showflap=0;
        char sendbuf[320*1024];
        //char tmp[640*1024];
        lens = pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
        memcpy(&sendbuf[0],pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,lens);
        //printf("lens = %d, count= %d\n",lens,count++);
        VENC_Sent(sendbuf,lens);        
        lens = 0;
        }


    }
    return HI_SUCCESS;
}

实际应用场景中,可能有很多路的rtsp流,这里我们宏定义只用一路

下面对rtsp的状态进行判断,如果处于发送状态就进行发送

然后调用VENC_Sent将准备好的包发出去

详细分析

RtspServer_init

RtspServerListen
这里主要就是网络编程中配置端口号,修改网络字节序的内容,端口号使用的是554,这是RTSP的默认端口号
setsockopt来设置socket属性,设置了SO_REUSEADDR可以用来复用

然后就是bind和listen

接下来到accept
如果没人连接,就阻塞到while的判断中,有人连接就进入while,并且将连接过来的ip地址打印出来

接着再进行socket的设置,选项是SO_SNDBUF,即设置发送缓冲区的大小

然后对新连接的rtspClient的status进行判断,如果等于RTSP_IDLE就进行处理。由于g_rtspClients是全局变量,定义之后自动初始化为0,就是status枚举里的第一个。这样处理,只要是新的ip地址,就一定是空闲状态,进if后,就把他的状态换成connected,避免反复操作

进入if之后,将接入的客户端的属性进行一一填充。

memset(&g_rtspClients[i],0,sizeof(RTSP_CLIENT));
g_rtspClients[i].index = i;
g_rtspClients[i].socket = s32CSocket;
g_rtspClients[i].status = RTSP_CONNECTED ;//RTSP_SENDING;
g_rtspClients[i].sessionid = nSessionId++;
strcpy(g_rtspClients[i].IP,inet_ntoa(addrAccept.sin_addr));

填充完属性后建立一个线程,进行当前接入客户端的处理。

如果超过最大限制,那么就将超过的覆盖到原来的0,这样循环覆盖下去(设计方面的思路,也可以超过直接不再连接)

处理的函数是RtspClientMsg,只服务当前接入的客户端
在看这一段代码的时候,就需要对RTSP传输协议有一定的了解,函数中ParseRequestString对RTSP消息头进行解析

RtspClientMsg函数一进去,做了一个RTSP_CLIENT * pClient来接受传进来的结构体,不直接用是避免修改原来结构体里的内容
当连接进来的客户端的状态不是架空的时候,就进行数据的receive,一次只读RTSP_RECV_SIZE这么多,没有读完的部分留到下一次继续读,如果读出来的字节数小于0,那么是错误的,把状态重新改为置空

接下来定义了一系列的局部变量,作为ParseRequestString的输出参数。
该函数原型如下

int ParseRequestString(char const* reqStr,
               unsigned reqStrSize,
               char* resultCmdName,
               unsigned resultCmdNameMaxSize,
               char* resultURLPreSuffix,
               unsigned resultURLPreSuffixMaxSize,
               char* resultURLSuffix,
               unsigned resultURLSuffixMaxSize,
               char* resultCSeq,
               unsigned resultCSeqMaxSize)

第一个参数为前面socket接收到的字符串,就是进行分析的目标
第二个参数是接受到字符串的大小
第三个参数用来存放解析出来的RTSP method
第四个参数是函数执行前申请存放第三个参数的数组的大小
第五个参数是解析出来的URL前缀
第六个参数是用来存放第五个参数的数组大小
第七个参数是解析出来的URL后缀
第八个参数是存放第七个参数的数组大小
第九个参数是解析出来的会话序列号
第十个参数是存放第九个参数的数组大小

以上解析出来的参数都和RTSP会话内容对应,详细参考我之前写的RTSP协议详解
后面的内容就是对解析出来的RTSP methods进行对应操作
接下来仔细分析ParseRequestString函数

ParseRequestString
这个函数的作用就是解析RTSP协议中CS交互信息,多为一些字符串操作

第一段

// Read everything up to the first space as the command name:
 int parseSucceeded = FALSE;
 unsigned i;
 for (i = 0; i < resultCmdNameMaxSize-1 && i < reqStrSize; ++i) 
 {
   char c = reqStr[i];
   if (c == ' ' || c == '\t') 
   {
     parseSucceeded = TRUE;
     break;
   }

   resultCmdName[i] = c;
 }
 resultCmdName[i] = '\0';
 if (!parseSucceeded) return FALSE;

首先做了一个flag,来表示是否成功分析
接着对获取的数据进行分段,标志是空格或制表符
这个循环就获取了RTSP 的method
由于解析出来的是字符串,就需要将最后一个字符替换为字符串的结束标志

第二段

// Skip over the prefix of any "rtsp://" or "rtsp:/" URL that follows:
  unsigned j = i+1;
  while (j < reqStrSize && (reqStr[j] == ' ' || reqStr[j] == '\t')) ++j; // skip over any additional white space
  for (j = i+1; j < reqStrSize-8; ++j) {
    if ((reqStr[j] == 'r' || reqStr[j] == 'R')
    && (reqStr[j+1] == 't' || reqStr[j+1] == 'T')
    && (reqStr[j+2] == 's' || reqStr[j+2] == 'S')
    && (reqStr[j+3] == 'p' || reqStr[j+3] == 'P')
    && reqStr[j+4] == ':' && reqStr[j+5] == '/') {
      j += 6;
      if (reqStr[j] == '/') {
    // This is a "rtsp://" URL; skip over the host:port part that follows:
    ++j;
    while (j < reqStrSize && reqStr[j] != '/' && reqStr[j] != ' ') ++j;
      } else {
    // This is a "rtsp:/" URL; back up to the "/":
    --j;
      }
      i = j;
      break;
    }
  }

 // Look for the URL suffix (before the following "RTSP/"):
  parseSucceeded = FALSE;
  unsigned k;
  for (k = i+1; k < reqStrSize-5; ++k) {
    if (reqStr[k] == 'R' && reqStr[k+1] == 'T' &&
    reqStr[k+2] == 'S' && reqStr[k+3] == 'P' && reqStr[k+4] == '/') {
      while (--k >= i && reqStr[k] == ' ') {} // go back over all spaces before "RTSP/"
      unsigned k1 = k;
      while (k1 > i && reqStr[k1] != '/' && reqStr[k1] != ' ') --k1;
      // the URL suffix comes from [k1+1,k]

      // Copy "resultURLSuffix":
      if (k - k1 + 1 > resultURLSuffixMaxSize) return FALSE; // there's no room
      unsigned n = 0, k2 = k1+1;
      while (k2 <= k) resultURLSuffix[n++] = reqStr[k2++];
      resultURLSuffix[n] = '\0';

      // Also look for the URL 'pre-suffix' before this:
      unsigned k3 = --k1;
      while (k3 > i && reqStr[k3] != '/' && reqStr[k3] != ' ') --k3;
      // the URL pre-suffix comes from [k3+1,k1]

      // Copy "resultURLPreSuffix":
      if (k1 - k3 + 1 > resultURLPreSuffixMaxSize) return FALSE; // there's no room
      n = 0; k2 = k3+1;
      while (k2 <= k1) resultURLPreSuffix[n++] = reqStr[k2++];
      resultURLPreSuffix[n] = '\0';

      i = k + 7; // to go past " RTSP/"
      parseSucceeded = TRUE;
      break;
    }
  }
  if (!parseSucceeded) return FALSE;

首先用j获取第二段关键信息,就是用上次的最后地址i再往后偏移一个地址
然后兼容大小写,作用是跳过rtsp://或rtsp:/
然后找到/后面的不是/或者空格的那一个地址,就是解析出来的URL的首地址

第三段

  // Look for "CSeq:", skip whitespace,
  // then read everything up to the next \r or \n as 'CSeq':
  parseSucceeded = FALSE;
  for (j = i; j < reqStrSize-5; ++j) {
    if (reqStr[j] == 'C' && reqStr[j+1] == 'S' && reqStr[j+2] == 'e' &&
    reqStr[j+3] == 'q' && reqStr[j+4] == ':') {
      j += 5;
      unsigned n;
      while (j < reqStrSize && (reqStr[j] ==  ' ' || reqStr[j] == '\t')) ++j;
      for (n = 0; n < resultCSeqMaxSize-1 && j < reqStrSize; ++n,++j) {
    char c = reqStr[j];
    if (c == '\r' || c == '\n') {
      parseSucceeded = TRUE;
      break;
    }

    resultCSeq[n] = c;
      }
      resultCSeq[n] = '\0';
      break;
    }
  }
  if (!parseSucceeded) return FALSE;

用于解析出CSeq字段

以上就是作为服务器的开发板对客户端发来的请求信息的解析,解析完毕后做相应的处理

OPTIONS
以OptionAnswer为例

int OptionAnswer(char *cseq, int sock)
{
    if (sock != 0)
    {
        char buf[1024];
        memset(buf,0,1024);
        char *pTemp = buf;
        pTemp += sprintf(pTemp,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sPublic: %s\r\n\r\n",
            cseq,dateHeader(),"OPTIONS,DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN");

        int reg = send(sock, buf,strlen(buf),0);
        if(reg <= 0)
        {
            return FALSE;
        }
        else
        {
            printf(">>>>>%s\n",buf);
        }
        return TRUE;
    }
    return FALSE;
}

服务端收到客户端的请求后,向客户端返回信息
返回的信息放在buf中,通过socket通道发送
返回的信息按照RTSP规则

DESCRIBE

int DescribeAnswer(char *cseq,int sock,char * urlSuffix,char* recvbuf)
{
    if (sock != 0)
    {
        char sdpMsg[1024];
        char buf[2048];
        memset(buf,0,2048);
        memset(sdpMsg,0,1024);
        char*localip;
        localip = GetLocalIP(sock);

        char *pTemp = buf;
        pTemp += sprintf(pTemp,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n",cseq);
        pTemp += sprintf(pTemp,"%s",dateHeader());
        pTemp += sprintf(pTemp,"Content-Type: application/sdp\r\n");


        char *pTemp2 = sdpMsg;
        pTemp2 += sprintf(pTemp2,"v=0\r\n");
        pTemp2 += sprintf(pTemp2,"o=StreamingServer 3331435948 1116907222000 IN IP4 %s\r\n",localip);
        pTemp2 += sprintf(pTemp2,"s=H.264\r\n");
        pTemp2 += sprintf(pTemp2,"c=IN IP4 0.0.0.0\r\n");
        pTemp2 += sprintf(pTemp2,"t=0 0\r\n");
        pTemp2 += sprintf(pTemp2,"a=control:*\r\n");


        /*H264 TrackID=0 RTP_PT 96*/
        pTemp2 += sprintf(pTemp2,"m=video 0 RTP/AVP 96\r\n");
        pTemp2 += sprintf(pTemp2,"a=control:trackID=0\r\n");
        pTemp2 += sprintf(pTemp2,"a=rtpmap:96 H264/90000\r\n");
        pTemp2 += sprintf(pTemp2,"a=fmtp:96 packetization-mode=1; sprop-parameter-sets=%s\r\n", "AAABBCCC");
#if 1

        /*G726*/

        pTemp2 += sprintf(pTemp2,"m=audio 0 RTP/AVP 97\r\n");
        pTemp2 += sprintf(pTemp2,"a=control:trackID=1\r\n");
        if(strcmp(g_rtp_playload,"AAC")==0)
        {
            pTemp2 += sprintf(pTemp2,"a=rtpmap:97 MPEG4-GENERIC/%d/2\r\n",16000);
            pTemp2 += sprintf(pTemp2,"a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1410\r\n");
        }
        else
        {
            pTemp2 += sprintf(pTemp2,"a=rtpmap:97 G726-32/%d/1\r\n",8000);
            pTemp2 += sprintf(pTemp2,"a=fmtp:97 packetization-mode=1\r\n");
        }    
#endif
        pTemp += sprintf(pTemp,"Content-length: %d\r\n", strlen(sdpMsg));     
        pTemp += sprintf(pTemp,"Content-Base: rtsp://%s/%s/\r\n\r\n",localip,urlSuffix);

        //printf("mem ready\n");
        strcat(pTemp, sdpMsg);
        free(localip);
        //printf("Describe ready sent\n");
        int re = send(sock, buf, strlen(buf),0);
        if(re <= 0)
        {
            return FALSE;
        }
        else
        {
            printf(">>>>>%s\n",buf);
        }
    }

    return TRUE;
}

传进来的参数

  • cseq——会话的序列号
  • sock——socket建立的网络通道
  • urlSuffix——解析出来的URL字符串
  • recvbuf——服务器接收到的整个信息

函数中,先获取了服务器本地的IP地址,接下来,就按照RTSP的DESCRIBE标准进行回复

RTP协议中,使用VLC进行播放,需要有一个SDP文件进行解析才能播放,RTP传的是裸流,没有控制信息,需要本地配置,而RTSP协议中,SDP包含在交互信息中,不需要用VLC打开SDP文件,通过解析交互信息,客户端就能够知道怎么去解析视频流,代码中的体现如下:

char *pTemp2 = sdpMsg;
pTemp2 += sprintf(pTemp2,"v=0\r\n");
pTemp2 += sprintf(pTemp2,"o=StreamingServer 3331435948 1116907222000 IN IP4 %s\r\n",localip);
pTemp2 += sprintf(pTemp2,"s=H.264\r\n");
pTemp2 += sprintf(pTemp2,"c=IN IP4 0.0.0.0\r\n");
pTemp2 += sprintf(pTemp2,"t=0 0\r\n");
pTemp2 += sprintf(pTemp2,"a=control:*\r\n");

PLAY

int PlayAnswer(char *cseq, int sock,int SessionId,char* urlPre,char* recvbuf)
{
    if (sock != 0)
    {
        char buf[1024];
        memset(buf,0,1024);
        char *pTemp = buf;
        char*localip;
        localip = GetLocalIP(sock);
        pTemp += sprintf(pTemp,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sRange: npt=0.000-\r\nSession: %d\r\nRTP-Info: url=rtsp://%s/%s;seq=0\r\n\r\n",
            cseq,dateHeader(),SessionId,localip,urlPre);

        free(localip);

        int reg = send(sock, buf,strlen(buf),0);
        if(reg <= 0)
        {
            return FALSE;
        }
        else
        {
            printf(">>>>>%s",buf);
            udpfd = socket(AF_INET,SOCK_DGRAM,0);//UDP
            struct sockaddr_in server;
            server.sin_family=AF_INET;
               server.sin_port=htons(g_rtspClients[0].rtpport[0]);          
               server.sin_addr.s_addr=inet_addr(g_rtspClients[0].IP);
            connect(udpfd,(struct sockaddr *)&server,sizeof(server));
            printf("udp up\n");
        }
        return TRUE;
    }
    return FALSE;
}

客户端play请求后,服务端开启一个UDP的SOCKET通道进行裸流的传输,命令走的是TCP通道。这就是RTSP的设计

因为考虑实时,所以用的是UDP

vdRTPSendThread

HI_VOID* vdRTPSendThread(HI_VOID *p)
{
    while(1)
    {
        if(!list_empty(&RTPbuf_head))
        {

            RTPbuf_s *p = get_first_item(&RTPbuf_head,RTPbuf_s,list);
            VENC_Sent(p->buf,p->len);
            list_del(&(p->list));
            free(p->buf);
            free(p);
            p = NULL;
            count--;
            //printf("count = %d\n",count);

        }
        usleep(5000);
    }
}

在client连接后,server收到play指令,创建裸流的UDP通道

发送是在vdRTPSendThread线程中进行。每隔5微妙在RTPbuf_head链表里查找数据,有数据就进行发送

get_first_item用来取出链表里的第一个非空节点
然后将节点中的内容通过VENC_Sent发送出去,函数的内容主要是为了H264封RTP包添加包头
发送完就将它释放掉
这里的send是消费者,不断发送链表中的数据

有消费者就有生产者,生产在编码的地方

发送有两种方法,一种直接发送,一种是环形buffer发送

直接发送的方法是编码一帧,就发一帧。直接发送的方法不是在vdRTPSendThread线程中,而是在编码的地方发送,编完一帧就发一帧

HI_S32 SAMPLE_COMM_VENC_Sentjin(VENC_STREAM_S *pstStream)
{
    HI_S32 i,flag=0;

    for(i=0;i<MAX_RTSP_CLIENT;i++)//have atleast a connect
    {
        if(g_rtspClients[i].status == RTSP_SENDING)
        {
            flag = 1;
            break;
        }
    }
    if(flag)
    {
        for (i = 0; i < pstStream->u32PackCount; i++)
        {
        HI_S32 lens=0,j,lastadd=0,newadd=0,showflap=0;
        char sendbuf[320*1024];
        //char tmp[640*1024];
        lens = pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
        memcpy(&sendbuf[0],pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,lens);
        //printf("lens = %d, count= %d\n",lens,count++);
        VENC_Sent(sendbuf,lens);        
        lens = 0;
        }


    }
    return HI_SUCCESS;
}

直接发送的方式优势是简单,但实际应用中不会这么去做。实际情况中会比较复杂,编码和传输的速度不一定适配。
如果发送的速度比编码的速度块,那么发送就要等编码。时间上会有冗余

环形buffer就可以有一定的缓冲。如果发送比编码快,那么就阻塞,就像视频缓冲;如果编码比发送快,甚至套了发送一圈了,那么可以增大环形buffer,但是早晚会有崩溃的一天,编码快得多就要考虑系统的设计。
实际谁快谁慢是不确定的,比如网络波动会影响发送的速度。有时候发送慢了,那编码的帧排列准备;有时候发送快了,那么在排队的编码完的帧等待的数量少一点。

环形buffer发送的代码如下:

HI_S32 saveStream(VENC_STREAM_S *pstStream)
{
    HI_S32 i,j,lens=0;

    for(j=0;j<MAX_RTSP_CLIENT;j++)//have atleast a connect
    {
        if(g_rtspClients[j].status == RTSP_SENDING)
        {
            for (i = 0; i < pstStream->u32PackCount; i++)
            {
                RTPbuf_s *p = (RTPbuf_s *)malloc(sizeof(RTPbuf_s));
                INIT_LIST_HEAD(&(p->list));

                lens = pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
                p->buf = (char *)malloc(lens);
                p->len = lens;
                memcpy(p->buf,pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,lens);

                list_add_tail(&(p->list),&RTPbuf_head);
                count++;
                //printf("count = %d\n",count);
            }
        }
    }

    return HI_SUCCESS;
}

环形buffer是通过链表实现
count是用来记录剩余编码完的帧的数量,发送的话就去减减,到0说明编码完的帧用完了。
这里就是上面说的生产者,把编码完的帧送到全局的链表中,由vdRTPSendThread发送
发送完一个节点,就把这个节点拿掉,并且把内存释放

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
blakmi
红包 1 1 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
blakmi
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区