iOS-BLE藍牙開發持續更新

在寫這個博客之前,空余時間抽看了近一個月的文檔和 Demo,系統給的解釋很詳細,接口也比較實用,唯獨有一點,對于設備的唯一標示,網上眾說紛紜,在這里我目前也還沒有自己的見解,只是在不斷的測試各種情況,親測同一設備的 UUID 對于每臺 iPhone設備都不一樣,只能盡量保證設備的唯一性,特別是自動重連的過程,讓用戶沒有感知。我之前也找了很久,發現CBCentralManager 和 CBPeripheral 里邊都找不到和Mac地址有關的東西,后來發現一般是外設在 Device Information 服務中的某個特征返回的。經過與硬件工程師的協商,決定 APP 端將從這個服務中獲取到藍牙設備以及我的 iPhone 手機的藍牙 Mac 地址,為自動連接的唯一性做準備。

這里經過和硬件工程師的測試,發現設備端在獲取手機藍牙 MAC地址的時候,當用戶手機重啟之后,這個地址也是會隨機變化的,也就是說,作為開發者,只有設備的 MAC 地址能夠保持唯一性不變化。

ble.png

有疑問的朋友可以先去這里瞅一瞅 藍牙 4.0的智能硬件示例

  • 下面是兩臺 iPhone6 連接同一臺藍牙設備的結果:
**成功連接**** peripheral: <CBPeripheral: 0x1700f4500, identifier = 50084F69-BA5A-34AC-8A6E-6F0CEADB21CD, name = 555555555588, state = connected> with UUID: <__NSConcreteUUID 0x17003d980> 50084F69-BA5A-34AC-8A6E-6F0CEADB21CD**
****
****
**成功連接**** peripheral: <CBPeripheral: 0x1742e3000, identifier = 55B7D759-0F1E-6271-EA14-BC5A9C9EEEEC, name = 555555555588, state = connected> with UUID: <__NSConcreteUUID 0x174036c00> 55B7D759-0F1E-6271-EA14-BC5A9C9EEEEC**

進入正題

iOS 的藍牙開發很簡單,只要包含一個庫,創建CBCentralManager 實例,實現代理方法,然后就可以直接和設備進行通信。

發現附近的特定藍牙設備
#import <CoreBluetooth/CoreBluetooth.h>

首先可以定義一些即將使用到的 UUID 的宏

#define kPeripheralName     @"360qws Electric Bike Service" //外圍設備名稱
#define kServiceUUID        @"7CACEB8B-DFC4-4A40-A942-AAD653D174DC" //服務的UUID
#define kCharacteristicUUID @"282A67B2-8DAB-4577-A42F-C4871A3EEC4F" //特征的UUID

如果不是把手機作為中心設備的話,這些沒有必要設置。
這里我也沒有用到,僅僅是提了一下,具體操作后續添加。

對于生成UUID,大家可以谷歌一下,直接通過 mac 終端生成 32 位 UUID。

  1. 聲明屬性
@property (weak, nonatomic) IBOutlet UITableView *bluetoothTable;
@property (weak, nonatomic) IBOutlet UITextView *resultTextView;

@property BOOL cbReady;
@property(nonatomic) float batteryValue;
@property (nonatomic, strong) CBCentralManager *manager;
@property (nonatomic, strong) CBPeripheral *peripheral;

@property (strong ,nonatomic) CBCharacteristic *writeCharacteristic;

@property (strong,nonatomic) NSMutableArray *nDevices;
@property (strong,nonatomic) NSMutableArray *nServices;
@property (strong,nonatomic) NSMutableArray *nCharacteristics;
  1. 遵守協議
@interface ViewController () <CBCentralManagerDelegate, CBPeripheralDelegate, UITableViewDataSource, UITableViewDelegate>
  1. 初始化數據
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    
    _cbReady = false;
    _nDevices = [[NSMutableArray alloc]init];
    _nServices = [[NSMutableArray alloc]init];
    _nCharacteristics = [[NSMutableArray alloc]init];
    
    _bluetoothTable.delegate = self;
    _bluetoothTable.dataSource = self;
    
    count = 0;
}
  1. 實現藍牙的協議方法
  • (1)檢測藍牙狀態
//開始查看服務,藍牙開啟
-(void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
        {
            [self updateLog:@"藍牙已打開,請掃描外設"];
            [_activity startAnimating];
            [_manager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:@"FF15"]]  options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
        }
            break;
        case CBCentralManagerStatePoweredOff:
            [self updateLog:@"藍牙沒有打開,請先打開藍牙"];
            break;
        default:
            break;
    }
}
  • 注:
// @[[CBUUID UUIDWithString:@"FF15"]]  是為了過濾掉其他設備,可以搜索特定標示的設備。
[_manager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:@"FF15"]]  options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
  • (2)檢測到外設后,停止掃描,連接設備
