iOS远程推送之(一):APNs原理和基本配置

一、前言

iOS中消息推送有两种方式,本地推送和远程推送。本地推送在iOS中使用本地通知为你的APP添加提示用户功能这篇博客中有详细的介绍,我们在此主要讨论远程推送的流程与配置过程以及注意事项。
官方文档:Local and Remote Notification Programming Guide

二、简介

什么是远程推送?

  • 苹果提供的一项给终端设备推送消息的服务

为何使用远程推送?

  • 当用户打开应用程序的通知中心之后,苹果远程推送服务器就能把消息推送到装有该应用的设备上,具有强制性、实时性的特点,并且用户无需打开应用都能收到推送的消息。
三、远程推送原理

说到远程推送不得不说下下面这张图,下面这张图把远程推送的过程大致描述勾画了一遍,在此解析一下下图。

WechatIMG52.jpeg
Snip20170705_169.png
Snip20170705_170.png

名词解释:

  • Provider:消息提供者,一般是我们的后台服务器或者第三方推送服务器后台
  • APNs(Apple push notification server):苹果的远程推送服务器,可以说是消息中转站,需要发送给iOS客户端的消息统一发往苹果的APNs服务器
  • notification:需要推送给iOS客户端(iPhone或者是iPad)上的消息
  • Client App:客户端App,一般是安装在iPhone或者是iPad上的应用程序(App)
  • deviceToken:是唯一的由APNs根据设备和App来生成的一串数据,那么在以下三种情况下会发生改变:
    ①同一个设备上重新安装同一款应用
    ②同一个应用安装在不同的设备上
    ③设备重新安装了系统,同一个应用对应的deviceToken也会改变
An app-specific device token is globally unique and identifies one app-device combination. 
释义:deviceToken是device是App和device结合的唯一编码
Upon receiving a device token from APNs in your app, it is your responsibility to open a network connection to your provider.
释义:在你的设备上接到来自于APNs的deviceToken之前,你应该或者有责任先打开provider和APNs之间的网络连接
It is also your responsibility, in your app, to then forward the device token along with any other relevant data you want to send to the provider.
释义:你依然应该在你的App上将deviceToken连同其他需要的数据发给你的后台服务器(provider)
When the provider later sends remote notification requests to APNs, it must include the device token, along with the notification payload. For more on this, see [APNs Overview](链接已贴出如下)
释义:当后台服务器稍后发送了远程通知请求给APNs的时候,应当包含deviceToken和远程推送消息内容
Never cache device tokens in your app; instead, get them from the system when you need them. 
释义:永远不要缓存deviceToken在你的应用上,而是需要的时候就从APNs系统获取
APNs issues a new device token to your app when certain events happen. 
释义:当某些事件发生的时候APNs会发送一个新的deviceToken到你的App
The device token is guaranteed to be different, for example, when a user restores a device from a backup, when the user installs your app on a new device, and when the user reinstalls the operating system. 
释义:deviceToken是确保不一样的,比如说以下情况:当一个用户在同一个设备上重新安装同一个应用或者将应用装在不同的设备上;或者用户重装手机系统都会导致deviceToken的不一样
Fetching the token, rather than relying on a cache, ensures that you have the current device token needed for your provider to communicate with APNs.
释义:直接获取deviceToken而不是依靠缓存的deviceToken,这样确保你的后台服务器和APNs通信的时候用的是当前有效的deviceToken
When you attempt to fetch a device token but it has not changed, the fetch method returns quickly.
释义:当你尝试去获取deviceToken但是这个deviceToken并没有改变,那么这个获取就会很快。

APNs Overview.
图意理解:

  • 目的意图:我们需要给我们的App推送一条消息(活动促销类信息,提醒用户升级等信息等)给我们用户,让用户了解应用的最新信息。一般出于用户留存的考虑。
  • 图解流程:消息提供者(Provider)将消息发送给苹果远程推送服务器(APNs),苹果远程推送服务器(APNs)再将消息推送给装有该应用的设备。

