Android多網(wǎng)絡(luò)連接探索-雙Wifi

摘要:

新增的多網(wǎng)絡(luò)功能允許應(yīng)用查詢可用網(wǎng)絡(luò)提供的功能镶殷,例如它們是 WLAN 網(wǎng)絡(luò)晦毙、蜂窩網(wǎng)絡(luò)還是按流量計(jì)費(fèi)網(wǎng)絡(luò)恋拷,或者它們是否提供特定網(wǎng)絡(luò)功能准给。然后應(yīng)用可以請求連接并對連接丟失或其他網(wǎng)絡(luò)變化作出響應(yīng)响巢。 Android 5.0 提供了新的多網(wǎng)絡(luò) API描滔,允許您的應(yīng)用動態(tài)掃描具有特定能力的可用網(wǎng)絡(luò),并與它們建立連接踪古。

Android 5.0 LOLLIPOP (API Level 21)

新增的多網(wǎng)絡(luò)功能允許應(yīng)用查詢可用網(wǎng)絡(luò)提供的功能含长,例如它們是 WLAN 網(wǎng)絡(luò)、蜂窩網(wǎng)絡(luò)還是按流量計(jì)費(fèi)網(wǎng)絡(luò)伏穆,或者它們是否提供特定網(wǎng)絡(luò)功能拘泞。然后應(yīng)用可以請求連接并對連接丟失或其他網(wǎng)絡(luò)變化作出響應(yīng)。

Android 5.0 提供了新的多網(wǎng)絡(luò) API枕扫,允許您的應(yīng)用動態(tài)掃描具有特定能力的可用網(wǎng)絡(luò)陪腌,并與它們建立連接。當(dāng)您的應(yīng)用需要 SUPL烟瞧、彩信或運(yùn)營商計(jì)費(fèi)網(wǎng)絡(luò)等專業(yè)化網(wǎng)絡(luò)時(shí)诗鸭,或者您想使用特定類型的傳輸協(xié)議發(fā)送數(shù)據(jù)時(shí),就可以使用此功能参滴。

通過以上的Android版本更新文檔可以看出强岸,Android 在 5.0 以上的系統(tǒng)中支持了多個網(wǎng)絡(luò)連接的特性。

Android 提供的這個特性意味著應(yīng)用可以選擇特定的網(wǎng)絡(luò)發(fā)送網(wǎng)絡(luò)數(shù)據(jù)砾赔。在用手機(jī)上網(wǎng)的時(shí)候很可能會遇到這種情況蝌箍,已經(jīng)連上了WiFi但是WiFi信號弱或者是該WiFi設(shè)備并沒有連接到互聯(lián)網(wǎng)青灼,因此導(dǎo)致網(wǎng)絡(luò)訪問非常的緩慢甚至無法訪問網(wǎng)絡(luò)。但是這個時(shí)候手機(jī)的移動網(wǎng)絡(luò)信號可能是非常好的十绑,那么如果是在 Android 5.0 以下的系統(tǒng)上聚至,我們只能關(guān)閉手機(jī)的WiFi功能,然后使用移動網(wǎng)絡(luò)重新訪問本橙。在 Android 5.0 及以上的系統(tǒng)中有了這個特性之后扳躬,意味著應(yīng)用可以自己處理好這種情況,直接切換到移動網(wǎng)絡(luò)上面訪問甚亭,為用戶提供更好的體驗(yàn)贷币。話不多說讓我們來看一下怎么使用吧

setProcessDefaultNetwork

要從您的應(yīng)用以動態(tài)方式選擇并連接網(wǎng)絡(luò),請執(zhí)行以下步驟:

1.創(chuàng)建一個 ConnectivityManager亏狰。
2.使用 NetworkRequest.Builder 類創(chuàng)建一個 NetworkRequest 對象役纹,并指定您的應(yīng)用感興趣的網(wǎng)絡(luò)功能和傳輸類型。
3.要掃描合適的網(wǎng)絡(luò)暇唾,請調(diào)用 requestNetwork() 或 registerNetworkCallback()促脉,并傳入 NetworkRequest 對象和 ConnectivityManager.NetworkCallback 的實(shí)現(xiàn)。如果您想在檢測到合適的網(wǎng)絡(luò)時(shí)主動切換到該網(wǎng)絡(luò)策州,請使用 requestNetwork() 方法瘸味;如果只是接收已掃描網(wǎng)絡(luò)的通知而不需要主動切換,請改用 registerNetworkCallback() 方法够挂。
4.當(dāng)系統(tǒng)檢測到合適的網(wǎng)絡(luò)時(shí)旁仿,它會連接到該網(wǎng)絡(luò)并調(diào)用 onAvailable() 回調(diào)。您可以使用回調(diào)中的 Network 對象來獲取有關(guān)網(wǎng)絡(luò)的更多信息孽糖,或者引導(dǎo)通信使用所選網(wǎng)絡(luò)枯冈。

