Android藍(lán)牙OBEX FTP服務(wù)端實(shí)現(xiàn)

最近有一個(gè)需求呕臂,要求在Android APP中朗儒,通過藍(lán)牙FTP協(xié)議實(shí)現(xiàn)文件接收功能。為此吮炕,找了很多資料,發(fā)現(xiàn)比較簡(jiǎn)單的實(shí)現(xiàn)方案是采用Bluecove庫通過OBEX協(xié)議來實(shí)現(xiàn)访得。

Bluecove庫中已經(jīng)實(shí)現(xiàn)了OBEX協(xié)議的解析龙亲,同時(shí)會(huì)調(diào)用Android系統(tǒng)的 BluetoothServerSocketBluetoothSocket 進(jìn)行藍(lán)牙通信監(jiān)聽與數(shù)據(jù)通信。所以利用該庫悍抑,直接調(diào)用相關(guān)接口即可鳄炉,使用比較簡(jiǎn)單。

Bluecove庫下載

  1. 使用implementation

    在Android Studio中可使用 implementation 的方式自動(dòng)下載bluecove.jar

    在build.gradle中加入(當(dāng)前最新版本是2.1.0)

    implementation 'net.sf.bluecove:bluecove:2.1.0'
    
  2. 網(wǎng)上下載jar包

    https://mvnrepository.com/artifact/net.sf.bluecove/bluecove/2.1.0可下載最新的jar包搜骡,點(diǎn)擊下圖紅圈標(biāo)記處直接下載拂盯,這一方式其實(shí)與方式1是同一源

image-20211229104509004.png
  1. 直接到github上下載源碼

    https://github.com/fallowu/bluecove

只需要兩個(gè)模塊:bluecovebluecove-android2

我們發(fā)現(xiàn)源碼比jar包多了很多模塊,尤其是bluecove-android2记靡,說明增加了對(duì)Android的支持谈竿,而jar包中其實(shí)是不支持Android系統(tǒng)的,在后來的運(yùn)行中也印證了這一點(diǎn)摸吠,使用jar包運(yùn)行時(shí)空凸,基本上通不過,會(huì)報(bào)各種錯(cuò)誤寸痢,比如缺少.so等呀洲,所以最終是采用直接拷貝bluecove源碼到工程中來實(shí)現(xiàn)的。

https://github.com/fallowu/bluecove/tree/master/bluecove/src/main/java
https://github.com/fallowu/bluecove/tree/master/bluecove-android2/src/main/java
中的package與java代碼拷貝到工程中即可啼止。

image-20211229140414844.png

如何使用Bluecove庫

我也是參考了網(wǎng)上的資料道逗,比如:

https://oomake.com/question/2117043

https://stackoverflow.com/questions/8063178/bluetooth-obex-ftp-server-on-android-2-x

等等,眾多的解決方案都提到了OBEXServer 這個(gè)類献烦,然后我到bluecove源碼中找了一下滓窍,發(fā)現(xiàn)其實(shí)OBEXServer是bluecove源碼中寫的一個(gè)使用示例,見:

https://github.com/fallowu/bluecove/blob/master/bluecove-examples/obex-server/src/main/java/net/sf/bluecove/obex/server/OBEXServer.java

如果在Android中直接使用OBEXServer.java會(huì)出現(xiàn)很多錯(cuò)誤仿荆, 所以還需要改造OBEXServer才能實(shí)現(xiàn)FTP服務(wù)贰您。

另外還可以參考bluecove源碼中對(duì)android支持的說明坏平,見bluecove/bluecove-android2/src/site/apt/index.apt

截取幾段說明:

......

BlueCove-Android2 is additional module for BlueCove to partially support JSR-82 on Android using Android 2.x bluetooth APIs.

This module doesn't need any use of Android NDK or any native libraries. Just include its jar in classpath and it should work.
......

Before calling any JSR-82 API, be sure that you called this passing a context object (typically, the activity from which you are using BlueCove).
---
BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT, context);
---

......

OBEXServer.java改造