详细流程:(以今日头条为例)

  • 在今日头条App的AppDelegate的didFinishLaunchingWithOptions方法中注册远程推送通知,此时只要iOS设备正常联网能够访问到外网,iOS设备默认就会和APNs建立长连接,就会把iOS设备的UDID(Unique Device Identifier:唯一设备标识码,用来标识唯一一台苹果设备)和今日头条的Bundle Identifier通过长连接发送给APNs服务器,然后苹果通过这两个的值根据一定的加密算法得出deviceToken,并将deviceToken返回给iOS设备。(注:APNs服务器会留有UDID+Bundle Identifier+deviceToken的映射表)
  • 实现UIApplicationDelegate代理中的有关于注册远程通知的相关方法,包括注册成功、注册失败、对接收到通知的处理等。
  • 如果注册成功,实现注册成功的代理方法,就能够接收到deviceToken,并将deviceToken发送给今日头条服务器,今日头条服务器将此deviceToken存储在数据库中(一般如果是及时通讯类应用那么还会与用户的账号进行映射)。
  • 如果注册失败,那么实现注册失败的协议方法,处理失败后的事情(包括发送给今日头条服务器注册失败等)。
  • 今日头条服务器接收到deviceToken之后,就可以根据这些deviceToken向APNs发送推送一条新闻简要消息。
  • APNs接收到deviceToken和新闻简要消息之后,根据deviceToken查找映射表找到对应的UDID和Bundle Identifier,根据UDID找到唯一一台苹果设备,再在找到的苹果设备上根据Bundle Identifier找到唯一的应用(此处为今日头条),然后推送消息。
  • 当设备接收到消息的时候,如果今日头条在前台也就是用户正在使用今日头条,那么不会在设备上方弹出横幅(如果使用了音效,还会触发音效的播放),直接调用我们实现的UIApplicationDelegate中的接收消息的方法。反之如果今日头条在后台或者未运行时就会在设备的上方弹出横幅(如果使用了音效,还会触发音效的播放),点击横幅才会触发调用我们实现的UIApplicationDelegate中的接收消息的方法,这个时候你直接点击应用图标进来是不会调用的。
四、条件前提

设备条件:

  • 苹果iOS设备一台:iPad/iPhone,此处选用iPhone
  • 装有Xcode的电脑一台:强烈建议MBP或者iMAC,切不要用mini,坑货!
  • 开发者账号:这个如果公司有就用公司的账号,如果处于自学阶段的买一个吧,不贵,¥688

证书和描述文件条件:

推送的调试证书和发布证书

  • 进入[开发者中心],选择Account,输入开发者账号和密码,进入如下页面:


    Snip20170614_28.png
  • 选择第一个之后进入如下页面然后选择Identifiers

    Snip20170614_29.png

    PS:当然做到这一步是需要 iOS- 最全的真机测试教程iOS-最全的App上架教程中的证书和描述文件都配置OK了的(注意如果只做测试用的话,那么就只需要看关于调试的即可,就不需要看上架部分的了)

  • 选中对应的App ID,点进去往下滚能看到如下页面:

Snip20170614_18.png
  • 点击Edit进行编辑,往下滚能看到未打钩
Snip20170614_19.png
  • 打上勾,然后状态变成黄色的configurable,然后配置调试证书,先创建CSR文件:

    Snip20170614_21.png
  • 点击Create Cerfificate然后进入证书创建页面,此处需要CSR文件,点击继续


    Snip20170614_22.png
  • 选择我们之前创建普通调试证书的CSR文件:


    Snip20170614_23.png

    就是这哥们:


    Snip20170614_30.png
  • 选择完成之后,然后点击Continue就可以看见推送调试证书已经创建好了,点击Download即可下载到本地:

Snip20170614_24.png
  • 配置好推送调试证书之后,那么接下来就是配置推送发布证书(如果有必要的话)
Snip20170614_25.png
  • 经过以上步骤后,检查是否推送配置成功,如果图中变绿了就是成功了:
Snip20170614_27.png

PS:想要生活过得去,头上必须带点绿!

经过以上的步骤,会有以下的文件:

  • CSR:
Snip20170614_30.png
  • 调试证书和描述文件:
Snip20170614_31.png
  • 发布证书和描述文件:

    Snip20170614_32.png
  • 推送调试和发布证书


    Snip20170614_33.png

