RTMP 握手

很多初学者就是看了恶心的握手就再也没有研究的兴趣了,不过,弄懂了就感觉没什么了.
socket建立连接以后,就需要认证了,也就是握手.只有握手完成以后才能进入下一步的操作,比如发送控制命令,交换消息,发送音视频包等.客户端需要发送C0,C1,C2给服务器端,服务器端需要发送S0,S1,S2给客户端,至于C0,C1,C2,S0,S1,S2是什么,下面会介绍,暂时认为是数据吧.
重点来了:

  • 握手顺序
    • 客户端首先发送C0,等待服务器返回S0
    • 服务器端收到C0后,发送S0给客户端
    • 客户端收到S0后,发送C1个服务器
    • 服务器收到C1后发送S1给客户端
    • 客户端收到S1后发送C2给服务器端
    • 服务器端收到C2后,发送S2给客户端
    • 握手完成

是不是感觉很复杂的样子,来一张图清晰明了:

客户端       服务器端

 C0  ------->  
     
     <------- S0
 
 C1  ------->  
     
     <------- S1
     
 C2  ------->  
     
     <------- S2
     

然而实际上,客户端通常都是C0和C1一起发送,或者C0发送完马上发送C1.而服务器端一般都做了兼容处理,可能按照顺序发送,也可能在收到C1后就把S0,S1,S2一起发送给客户端.

  • C0,C1,C2,S0,S1,S2 的数据格式

    • C0 和 S0 的格式
      C0 和 S0 包都是一个字节(8bit),表示版本号

      Paste_Image.png

      在 C0 中,这一字段指示出客户端要求的 RTMP 版本号。在 S0 中,这一字段指示出服务器端选择的 RTMP 版本号。默认为 3。0、1、2 三个值是由早期其他产品使用的,是废弃值;4 - 31 被保留为 RTMP 协议的未来实现版本使用;32 - 255 不允许使用 (以区分开 RTMP 和其他常以一个可打印字符开始的文本协议)。无法识别客户端所请求版本号的服务器应该以版本 3 响应,(收到响应的) 客户端可以选择降低到版本 3,或者放弃握手。

    • C1 和 S1 的格式
      C1 和 S1 数据包的长度都是 1536 字节,包含以下字段:

      Paste_Image.png

      Time (前四个字节):这个字段包含一个 timestamp,用于本终端发送的所有后续块的时间起点。这个值可以是 0,或者一些任意值。要同步多个块流,终端可以发送其他块流当前的 timestamp 的值。
      Zero (紧跟着的四个字节):这个字段必须都是 0。
      Random data (剩下的1528 个字节):这个字段可以包含任意值。终端需要区分出响应来自它发起的握手还是对端发起的握手,这个数据应该发送一些足够随机的数。简单点,就是随机数.

    • C2 和 S2 的格式
      C2 和 S2 数据包长度都是 1536 字节,基本就是 S1 和 C1 的副本 (分别),包含有以下字段:


      Paste_Image.png
      • Time (前四个字节):这个字段必须包含终端在 S1 (给 C2) 或者 C1 (给 S2) 发的 timestamp.例如:C1的前四个字节为0,那么S2的前四个字节也是0.
      • Time2 (紧跟着的四个字节):这个字段必须包含终端先前发出数据包 (s1 或者 c1) timestamp,例如,S1的前四个字节为 0x00 00 00 01,那么S2的第4~8字节就是0x 00 00 00 01
      • Random echo (剩下1528 个字节):这个字段必须包含终端发的 S1 (给 C2) 或者 S2 (给 C1) 的随机数。两端都可以一起使用 time 和 time2 字段再加当前 timestamp 以快速估算带宽和/或者连接延迟,但这不太可能是有多大用处。
  • 如果握手失败,服务器会终止响应,并断开socket连接

  • 如果握手成功,则可以进入到下一个环节,可以开始交换消息了.

  • 实际测试(用的SRS测的)发现,服务器端根本不鸟你,只要收到C1,后面就可能先发一部分,再发一部分(长度不定,不一定是上文所说的1536),也可能一次性全部给你,但是S0+S1+S2的总字节数(也就是3073个字节)是对的
    附上代码:

先来个分类:主要是解析url的各个部分:

//解析推流地址
@interface NSString (URL)

@property(readonly) NSString *scheme;
@property(readonly) NSString *host;
@property(readonly) NSString *app;
@property(readonly) NSString *playPath;
@property(readonly) UInt32    port;

@end

//------------华丽的分割线 .m文件---------------------

#import "NSString+URL.h"

@implementation NSString (URL)
- (NSString *)scheme{
    return [self componentsSeparatedByString:@"://"].firstObject;
}
- (NSString *)host{
    NSURL *url = [NSURL URLWithString:self];
    return url.host;
}
- (NSString *)app{
    NSString *sep = [NSString stringWithFormat:@"%@/",self.host];
    NSString *res = [self componentsSeparatedByString:sep].lastObject;
    return [res componentsSeparatedByString:@"/"].firstObject;
}
- (NSString *)playPath{
    NSString *sep = [NSString stringWithFormat:@"%@/",self.host];
    NSString *res = [self componentsSeparatedByString:sep].lastObject;
    NSString *reu = [res componentsSeparatedByString:@"/"].lastObject;
    return [reu componentsSeparatedByString:@":"].firstObject;
}
- (UInt32)port{
    NSString *sep = [NSString stringWithFormat:@"%@/",self.host];
    NSString *res = [self componentsSeparatedByString:sep].lastObject;
    NSString *reu = [res componentsSeparatedByString:@"/"].lastObject;
    NSArray  *ret = [reu componentsSeparatedByString:@":"];
    if (ret.count < 2) {
    return 0;
    }
    return [ret.lastObject intValue];
}
@end
//核心代码

//SGRtmpSession.h
#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, SGRtmpSessionStatus) {
    SGRtmpSessionStatusNone              = 0,
    SGRtmpSessionStatusConnected         = 1,

    SGRtmpSessionStatusHandshake0        = 2,
    SGRtmpSessionStatusHandshake1        = 3,
    SGRtmpSessionStatusHandshake2        = 4,
    SGRtmpSessionStatusHandshakeComplete = 5,

    SGRtmpSessionStatusFCPublish         = 6,
    SGRtmpSessionStatusReady             = 7,
    SGRtmpSessionStatusSessionStarted    = 8,

    SGRtmpSessionStatusError             = 9,
    SGRtmpSessionStatusNotConnected      = 10
};

@class SGRtmpSession;
@protocol SGRtmpSessionDeleagte <NSObject>

- (void)rtmpSession:(SGRtmpSession *)rtmpSession didChangeStatus:(SGRtmpSessionStatus)rtmpStatus;

@end

@interface SGRtmpSession : NSObject

@property (nonatomic,weak) id<SGRtmpSessionDeleagte> delegate;
@property (nonatomic,copy) NSString *url;

- (void)connect;

- (void)disConnect;
@end

//------------华丽的分割线 .m文件---------------------

#import "SGRtmpSession.h"
#import "SGStreamSession.h"
#import "NSString+URL.h"

//c1,c2,s1,s2的大小
static const size_t kRTMPSignatureSize = 1536;

@interface SGRtmpSession()<SGStreamSessionDelegate>


@property (nonatomic,strong) SGStreamSession *session;
/**
 *  状态很重要,贯穿整个项目
 */
@property (nonatomic,assign) SGRtmpSessionStatus rtmpStatus;

@property (nonatomic,strong) NSMutableData *handshake;

@end


@implementation SGRtmpSession

- (void)dealloc{
    NSLog(@"%s",__func__);
    self.url = nil;
    self.delegate = nil;
    self.session = nil;
    _rtmpStatus = SGRtmpSessionStatusNone;
}


- (SGStreamSession *)session{
    if (_session == nil) {
        _session = [[SGStreamSession alloc] init];
        _session.delegate = self;
    }
    return _session;
}

- (void)setUrl:(NSString *)url{
    _url = url;
    NSLog(@"scheme:%@",url.scheme);
    NSLog(@"host:%@",url.host);
    NSLog(@"app:%@",url.app);
    NSLog(@"playPath:%@",url.playPath);
    NSLog(@"port:%zd",url.port);
}

