很多初学者就是看了恶心的握手就再也没有研究的兴趣了,不过,弄懂了就感觉没什么了.
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),表示版本号在 C0 中,这一字段指示出客户端要求的 RTMP 版本号。在 S0 中,这一字段指示出服务器端选择的 RTMP 版本号。默认为 3。0、1、2 三个值是由早期其他产品使用的,是废弃值;4 - 31 被保留为 RTMP 协议的未来实现版本使用;32 - 255 不允许使用 (以区分开 RTMP 和其他常以一个可打印字符开始的文本协议)。无法识别客户端所请求版本号的服务器应该以版本 3 响应,(收到响应的) 客户端可以选择降低到版本 3,或者放弃握手。
-
C1 和 S1 的格式
C1 和 S1 数据包的长度都是 1536 字节,包含以下字段:Time (前四个字节):这个字段包含一个 timestamp,用于本终端发送的所有后续块的时间起点。这个值可以是 0,或者一些任意值。要同步多个块流,终端可以发送其他块流当前的 timestamp 的值。
Zero (紧跟着的四个字节):这个字段必须都是 0。
Random data (剩下的1528 个字节):这个字段可以包含任意值。终端需要区分出响应来自它发起的握手还是对端发起的握手,这个数据应该发送一些足够随机的数。简单点,就是随机数. -
C2 和 S2 的格式
C2 和 S2 数据包长度都是 1536 字节,基本就是 S1 和 C1 的副本 (分别),包含有以下字段:
- 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