RTMP网络连接分析

RTMP_Connect

librtmp用RTMP_Connect这个方法建立网络连接,网络连接代表了服务端应用程序和客户端之间基本的连通关系。 在RTMP_ParseURL方法中, 可以通过传入的url解出来hostportapplication
用解出来的host和port建立socket,然后做一些rtmp握手之类的工作。

int
RTMP_Connect(RTMP *r, RTMPPacket *cp)
{
    struct sockaddr_in service;
    if (!r->Link.hostname.av_len)
        return FALSE;

    memset(&service, 0, sizeof(struct sockaddr_in));
    service.sin_family = AF_INET;
    RTMP_Log(RTMP_LOGDEBUG, "Link.sockport = %d", r->Link.sockshost);
    if (r->Link.socksport) {
        /* Connect via SOCKS */
        if (!add_addr_info(&service, &r->Link.sockshost, r->Link.socksport))
            return FALSE;
    } else {
        RTMP_Log(RTMP_LOGDEBUG, "hostname: %s, port = %d\n", r->Link.hostname.av_val, r->Link.port);
        /* Connect directly */
        if (!add_addr_info(&service, &r->Link.hostname, r->Link.port))
            return FALSE;
    }

    // 创建socket, 设置一些参数, 接收数据超时时间,禁用Nagle算法 
    if (!RTMP_Connect0(r, (struct sockaddr *)&service))
        return FALSE;

    r->m_bSendCounter = TRUE;

    return RTMP_Connect1(r, cp);
}

RTMP_Connect0

RTMP_Connect0方法中主要就是创建tcp socket, 然后连接服务器, 设置超时, 禁用Nagle算法

int
RTMP_Connect0(RTMP *r, struct sockaddr * service)
{
    int on = 1;
    r->m_sb.sb_timedout = FALSE;
    r->m_pausing = 0;
    r->m_fDuration = 0.0;

    r->m_sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (r->m_sb.sb_socket != -1) {
        if (connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr)) < 0) {
            int err = GetSockError();
            RTMP_Log(RTMP_LOGERROR, "%s, failed to connect socket. %d (%s)",
            __FUNCTION__, err, strerror(err));
            RTMP_Close(r);
            return FALSE;
        }

        if (r->Link.socksport) {
            RTMP_Log(RTMP_LOGDEBUG, "%s ... SOCKS negotiation", __FUNCTION__);
            if (!SocksNegotiate(r)) {
                RTMP_Log(RTMP_LOGERROR, "%s, SOCKS negotiation failed.", __FUNCTION__);
                RTMP_Close(r);
                return FALSE;
            }
        }
    } else {
        RTMP_Log(RTMP_LOGERROR, "%s, failed to create socket. Error: %d", __FUNCTION__,
        GetSockError());
        return FALSE;
    }

    /* set timeout */
    {
        SET_RCVTIMEO(tv, r->Link.timeout);
        if (setsockopt
        (r->m_sb.sb_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv))) {
            RTMP_Log(RTMP_LOGERROR, "%s, Setting socket timeout to %ds failed!",
            __FUNCTION__, r->Link.timeout);
        }
    }
    // 禁用Nagle算法, 保证了数据实时性
    setsockopt(r->m_sb.sb_socket, IPPROTO_TCP, TCP_NODELAY, (char *) &on, sizeof(on));

    // @remark debug info by http://github.com/ossrs/srs
    _srs_state = 2;

    return TRUE;
}

RTMP_Connect1

RTMP_Connect1主要包括握手(HandShake),还有向服务器发送连接命令(SendConnectPacket)

int
RTMP_Connect1(RTMP *r, RTMPPacket *cp)
{
    if (r->Link.protocol & RTMP_FEATURE_SSL) {
#if defined(CRYPTO) && !defined(NO_SSL)
        TLS_client(RTMP_TLS_ctx, r->m_sb.sb_ssl);
        TLS_setfd(r->m_sb.sb_ssl, r->m_sb.sb_socket);
        if (TLS_connect(r->m_sb.sb_ssl) < 0) {
            RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__);
            RTMP_Close(r);
            return FALSE;
        }
#else
        RTMP_Log(RTMP_LOGERROR, "%s, no SSL/TLS support", __FUNCTION__);
        RTMP_Close(r);
        return FALSE;

#endif
    }
    RTMP_Log(RTMP_LOGDEBUG, "Link.protocal: %d", r->Link.protocol);
    if (r->Link.protocol & RTMP_FEATURE_HTTP) {
        r->m_msgCounter = 1;
        r->m_clientID.av_val = NULL;
        r->m_clientID.av_len = 0;
        HTTP_Post(r, RTMPT_OPEN, "", 1);
        if (HTTP_read(r, 1) != 0) {
            r->m_msgCounter = 0;
            RTMP_Log(RTMP_LOGDEBUG, "%s, Could not connect for handshake", __FUNCTION__);
            RTMP_Close(r);
            return 0;
        }
        r->m_msgCounter = 0;
    }
    RTMP_Log(RTMP_LOGDEBUG, "%s, ... connected, handshaking", __FUNCTION__);
    if (!HandShake(r, TRUE)) {
        RTMP_Log(RTMP_LOGERROR, "%s, handshake failed.", __FUNCTION__);
        RTMP_Close(r);
        return FALSE;
    }
    RTMP_Log(RTMP_LOGDEBUG, "%s, handshaked", __FUNCTION__);

    if (!SendConnectPacket(r, cp)) {
        RTMP_Log(RTMP_LOGERROR, "%s, RTMP connect failed.", __FUNCTION__);
        RTMP_Close(r);
        return FALSE;
    }
    return TRUE;
}