证书和描述文件添加:

  • 双击证书,将证书添加到钥匙串中
  • 双击描述文件,将描述文件放入路径中,这个自动就放入了,无需手动,只有当我们需要删除这个描述文件的时候,才会手动找到以下路径去删除掉这些描述文件
~/Library/MobileDevice/Provisioning Profiles
五、工程配置:

1>在对应Bundle ID的工程中开启ATS:
方式一:


Snip20170614_36.png

Snip20170614_37.png

代码如下:

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

方式二:


Snip20170614_38.png

Snip20170614_39.png

2>在Targets->Capabilities->Push Notifications,右边开启次功能


Snip20170614_40.png

开启完成之后,会在左边文件列表中多一个entitlements文件
Snip20170614_41.png

然后再开启Targets->Capabilities->Background Modes:

Snip20170614_42.png

3>按照接下来的文章中第五点开始一直看到最后即可完成推送测试了
文章如右:iOS 远程消息推送 APNS推送原理和一步一步开发详解篇

Snip20170614_44.png
Snip20170627_37.png
Snip20170627_38.png

注意其中苹果的远程推送是分测试和发布服务器的:
测试服务器地址:gateway.sandbox.push.apple.com 2195
发布服务器地址:gateway.push.apple.com 2195

4>推荐我们自己MAC端服务器测试工具:SmartPush

这里还额外推荐一款应用程序:APNS-Tool


Snip20180119_128.png

推送消息的格式为:

{"aps":
    {"alert":"I'm a very handsome boy! Nice IT guys!",
     "badge":6,
     "sound": "default"
    }
}

当然还可以加上自定义的:

{"aps":
    {"alert":"I'm a very handsome boy! Nice IT guys!",
     "badge":6,
     "sound": "default"
    },
 "custom":"http://www.baidu.com"
}

针对于以上文章中的第七点项目测试此处提上我的代码:

我是把这个推送功能直接封装成一个类了DSPushService:

/*              ________      ________
 *  | |  / /   / ______ \    |  _____ \
 *  | | / /   / /      \ \   | |     \ \
 *  | |/ /    | |      | |   | |     | |
 *  | |\ \    | |      | |   | |     | |
 *  | | \ \   \ \______/ /   | |_____/ /
 *  | |  \ \   \________/    |________/
 *
 *  Copyright © 2014~2017年 KODIE. All rights reserved.
 */

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

@interface DSPushService : NSObject

+ (instancetype)defaultPushService;

//授权和注册
- (BOOL)DSPushApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

//这个是为了在HomeScreen点击App图标进程序
- (void)DSBecomeActive:(UIApplication *)application;

//注册成功得到deviceToken
- (void)DSPushApplication:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
//注册失败报错
- (void)DSPushApplication:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error;

//这是处理发送过来的推送
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;

@end

/*              ________      ________
 *  | |  / /   / ______ \    |  _____ \
 *  | | / /   / /      \ \   | |     \ \
 *  | |/ /    | |      | |   | |     | |
 *  | |\ \    | |      | |   | |     | |
 *  | | \ \   \ \______/ /   | |_____/ /
 *  | |  \ \   \________/    |________/
 *
 *  Copyright © 2014~2017年 KODIE. All rights reserved.
 */

#import "DSPushService.h"
#import <UserNotifications/UserNotifications.h>

@interface DSPushService ()

@end

@implementation DSPushService

#pragma mark - lifeCycle
- (instancetype)init{
    if (self = [super init]) {
        //code here...
    }
    return self;
}
+ (instancetype)defaultPushService{
    static DSPushService *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[super alloc]init];
    });
    return instance;
}

#pragma mark - 注册和授权
- (BOOL)DSPushApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0f) {
        //iOS10-
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        UNAuthorizationOptions options = UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert;
        [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
            //判断
        }];
    }else if([[UIDevice currentDevice].systemVersion floatValue] >= 8.0f){
        //iOS8-iOS10
        [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge categories:nil]];
    }else{
        //iOS8以下
        [application registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound];
    }
    // 注册远程推送通知 (获取DeviceToken)
    [application registerForRemoteNotifications];
    
    //这个是应用未启动但是通过点击通知的横幅来启动应用的时候
    NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
    if (userInfo != nil) {
        //如果有值,说明是通过远程推送来启动的
        //code here...
    }
    
    return YES;
}

