android蓝牙入门知识和优秀蓝牙第三方库BluetoothKit的使用

   这篇博客的作用是为了让小白朋友了解andorid蓝牙的一些基本概念,同时学习总结下目前我实际项目中用到的蓝牙库 BluetoothKit ,包括其优点 、基本使用,最后以库中的源码为基石深入探究这个优秀的蓝牙库的设计理念。
   

一 . 蓝牙基础知识:

1、BLE蓝牙的基本介绍

1.1BLe蓝牙介绍

Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。

Android BLE 使用的蓝牙协议是 GATT 协议,有关该协议的详细内容可以参见蓝牙官方文档或者这篇博客(https://blog.csdn.net/u013378580/article/details/52891462)。以下我引用一张官网的图来大概说明 Android 开发中我们 用到的一些专业术语。(专业名词参考1.2节内容)

蓝牙协议图.png

1.2、Android BLE的相关系统API

Profile

一个通用的规范,即Ble的蓝牙通讯协议,Ble蓝牙的必须按照这个规范来收发数据。

Service

一个低功耗蓝牙设备可以定义许多 Service, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:0x0000xxxx-0000-1000-8000-00805F9B34FB 为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为: 0x00002A37-0000-1000-8000-00805F9B34FB

BluetoothAdapter

BluetoothAdapter 拥有系统调用蓝牙基本的蓝牙操作,例如开启蓝牙扫描 连接,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)实例化一个 BluetoothDevice 用于连接蓝牙设备的操作等等。

BluetoothDevice

代表一个远程蓝牙设备。这个类可以让你连接所代表的蓝牙设备或者获取一些有关它的信息,例如它的名字,地址和绑定状态等等。

RSSI

Received Signal Strength Indication.用来标识搜索到的设备的信号强度值。

BluetoothGatt

这个类提供了 Bluetooth GATT 的基本功能。例如重新连接蓝牙设备,发现蓝牙设备的 Service 等等,是在一个中心设备(如手机)和外围设备(手环等Ble设备)之间建立的数据通道,通过调用gatt对象的一系列方法来操作蓝牙。

UUID

一个service对应一个UUID

一蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:0x0000xxxx-0000-1000-8000-00805F9B34FB

为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:0x00002A37-0000-1000-8000-00805F9B34FB

BluetoothGattService

一个低功耗蓝牙设备可以定义许多 Service, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。
这一个类通过 BluetoothGatt#getService 获得,如果当前服务不可见那么将返回一个 null。这一个类对应上面说过的 Service。我们可以通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现蓝牙数据的双向传输。

BluetoothGattCharacteristic

在 Service之下,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。

这个类对应上面提到的 Characteristic。通过这个类定义需要往外围设备写入的数据和读取外围设备发送过来的数据,这个类是中心设备和BLE设备之间数据通信的载体。

相当于一个数据类型,它包括一个value和0~n个value的描述(BluetoothGattDescriptor)

BluetoothGattDescriptor

Characteristic之下,描述符,对Characteristic的描述,包括范围、计量单位等

Notification和Indication

Notification 外围设备(硬件)设备给中心设备(手机)发送一个数据,无需接收方确认。接收通知

Indication 外围设备给中心设备(手机)发送一个数据,需要接收方确认。二者关系类似于TCP协议和UDP协议,效率上来讲Notification比Indication要高。在蓝牙API中体现在notify() 方法和indicate()方法。

1.3与传统蓝牙ClassicBluetooth的比较

1.3.1蓝牙模块分类

蓝牙模块分类.png

1.3.2BLE和CLASSIC蓝牙的比较