HandShake

一个RTMP连接以握手开始, RTMP的握手不同以其他协议:RTMP握手由三个固定长度的块组成,而不像其他协议一样带有报头的可变长度的块。客户端和服务端各自发送三块数据,

  • 客户端:C0、C1、C2
  • 服务端:S0、S1、S2

下面时librtmp中对于握手的处理

static int
HandShake(RTMP *r, int FP9HandShake)
{
    int i;
    uint32_t uptime, suptime;
    int bMatch;
    char type;
    char clientbuf[RTMP_SIG_SIZE + 1], *clientsig = clientbuf + 1;
    char serversig[RTMP_SIG_SIZE];

    clientbuf[0] = 0x03;        /* not encrypted */

    uptime = htonl(RTMP_GetTime());
    memcpy(clientsig, &uptime, 4);

    memset(&clientsig[4], 0, 4);

#ifdef _DEBUG
    for (i = 8; i < RTMP_SIG_SIZE; i++)
        clientsig[i] = 0xff;
#else
    for (i = 8; i < RTMP_SIG_SIZE; i++)
        clientsig[i] = (char)(rand() % 256);
#endif
    // 发送C0、C1数据块
    if (!WriteN(r, clientbuf, RTMP_SIG_SIZE + 1))
        return FALSE;

    /* 接收S0块 */
    if (ReadN(r, &type, 1) != 1)    /* 0x03 or 0x06 */
        return FALSE;

    RTMP_Log(RTMP_LOGDEBUG, "%s: Type Answer   : %02X", __FUNCTION__, type);

    if (type != clientbuf[0])
        RTMP_Log(RTMP_LOGWARNING, "%s: Type mismatch: client sent %d, server answered %d",
                 __FUNCTION__, clientbuf[0], type);
    /* 接收S1块 */
    if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
        return FALSE;

    /* decode server response */

    memcpy(&suptime, serversig, 4);
    suptime = ntohl(suptime);

    RTMP_Log(RTMP_LOGDEBUG, "%s: Server Uptime : %d", __FUNCTION__, suptime);
    RTMP_Log(RTMP_LOGDEBUG, "%s: FMS Version   : %d.%d.%d.%d", __FUNCTION__,
             serversig[4], serversig[5], serversig[6], serversig[7]);

    /* 2nd part of handshake 发送C2块*/
    if (!WriteN(r, serversig, RTMP_SIG_SIZE))
        return FALSE;

    /* 接收S2块 */
    if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
        return FALSE;

    bMatch = (memcmp(serversig, clientsig, RTMP_SIG_SIZE) == 0);
    if (!bMatch) {
        RTMP_Log(RTMP_LOGWARNING, "%s, client signature does not match!", __FUNCTION__);
    }
    return TRUE;
}

握手顺序

握手的顺序大致可以描述如下

  • 客户端首先发送C0C1
  • 服务端接收到C0C1块之后,向客户端发送S0S1
  • 客户端相继接收数据, 直到接收到S1数据块之后, 再服务端发送C2
  • 服务端必须等待接收到C1块之后才能发送S2
  • 客户端必须等待接收到S2块才能发送其他数据
  • 服务端必须等待接收到C1块才能发送其他数据

握手数据块格式

下面介绍一下C0、C1、C2和S0、S1、S2数据块的格式。

C0和S0数据块格式

C0和S0块都是一个单一的八位字节,以一个单独的八位整形域进行处理。

 0 1 2 3 4 5 6 7 
+-+-+-+-+-+-+-+-+
|    version    |
+-+-+-+-+-+-+-+-+
C1和S1的格式