除了在項(xiàng)目中引入 Bluecove庫 ,我們還需要將 OBEXServer.java 引入锦亦,但是直接引入使用時(shí)有問題舶替,還需要進(jìn)行小小的改造:

  1. public final UUID OBEX_OBJECT_PUSH = new UUID(0x1105);
    

    改為

    public final UUID OBEX_OBJECT_PUSH = new UUID(0x1106);
    

    為什么這么改,在bluecove源碼中 BluetoothStackAndroid.java 已經(jīng)給了說明:

    ......
    
    private static final UUID UUID_OBEX = new UUID(0x0008);
    private static final UUID UUID_OBEX_OBJECT_PUSH = new UUID(0x1105);
    private static final UUID UUID_OBEX_FILE_TRANSFER = new UUID(0x1106);
    
    ......
    

    UUID(0x1106)才是專門傳輸文件的

  2. run()函數(shù)中設(shè)置context object

    public void run() {
            //add start
            BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context);
            //add end
            
         isStoped = false;
         LocalDevice localDevice;
         try {
             localDevice = LocalDevice.getLocalDevice();
             if (!localDevice.setDiscoverable(DiscoveryAgent.GIAC)) {
                 Logger.error("Fail to set LocalDevice Discoverable");
             }
             serverConnection = (SessionNotifier) Connector.open("btgoep://localhost:" + OBEX_OBJECT_PUSH + ";name="
                     + SERVER_NAME);
         } catch (Throwable e) {
             Logger.error("OBEX Server start error", e);
             isStoped = true;
             return;
         }
         
         ......
     }
    

    我們加了一行

    BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context )
    

    其中變量context為Context類型杠园,需要啟動(dòng)服務(wù)時(shí)將Activity作為參數(shù)傳進(jìn)來顾瞪。

  3. 去掉不需要的代碼

    還是在run()函數(shù)里

    public void run() {
            //add start
            BlueCoveImpl.setConfigObject(BlueCoveConfigProperties.PROPERTY_ANDROID_CONTEXT,context);
            //add end
            
         isStoped = false;
         //LocalDevice localDevice; --del
         try {
            /*
             localDevice = LocalDevice.getLocalDevice();
             if (!localDevice.setDiscoverable(DiscoveryAgent.GIAC)) {
                 Logger.error("Fail to set LocalDevice Discoverable");
             }
           */ --del
             serverConnection = (SessionNotifier) Connector.open("btgoep://localhost:" + OBEX_OBJECT_PUSH + ";name="
                     + SERVER_NAME);
         } catch (Throwable e) {
             Logger.error("OBEX Server start error", e);
             isStoped = true;
             return;
         }
    
            //下面的try catch 全部去掉
            /*
         try {
             ServiceRecord record = localDevice.getRecord(serverConnection);
             String url = record.getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
             Logger.debug("BT server url: " + url);
                
             final int OBJECT_TRANSFER_SERVICE = 0x100000;
    
             try {
                 record.setDeviceServiceClasses(OBJECT_TRANSFER_SERVICE);
             } catch (Throwable e) {
                 Logger.debug("setDeviceServiceClasses", e);
             }
    
             DataElement bluetoothProfileDescriptorList = new DataElement(DataElement.DATSEQ);
             DataElement obbexPushProfileDescriptor = new DataElement(DataElement.DATSEQ);
             obbexPushProfileDescriptor.addElement(new DataElement(DataElement.UUID, OBEX_OBJECT_PUSH));
             obbexPushProfileDescriptor.addElement(new DataElement(DataElement.U_INT_2, 0x100));
             bluetoothProfileDescriptorList.addElement(obbexPushProfileDescriptor);
             record.setAttributeValue(0x0009, bluetoothProfileDescriptorList);
    
             final short ATTR_SUPPORTED_FORMAT_LIST_LIST = 0x0303;
             DataElement supportedFormatList = new DataElement(DataElement.DATSEQ);
             // any type of object.
             supportedFormatList.addElement(new DataElement(DataElement.U_INT_1, 0xFF));
             record.setAttributeValue(ATTR_SUPPORTED_FORMAT_LIST_LIST, supportedFormatList);
    
             final short UUID_PUBLICBROWSE_GROUP = 0x1002;
             final short ATTR_BROWSE_GRP_LIST = 0x0005;
             DataElement browseClassIDList = new DataElement(DataElement.DATSEQ);
             UUID browseClassUUID = new UUID(UUID_PUBLICBROWSE_GROUP);
             browseClassIDList.addElement(new DataElement(DataElement.UUID, browseClassUUID));
             record.setAttributeValue(ATTR_BROWSE_GRP_LIST, browseClassIDList);
    
             localDevice.updateRecord(record);
         } catch (Throwable e) {
             Logger.error("Updating SDP", e);
         }
         */
    
            ......
      }
    

