iOS P2P通信實(shí)現(xiàn)流程

一、背景

  • 背景:記錄下iOS P2P通信的實(shí)現(xiàn)的流程废膘,以及實(shí)現(xiàn)過程中遇到的問題旬薯,方便后續(xù)bug調(diào)試,為代碼整潔規(guī)范化提供設(shè)計(jì)思路
  • 目標(biāo)群體:iOS及客戶端開發(fā)人員
  • 技術(shù)應(yīng)用場景:iPhone敛劝、iPad端音視頻會(huì)議通話
  • 整體思路:1余爆、使用WebSocket連接信令服務(wù)器,執(zhí)行登錄操作(userId登錄夸盟,沒有則用游客身份登錄)蛾方,2、WebRTC實(shí)現(xiàn)P2P連接
    • 先入會(huì)者:1上陕、監(jiān)聽新人加入-->2桩砰、創(chuàng)建與新人的PeerConnection連接對(duì)象,發(fā)送_start信令-->3释簿、收到_startResp后創(chuàng)建offer亚隅,設(shè)置setLocalDescription為offer,發(fā)送_offer信令-->4庶溶、收到_answer信令煮纵,setRemoteDescription為answer-->5、互相發(fā)送可用的ice_candidate偏螺,WebRTC會(huì)篩選出最合適的ICE通道連接-->6行疏、雙方建立P2P連接,開始通信砖茸;
    • 后入會(huì)者:1隘擎、查詢會(huì)議成員列表,創(chuàng)建與會(huì)中所有人的PeerConnection對(duì)象-->2凉夯、收到_start信令后回復(fù)_startResp-->3货葬、收到_offer信令,setRemoteDescription為offer-->4劲够、創(chuàng)建answer震桶,setLocalDescription為answer,發(fā)送_answer給對(duì)方-->后續(xù)步驟同先入會(huì)者步驟5征绎、6

流程圖:

暫時(shí)無法在文檔外展示此內(nèi)容

二蹲姐、操作步驟

2.1 開發(fā)前的準(zhǔn)備工作

準(zhǔn)備工作一

  • Xcode安裝好動(dòng)態(tài)庫管理器cocoaPods磨取,引入WebRTC、WebSocket兩個(gè)庫柴墩,代碼如下

pod 'GoogleWebRTC'

pod 'SocketRocket'

準(zhǔn)備工作二

創(chuàng)建EMServerBaseManager信令基類忙厌,實(shí)現(xiàn)EMServerManagerProtocol,公共的連接江咳、斷開的連接逢净、發(fā)送數(shù)據(jù)等方法聲明放此協(xié)議中,方便擴(kuò)展新的信令協(xié)議(WebSocket歼指、MQTT等爹土,目前服務(wù)端用的是WebSocket協(xié)議);

2.2 進(jìn)入開發(fā)階段

1踩身、連接信令服務(wù)器

建立webSocket長連接胀茵,連接失敗就執(zhí)行重連操作,重連時(shí)間間隔以2的指數(shù)倍增長挟阻,網(wǎng)絡(luò)斷開的情況下就開啟一個(gè)定時(shí)器去檢查網(wǎng)絡(luò)情況琼娘,有網(wǎng)絡(luò)了就更新重連時(shí)間,馬上進(jìn)行重連赁濒;

- (void)connectServer {
  if (self.connectState == EMSocketConnectStateConnected || self.connectState == EMSocketConnectStateConnecting) return;
  self.isActivelyClose = NO; 
  [self webSocketClose]; 
  NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:EMSocketBaseUrl]];
  self.webSocket = [[SRWebSocket alloc] initWithURLRequest:request];
  self.webSocket.delegate = self;
  [self.webSocket open]; 
  self.connectState = EMSocketConnectStateConnecting;
}

//連接成功回調(diào)
- (void)webSocketDidOpen:(SRWebSocket *)webSocket { 
  self.reConnectTime = 0; // 重連間隔時(shí)長
  [self sendDataToServer:@{EMSocketMsg:EMSocketMsg_login}]; // 登錄
  [self initHeartBeat]; //開啟心跳
  self.connectState = EMSocketConnectStateConnected; 
} 