//处理从后台到前台后的角标处理
-(void) DSBecomeActive:(UIApplication *)application{
    if (application.applicationIconBadgeNumber > 0) {
        application.applicationIconBadgeNumber = 0;
    }
}

#pragma mark - 远程推送的注册结果的相关方法
//成功
- (void)DSPushApplication:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{   //获取设备相关信息
    NSString *deviceName = dev.name;
    NSString *deviceModel = dev.model;
    NSString *deviceSystemVersion = dev.systemVersion;
    UIDevice *myDevice = [UIDevice currentDevice];
    NSString *deviceUDID = [myDevice identifierForVendor].UUIDString;
    NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
    NSString *appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
    
    //获取用户的通知设置状态
    NSString *pushBadge= @"disabled";
    NSString *pushAlert = @"disabled";
    NSString *pushSound = @"disabled";
    if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0f) {
        UIUserNotificationSettings *setting = [[UIApplication sharedApplication] currentUserNotificationSettings];
        if (UIUserNotificationTypeNone == setting.types) {
            NSLog(@"推送关闭");
        }else{
            NSLog(@"推送打开");
            pushBadge = (setting.types & UIRemoteNotificationTypeBadge) ? @"enabled" : @"disabled";
            pushAlert = (setting.types & UIRemoteNotificationTypeAlert) ? @"enabled" : @"disabled";
            pushSound = (setting.types & UIRemoteNotificationTypeSound) ? @"enabled" : @"disabled";
        }
    }else{
        UIRemoteNotificationType type = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
        if(UIRemoteNotificationTypeNone == type){
            NSLog(@"推送关闭");
        }else{
            NSLog(@"推送打开");
            pushBadge = (type & UIRemoteNotificationTypeBadge) ? @"enabled" : @"disabled";
            pushAlert = (type & UIRemoteNotificationTypeAlert) ? @"enabled" : @"disabled";
            pushSound = (type & UIRemoteNotificationTypeSound) ? @"enabled" : @"disabled";
        }
    }
    
    //获取设备的UUID
    NSString *deviceUuid;
    UIDevice *dev = [UIDevice currentDevice];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    id uuid = [defaults objectForKey:@"deviceUuid"];
    if (uuid)
    deviceUuid = (NSString *)uuid;
    else {
        CFStringRef cfUuid = CFUUIDCreateString(NULL, CFUUIDCreate(NULL));
        deviceUuid = (NSString *)CFBridgingRelease(cfUuid);
        NSLog(@"%@",deviceUuid);
        NSLog(@"%@",deviceUuid);
        [defaults setObject:deviceUuid forKey:@"deviceUuid"];
        [defaults release];
    }
    
    
    
    NSString *deviceTokenString = [[[[deviceToken description]
                                     stringByReplacingOccurrencesOfString:@"<"withString:@""]
                                    stringByReplacingOccurrencesOfString:@">" withString:@""]
                                   stringByReplacingOccurrencesOfString: @" " withString: @""];
    
    NSString *host = @"http://www.baidu.com";//你们自己后台服务器的地址
    
    //时间戳
    NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] * 1000;
    NSInteger time = interval;
    NSString *timestamp = [NSString stringWithFormat:@"%zd",time];
    
    //MD5校验
    NSString *md5String = [NSString stringWithFormat:@"%@%@%@",deviceTokenString,deviceUDID,timestamp];
    NSString *credential = [Util encodeToMd5WithStr:md5String];
    
    
    NSString *urlString = [NSString stringWithFormat:@"%@?device_token=%@&device_uuid=%@&device_name=%@&device_version=%@&app_name=%@&timestamp=%@&push_badge=%@&push_alert=%@&push_sound=%@&credential=%@", host, deviceTokenString, deviceUDID, deviceModel, deviceSystemVersion, appName, timestamp, pushBadge, pushAlert, pushSound, credential];
    
    //打印值看一下,是否正确,当然打印的可以用一个宏判断一下
    NSLog(@"😊😊%@", host);
    NSLog(@"😊😊%@", gameId);
    NSLog(@"😊😊%@", deviceTokenString);
    NSLog(@"😊😊%@", deviceUDID);
    NSLog(@"😊😊%@", deviceModel);
    NSLog(@"😊😊%@", deviceSystemVersion);
    NSLog(@"😊😊%@", appName);
    NSLog(@"😊😊%@", timestamp);
    NSLog(@"😊😊%@", pushBadge);
    NSLog(@"😊😊%@", pushAlert);
    NSLog(@"😊😊%@", pushSound);
    NSLog(@"😊😊%@", credential);
    NSLog(@"😊😊%@", urlString);
    
    //以下是发送DeviceToken给后台了,有人会问,为什么要传这么多参数,这个具体根据你们后台来哈,不要问我,问你们后台要传什么就传什么,但是DeviceToken是一定要传的
    NSURL *url = [NSURL URLWithString:urlString];
    if (!url) {
        NSLog(@"传入的URL为空或者有非法字符,请检查参数");
        return;
    }
    NSLog(@"%@",url);
    
    //发送异步请求
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setTimeoutInterval:5.0];
    [request setHTTPMethod:@"GET"];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            if (httpResponse.statusCode == 200 && data) {
                NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
                if (dict && [dict[@"ret"] integerValue] == 0) {
                    NSLog(@"上传deviceToken成功!deviceToken dict = %@",dict);
                }else{
                    NSLog(@"返回ret = %zd, msg = %@",[dict[@"ret"] integerValue],dict[@"msg"]);
                }
            }else if (error) {
                NSLog(@"请求失败,error = %@",error);
            }
        });
    }];
    [task resume];
}