//查到外設后,停止掃描,連接設備
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    [self updateLog:[NSString stringWithFormat:@"已發現 peripheral: %@ rssi: %@, UUID: %@ advertisementData: %@ ", peripheral, RSSI, peripheral.identifier, advertisementData]];
    
    _peripheral = peripheral;
    [_manager connectPeripheral:_peripheral options:nil];
    
    [self.manager stopScan];
    [_activity stopAnimating];
    
    BOOL replace = NO;
    // Match if we have this device from before
    for (int i=0; i < _nDevices.count; i++) {
        CBPeripheral *p = [_nDevices objectAtIndex:i];
        if ([p isEqual:peripheral]) {
            [_nDevices replaceObjectAtIndex:i withObject:peripheral];
            replace = YES;
        }
    }
    if (!replace) {
        [_nDevices addObject:peripheral];
        [_bluetoothTable reloadData];
    }
}
  • (3)連接外設后的處理
//連接外設成功,開始發現服務
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"%@", [NSString stringWithFormat:@"成功連接 peripheral: %@ with UUID: %@",peripheral,peripheral.identifier]);
    
    [self updateLog:[NSString stringWithFormat:@"成功連接 peripheral: %@ with UUID: %@",peripheral,peripheral.identifier]];
    
    [self.peripheral setDelegate:self];
    [self.peripheral discoverServices:nil];
    [self updateLog:@"掃描服務"];
}
//連接外設失敗
-(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%@",error);
}

-(void)peripheralDidUpdateRSSI:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%s,%@",__PRETTY_FUNCTION__,peripheral);
    int rssi = abs([peripheral.RSSI intValue]);
    CGFloat ci = (rssi - 49) / (10 * 4.);
    NSString *length = [NSString stringWithFormat:@"發現BLT4.0熱點:%@,距離:%.1fm",_peripheral,pow(10,ci)];
    [self updateLog:[NSString stringWithFormat:@"距離:%@", length]];
}
  • (4)發現服務和搜索特征值
//已發現服務
-(void) peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
    
    [self updateLog:@"發現服務."];
    int i=0;
    for (CBService *s in peripheral.services) {
        [self.nServices addObject:s];
    }
    for (CBService *s in peripheral.services) {
        [self updateLog:[NSString stringWithFormat:@"%d :服務 UUID: %@(%@)",i,s.UUID.data,s.UUID]];
        i++;
        [peripheral discoverCharacteristics:nil forService:s];
        
        if ([s.UUID isEqual:[CBUUID UUIDWithString:@"FF15"]]) {
            BOOL replace = NO;
            // Match if we have this device from before
            for (int i=0; i < _nDevices.count; i++) {
                CBPeripheral *p = [_nDevices objectAtIndex:i];
                if ([p isEqual:peripheral]) {
                    [_nDevices replaceObjectAtIndex:i withObject:peripheral];
                    replace = YES;
                }
            }
            if (!replace) {
                [_nDevices addObject:peripheral];
                [_bluetoothTable reloadData];
            }
        }
    }
}

//已搜索到Characteristics
-(void) peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
    [self updateLog:[NSString stringWithFormat:@"發現特征的服務:%@ (%@)",service.UUID.data ,service.UUID]];
    
    for (CBCharacteristic *c in service.characteristics) {
        [self updateLog:[NSString stringWithFormat:@"特征 UUID: %@ (%@)",c.UUID.data,c.UUID]];
        
        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FF01"]]) {
            _writeCharacteristic = c;
        }
        
        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FF02"]]) {
            [_peripheral readValueForCharacteristic:c];
            [_peripheral setNotifyValue:YES forCharacteristic:c];
        }
        
        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FF04"]]) {
            [_peripheral readValueForCharacteristic:c];
        }
        
        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FF05"]]) {
            [_peripheral readValueForCharacteristic:c];
            [_peripheral setNotifyValue:YES forCharacteristic:c];
        }
        
        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FFA1"]]) {
            [_peripheral readRSSI];
        }
        
        [_nCharacteristics addObject:c];
    }
}

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    [self updateLog:[NSString stringWithFormat:@"已斷開與設備:[%@]的連接", peripheral.name]];
}
  • (5)獲取外設發來的數據