经典蓝牙模块(BT):泛指支持蓝牙协议在4.0以下的模块,一般用于数据量比较大的传输,如:语音、音乐、较高数据量传输等。经典蓝牙模块可再细分为:传统蓝牙模块和高速蓝牙模块。传统蓝牙模块在2004年推出,主要代表是支持蓝牙2.1协议的模块,在智能手机爆发的时期得到广泛支持。高速蓝牙模块在2009年推出,速率提高到约24Mbps,是传统蓝牙模块的八倍,可以轻松用于录像机至高清电视、PC至PMP、UMPC至打印机之间的资料传输。

低功耗蓝牙模块(BLE):是指支持蓝牙协议4.0或更高的模块,也称为BLE模块(BluetoohLow EnergyModule),最大的特点是成本和功耗的降低,应用于实时性要求比较高,但是数据速率比较低的产品,如:遥控类的(鼠标、键盘)、传感设备的数据发送(心跳带、血压计、温度传感器)等。


接下来是第2个部分 ,实际工作中我们使用到的蓝牙库 BluetoothKit的使用:

二 . 框架介绍:

1.框架项目地址

https://github.com/dingjikerbo/BluetoothKit

2.框架优点

一、统一解决Android蓝牙通信过程中的兼容性问题

二、提供尽可能简单易用的接口,屏蔽蓝牙通信中的技术细节,只开放连接,读写,通知等语义。

三、实现串行化任务队列,统一处理蓝牙通信中的失败以及超时,支持可配置的容错处理

四、统一管理连接句柄,避免句柄泄露

五、方便监控各设备连接状态,在尽可能维持连接的情况下,将最不活跃的设备自动断开。

六、便于多进程APP架构下蓝牙连接的统一管理

七、支持拦截所有对蓝牙原生接口的调用

3.基本使用

1.添加依赖compile 'com.inuker.bluetooth:library:1.4.0' 或直接导入依赖Liabrary模块

2.在app模块的Manifest中添加蓝牙权限

 <!--蓝牙相关权限-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

2. 扫描


BluetoothClient mClient = new BluetoothClient(context);

SearchRequest request = new SearchRequest.Builder()

        .searchBluetoothLeDevice(3000, 3)   // 先扫BLE设备3次,每次3s 

        .searchBluetoothClassicDevice(5000) // 再扫经典蓝牙5s,在实际工作中没用到经典蓝牙的扫描

        .searchBluetoothLeDevice(2000)      // 再扫BLE设备2s

        .build();

mClient.search(request, new SearchResponse() {

    @Override

    public void onSearchStarted() {//开始搜素 

    }

    @Override

    public void onDeviceFounded(SearchResult device) {//找到设备 可通过manufacture过滤

        Beacon beacon = new Beacon(device.scanRecord);

        BluetoothLog.v(String.format("beacon for %s\n%s", device.getAddress(), beacon.toString()));

    }

    @Override

    public void onSearchStopped() {//搜索停止

    }

    @Override

    public void onSearchCanceled() {//搜索取消

    }

});

4.2连接

可以配置连接参数如下,

BleConnectOptions options = new BleConnectOptions.Builder()

        .setConnectRetry(3)   // 连接如果失败重试3次

        .setConnectTimeout(30000)   // 连接超时30s

        .setServiceDiscoverRetry(3)  // 发现服务如果失败重试3次

        .setServiceDiscoverTimeout(20000)  // 发现服务超时20s

        .build();

mClient.connect(MAC, options, new BleConnectResponse() {

    @Override

    public void onResponse(int code, BleGattProfile data) {

    }

});

监听连接状态:

mClient.registerConnectStatusListener(MAC, mBleConnectStatusListener);

private final BleConnectStatusListener mBleConnectStatusListener = new BleConnectStatusListener() {

    @Override

    public void onConnectStatusChanged(String mac, int status) {

        if (status == STATUS_CONNECTED) {

        } else if (status == STATUS_DISCONNECTED) {

        }

    }

};

mClient.unregisterConnectStatusListener(MAC, mBleConnectStatusListener);

4.3 通讯


**读Characteristic**