//連接失敗回調(diào)
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
  self.connectState = EMSocketConnectStateDisConnected;
  //用戶主動(dòng)斷開連接轨奄,就不去進(jìn)行重連
  if(self.isActivelyClose) {
    return;
  }

  [self destoryHeartBeat]; //斷開連接時(shí)銷毀心跳

  /// 剛斷開連接,網(wǎng)絡(luò)狀態(tài)可能還沒有改變拒炎,導(dǎo)致網(wǎng)絡(luò)檢測定時(shí)器沒有打開
  __weak typeof(self) wSelf = self;
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    if(AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable && !wSelf.netWorkTestingTimer) {
      [wSelf noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測定時(shí)器
    }
  });
  //判斷網(wǎng)絡(luò)環(huán)境
  if (![self checkNetWork]) //沒有網(wǎng)絡(luò)
  {
    [self noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測定時(shí)器
  }
  else //有網(wǎng)絡(luò)
  {
    [self reConnectServer];//連接失敗就重連
  }
}

//連接關(guān)閉,注意連接關(guān)閉不是連接斷開挪拟,關(guān)閉是 [socket close] 客戶端主動(dòng)關(guān)閉,斷開可能是斷網(wǎng)了击你,被動(dòng)斷開的玉组。
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
  // 在這里判斷 webSocket 的狀態(tài) 是否為 open
  self.connectState = EMSocketConnectStateDisConnected;
  if(self.webSocket.readyState == SR_OPEN || self.isActivelyClose) {
    return;
  }

  DLog(@"被關(guān)閉連接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);

  [self destoryHeartBeat]; //斷開連接時(shí)銷毀心跳

  //判斷網(wǎng)絡(luò)環(huán)境
  if (![self checkNetWork]) //沒有網(wǎng)絡(luò)
  {
    [self noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測
  }
  else //有網(wǎng)絡(luò)
  {
    [self reConnectServer];//連接失敗就重連
  }
}

2丁侄、WebRtc P2P連接過程

1惯雳、創(chuàng)建peerConnectionFactory對(duì)象,設(shè)置默認(rèn)的視頻編解碼工廠類鸿摇,此處可以自定義視頻編解碼類石景;

- (RTCPeerConnectionFactory *)peerConnectionFactory {
  if (!_peerConnectionFactory) {
    [RTCPeerConnectionFactory initialize];
    RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init];
    RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init];
    NSArray *codecs = [encoderFactory supportedCodecs];
    [encoderFactory setPreferredCodec:codecs[2]];
    _peerConnectionFactory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory:encoderFactory decoderFactory:decoderFactory];
  }
  return _peerConnectionFactory;
}

2、創(chuàng)建新的PeerConnection對(duì)象拙吉,需要設(shè)置配置RTCConfiguration潮孽、約束RTCMediaConstraints;

// 創(chuàng)建新的PeerConnection
- (RTCPeerConnection *)createPeerConnection:(NSString *)connectionId {
  RTCPeerConnection *peerConnection = [self.peerConnectionFactory peerConnectionWithConfiguration:self.rtcConfig constraints:[self defaultPeerConnContraints] delegate:self];
  //把本地流加到連接中去
  [peerConnection addStream:self.localStream];
  return peerConnection;
}

- (RTCConfiguration *)rtcConfig {
  if (!_rtcConfig) {
    NSArray *ICEServers = [NSArray arrayWithObject:[self defaultSTUNServer]];
    RTCConfiguration *configuration = [[RTCConfiguration alloc] init];
    [configuration setIceServers:ICEServers];
    configuration.iceConnectionReceivingTimeout = 90000;// 90s超時(shí)
    _rtcConfig = configuration;
  }
  return _rtcConfig;
}

// 此處填寫透傳服務(wù)器的地址筷黔,username往史,password等
- (RTCIceServer *)defaultSTUNServer {
  RTCIceServer *defaultServer = [[RTCIceServer alloc] initWithURLStrings:@[@""] username:@"" credential:@""];
  return defaultServer;
}

- (RTCMediaConstraints *)defaultPeerConnContraints {
  // 配置信息的基礎(chǔ)單元,以鍵值對(duì)的方式
   NSMutableDictionary *mandatory = @{kRTCMediaConstraintsOfferToReceiveAudio:kRTCMediaConstraintsValueTrue}.mutableCopy;
   [mandatory setValue:kRTCMediaConstraintsValueTrue forKey:kRTCMediaConstraintsOfferToReceiveVideo];
  RTCMediaConstraints *media = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:nil];
  return media;
}

3佛舱、創(chuàng)建本地媒體流信息椎例,添加本地音視頻軌道到媒體流中挨决,其中初始化數(shù)據(jù)源中,adaptOutputFormatToWidth可以指定視頻的寬高比订歪,幀率等等脖祈,當(dāng)前先用默認(rèn)的;