- (void)setRtmpStatus:(SGRtmpSessionStatus)rtmpStatus{
    _rtmpStatus = rtmpStatus;
    NSLog(@"rtmpStatus-----%zd",rtmpStatus);
    if ([self.delegate respondsToSelector:@selector(rtmpSession:didChangeStatus:)]) {
        [self.delegate rtmpSession:self didChangeStatus:_rtmpStatus];
    }
}

- (instancetype)init{
   
    if (self = [super init]) {
        _rtmpStatus = SGRtmpSessionStatusNone;
    }
    
    return self;
    
}

- (void)connect{
    [self.session connectToServer:self.url.host port:self.url.port];
}
- (void)disConnect{
    [self.session disConnect];
}

#pragma mark -------delegate---------
- (void)streamSession:(SGStreamSession *)session didChangeStatus:(SGStreamStatus)streamStatus{
    
    if (streamStatus & NSStreamEventHasBytesAvailable) {//收到数据
        [self didReceivedata];
        return;//return
    }
    
    if (streamStatus & NSStreamEventHasSpaceAvailable){ //可以写数据
        if (_rtmpStatus == SGRtmpSessionStatusConnected) {
           [self handshake0];
        }
        return;//return
    }
    
    if ((streamStatus & NSStreamEventOpenCompleted) &&
        _rtmpStatus < SGRtmpSessionStatusConnected) {
        self.rtmpStatus = SGRtmpSessionStatusConnected;
    }
    
    if (streamStatus & NSStreamEventErrorOccurred) {
        self.rtmpStatus = SGRtmpSessionStatusError;
    }
    
    if (streamStatus & NSStreamEventEndEncountered) {
        self.rtmpStatus = SGRtmpSessionStatusNotConnected;
    }
}

- (void)handshake0{
    
    self.rtmpStatus = SGRtmpSessionStatusHandshake0;
    
    //c0
    char c0Byte = 0x03;
    NSData *c0 = [NSData dataWithBytes:&c0Byte length:1];
    [self writeData:c0];
    
    //c1
    uint8_t *c1Bytes = (uint8_t *)malloc(kRTMPSignatureSize);
    memset(c1Bytes, 0, 4 + 4);
    NSData *c1 = [NSData dataWithBytes:c1Bytes length:kRTMPSignatureSize];
    free(c1Bytes);
    [self writeData:c1];
}

- (void)handshake1{
    self.rtmpStatus = SGRtmpSessionStatusHandshake2;
    NSData *s1 = [self.handshake subdataWithRange:NSMakeRange(0, kRTMPSignatureSize)];
    //c2
    uint8_t *s1Bytes = (uint8_t *)s1.bytes;
    memset(s1Bytes + 4, 0, 4);
    NSData *c2 = [NSData dataWithBytes:s1Bytes length:s1.length];
    [self writeData:c2];
}



//接收到数据
- (void)didReceivedata{
    NSData *data = [self.session readData];
    
    if (self.rtmpStatus >= SGRtmpSessionStatusConnected &&
        self.rtmpStatus < SGRtmpSessionStatusHandshakeComplete) {
        //将我收的数据保存起来,因为总数是3073个字节
        [self.handshake appendData:data];
    }
    
    NSLog(@"%zd",data.length);
    
    //handshke 服务气端情况
    //          1.按照官方文档c0,c1,c2
    //          2.一起发3073个字节
    //          3.先发一部分,再发一部分,每部分大小不确定,总数正确
    switch (_rtmpStatus) {
        case SGRtmpSessionStatusHandshake0:{
            uint8_t s0;
            [data getBytes:&s0 length:1];
            if (s0 == 0x03) {//s0
                self.rtmpStatus = SGRtmpSessionStatusHandshake1;
                if (data.length > 1) {//后面还有数据,但不确定长度
                    data = [data subdataWithRange:NSMakeRange(1, data.length -1)];
                    self.handshake = data.mutableCopy;
                }else{
                    break;
                }
            }else{
                NSLog(@"握手失败");
                break;
            }
        }
        case SGRtmpSessionStatusHandshake1:{
            
            if (self.handshake.length >= kRTMPSignatureSize) {//s1
                [self handshake1];
                
                if (self.handshake.length > kRTMPSignatureSize) {//>
                    NSData *subData = [self.handshake subdataWithRange:NSMakeRange(kRTMPSignatureSize, self.handshake.length - kRTMPSignatureSize)];
                    self.handshake = subData.mutableCopy;
                }else{// =
                    self.handshake = [NSMutableData data];
                    break;
                }
            }else{//<
                break;
            }
        }
            
        case SGRtmpSessionStatusHandshake2:{//s2
            if (data.length >= kRTMPSignatureSize) {
                NSLog(@"握手完成");
                self.rtmpStatus = SGRtmpSessionStatusHandshakeComplete;
 
            }
            break;
        }
        default:

            break;
    }
}

