RTMP 握手

作者: youngyunxing | 来源:发表于2016-08-02 01:49 被阅读953次

    很多初学者就是看了恶心的握手就再也没有研究的兴趣了,不过,弄懂了就感觉没什么了.
    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
    
    

    相关文章

      网友评论

        本文标题:RTMP 握手

        本文链接:https://www.haomeiwen.com/subject/uqdfsttx.html