C1和S1的数据包的长度为1536字节, 包括以下字段

 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            time(4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            zero(4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            random bytes                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            random bytes                       |
|                              (const)                          |
|                                ...                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Time(四个字节):这个字段包括了一个timestamp, 用于本终端发送的所有后续块的时间起点。这个值可以时0或者一些任意值。
  • Zero(四个字节):这个字段必须时0
  • Random data(四个字节):这个字段可以包含任意值
C2和S2的格式

C2和S2数据包长度都是1536字节, 基本上就是S1和C1的副本, 包含以下字段:

 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            time(4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            time2(4 bytes)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            random echo                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            random echo                        |
|                              (const)                          |
|                                ...                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Time(四个字节):这个字段必须包含终端在S1(给C2)或者C1(给S2)发的timestamp
  • Time2(四个字节):这个字段必须包含终端先前发出数据包(S1或者C1)timestamp
  • Random data(四个字节):这个字段必须包含终端发出的S1(给C2)或者S2(给C1)的随机数。

SendConnectPacket

SendConnectPacket方法的作用是客户端发送connect命令到服务器端来请求连接到一个服务器应用的实例

connect命令结构如下:

字段名 类型 描述
Command Name 字符串 命令的名字,这里是connect
Transaction ID 数字 总是设置为1
Command Object 对象 具有名值对的命令信息对象
Optional User Arguments 对象 任意可选对象

任意可选对象里面是相当与是一些用户参数, appflashverswfUrl

下面是librtmp中的连接代码

static int
SendConnectPacket(RTMP *r, RTMPPacket *cp)
{
    RTMPPacket packet;
    char pbuf[4096], *pend = pbuf + sizeof(pbuf);
    char *enc;

    if (cp)
        return RTMP_SendPacket(r, cp, TRUE);

    packet.m_nChannel = 0x03;   /* control channel (invoke) */
    packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet.m_packetType = RTMP_PACKET_TYPE_INVOKE;
    packet.m_nTimeStamp = 0;
    packet.m_nInfoField2 = 0;
    packet.m_hasAbsTimestamp = 0;
    packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

    enc = packet.m_body;
    enc = AMF_EncodeString(enc, pend, &av_connect);
    enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
    *enc++ = AMF_OBJECT;

    enc = AMF_EncodeNamedString(enc, pend, &av_app, &r->Link.app);
    if (!enc)
        return FALSE;
    if (r->Link.protocol & RTMP_FEATURE_WRITE) {
        enc = AMF_EncodeNamedString(enc, pend, &av_type, &av_nonprivate);
        if (!enc)
            return FALSE;
    }
    if (r->Link.flashVer.av_len) {
        enc = AMF_EncodeNamedString(enc, pend, &av_flashVer, &r->Link.flashVer);
        if (!enc)
            return FALSE;
    }
    if (r->Link.swfUrl.av_len) {
        enc = AMF_EncodeNamedString(enc, pend, &av_swfUrl, &r->Link.swfUrl);
        if (!enc)
            return FALSE;
    }
    if (r->Link.tcUrl.av_len) {
        enc = AMF_EncodeNamedString(enc, pend, &av_tcUrl, &r->Link.tcUrl);
        if (!enc)
            return FALSE;
    }
    if (!(r->Link.protocol & RTMP_FEATURE_WRITE)) {
        enc = AMF_EncodeNamedBoolean(enc, pend, &av_fpad, FALSE);
        if (!enc)
            return FALSE;
        enc = AMF_EncodeNamedNumber(enc, pend, &av_capabilities, 15.0);
        if (!enc)
            return FALSE;
        enc = AMF_EncodeNamedNumber(enc, pend, &av_audioCodecs, r->m_fAudioCodecs);
        if (!enc)
            return FALSE;
        enc = AMF_EncodeNamedNumber(enc, pend, &av_videoCodecs, r->m_fVideoCodecs);
        if (!enc)
            return FALSE;
        enc = AMF_EncodeNamedNumber(enc, pend, &av_videoFunction, 1.0);
        if (!enc)
            return FALSE;
        if (r->Link.pageUrl.av_len) {
            enc = AMF_EncodeNamedString(enc, pend, &av_pageUrl, &r->Link.pageUrl);
            if (!enc)
                return FALSE;
        }
    }
    if (r->m_fEncoding != 0.0 || r->m_bSendEncoding) {
        /* AMF0, AMF3 not fully supported yet */
        enc = AMF_EncodeNamedNumber(enc, pend, &av_objectEncoding, r->m_fEncoding);
        if (!enc)
            return FALSE;
    }
    if (enc + 3 >= pend)
        return FALSE;
    *enc++ = 0;
    *enc++ = 0;         /* end of object - 0x00 0x00 0x09 */
    *enc++ = AMF_OBJECT_END;

    /* add auth string */
    if (r->Link.auth.av_len) {
        enc = AMF_EncodeBoolean(enc, pend, r->Link.lFlags & RTMP_LF_AUTH);
        if (!enc)
            return FALSE;
        enc = AMF_EncodeString(enc, pend, &r->Link.auth);
        if (!enc)
            return FALSE;
    }
    if (r->Link.extras.o_num) {
        int i;
        for (i = 0; i < r->Link.extras.o_num; i++) {
            enc = AMFProp_Encode(&r->Link.extras.o_props[i], enc, pend);
            if (!enc)
                return FALSE;
        }
    }
    packet.m_nBodySize = enc - packet.m_body;

    return RTMP_SendPacket(r, &packet, TRUE);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343