app都采用指定的網(wǎng)絡(luò)

ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder req = newNetworkRequest.Builder();
req.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
cm.requestNetwork(req.build(), new ConnectivityManager.NetworkCallback() { @Override
public void onAvailable(Network network) {
 try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
ConnectivityManager.setProcessDefaultNetwork(network);
} else { 
connectivityManager.bindProcessToNetwork(network); 
}
} catch (IllegalStateException e) {
Log.e(TAG, "ConnectivityManager.NetworkCallback.onAvailable: ", e);       
 }    
}    
 // Be sure to override other options in NetworkCallback() too...}復(fù)制代碼

指定某個請求采用指定的網(wǎng)絡(luò)

ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);                       
NetworkRequest.Builder req = new NetworkRequest.Builder();
req.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
cm.requestNetwork(req.build(), new ConnectivityManager.NetworkCallback() {     

@Override    
public void onAvailable(Network network) {       
 // If you want to use a raw socket...        
network.bindSocket(...);       
 // Or if you want a managed URL connection...        
URLConnection conn = network.openConnection(new URL("http://www.baidu.com/"));   
 }     
// Be sure to override other options in NetworkCallback() too...}

android支持多種網(wǎng)絡(luò)類型(WAN口),例如WIFI办悟、3G等尘奏。目前android的實(shí)現(xiàn)是,WIFI和3G只能同時(shí)存在一個(優(yōu)先級)病蛉,例如當(dāng)WIFI連接后炫加,數(shù)據(jù)通路就從3G切換到WIFI。對上層app而言铡恕,這時(shí)候數(shù)據(jù)通路也就從3G切換到WIFI上琢感。

考慮一個特殊的需求丢间,某app只能通過WIFI接口去傳輸數(shù)據(jù)探熔,是否可以實(shí)現(xiàn)?較新版本的android已經(jīng)支持了該功能烘挫,通過調(diào)用setProcessDefaultNetwork()可以指定某一進(jìn)程的網(wǎng)絡(luò)接口诀艰,

    /**
 * Binds the current process to {@code network}.  All Sockets created in the future
 * (and not explicitly bound via a bound SocketFactory from
 * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
 * {@code network}.  All host name resolutions will be limited to {@code network} as well.
 * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
 * work and all host name resolutions will fail.  This is by design so an application doesn't
 * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
 * To clear binding pass {@code null} for {@code network}.  Using individually bound
 * Sockets created by Network.getSocketFactory().createSocket() and
 * performing network-specific host name resolutions via
 * {@link Network#getAllByName Network.getAllByName} is preferred to calling
 * {@code setProcessDefaultNetwork}.
 *
 * @param network The {@link Network} to bind the current process to, or {@code null} to clear
 *                the current binding.
 * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
 */
public static boolean setProcessDefaultNetwork(Network network) {

}

該函數(shù)的實(shí)現(xiàn)原理大致為柬甥,

  1. 該進(jìn)程在創(chuàng)建socket時(shí)(app首先調(diào)用setProcessDefaultNetwork()),android底層會利用setsockopt函數(shù)設(shè)置該socket的SO_MARK為netId(android有自己的管理邏輯其垄,每個Network有對應(yīng)的ID)苛蒲,以后利用該socket發(fā)送的數(shù)據(jù)都會被打上netId的標(biāo)記(fwmark 值)。
  2. 利用策略路由绿满,將打著netId標(biāo)記的數(shù)據(jù)包都路由到WIFI的接口wlan0臂外。
    這里先介紹打標(biāo)簽的原理,至于策略路由的創(chuàng)建喇颁,后續(xù)再分析漏健,下面是策略路由表的一個簡單例子。
    shell@msm8916_64:/ $ ip rule list
    ip rule list
    0:      from all lookup local
    10000:  from all fwmark 0xc0000/0xd0000 lookup 99
    13000:  from all fwmark 0x10063/0x1ffff lookup 97
    13000:  from all fwmark 0x10064/0x1ffff lookup 1012
    14000:  from all oif rmnet_data0 lookup 1012
    15000:  from all fwmark 0x0/0x10000 lookup 99
    16000:  from all fwmark 0x0/0x10000 lookup 98
    17000:  from all fwmark 0x0/0x10000 lookup 97
    19000:  from all fwmark 0x64/0x1ffff lookup 1012
    22000:  from all fwmark 0x0/0xffff lookup 1012
    23000:  from all fwmark 0x0/0xffff uidrange 0-0 lookup main
    32000:  from all unreachable
    shell@msm8916_64:/ $

Android 中的實(shí)現(xiàn)

1. 先看一下 frameworks/base/core/java/android/net/ConnectivityManager.java 中 setProcessDefaultNetwork 的實(shí)現(xiàn)

public static boolean setProcessDefaultNetwork(Network network) {    int netId = (network == null) ? NETID_UNSET : network.netId;    if (netId == NetworkUtils.getBoundNetworkForProcess()) {        return true;    }    if (NetworkUtils.bindProcessToNetwork(netId)) {        // Set HTTP proxy system properties to match network.        // TODO: Deprecate this static method and replace it with a non-static version.        try {            Proxy.setHttpProxySystemProperty(getInstance().getDefaultProxy());        } catch (SecurityException e) {            // The process doesn't have ACCESS_NETWORK_STATE, so we can't fetch the proxy.            Log.e(TAG, "Can't set proxy properties", e);        }        // Must flush DNS cache as new network may have different DNS resolutions.        InetAddress.clearDnsCache();        // Must flush socket pool as idle sockets will be bound to previous network and may        // cause subsequent fetches to be performed on old network.        NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged();        return true;    } else {        return false;    }}復(fù)制代碼

2. 在 setProcessDefaultNetwork 的時(shí)候橘霎,HttpProxy蔫浆,DNS 都會使用當(dāng)前網(wǎng)絡(luò)的配置,再來看一下 NetworkUtils.bindProcessToNetwork
/frameworks/base/core/java/android/net/NetworkUtils.bindProcessToNetwork 其實(shí)是直接轉(zhuǎn)到了 /system/netd/client/NetdClient.cpp 中

int setNetworkForTarget(unsigned netId, std::atomic_uint* target) {    if (netId == NETID_UNSET) {        *target = netId;        return 0;    }    // Verify that we are allowed to use |netId|, by creating a socket and trying to have it marked    // with the netId. Call libcSocket() directly; else the socket creation (via netdClientSocket())    // might itself cause another check with the fwmark server, which would be wasteful.    int socketFd;    if (libcSocket) {        socketFd = libcSocket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);    } else {        socketFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);    }    if (socketFd < 0) {        return -errno;    }    int error = setNetworkForSocket(netId, socketFd);    if (!error) {        *target = netId;    }    close(socketFd);    return error;} extern "C" int setNetworkForSocket(unsigned netId, int socketFd) {    if (socketFd < 0) {        return -EBADF;    }    FwmarkCommand command = {FwmarkCommand::SELECT_NETWORK, netId, 0};    return FwmarkClient().send(&command, socketFd);} extern "C" int setNetworkForProcess(unsigned netId) {    return setNetworkForTarget(netId, &netIdForProcess);}復(fù)制代碼

3. 客戶端發(fā)送 FwmarkCommand::SELECT_NETWORK 通知服務(wù)端處理姐叁,代碼在 /system/netd/server/FwmarkServer.cpp

int FwmarkServer::processClient(SocketClient* client, int* socketFd) {   // .................   Fwmark fwmark;   socklen_t fwmarkLen = sizeof(fwmark.intValue);   if (getsockopt(*socketFd, SOL_SOCKET, SO_MARK, &fwmark.intValue, &fwmarkLen) == -1) {        return -errno;    }     switch (command.cmdId) {        // .................        case FwmarkCommand::SELECT_NETWORK: {            fwmark.netId = command.netId;            if (command.netId == NETID_UNSET) {                fwmark.explicitlySelected = false;                fwmark.protectedFromVpn = false;                permission = PERMISSION_NONE;            } else {                if (int ret = mNetworkController->checkUserNetworkAccess(client->getUid(),                                                                         command.netId)) {                    return ret;                }                fwmark.explicitlySelected = true;                fwmark.protectedFromVpn = mNetworkController->canProtect(client->getUid());            }            break;        }        // .................    }     fwmark.permission = permission;     if (setsockopt(*socketFd, SOL_SOCKET, SO_MARK, &fwmark.intValue,                   sizeof(fwmark.intValue)) == -1) {        return -errno;    }     return 0;}   union Fwmark {    uint32_t intValue;    struct {        unsigned netId          : 16;        bool explicitlySelected :  1;        bool protectedFromVpn   :  1;        Permission permission   :  2;    };    Fwmark() : intValue(0) {}};復(fù)制代碼

最后其實(shí)只是給 socketFd 設(shè)置了 mark瓦盛,為什么這樣就可以達(dá)到使用特定網(wǎng)絡(luò)的目的呢。這里的實(shí)現(xiàn)原理大致為:
1. 該進(jìn)程在創(chuàng)建socket時(shí)(app首先調(diào)用setProcessDefaultNetwork())外潜,android底層會利用setsockopt函數(shù)設(shè)置該socket的SO_MARK為netId(android有自己的管理邏輯原环,每個Network有對應(yīng)的ID),以后利用該socket發(fā)送的數(shù)據(jù)都會被打上netId的標(biāo)記(fwmark 值)橡卤。
2. 利用策略路由扮念,將打著netId標(biāo)記的數(shù)據(jù)包都路由到指定的網(wǎng)絡(luò)接口,例如WIFI的接口wlan0碧库。
Linux 中的策略路由暫不在本章展開討論柜与,這里只需要了解通過這種方式就能達(dá)到我們的目的。

Hook socket api

也就是說只要在當(dāng)前進(jìn)程中利用setsockopt函數(shù)設(shè)置所有socket的SO_MARK為netId嵌灰,就可以完成所有的請求都走特定的網(wǎng)絡(luò)接口弄匕。

1. 先來看一下 /bionic/libc/bionic/socket.cpp

int socket(int domain, int type, int protocol) {    return __netdClientDispatch.socket(domain, type, protocol);}復(fù)制代碼

2. /bionic/libc/private/NetdClientDispatch.h

struct NetdClientDispatch {    int (*accept4)(int, struct sockaddr*, socklen_t*, int);    int (*connect)(int, const struct sockaddr*, socklen_t);    int (*socket)(int, int, int);    unsigned (*netIdForResolv)(unsigned);}; extern __LIBC_HIDDEN__ struct NetdClientDispatch __netdClientDispatch;復(fù)制代碼

3. /bionic/libc/bionic/NetdClientDispatch.cpp

extern "C" __socketcall int __accept4(int, sockaddr*, socklen_t*, int);extern "C" __socketcall int __connect(int, const sockaddr*, socklen_t);extern "C" __socketcall int __socket(int, int, int); static unsigned fallBackNetIdForResolv(unsigned netId) {    return netId;} // This structure is modified only at startup (when libc.so is loaded) and never// afterwards, so it's okay that it's read later at runtime without a lock.__LIBC_HIDDEN__ NetdClientDispatch __netdClientDispatch __attribute__((aligned(32))) = {    __accept4,    __connect,    __socket,    fallBackNetIdForResolv,};復(fù)制代碼

4. /bionic/libc/bionic/NetdClient.cpp

template <typename FunctionType>static void netdClientInitFunction(void* handle, const char* symbol, FunctionType* function) {    typedef void (*InitFunctionType)(FunctionType*);    InitFunctionType initFunction = reinterpret_cast<InitFunctionType>(dlsym(handle, symbol));    if (initFunction != NULL) {        initFunction(function);    }} static void netdClientInitImpl() {    void* netdClientHandle = dlopen("libnetd_client.so", RTLD_NOW);    if (netdClientHandle == NULL) {        // If the library is not available, it's not an error. We'll just use        // default implementations of functions that it would've overridden.        return;    }    netdClientInitFunction(netdClientHandle, "netdClientInitAccept4",                           &__netdClientDispatch.accept4);    netdClientInitFunction(netdClientHandle, "netdClientInitConnect",                           &__netdClientDispatch.connect);    netdClientInitFunction(netdClientHandle, "netdClientInitNetIdForResolv",                           &__netdClientDispatch.netIdForResolv);    netdClientInitFunction(netdClientHandle, "netdClientInitSocket", &__netdClientDispatch.socket);}static pthread_once_t netdClientInitOnce = PTHREAD_ONCE_INIT;extern "C" __LIBC_HIDDEN__ void netdClientInit() {    if (pthread_once(&netdClientInitOnce, netdClientInitImpl)) {        __libc_format_log(ANDROID_LOG_ERROR, "netdClient", "Failed to initialize netd_client");    }}復(fù)制代碼

5. /system/netd/client/NetdClient.cpp

extern "C" void netdClientInitSocket(SocketFunctionType* function) {    if (function && *function) {        libcSocket = *function;        *function = netdClientSocket;    }} int netdClientSocket(int domain, int type, int protocol) {    int socketFd = libcSocket(domain, type, protocol);    if (socketFd == -1) {        return -1;    }    unsigned netId = netIdForProcess;    if (netId != NETID_UNSET && FwmarkClient::shouldSetFwmark(domain)) {        if (int error = setNetworkForSocket(netId, socketFd)) {            return closeFdAndSetErrno(socketFd, error);        }    }    return socketFd;}復(fù)制代碼

int netdClientAccept4(int sockfd, sockaddr* addr, socklen_t* addrlen, int flags);
int netdClientConnect(int sockfd, const sockaddr* addr, socklen_t addrlen);
int netdClientSocket(int domain, int type, int protocol);

看到這里應(yīng)該明白了,以上的函數(shù)和 libc 中的 accpet / connect / socket 功能相同沽瞭,只是額外的將 socket 的SO_MARK設(shè)為netId迁匠。注意:netIdForProcess 為之前調(diào)用 setProcessDefaultNetwork 時(shí)保存下來的值。

所以當(dāng)調(diào)用 libc 中的 connect() 的時(shí)候, connect() -> netdClientConnect() -> __connect()驹溃,也就完成了將所有 socket 的SO_MARK設(shè)置為netId了城丧。

自然在應(yīng)用中無論是通過 Java 新建的網(wǎng)絡(luò)連接,還是通過 native 代碼新建的網(wǎng)絡(luò)連接豌鹤,只要最后是通過 libc 中的接口就能使用該功能亡哄。至于連著WiFi最后流量耗了一大堆的問題,可能會讓用戶再次陷入是否應(yīng)該關(guān)閉iOS 11中WiFi助理功能類似的糾結(jié)布疙。無論如何從技術(shù)上來講這是一個優(yōu)化點(diǎn)蚊惯,說來 Linux 本身是支持的愿卸,也許在 Android 5.0 以下也是可以實(shí)現(xiàn)的?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末截型,一起剝皮案震驚了整個濱河市趴荸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宦焦,老刑警劉巖发钝,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異波闹,居然都是意外死亡笼平,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門舔痪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寓调,“玉大人,你說我怎么就攤上這事锄码《嵊ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵滋捶,是天一觀的道長痛悯。 經(jīng)常有香客問我,道長重窟,這世上最難降的妖魔是什么载萌? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮巡扇,結(jié)果婚禮上扭仁,老公的妹妹穿的比我還像新娘。我一直安慰自己厅翔,他們只是感情好乖坠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著刀闷,像睡著了一般熊泵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上甸昏,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天顽分,我揣著相機(jī)與錄音,去河邊找鬼施蜜。 笑死卒蘸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的花墩。 我是一名探鬼主播悬秉,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冰蘑!你這毒婦竟也來了和泌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤祠肥,失蹤者是張志新(化名)和其女友劉穎武氓,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仇箱,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡县恕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了剂桥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忠烛。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖权逗,靈堂內(nèi)的尸體忽然破棺而出美尸,到底是詐尸還是另有隱情,我是刑警寧澤斟薇,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布师坎,位于F島的核電站,受9級特大地震影響堪滨,放射性物質(zhì)發(fā)生泄漏胯陋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一袱箱、第九天 我趴在偏房一處隱蔽的房頂上張望遏乔。 院中可真熱鬧,春花似錦发笔、人聲如沸按灶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锦援。三九已至达布,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酌住。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留半等,地道東北人筒捺。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像势就,于是被迫代替她去往敵國和親泉瞻。 傳聞我的和親對象是個殘疾皇子脉漏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353