//獲取外設發來的數據,不論是read和notify,獲取數據都是從這個方法中讀取。
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FF02"]]) {
        NSData * data = characteristic.value;
        Byte * resultByte = (Byte *)[data bytes];
        
        for(int i=0;i<[data length];i++)
            printf("testByteFF02[%d] = %d\n",i,resultByte[i]);
        
        if (resultByte[1] == 0) {
            switch (resultByte[0]) {
                case 3: // 加解鎖
                {
                    if (resultByte[2] == 0) {
                        [self updateLog:@"撤防成功!!!"];
                    }else if (resultByte[2] == 1) {
                        [self updateLog:@"設防成功!!!"];
                    }
                }
                    break;
                case 4: // 開坐桶
                {
                    if (resultByte[2] == 0) {
                        [self updateLog:@"關坐桶成功!!!"];
                    }else if (resultByte[2] == 1) {
                        [self updateLog:@"開坐桶成功!!!"];
                    }
                }
                    break;
                case 5: // 鎖定電機
                {
                    if (resultByte[2] == 0) {
                        [self updateLog:@"解鎖電機控制器成功!!!"];
                    }else if (resultByte[2] == 1) {
                        [self updateLog:@"鎖定電機控制器成功!!!"];
                    }
                }
                    break;
                default:
                    break;
            }
        }else if (resultByte[1] == 1) {
            [self updateLog:@"未知錯誤"];
        }else if (resultByte[1] == 2) {
            [self updateLog:@"鑒權失敗"];
        }
    }
    
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FF04"]]) {
        NSData * data = characteristic.value;
        Byte * resultByte = (Byte *)[data bytes];
        
        for(int i=0;i<[data length];i++)
            printf("testByteFF04[%d] = %d\n",i,resultByte[i]);
        
        if (resultByte[0] == 0) {
            // 未綁定 -》寫鑒權碼
            [self updateLog:@"當前車輛未綁定,請鑒權"];
            [self authentication];  // 鑒權
            [self writePassword:nil newPw:nil];
        }else if (resultByte[0] == 1) {
            // 已綁定 -》鑒權
            [self updateLog:@"當前車輛已經綁定,請鑒權"];
            [self writePassword:nil newPw:nil];
        }else if (resultByte[0] == 2) {
            // 允許綁定
            [self updateLog:@"當前車輛允許綁定"];
        }
    }
    
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FF05"]]) {
        NSData * data = characteristic.value;
        Byte * resultByte = (Byte *)[data bytes];
        
        for(int i=0;i<[data length];i++)
            printf("testByteFF05[%d] = %d\n",i,resultByte[i]);
        
        if (resultByte[0] == 0) {
            // 設備加解鎖狀態 0 撤防     1 設防
            [self updateLog:@"當前車輛撤防狀態"];
        }else if (resultByte[0] == 1) {
            // 設備加解鎖狀態 0 撤防     1 設防
            [self updateLog:@"當前車輛設防狀態"];
        }
    }
}

//中心讀取外設實時數據
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error changing notification state: %@", error.localizedDescription);
    }
    
    // Notification has started
    if (characteristic.isNotifying) {
        [peripheral readValueForCharacteristic:characteristic];
        
    } else { // Notification has stopped
        // so disconnect from the peripheral
        NSLog(@"Notification stopped on %@.  Disconnecting", characteristic);
        [self updateLog:[NSString stringWithFormat:@"Notification stopped on %@.  Disconnecting", characteristic]];
        [self.manager cancelPeripheralConnection:self.peripheral];
    }
}
//用于檢測中心向外設寫數據是否成功
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error) {
        NSLog(@"=======%@",error.userInfo);
        [self updateLog:[error.userInfo JSONString]];
    }else{
        NSLog(@"發送數據成功");
        [self updateLog:@"發送數據成功"];
    }
    
    /* When a write occurs, need to set off a re-read of the local CBCharacteristic to update its value */
    [peripheral readValueForCharacteristic:characteristic];
}

后記