主要去掉了兩塊:localDevice.setDiscoverablelocalDevice.updateRecord 這兩個(gè)函數(shù)的調(diào)用

去掉:localDevice.setDiscoverable(DiscoveryAgent.GIAC),可防止開啟服務(wù)時(shí)抛蚁,手機(jī)彈出對(duì)話框提示
去掉:localDevice.updateRecord(record); 這段代碼的作用陈醒,原因可以查看 BluetoothStackAndroid.java 源碼

public void rfServerUpdateServiceRecord(long handle, ServiceRecordImpl serviceRecord, boolean acceptAndOpen) throws ServiceRegistrationException {
     throw new UnsupportedOperationException("Not supported yet.");
 }

因?yàn)?localDevice.updateRecord(record) 最終會(huì)調(diào)用 BluetoothStackAndroid 類的 rfServerUpdateServiceRecord 函數(shù),此函數(shù)會(huì)拋出異常瞧甩,告知不支持該操作钉跷。

4.將每個(gè)Client連接變量放入集合里,方便退出時(shí)關(guān)閉
目的是在APP退出時(shí)肚逸,調(diào)用close函數(shù)爷辙,能關(guān)掉socket連接

public class OBEXServer implements Runnable {

    private SessionNotifier serverConnection;

    private boolean isStoped = false;

    private boolean isRunning = false;

    public final UUID OBEX_OBJECT_PUSH = new UUID(0x1106);

    public static final String SERVER_NAME = "OBEX Object Push";

    private UserInteraction interaction;

    //add
    private HashSet<RequestHandler> requestHandlerSet = new HashSet<RequestHandler>();

    ......

}

RequestHandlerconnectionAccepted函數(shù)中增加一條語句:

void connectionAccepted(Connection cconn) {
            Logger.debug("Received OBEX connection");
            showStatus("Client connected");
            this.cconn = cconn;

            //add
            requestHandlerSet.add(this);

            if (!isConnected) {
                notConnectedTimer.schedule(new TimerTask() {
                    public void run() {
                        notConnectedClose();
                    }
                }, 1000 * 30);
            }
        }

RequestHandler中增加一個(gè)函數(shù)close:

void close() {
    try {
         if (cconn != null)  {
               cconn.close();
         }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

OBEXServerclose函數(shù)增加:

public void close() {
        isStoped = true;
  
        //add
        for (RequestHandler handler : requestHandlerSet) {
            handler.close();
        }
        requestHandlerSet.clear();
        //add end
                
        try {
            if (serverConnection != null) {
                serverConnection.close();
            }
            Logger.debug("OBEX ServerConnection closed");
        } catch (Throwable e) {
            Logger.error("OBEX Server stop error", e);
        }
    }

總結(jié):基本上改了這四處后,OBEXServer已經(jīng)支持Android作為藍(lán)牙FTP服務(wù)端開啟正常運(yùn)行朦促,其它一些細(xì)節(jié)膝晾,根據(jù)需要進(jìn)行更改與優(yōu)化即可,比如文件存儲(chǔ)的目錄务冕,需要根據(jù)Android系統(tǒng)進(jìn)行更改血当,要更改homePath函數(shù)。

bluecove庫源碼改造

如果不改造bluecove庫禀忆,APP可以收到Client端的連接請(qǐng)求臊旭,并回復(fù)連接成功消息,但是就沒有后續(xù)了油湖,Socket會(huì)被Client端斷開巍扛,所以就需要進(jìn)行一些改動(dòng)。

  1. OBEXSessionBase.java

    ......
    
    protected static int id = 1; //add
    public OBEXSessionBase(StreamConnection conn, OBEXConnectionParams obexConnectionParams) throws IOException {
        if (obexConnectionParams == null) {
            throw new NullPointerException("obexConnectionParams is null");
        }
        this.isConnected = false;
        this.conn = conn;
        this.obexConnectionParams = obexConnectionParams;
        this.mtu = obexConnectionParams.mtu;
        this.connectionID = id++; //modify
        this.packetsCountWrite = 0;
        this.packetsCountRead = 0;
        boolean initOK = false;
        try {
            this.os = conn.openOutputStream();
            this.is = conn.openInputStream();
            initOK = true;
        } finally {
            if (!initOK) {
                try {
                    this.close();
                } catch (IOException e) {
                    DebugLog.error("close error", e);
                }
            }
        }
    }
    
    ......
    

    增加了一個(gè)變量

    protected static int id = 1;
    

    同時(shí)將

    this.connectionID = 0;
    

    改為

    this.connectionID = id++; 
    

    源碼里connectionID是一直為0乏德,明顯不正常撤奸,所以每次新建對(duì)象時(shí)connectionID自增1

另外有一處bug需要更改:

函數(shù)handleAuthenticationResponse

if ((authChallengesSent == null) && (authChallengesSent.size() == 0)) {
     throw new IOException("Authentication challenges had not been sent");
}

改為

if ((authChallengesSent == null) || (authChallengesSent.size() == 0)) {
     throw new IOException("Authentication challenges had not been sent");
}

這個(gè)bug非常明顯,如果用&&符喊括,前面為null胧瓜,還要去執(zhí)行size,會(huì)引起空指針異常郑什。

  1. OBEXServerSessionImpl.java

    增加一個(gè)函數(shù):

    private void connectHeaderTargetCopy(OBEXHeaderSetImpl paramOBEXHeaderSetImpl1, OBEXHeaderSetImpl paramOBEXHeaderSetImpl2) {
         if (paramOBEXHeaderSetImpl1 != null && paramOBEXHeaderSetImpl2 != null && paramOBEXHeaderSetImpl1.headerValues != null && paramOBEXHeaderSetImpl2.headerValues != null)
             for (Object entry : paramOBEXHeaderSetImpl1.headerValues.entrySet()) {
                 if (((Map.Entry)entry).getKey() instanceof Integer && ((Map.Entry)entry).getValue() instanceof byte[] && ((Integer)((Map.Entry)entry).getKey()).intValue() == 70 && !paramOBEXHeaderSetImpl2.headerValues.containsKey(Integer.valueOf(74))) {
                     paramOBEXHeaderSetImpl2.headerValues.put(Integer.valueOf(74), ((Map.Entry)entry).getValue());
                     break;
                 }
         }
    }
    

    然后在函數(shù)processConnect 中調(diào)用

    private void processConnect(byte[] b) throws IOException {
        
        ......
            
         byte[] connectResponse = new byte[4];
         connectResponse[0] = OBEXOperationCodes.OBEX_VERSION;
         connectResponse[1] = 0; /* Flags */
         connectResponse[2] = OBEXUtils.hiByte(obexConnectionParams.mtu);
         connectResponse[3] = OBEXUtils.loByte(obexConnectionParams.mtu);  
    
         connectHeaderTargetCopy(requestHeaders,replyHeaders); //add
        
         writePacketWithFlags(rc, connectResponse, replyHeaders);
         if (rc == ResponseCodes.OBEX_HTTP_OK) {
            this.isConnected = true;
         }
    }
    

    目的就是在回復(fù)Client端的連接請(qǐng)求時(shí)府喳,將Client端連接請(qǐng)求Headers信息中的Target數(shù)據(jù)拷貝到Server端Response消息中,如果不拷貝蘑拯,Client會(huì)將Socket連接斷開钝满。
    注:
    70:0x46 Target兜粘,操作的目的服務(wù)名
    74:0x4A Who,OBEX Application標(biāo)識(shí)弯蚜,用于表明是否是同一個(gè)應(yīng)用

Headers涉及到了OBEX協(xié)議孔轴,具體可以參考https://blog.csdn.net/feelinghappy/article/details/107967796

image-20211229155336206.png
  1. OBEXHeaderSetImpl.java

    修改hasIncommingData函數(shù)

    boolean hasIncommingData() {
     return headerValues.contains(new Integer(OBEX_HDR_BODY))
             || headerValues.contains(new Integer(OBEX_HDR_BODY_END));
    }
    

    改為

    boolean hasIncommingData() {
     return headerValues.containsKey(new Integer(OBEX_HDR_BODY))
         || headerValues.containsKey(new Integer(OBEX_HDR_BODY_END));
    }
    

    此處估計(jì)是一個(gè)bug,應(yīng)該判斷的是headerValues的key是否包含那兩個(gè)值

  2. BluetoothStackAndroid.java

    將函數(shù)rfServerAcceptAndOpenRfServerConnection 中的一行serverSocket.close();去掉

    修改后的函數(shù)如下:

    public long rfServerAcceptAndOpenRfServerConnection(long handle) throws IOException {
         AndroidBluetoothConnection bluetoothConnection =      AndroidBluetoothConnection.getBluetoothConnection(handle);
         BluetoothServerSocket serverSocket = bluetoothConnection.getServerSocket();
         BluetoothSocket socket = serverSocket.accept();
    //       serverSocket.close();  --del
         AndroidBluetoothConnection connection = AndroidBluetoothConnection.createConnection(socket, true);
         return connection.getHandle();
     }
    

    rfServerAcceptAndOpenRfServerConnection函數(shù)最開始是由OBEXServer的循環(huán)語句內(nèi)

    handler.connectionAccepted(serverConnection.acceptAndOpen(handler));
    

    調(diào)用到的碎捺,如果在rfServerAcceptAndOpenRfServerConnection函數(shù)中關(guān)閉了serverSocket路鹰,將導(dǎo)致第一次成功acceptAndOpen后的后續(xù)acceptAndOpen調(diào)用全部產(chǎn)生異常

     java.io.IOException: bt socket is not in listen state
            at android.bluetooth.BluetoothSocket.accept(BluetoothSocket.java:493)
            at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:171)
            at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:157)
            at com.intel.bluetooth.BluetoothStackAndroid.rfServerAcceptAndOpenRfServerConnection(BluetoothStackAndroid.java:461)
            at com.intel.bluetooth.BluetoothRFCommConnectionNotifier.acceptAndOpen(BluetoothRFCommConnectionNotifier.java:74)
            at com.intel.bluetooth.obex.OBEXSessionNotifierImpl.acceptAndOpen(OBEXSessionNotifierImpl.java:89)
            at com.intel.bluetooth.obex.OBEXSessionNotifierImpl.acceptAndOpen(OBEXSessionNotifierImpl.java:79)
            .......
    