// 創(chuàng)建本地音視頻軌道
- (RTCMediaStream *)localStream {
  if (!_localStream) { 
    // 初始化媒體流陌粹,kEM_STREAM和kEM_AUDIO_0撒犀、kEM_VIDEO_0為專業(yè)標(biāo)識(shí)符。
    _localStream = [self.peerConnectionFactory mediaStreamWithStreamId:kEM_STREAM];
    // 添加音頻軌道
    self.localAudioTrack = [self.peerConnectionFactory audioTrackWithTrackId:kEM_AUDIO_0];
    [_localStream addAudioTrack:self.localAudioTrack];
    // 添加視頻軌道
    [_localStream addVideoTrack:self.localVideoTrack];
  }
  return _localStream;
}

- (RTCVideoTrack *)localVideoTrack {
  if (!_localVideoTrack) {
    // 獲取攝像頭
    AVCaptureDevice * device = [self defaultDevice]; 
    //獲取數(shù)據(jù)源
    RTCVideoSource *_source = [self.peerConnectionFactory videoSource];
//    [_source adaptOutputFormatToWidth:720 height:1280 fps:20];
    //拿到capture對(duì)象
    RTCCameraVideoCapturer *capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:_source];
    AVCaptureDeviceFormat *format = [[RTCCameraVideoCapturer supportedFormatsForDevice:device] lastObject];
//    CGFloat fps = [[format videoSupportedFrameRateRanges] firstObject].maxFrameRate;
    if (self.cameraState == EMMediaDeviceStatePlay) {
      [capture startCaptureWithDevice:device format:format fps:20 completionHandler:^(NSError * error) {
      }];
    } 
    self.capture = capture; 
    _localVideoTrack = [self.peerConnectionFactory videoTrackWithSource:_source trackId:kEM_VIDEO_0];
  }
  return _localVideoTrack;
}

4掏秩、先入會(huì)者發(fā)送_start、創(chuàng)建offer荆姆,設(shè)置本地描述蒙幻,成功就發(fā)送_offer信令給對(duì)方,等待對(duì)方發(fā)送的answer存到本地遠(yuǎn)程描述胆筒;setStartState為保存start狀態(tài)邮破,方便后續(xù)斷線重連階段判斷是有誰發(fā)起start

/// 請求開始進(jìn)行 P2P 傳輸準(zhǔn)備
- (void)sendStartMsgToMemberId:(NSString *)memberId {
  NSDictionary *data = @{EMSocketMsg:EMSocketMsg_start,
              EMSocketToId: memberId};
  [self sendDataToServer:data completionHandler:nil]; 
  [self setStartState:false isSender:true fromId:memberId];
}

- (void)getSocketMsg:(NSNotification *)x {
  NSDictionary *dict = x.userInfo;
  NSString *msg = dict[EMSocketMsg];
  // 收到start回復(fù)
  if ([msg isEqualToString:EMSocketMsg_start_resp]) {
    NSString *fromId = dict[EMSocketFromId];
    [self setStartState:true isSender:true fromId:fromId];
    RTCPeerConnection *peerConnection = [wSelf.connectionDic objectForKey:fromId];
    /// 發(fā)送offer
    [wSelf createOffer:peerConnection];
  }
}

/**
 * 創(chuàng)建offer
 */
- (void)createOffer:(RTCPeerConnection *)peerConnection{
  __weak typeof(self) wSelf = self;
  [peerConnection offerForConstraints:[self defaultPeerConnContraints] completionHandler:^(RTCSessionDescription *_Nullable sdp, NSError *_Nullable error) {
    if(error){
      DLog(@"Failed to create offer SDP, err=%@", error);
    } else {
      RTCSessionDescription *newSdp = [wSelf getNewSdpWithType:(RTCSdpTypeOffer) sdp:sdp];
      [wSelf setLocalOffer:peerConnection withSdp:newSdp];
    }
  }];
}

- (void)setLocalOffer:(RTCPeerConnection *)pc withSdp:(RTCSessionDescription *)sdp {
  __weak RTCPeerConnection *weakPeerConnection = pc;
  __weak typeof(self) wSelf = self;
  [pc setLocalDescription:sdp completionHandler:^(NSError *_Nullable error) {
    if (!error) {
      DLog(@"Successed to set local offer sdp!");
      [wSelf sendOffer:weakPeerConnection withSdp:sdp];
    }else {
      DLog(@"Failed to set local offer sdp, err=%@", error);
    }
  }];
}