最主要是用 UUID 來確定你要干的事情,特征和服務的 UUID 都是外設定義好的。我們只需要讀取,確定你要讀取什么的時候,就去判斷UUID是否相符。 一般來說我們使用的 iPhone 都是做centralManager 的,藍牙模塊是 peripheral 的,所以我們是 want datas,需要接受數據。

  1. 判斷狀態為 powerOn,然后執行掃描
  2. 停止掃描,連接外設
  3. 連接成功,尋找服務
  4. 在服務里尋找特征
  5. 為特征添加通知
  6. 通知添加成功,那么就可以實時的讀取value[也就是說只要外設發送數據[一般外設的頻率為10Hz],代理就會調用此方法
  7. 處理接收到的 value,[hex值,得轉換] 之后就自由發揮了,在這期間都是通過代理來實現的,也就是說你只需要處理你想要做的事情,代理會幫你調用方法

2015-07-28 更

關于 Write 這里還有些注意的地方!!!!

并不是每一個 Characteristic 都可以通過回調函數來查看它寫入狀態的。就比如針對 immediateAlertService(1802) 的 alertLevelCharacteristic(2A06),就是一個不能有 response 的 Characteristic。剛開始我就一直用 CBCharacteristicWriteType.WithResponse 來進行寫入始終不成功,郁悶壞了,最后看到每個 Characteristic 還有個屬性值是指示這個的,我將每個 Characteristic 打印出來有如下信息:

immediateAlertService Discover characteristic <CBCharacteristic: 0x15574d00, UUID = 2A06, properties = 0x4, value = (null), notifying = NO>
linkLossAlertService Discover characteristic <CBCharacteristic: 0x15671d00, UUID = 2A06, properties = 0xA, value = (null), notifying = NO>

這個的 properties 是什么剛開始不知道,覺得他沒意義,后面才注意到 properties 是 Characteristic 的一個參數,具體解釋如下:

Declaration
SWIFT
struct CBCharacteristicProperties : RawOptionSetType {
    init(_ value: UInt)
    var value: UInt
    static var Broadcast: CBCharacteristicProperties { get }
    static var Read: CBCharacteristicProperties { get }
    static var WriteWithoutResponse: CBCharacteristicProperties { get }
    static var Write: CBCharacteristicProperties { get }
    static var Notify: CBCharacteristicProperties { get }
    static var Indicate: CBCharacteristicProperties { get }
    static var AuthenticatedSignedWrites: CBCharacteristicProperties { get }
    static var ExtendedProperties: CBCharacteristicProperties { get }
    static var NotifyEncryptionRequired: CBCharacteristicProperties { get }
    static var IndicateEncryptionRequired: CBCharacteristicProperties { get }
}
OBJECTIVE-C
typedef enum {
   CBCharacteristicPropertyBroadcast = 0x01,
   CBCharacteristicPropertyRead = 0x02,
   CBCharacteristicPropertyWriteWithoutResponse = 0x04,
   CBCharacteristicPropertyWrite = 0x08,
   CBCharacteristicPropertyNotify = 0x10,
   CBCharacteristicPropertyIndicate = 0x20,
   CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
   CBCharacteristicPropertyExtendedProperties = 0x80,
   CBCharacteristicPropertyNotifyEncryptionRequired = 0x100,
   CBCharacteristicPropertyIndicateEncryptionRequired = 0x200,
} CBCharacteristicProperties;

可以看到

  • 0x04: CBCharacteristicPropertyWriteWithoutResponse
  • 0x10: CBCharacteristicPropertyNotify

所以 immediateAlertService(1802) 的 alertLevelCharacteristic(2A06)是不能用 CBCharacteristicWriteType.WithRespons 進行寫入,只能用 CBCharacteristicWriteType.WithOutRespons。這樣在以后的開發中可以對每個Characteristic的這個參數進行檢查再進行設置。

最后講一下關于藍牙綁定的過程,在 iOS 中,沒有講當綁定的過程,直接就是掃描、連接、交互。從而很多人會認為,連接就是綁定了,其實不然。在 iOS 開發中,連接并沒有完成綁定,在網上找到了個很好的解釋:

you cannot initiate pairing from the iOS central side. Instead, you have to read/write a characteristic value,
and then let your peripheral respond with an "Insufficient Authentication" error.
iOS will then initiate pairing, will store the keys for later use (bonding) and encrypts the link. As far as I know,
it also caches discovery information, so that future connections can be set up faster.

就是當發生讀寫交互時,系統在會和外設進行綁定操作!!!

感謝網友 @treebug 對于配對的解釋說明

偶然看到你文章,綁定這個概念更標準的術語是“Paired Connection”,也就是“Require a Paired Connection to Access Sensitive Data”,翻譯過來就是:訪問敏感數據時,要求配對連接,就是你上文提到的綁定意思。
那么實際上代碼就是:
peripheral生成一個characteristic時,它的屬性CBCharacteristicPropertyNotifyEncryptionRequired,permissions為CBAttributePermissionsReadEncryptionRequired,后續請求就會要求配對加密數據

2016-02-20 更

ios藍牙如何獲取廣播包數據

如題,手機作為主設備,在使用 CoreBluetooth 時候,想獲取藍牙的數據廣播包。在使用

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)aPeripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI

方法時候獲取的 advertisementData 打印出來只有

** ****kCBAdvDataIsConnectable****kCBAdvDataLocalName****kCBAdvDataManufacturerData**

三個屬性對應的值,但這并非廣播包的數據。例如安卓可以通過 scandata 來獲取到廣播包的值,那么 iOS 這邊我應該怎么做呢?

好像蘋果這邊禁止讀取這種廣播內容的的,真要的話你可以讓硬件那邊把數據做到 kCBAdvDataManufacturerData 這個字段里面。

Demo地址:藍牙 4.0的智能硬件示例

進一步交流 QQ群:361736344

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容