//失败
- (void)DSPushApplication:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"注册推送失败,error = %@", error);
    //failed fix code here...
}

#pragma mark - 收到远程推送通知的相关方法
//iOS6及以下(前台是直接走这个方法不会出现提示的,后台是需要点击相应的通知才会走这个方法的)
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    [self DSPushApplication:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:nil];
}

//iOS7及以上
- (void)DSPushApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"%@", userInfo);
   
    
    //注意HomeScreen上一经弹出推送系统就会给App的applicationIconBadgeNumber设为对应值
    if (application.applicationIconBadgeNumber > 0) {
        application.applicationIconBadgeNumber = 0;
    }
    
   
    
    NSLog(@"remote notification: %@",[userInfo description]);
    NSDictionary *apsInfo = [userInfo objectForKey:@"aps"];
    
    NSString *alert = [apsInfo objectForKey:@"alert"];
    NSLog(@"Received Push Alert: %@", alert);
    NSString *sound = [apsInfo objectForKey:@"sound"];
    NSLog(@"Received Push Sound: %@", sound);
    NSString *badge = [apsInfo objectForKey:@"badge"];
    NSLog(@"Received Push Badge: %@", badge);
    
    //这是播放音效
    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
    
    //处理customInfo
    if ([userInfo objectForKey:@"custom"] != nil) {
        //custom handle code here...
    }
    completionHandler(UIBackgroundFetchResultNoData);
}

@end

其中修改一下需要上传的参数还有后台的主机地址以及增加相应的处理就可以了。

JAVA后台的配置:

注意以上是PHP后台的配置方式,那么如果是JAVA后台又该怎么配置呢,请自行阅读下一篇文章,对比发现其中的不同之处:
IOS 基于APNS消息推送原理与实现(JAVA后台)--转
此处稍微细说一下,就是PHP用的是pem文件,而JAVA用的是p12文件

六、角标问题

最后一部分内容就是处理我们的角标问题:iOS远程推送之(二):角标applicationIconNumber设置

iOS远程推送之(三):点击通知横幅启动应用

Local and Remote Notification Programming Guide
iOS中使用本地通知为你的APP添加提示用户功能
APNs Overview.
iOS- 最全的真机测试教程
iOS-最全的App上架教程
iOS 远程消息推送 APNS推送原理和一步一步开发详解篇
SmartPush
IOS 基于APNS消息推送原理与实现(JAVA后台)--转

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

推荐阅读更多精彩内容