- (void)sendOffer:(RTCPeerConnection *)pc withSdp:(RTCSessionDescription *)sdp {
  NSString *currentId = [self getKeyFromConnectionDic:pc];
  NSDictionary *dict = [[NSDictionary alloc] initWithObjects:@[@"offer", sdp.sdp] forKeys: @[@"type", @"sdp"]];
  NSDictionary *data = @{EMSocketMsg:EMSocketMsg_offer,
             @"data":@{@"sdp":dict},
              EMSocketToId:currentId};
  [self sendDataToServer:data completionHandler:nil];
}

#pragma mark --收到 answer
- (void)answerWith:(NSDictionary *)dic{
  NSDictionary *dataDic = dic[@"data"]; 
  NSDictionary *sdp = dataDic[@"sdp"];
  NSString *sdpStr = sdp[@"sdp"];

  NSString *fromId = dic[EMSocketFromId];
  RTCPeerConnection *peerConnection = [self.connectionDic objectForKey:fromId];
  RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeAnswer sdp:sdpStr]; 
  [peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
    if (error){
      DLog(@"Failed to setRemoteDescription, err=%@", error);
    }
  }];
}

5、后入會(huì)者查詢?nèi)簝?nèi)成員列表仆救,建立與他們的peerConnection抒和,等待_start信令,發(fā)送_start_resp響應(yīng)彤蔽,等待_offer信令到來摧莽,把offer設(shè)置到遠(yuǎn)程描述,創(chuàng)建answer設(shè)置到本地顿痪,發(fā)送給對(duì)方镊辕;

#pragma mark --收到 offer
- (void)offerWith:(NSDictionary *)dic {
  NSDictionary *dataDic = dic[@"data"];
  NSString *fromId = dic[EMSocketFromId];
  //拿到SDP
  NSDictionary *sdp = dataDic[@"sdp"];
  NSString *sdpStr = sdp[@"sdp"];
  //根據(jù)類型和SDP 生成SDP描述對(duì)象
  RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdpStr];
  //拿到當(dāng)前對(duì)應(yīng)的點(diǎn)對(duì)點(diǎn)連接
  RTCPeerConnection *peerConnection = [self.connectionDic objectForKey:fromId];
  // 需要判斷當(dāng)前peerConnection是否有answer或者offer,有就重置
  if (peerConnection.remoteDescription || peerConnection.localDescription) {
    // 創(chuàng)建一個(gè)新的連接
    peerConnection = [self createPeerConnection:fromId];
    //并且設(shè)置到Dic中去
    [self.connectionDic setObject:peerConnection forKey:fromId];
  }
  //設(shè)置給這個(gè)點(diǎn)對(duì)點(diǎn)連接
  __weak RTCPeerConnection *weakPeerConnection = peerConnection;
  __weak typeof(self) wSelf = self;
  [peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
    if (!error) {
      //創(chuàng)建一個(gè)answer,會(huì)把自己的SDP信息返回出去
      [weakPeerConnection answerForConstraints:[wSelf defaultPeerConnContraints] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
        if (!error) {
          RTCSessionDescription *newSdp = sdp;// [wSelf getNewSdpWithType:(RTCSdpTypeAnswer) sdp:sdp];
          [weakPeerConnection setLocalDescription:newSdp completionHandler:^(NSError * _Nullable error) {
            NSDictionary *dic = @{EMSocketMsg: EMSocketMsg_answer,
                       @"data": @{
                         @"sdp": @{@"sdp":newSdp.sdp,@"type":@"answer",}
                       },
                       EMSocketToId:fromId};
            [wSelf sendDataToServer:dic completionHandler:nil];
          }];
        } else {
          DLog(@"Failed to create answer an sdp, err=%@", error);
        }
      }];
    } else {
      DLog(@"Failed to setRemoteDescription, err=%@", error);
    }
  }];
}

6蚁袭、雙方互相設(shè)置好offer征懈、answer,建立P2P連接揩悄,WebRtc會(huì)把收集到的可用的candidate回調(diào)給本地卖哎,本地在通過信令服務(wù)器發(fā)送給對(duì)方就可以,對(duì)方收到遠(yuǎn)端的ice_candidate删性,給本地peerConnection addIceCandidate就可以了亏娜,IceCandidate可能會(huì)轉(zhuǎn)發(fā)多次,WebRtc會(huì)找到最合適的通道建立連接镇匀;