- (void)writeData:(NSData *)data{
    if (data.length == 0) {
        return;
    }
    [self.session writeData:data];

}
@end


握手部分相对来说比较恶心,虽然很简单,处理起来很麻烦,仔细打上断点试试.主要是解析服务器端数据比较绕,建议先从3073字节的那种情况做处理,其他的俩种情况,自己可以根据思路写一套逻辑.握手看明白了,后面就简单多了.

下面附上测试代码:

@interface ViewController ()
@property (nonatomic,strong) SGRtmpSession *session;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

- (SGRtmpSession *)session{
    if (_session == nil) {
        _session = [[SGRtmpSession alloc] init];
        _session.url = @"rtmp://192.168.1.106/live/2005";
    }
    return _session;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.session connect];
}

输出结果:

2016-08-02 22:23:15.764 SGRtmpPublisher[650:225659] scheme:rtmp
2016-08-02 22:23:15.766 SGRtmpPublisher[650:225659] host:192.168.1.106
2016-08-02 22:23:15.766 SGRtmpPublisher[650:225659] app:live
2016-08-02 22:23:15.766 SGRtmpPublisher[650:225659] playPath:2005
2016-08-02 22:23:15.766 SGRtmpPublisher[650:225659] port:0
2016-08-02 22:23:15.884 SGRtmpPublisher[650:225659] 连接成功
2016-08-02 22:23:15.885 SGRtmpPublisher[650:225659] rtmpStatus-----1
2016-08-02 22:23:15.886 SGRtmpPublisher[650:225659] 可以发送字节
2016-08-02 22:23:15.886 SGRtmpPublisher[650:225659] rtmpStatus-----2
2016-08-02 22:23:15.886 SGRtmpPublisher[650:225659] 可以发送字节
2016-08-02 22:23:15.898 SGRtmpPublisher[650:225659] 有字节可读
2016-08-02 22:23:15.898 SGRtmpPublisher[650:225659] 1537
2016-08-02 22:23:15.899 SGRtmpPublisher[650:225659] rtmpStatus-----3
2016-08-02 22:23:15.899 SGRtmpPublisher[650:225659] rtmpStatus-----4
2016-08-02 22:23:15.899 SGRtmpPublisher[650:225659] 可以发送字节
2016-08-02 22:23:15.900 SGRtmpPublisher[650:225659] 有字节可读
2016-08-02 22:23:15.900 SGRtmpPublisher[650:225659] 1536
2016-08-02 22:23:15.900 SGRtmpPublisher[650:225659] 握手完成
2016-08-02 22:23:15.900 SGRtmpPublisher[650:225659] rtmpStatus-----5

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

推荐阅读更多精彩内容

  • 作者原创,转载请联系作者 RTMP简介 Real Time Messaging Protocol(实时消息传送协议...
    Alfie20阅读 1,358评论 0 4
  • 实时消息协议---流的分块 版权声明: 版权(c)2009 Adobe系统有限公司。全权所有。 摘要: 本备忘录描...
    一个人zy阅读 1,881评论 0 9
  • 个人翻译,转载请注明出处,谢谢! Adobe's Real Time Messaging Protocol 摘要 ...
    SniperPan阅读 2,712评论 1 17
  • 写在前面的话 前面一篇文章已经对移动端数据源采集与编码进行了说明,接下来就是将之前采集的数据上传给我们的视频服务器...
    前世小书童阅读 8,886评论 3 25
  • 例行打卡ヾ ^_^ 依稀想起灵动轻快的《晨曲》,描述的大概也是这样一幕日出吧(・・)
    苔原小雪狐阅读 198评论 0 2