RTMP 握手

很多初學(xué)者就是看了惡心的握手就再也沒有研究的興趣了,不過,弄懂了就感覺沒什么了.
socket建立連接以后,就需要認(rèn)證了,也就是握手.只有握手完成以后才能進(jìn)入下一步的操作,比如發(fā)送控制命令,交換消息,發(fā)送音視頻包等.客戶端需要發(fā)送C0,C1,C2給服務(wù)器端,服務(wù)器端需要發(fā)送S0,S1,S2給客戶端,至于C0,C1,C2,S0,S1,S2是什么,下面會(huì)介紹,暫時(shí)認(rèn)為是數(shù)據(jù)吧.
重點(diǎn)來(lái)了:

  • 握手順序
    • 客戶端首先發(fā)送C0,等待服務(wù)器返回S0
    • 服務(wù)器端收到C0后,發(fā)送S0給客戶端
    • 客戶端收到S0后,發(fā)送C1個(gè)服務(wù)器
    • 服務(wù)器收到C1后發(fā)送S1給客戶端
    • 客戶端收到S1后發(fā)送C2給服務(wù)器端
    • 服務(wù)器端收到C2后,發(fā)送S2給客戶端
    • 握手完成

是不是感覺很復(fù)雜的樣子,來(lái)一張圖清晰明了:

客戶端       服務(wù)器端

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

然而實(shí)際上,客戶端通常都是C0和C1一起發(fā)送,或者C0發(fā)送完馬上發(fā)送C1.而服務(wù)器端一般都做了兼容處理,可能按照順序發(fā)送,也可能在收到C1后就把S0,S1,S2一起發(fā)送給客戶端.

  • C0,C1,C2,S0,S1,S2 的數(shù)據(jù)格式

    • C0 和 S0 的格式
      C0 和 S0 包都是一個(gè)字節(jié)(8bit),表示版本號(hào)

      Paste_Image.png

      在 C0 中取胎,這一字段指示出客戶端要求的 RTMP 版本號(hào)嘶朱。在 S0 中,這一字段指示出服務(wù)器端選擇的 RTMP 版本號(hào)。默認(rèn)為 3溯壶。0哈蝇、1、2 三個(gè)值是由早期其他產(chǎn)品使用的薪寓,是廢棄值亡资;4 - 31 被保留為 RTMP 協(xié)議的未來(lái)實(shí)現(xiàn)版本使用;32 - 255 不允許使用 (以區(qū)分開 RTMP 和其他常以一個(gè)可打印字符開始的文本協(xié)議)向叉。無(wú)法識(shí)別客戶端所請(qǐng)求版本號(hào)的服務(wù)器應(yīng)該以版本 3 響應(yīng)锥腻,(收到響應(yīng)的) 客戶端可以選擇降低到版本 3,或者放棄握手母谎。

    • C1 和 S1 的格式
      C1 和 S1 數(shù)據(jù)包的長(zhǎng)度都是 1536 字節(jié)瘦黑,包含以下字段:

      Paste_Image.png

      Time (前四個(gè)字節(jié)):這個(gè)字段包含一個(gè) timestamp,用于本終端發(fā)送的所有后續(xù)塊的時(shí)間起點(diǎn)奇唤。這個(gè)值可以是 0幸斥,或者一些任意值。要同步多個(gè)塊流咬扇,終端可以發(fā)送其他塊流當(dāng)前的 timestamp 的值甲葬。
      Zero (緊跟著的四個(gè)字節(jié)):這個(gè)字段必須都是 0。
      Random data (剩下的1528 個(gè)字節(jié)):這個(gè)字段可以包含任意值懈贺。終端需要區(qū)分出響應(yīng)來(lái)自它發(fā)起的握手還是對(duì)端發(fā)起的握手经窖,這個(gè)數(shù)據(jù)應(yīng)該發(fā)送一些足夠隨機(jī)的數(shù)。簡(jiǎn)單點(diǎn),就是隨機(jī)數(shù).

    • C2 和 S2 的格式
      C2 和 S2 數(shù)據(jù)包長(zhǎng)度都是 1536 字節(jié)梭灿,基本就是 S1 和 C1 的副本 (分別)画侣,包含有以下字段:


      Paste_Image.png
      • Time (前四個(gè)字節(jié)):這個(gè)字段必須包含終端在 S1 (給 C2) 或者 C1 (給 S2) 發(fā)的 timestamp.例如:C1的前四個(gè)字節(jié)為0,那么S2的前四個(gè)字節(jié)也是0.
      • Time2 (緊跟著的四個(gè)字節(jié)):這個(gè)字段必須包含終端先前發(fā)出數(shù)據(jù)包 (s1 或者 c1) timestamp,例如,S1的前四個(gè)字節(jié)為 0x00 00 00 01,那么S2的第4~8字節(jié)就是0x 00 00 00 01
      • Random echo (剩下1528 個(gè)字節(jié)):這個(gè)字段必須包含終端發(fā)的 S1 (給 C2) 或者 S2 (給 C1) 的隨機(jī)數(shù)。兩端都可以一起使用 time 和 time2 字段再加當(dāng)前 timestamp 以快速估算帶寬和/或者連接延遲堡妒,但這不太可能是有多大用處配乱。
  • 如果握手失敗,服務(wù)器會(huì)終止響應(yīng),并斷開socket連接

  • 如果握手成功,則可以進(jìn)入到下一個(gè)環(huán)節(jié),可以開始交換消息了.

  • 實(shí)際測(cè)試(用的SRS測(cè)的)發(fā)現(xiàn),服務(wù)器端根本不鳥你,只要收到C1,后面就可能先發(fā)一部分,再發(fā)一部分(長(zhǎng)度不定,不一定是上文所說(shuō)的1536),也可能一次性全部給你,但是S0+S1+S2的總字節(jié)數(shù)(也就是3073個(gè)字節(jié))是對(duì)的
    附上代碼:

先來(lái)個(gè)分類:主要是解析url的各個(gè)部分:

//解析推流地址
@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;
/**
 *  狀態(tài)很重要,貫穿整個(gè)項(xiàng)目
 */
@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) {//收到數(shù)據(jù)
        [self didReceivedata];
        return;//return
    }
    
    if (streamStatus & NSStreamEventHasSpaceAvailable){ //可以寫數(shù)據(jù)
        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];
}



//接收到數(shù)據(jù)
- (void)didReceivedata{
    NSData *data = [self.session readData];
    
    if (self.rtmpStatus >= SGRtmpSessionStatusConnected &&
        self.rtmpStatus < SGRtmpSessionStatusHandshakeComplete) {
        //將我收的數(shù)據(jù)保存起來(lái),因?yàn)榭倲?shù)是3073個(gè)字節(jié)
        [self.handshake appendData:data];
    }
    
    NSLog(@"%zd",data.length);
    
    //handshke 服務(wù)氣端情況
    //          1.按照官方文檔c0,c1,c2
    //          2.一起發(fā)3073個(gè)字節(jié)
    //          3.先發(fā)一部分,再發(fā)一部分,每部分大小不確定,總數(shù)正確
    switch (_rtmpStatus) {
        case SGRtmpSessionStatusHandshake0:{
            uint8_t s0;
            [data getBytes:&s0 length:1];
            if (s0 == 0x03) {//s0
                self.rtmpStatus = SGRtmpSessionStatusHandshake1;
                if (data.length > 1) {//后面還有數(shù)據(jù),但不確定長(zhǎng)度
                    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


握手部分相對(duì)來(lái)說(shuō)比較惡心,雖然很簡(jiǎn)單,處理起來(lái)很麻煩,仔細(xì)打上斷點(diǎn)試試.主要是解析服務(wù)器端數(shù)據(jù)比較繞,建議先從3073字節(jié)的那種情況做處理,其他的倆種情況,自己可以根據(jù)思路寫一套邏輯.握手看明白了,后面就簡(jiǎn)單多了.

下面附上測(cè)試代碼:

@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];
}

輸出結(jié)果:

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] 可以發(fā)送字節(jié)
2016-08-02 22:23:15.886 SGRtmpPublisher[650:225659] rtmpStatus-----2
2016-08-02 22:23:15.886 SGRtmpPublisher[650:225659] 可以發(fā)送字節(jié)
2016-08-02 22:23:15.898 SGRtmpPublisher[650:225659] 有字節(jié)可讀
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] 可以發(fā)送字節(jié)
2016-08-02 22:23:15.900 SGRtmpPublisher[650:225659] 有字節(jié)可讀
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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市涕蚤,隨后出現(xiàn)的幾起案子宪卿,更是在濱河造成了極大的恐慌,老刑警劉巖万栅,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佑钾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡烦粒,警方通過查閱死者的電腦和手機(jī)休溶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門代赁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人兽掰,你說(shuō)我怎么就攤上這事芭碍。” “怎么了孽尽?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵窖壕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我杉女,道長(zhǎng)瞻讽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任熏挎,我火速辦了婚禮速勇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坎拐。我一直安慰自己烦磁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布哼勇。 她就那樣靜靜地躺著都伪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猴蹂。 梳的紋絲不亂的頭發(fā)上院溺,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音磅轻,去河邊找鬼珍逸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛聋溜,可吹牛的內(nèi)容都是我干的谆膳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼撮躁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼漱病!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起把曼,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤杨帽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后嗤军,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體注盈,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年叙赚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了老客。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片僚饭。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖胧砰,靈堂內(nèi)的尸體忽然破棺而出鳍鸵,到底是詐尸還是另有隱情,我是刑警寧澤尉间,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布偿乖,位于F島的核電站,受9級(jí)特大地震影響乌妒,放射性物質(zhì)發(fā)生泄漏汹想。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一撤蚊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧损话,春花似錦侦啸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至拧烦,卻和暖如春忘闻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恋博。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工齐佳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人债沮。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓炼吴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親疫衩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子硅蹦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • 作者原創(chuàng),轉(zhuǎn)載請(qǐng)聯(lián)系作者 RTMP簡(jiǎn)介 Real Time Messaging Protocol(實(shí)時(shí)消息傳送協(xié)議...
    Alfie20閱讀 1,376評(píng)論 0 4
  • 實(shí)時(shí)消息協(xié)議---流的分塊 版權(quán)聲明: 版權(quán)(c)2009 Adobe系統(tǒng)有限公司闷煤。全權(quán)所有童芹。 摘要: 本備忘錄描...
    一個(gè)人zy閱讀 1,909評(píng)論 0 9
  • 個(gè)人翻譯,轉(zhuǎn)載請(qǐng)注明出處鲤拿,謝謝假褪! Adobe's Real Time Messaging Protocol 摘要 ...
    SniperPan閱讀 2,750評(píng)論 1 17
  • 寫在前面的話 前面一篇文章已經(jīng)對(duì)移動(dòng)端數(shù)據(jù)源采集與編碼進(jìn)行了說(shuō)明,接下來(lái)就是將之前采集的數(shù)據(jù)上傳給我們的視頻服務(wù)器...
    前世小書童閱讀 8,958評(píng)論 3 25
  • 例行打卡ヾ ^_^ 依稀想起靈動(dòng)輕快的《晨曲》皆愉,描述的大概也是這樣一幕日出吧(??)
    苔原小雪狐閱讀 204評(píng)論 0 2