5.OBEXServerOperationPut.java
構(gòu)造函數(shù)OBEXServerOperationPut,最后增加一句:

protected OBEXServerOperationPut(OBEXServerSessionImpl session, OBEXHeaderSetImpl receivedHeaders,
           boolean finalPacket) throws IOException {
       super(session, receivedHeaders);
       this.inputStream = new OBEXOperationInputStream(this);
       processIncommingData(receivedHeaders, finalPacket);
       //下面是增加的代碼收厨,主要是解決put操作時(shí)晋柱,如果接收到最后一條數(shù)據(jù)
       //程序沒有及時(shí)設(shè)置成最后一條,導(dǎo)致仍然在put操作中诵叁,沒有退出雁竞,
       //后續(xù)上傳新的文件時(shí),會(huì)當(dāng)成上一個(gè)文件的后續(xù)黎休,上傳會(huì)失敗
       finalPacketReceived = finalPacket; 
   }

總結(jié)

應(yīng)用內(nèi)還需要增加權(quán)限的支持浓领、藍(lán)牙配對(duì)等功能,OBEXServer也可以優(yōu)化势腮,但是經(jīng)過上述修改后,APP已經(jīng)具備通過藍(lán)牙FTP接收文件并保存到手機(jī)的功能漫仆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末捎拯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盲厌,更是在濱河造成了極大的恐慌署照,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吗浩,死亡現(xiàn)場(chǎng)離奇詭異建芙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)懂扼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門禁荸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阀湿,你說我怎么就攤上這事赶熟。” “怎么了陷嘴?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵映砖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我灾挨,道長(zhǎng)邑退,這世上最難降的妖魔是什么竹宋? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮地技,結(jié)果婚禮上蜈七,老公的妹妹穿的比我還像新娘。我一直安慰自己乓土,他們只是感情好宪潮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趣苏,像睡著了一般狡相。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上食磕,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天尽棕,我揣著相機(jī)與錄音,去河邊找鬼彬伦。 笑死滔悉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的单绑。 我是一名探鬼主播回官,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼搂橙!你這毒婦竟也來了歉提?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤区转,失蹤者是張志新(化名)和其女友劉穎苔巨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體废离,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡侄泽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜻韭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悼尾。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖湘捎,靈堂內(nèi)的尸體忽然破棺而出诀豁,到底是詐尸還是另有隱情,我是刑警寧澤窥妇,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布舷胜,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏烹骨。R本人自食惡果不足惜翻伺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沮焕。 院中可真熱鬧吨岭,春花似錦、人聲如沸峦树。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽魁巩。三九已至急灭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谷遂,已是汗流浹背葬馋。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肾扰,地道東北人畴嘶。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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