mClient.read(MAC, serviceUUID, characterUUID, new BleReadResponse() {

    @Override

    public void onResponse(int code, byte[] data) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

**写Characteristic**

要注意这里写的byte[]不能超过20字节,如果超过了需要自己分成几次写。建议的办法是第一个byte放剩余要写的字节的长度。

mClient.write(MAC, serviceUUID, characterUUID, bytes, new BleWriteResponse() {

    @Override

    public void onResponse(int code) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

这个写是带了WRITE_TYPE_NO_RESPONSE标志的,实践中发现比普通的write快2~3倍,建议用于固件升级。

mClient.writeNoRsp(MAC, serviceUUID, characterUUID, bytes, new BleWriteResponse() {

    @Override

    public void onResponse(int code) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

**读Descriptor**

mClient.readDescriptor(MAC, serviceUUID, characterUUID, descriptorUUID, new BleReadResponse() {

    @Override

    public void onResponse(int code, byte[] data) {

    }

});

**写Descriptor**

mClient.writeDescriptor(MAC, serviceUUID, characterUUID, descriptorUUID, bytes, new BleWriteResponse() {

    @Override

    public void onResponse(int code) {

    }

});

**打开Notify**

这里有两个回调,onNotify是接收通知的。

mClient.notify(MAC, serviceUUID, characterUUID, new BleNotifyResponse() {

    @Override

    public void onNotify(UUID service, UUID character, byte[] value) {

    }

    @Override

    public void onResponse(int code) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

**关闭Notify**

mClient.unnotify(MAC, serviceUUID, characterUUID, new BleUnnotifyResponse() {

    @Override

    public void onResponse(int code) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

**打开Indicate**

和Notify类似,

mClient.indicate(MAC, serviceUUID, characterUUID, new BleNotifyResponse() {

    @Override

    public void onNotify(UUID service, UUID character, byte[] value) {

    }

    @Override

    public void onResponse(int code) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

**关闭Indicate**

mClient.unindicate(MAC, serviceUUID, characterUUID, new BleUnnotifyResponse() {

    @Override

    public void onResponse(int code) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

**读Rssi**

mClient.readRssi(MAC, new BleReadRssiResponse() {

    @Override

    public void onResponse(int code, Integer rssi) {

        if(code == REQUEST_SUCCESS) {

        }

    }

});

4.4 断开

mClient.disconnect(MAC);

3.蓝牙框架源码解析

接下来 , 我以蓝牙框架的"连接"功能为例来追踪下源码

@Override
    public void connect(String mac, BleConnectOptions options, BleConnectResponse response) {
        BluetoothLog.v(String.format("connect %s", mac));
        response = ProxyUtils.getUIProxy(response);
        mClient.connect(mac, options, response);
    }

1.BluetoothClientImpl 在这个实现里真正干了BluetoothClient的活

 @Override
    public void connect(String mac, BleConnectOptions options, final BleConnectResponse response) {
        Bundle args = new Bundle();
        args.putString(EXTRA_MAC, mac);
        args.putParcelable(EXTRA_OPTIONS, options);
        safeCallBluetoothApi(CODE_CONNECT, args, new BluetoothResponse() {
            @Override
            protected void onAsyncResponse(int code, Bundle data) {
                checkRuntime(true);
                if (response != null) {
                    data.setClassLoader(getClass().getClassLoader());
                    BleGattProfile profile = data.getParcelable(EXTRA_GATT_PROFILE);
                    response.onResponse(code, profile);
                }
            }
        });
    }

其中有一个关键方法 safeCallBluetoothApi

 private void safeCallBluetoothApi(int code, Bundle args, final BluetoothResponse response) {
        checkRuntime(true);

//        BluetoothLog.v(String.format("safeCallBluetoothApi code = %d", code));

        try {
            IBluetoothService service = getBluetoothService();

//            BluetoothLog.v(String.format("IBluetoothService = %s", service));

            if (service != null) {
                args = (args != null ? args : new Bundle());
                service.callBluetoothApi(code, args, response);
            } else {
                response.onResponse(SERVICE_UNREADY, null);
            }
        } catch (Throwable e) {
            BluetoothLog.e(e);
        }
    }

在这个方法里,首先拿到了蓝牙的service(通过bindservice的方法),再调用了callBluetoothApi的方法
接下来我们来重点看下 BluetoothServiceImpl 中的callBluetoothApi方法 :

  @Override
    public void callBluetoothApi(int code, Bundle args, final IResponse response) throws RemoteException {
        Message msg = mHandler.obtainMessage(code, new BleGeneralResponse() {

            @Override
            public void onResponse(int code, Bundle data) {
                if (response != null) {
                    if (data == null) {
                        data = new Bundle();
                    }
                    try {
                        response.onResponse(code, data);
                    } catch (Throwable e) {
                        BluetoothLog.e(e);
                    }
                }
            }
        });

        args.setClassLoader(getClass().getClassLoader());
        msg.setData(args);
        msg.sendToTarget();
    }

这里面用了handler发送消息,在handleMessage方法中处理消息 :

@Override
    public boolean handleMessage(Message msg) {
        Bundle args = msg.getData();
        String mac = args.getString(EXTRA_MAC);
        UUID service = (UUID) args.getSerializable(EXTRA_SERVICE_UUID);
        UUID character = (UUID) args.getSerializable(EXTRA_CHARACTER_UUID);
        UUID descriptor = (UUID) args.getSerializable(EXTRA_DESCRIPTOR_UUID);
        byte[] value = args.getByteArray(EXTRA_BYTE_VALUE);
        BleGeneralResponse response = (BleGeneralResponse) msg.obj;

        switch (msg.what) {
            case CODE_CONNECT:
                BleConnectOptions options = args.getParcelable(EXTRA_OPTIONS);
                BleConnectManager.connect(mac, options, response);
                break;

            case CODE_DISCONNECT:
                BleConnectManager.disconnect(mac);
                break;

            case CODE_READ:
                BleConnectManager.read(mac, service, character, response);
                break;

            case CODE_WRITE:
                BleConnectManager.write(mac, service, character, value, response);
                break;

            case CODE_WRITE_NORSP:
                BleConnectManager.writeNoRsp(mac, service, character, value, response);
                break;

            case CODE_READ_DESCRIPTOR:
                BleConnectManager.readDescriptor(mac, service, character, descriptor, response);
                break;

            case CODE_WRITE_DESCRIPTOR:
                BleConnectManager.writeDescriptor(mac, service, character, descriptor, value, response);
                break;

            case CODE_NOTIFY:
                BleConnectManager.notify(mac, service, character, response);
                break;

            case CODE_UNNOTIFY:
                BleConnectManager.unnotify(mac, service, character, response);
                break;

            case CODE_READ_RSSI:
                BleConnectManager.readRssi(mac, response);
                break;

            case CODE_SEARCH:
                SearchRequest request = args.getParcelable(EXTRA_REQUEST);
                BluetoothSearchManager.search(request, response);
                break;

            case CODE_STOP_SESARCH:
                BluetoothSearchManager.stopSearch();
                break;

            case CODE_INDICATE:
                BleConnectManager.indicate(mac, service, character, response);
                break;

            case CODE_REQUEST_MTU:
                int mtu = args.getInt(EXTRA_MTU);
                BleConnectManager.requestMtu(mac, mtu, response);
                break;

            case CODE_CLEAR_REQUEST:
                int clearType = args.getInt(EXTRA_TYPE, 0);
                BleConnectManager.clearRequest(mac, clearType);
                break;

            case CODE_REFRESH_CACHE:
                BleConnectManager.refreshCache(mac);
                break;
        }
        return true;
    }

handleMessage方法中首先解析了bundle,然后区分不同的CODE处理不同的操作,针对"连接"的connect方法,接下来追踪到BleConnectMaster.connect方法

@Override
    public void connect(BleConnectOptions options, BleGeneralResponse response) {
        getConnectDispatcher().connect(options, response);
    }

以上可以看出通过拿到dispatcher这个分发类来处理任务,跳转到dispatcher中的addNewRequest方法 ,

  private void addNewRequest(BleRequest request) {
        checkRuntime();

        if (mBleWorkList.size() < MAX_REQUEST_COUNT) { //最多的请求数是100
            request.setRuntimeChecker(this);
            request.setAddress(mAddress);
            request.setWorker(mWorker);//将worker设置为request,worker才是真正干活的
            mBleWorkList.add(request);
        } else {
            request.onResponse(Code.REQUEST_OVERFLOW);
        }

        scheduleNextRequest(10);
    }

scheduleNextRequest中通过handler发送了一个延时消息,消息处理的过程中调用了scheduleNextRequest()方法

   private void scheduleNextRequest(long delayInMillis) {
        mHandler.sendEmptyMessageDelayed(MSG_SCHEDULE_NEXT, delayInMillis);
    }
 @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SCHEDULE_NEXT:
                scheduleNextRequest();
                break;
        }
        return true;
    }
 private void scheduleNextRequest() {
        if (mCurrentRequest != null) {
            return;
        }

        if (!ListUtils.isEmpty(mBleWorkList)) {
            mCurrentRequest = mBleWorkList.remove(0);
            //以下为重要代码
            mCurrentRequest.process(this);
        }
    }

其中有一行关键的代码 mCurrentRequest.process(this),接下来我们看下BleRequest 到底是怎么干活的?

  @Override
    final public void process(IBleConnectDispatcher dispatcher) {
        checkRuntime();

        mDispatcher = dispatcher;

        BluetoothLog.w(String.format("Process %s, status = %s", getClass().getSimpleName(), getStatusText()));
        //兼容性的的判断
        if (!BluetoothUtils.isBleSupported()) {
            onRequestCompleted(Code.BLE_NOT_SUPPORTED);
        } else if (!BluetoothUtils.isBluetoothEnabled()) {
            onRequestCompleted(Code.BLUETOOTH_DISABLED);
        } else {
            try {
                registerGattResponseListener(this);
                processRequest();
            } catch (Throwable e) {
                BluetoothLog.e(e);
                onRequestCompleted(Code.REQUEST_EXCEPTION);
            }
        }
    }

这里面有一个关键的方法, processRequest(),这是一个抽象方法,子类需要实现的自己实现,关于"连接"我们可以追踪到 BleConnectRequest,以下的方法可以看到,最终判断现在的连接状态后 进行连接的回调

 @Override
    public void processRequest() {
        processConnect();
    }

    private void processConnect() {
        mHandler.removeCallbacksAndMessages(null);
        mServiceDiscoverCount = 0;

        switch (getCurrentStatus()) {
            case Constants.STATUS_DEVICE_CONNECTED://连接成功
                processDiscoverService();//处理服务 -- 读服务
                break;

            case Constants.STATUS_DEVICE_DISCONNECTED://连接失败,打开Gatt
                if (!doOpenNewGatt()) {
                    closeGatt();
                } else {
                    mHandler.sendEmptyMessageDelayed(MSG_CONNECT_TIMEOUT, mConnectOptions.getConnectTimeout());
                }
                break;

            case Constants.STATUS_DEVICE_SERVICE_READY:
                onConnectSuccess();
                break;
        }
    }

至此,分析告一段落,现在我们来分析下连接的另一个关键内容 response 回调的处理,以连接为例,在BleConnectWorker中有一个连接回调,根据不同的情况回调回去.

以上为初学蓝牙和蓝牙框架的一些感悟,以后有机会务必会完善此博客~

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

推荐阅读更多精彩内容