// 收到遠(yuǎn)端的ice_candidate
- (void)_ice_candidateWith:(NSDictionary *)dic {
  NSDictionary *dataDic = dic[@"data"];
  NSDictionary *candidateDic =dataDic[@"candidate"];
  NSString *sdpMid = candidateDic[@"sdpMid"];

  int sdpMLineIndex = [candidateDic[@"sdpMLineIndex"] intValue];
  NSString *sdp = candidateDic[@"candidate"];
  //生成遠(yuǎn)端網(wǎng)絡(luò)地址對(duì)象
  RTCIceCandidate *candidate = [[RTCIceCandidate alloc] initWithSdp:sdp sdpMLineIndex:sdpMLineIndex sdpMid:sdpMid];
  //添加到點(diǎn)對(duì)點(diǎn)連接中
  NSString *fromId = dic[EMSocketFromId];
  RTCPeerConnection *peerConnection = [_connectionDic objectForKey:fromId];
  [peerConnection addIceCandidate:candidate];
}

#pragma mark - RTCPeerConnectionDelegate
// 該方法用于收集可用的candidate
- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate {
  NSString *userId = [self getKeyFromConnectionDic:peerConnection];
  NSDictionary *dic = @{
             EMSocketMsg: EMSocketMsg_ice_candidate,
             @"data":@{
                 @"candidate":@{
                     @"sdpMid":candidate.sdpMid ,
                     @"sdpMLineIndex":[NSNumber numberWithInteger:candidate.sdpMLineIndex],
                     @"candidate": candidate.sdp
                     }
                 } ,
             EMSocketToId:userId
             };
  [self sendDataToServer:dic completionHandler:nil];
}

7照藻、將對(duì)方視頻渲染到本地

/* *Called when media is received on a new stream from remote peer. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didAddStream:(RTCMediaStream *)stream {
  LOG_ME 
  dispatch_async(dispatch_get_main_queue(), ^{ 
    if (stream.videoTracks.count) {
      NSString *userId = [self getKeyFromConnectionDic:peerConnection];
//      RTCVideoTrack *remoteVideoTrack = stream.videoTracks[0]; 
      // 遠(yuǎn)端視頻軌道一定要保存在本地,否則會(huì)渲染不出來
      if ([self->_delegate respondsToSelector:@selector(webRtcManager:addRemoteStream:userId:)]) {
        [self->_delegate webRtcManager:self addRemoteStream:stream userId:userId];
      }
    }
  });
}

2.3 注意事項(xiàng)

1汗侵、iOS視頻編碼格式

iOS并不支持VP8的視頻編碼格式幸缕,但是WebRtc生成的offer群发、answer描述又都是把VP8放前,默認(rèn)是VP8編碼格式发乔,所以此處要對(duì)WebRTC生成的描述進(jìn)行處理熟妓,交互VP8跟H264編碼的順序(iOS對(duì)H264的支持度很高),然后再生成新的描述對(duì)象栏尚,存到本地起愈;

RTCSessionDescription *newSdp = [self getNewSdpWithType:(RTCSdpTypeOffer) sdp:sdp];
[elf setLocalOffer:peerConnection withSdp:newSdp];

2.4 參考資料

iOS WebSocket長鏈接

iOS WebRTC的使用

iOS webRTC SDP介紹及設(shè)置

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市译仗,隨后出現(xiàn)的幾起案子抬虽,更是在濱河造成了極大的恐慌,老刑警劉巖纵菌,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阐污,死亡現(xiàn)場離奇詭異,居然都是意外死亡咱圆,警方通過查閱死者的電腦和手機(jī)笛辟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來序苏,“玉大人手幢,你說我怎么就攤上這事〕老辏” “怎么了围来?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長踱阿。 經(jīng)常有香客問我管钳,道長,這世上最難降的妖魔是什么软舌? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任才漆,我火速辦了婚禮,結(jié)果婚禮上佛点,老公的妹妹穿的比我還像新娘醇滥。我一直安慰自己,他們只是感情好超营,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布鸳玩。 她就那樣靜靜地躺著,像睡著了一般演闭。 火紅的嫁衣襯著肌膚如雪不跟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天米碰,我揣著相機(jī)與錄音窝革,去河邊找鬼购城。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虐译,可吹牛的內(nèi)容都是我干的瘪板。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼漆诽,長吁一口氣:“原來是場噩夢啊……” “哼侮攀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起厢拭,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤兰英,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后供鸠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體箭昵,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年回季,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片正林。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泡一,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出觅廓,到底是詐尸還是另有隱情鼻忠,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布杈绸,位于F島的核電站帖蔓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瞳脓。R本人自食惡果不足惜塑娇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劫侧。 院中可真熱鬧埋酬,春花似錦、人聲如沸烧栋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽审姓。三九已至珍特,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魔吐,已是汗流浹背扎筒。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來泰國打工莱找, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砸琅。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓宋距,卻偏偏與公主長得像,于是被迫代替她去往敵國和親症脂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谚赎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

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