Flask中的上下文

前言

上一篇中我們已經(jīng)知道flask運(yùn)行的大體流程(Flask的工作原理)民镜,其中進(jìn)入wsgi_app中首先創(chuàng)建的就是上下文環(huán)境乍桂,那么什么是上下文呢袜瞬,又有什么作用怜俐。在了解上下文之前,先要弄清楚LocalProxy邓尤,Local拍鲤,LocalStack這三個(gè)概念。

Local

根據(jù)werkzeug文檔介紹汞扎,local是提供了線程隔離的數(shù)據(jù)訪問方式季稳,類似于python中的 thread locals∨謇蹋可以理解為存在一個(gè)全局的數(shù)據(jù)绞幌,不同線程可以讀取和修改它。不同線程之間是彼此隔離的一忱。

什么是線程隔離呢莲蜘?

比如存在一個(gè)全局變量數(shù)字10,在線程1中把10改為1帘营,主線程中讀取這個(gè)數(shù)字票渠,發(fā)現(xiàn)數(shù)字變成了1,也就是說新線程數(shù)據(jù)影響了主線程數(shù)據(jù)芬迄。這樣一來问顷,多線程之間考慮其他線程帶來的影響,從而不能安全地讀取和修改數(shù)據(jù)禀梳。

import threading


class A:
    a = 10


obj = A()


def worker1():
    """線程1"""
    obj.a = 1


t1 = threading.Thread(target=worker1, name='線程1')
t1.start()
t1.join()
print(obj.a)

結(jié)果

1

為什么不使用python thread local呢杜窄?因?yàn)檫@個(gè)有一些缺陷,比如

  • 有些應(yīng)用使用的是greenlet協(xié)程算途,這種情況下無法保證協(xié)程之間數(shù)據(jù)的隔離塞耕,因?yàn)椴煌膮f(xié)程可以在同一個(gè)線程當(dāng)中。
  • 即使使用的是線程嘴瓤,WSGI應(yīng)用也無法保證每個(gè)http請求使用的都是不同的線程扫外,因?yàn)楹笠粋€(gè)http請求可能使用的是之前的http請求的線程莉钙,這樣的話存儲于thread local中的數(shù)據(jù)可能是之前殘留的數(shù)據(jù)。

而Local解決了上面的問題筛谚,實(shí)現(xiàn)了線程之間的數(shù)據(jù)隔離磁玉,從而能夠安全的讀取和修改數(shù)據(jù)。

werkzeug中Local的實(shí)現(xiàn)

class Local(object):
    __slots__ = ("__storage__", "__ident_func__")

    def __init__(self):
        object.__setattr__(self, "__storage__", {})
        object.__setattr__(self, "__ident_func__", get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

local的實(shí)現(xiàn)方式是使用python中的dict結(jié)構(gòu)驾讲,根據(jù)每個(gè)線程的id去創(chuàng)建獨(dú)立的數(shù)據(jù)蚊伞,當(dāng)訪問數(shù)據(jù)時(shí),根據(jù)當(dāng)前線程的id進(jìn)行訪問蝎毡,每個(gè)線程訪問自己的數(shù)據(jù)即可厚柳。這樣就實(shí)現(xiàn)了線程隔離的數(shù)據(jù)訪問。

Local引入了get_ident方法用于獲取當(dāng)前線程的id沐兵,將get_ident方法存儲至類的__ident_func__屬性中,將所有線程的數(shù)據(jù)存儲至__storage__屬性中便监,此屬性對應(yīng)的是一個(gè)二維dict扎谎,每個(gè)線程使用一個(gè)dict存儲數(shù)據(jù),而每個(gè)線程的dict數(shù)據(jù)作為__storage__的線程id對應(yīng)的值烧董。因此線程訪問數(shù)據(jù)事實(shí)上是訪問__storage__[ident][name]毁靶,其中前面的ident為線程的id,后面name才是用戶指定的數(shù)據(jù)key逊移。而ident是Local自動(dòng)獲取的预吆,用戶可以透明進(jìn)行線程隔離的數(shù)據(jù)訪問與存儲。

Local使用

我們可以單獨(dú)使用Local胳泉,實(shí)現(xiàn)線程隔離拐叉。

import threading
from werkzeug.local import Local


obj = Local()
obj.a = 10


def worker1():
    """線程1"""
    obj.a = 1
    print('線程1中的a的值為:{}'.format(obj.a))


t1 = threading.Thread(target=worker1, name='線程1')
t1.start()
t1.join()
print('主線程中的a的值為:{}'.format(obj.a))

結(jié)果

線程1中的a的值為:1
主線程中的a的值為:10

obj是一個(gè)線程隔離的對象,所以線程1的改變沒有影響到主線程扇商。

LocalStack

LocalStack是一個(gè)多線程隔離棧結(jié)構(gòu)凤瘦,通過源碼發(fā)現(xiàn)是基于Local實(shí)現(xiàn)的,提供了棧結(jié)構(gòu)push案铺,pop蔬芥,top方法。

class LocalStack(object):

    def __init__(self):
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()
    
   #  獲取線程對應(yīng)的id控汉,直接復(fù)用Local的__ident_func__笔诵,即使用get_ident獲取線程id
    @property
    def __ident_func__(self):
        return self._local.__ident_func__
    
    @__ident_func__.setter
    def __ident_func__(self, value):
        object.__setattr__(self._local, "__ident_func__", value)

    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError("object unbound")
            return rv

        return LocalProxy(_lookup)
    # 入棧
    def push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self._local, "stack", None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv
  # 出棧
    def pop(self):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        stack = getattr(self._local, "stack", None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()
  # 彈出棧頂元素
    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

LocalStack使用

LockStack也可以單獨(dú)使用,可以理解為就是普通的棧結(jié)構(gòu),只是這個(gè)棧結(jié)構(gòu)是數(shù)據(jù)安全的姑子。

import threading
from werkzeug.local import LocalStack


ls = LocalStack()
ls.push(1)


def worker1():
    """線程1"""
    print('線程1中的棧頂?shù)闹禐椋簕}'.format(ls.top))
    ls.push(2)
    print('線程1中的棧頂?shù)闹禐椋簕}'.format(ls.top))


t1 = threading.Thread(target=worker1, name='線程1')
t1.start()
t1.join()
print('主線程中的棧頂?shù)闹禐椋簕}'.format(ls.top))

結(jié)果

線程1中的棧頂?shù)闹禐椋篘one
線程1中的棧頂?shù)闹禐椋?
主線程中的棧頂?shù)闹禐椋?

LocalProxy

LocalProxy是用來代理Local和LocalStack對象的乎婿。

class LocalProxy(object):

    __slots__ = ("__local", "__dict__", "__name__", "__wrapped__")

    def __init__(self, local, name=None):
        object.__setattr__(self, "_LocalProxy__local", local)
        object.__setattr__(self, "__name__", name)
        if callable(local) and not hasattr(local, "__release_local__"):
            # "local" is a callable that is not an instance of Local or
            # LocalManager: mark it as a wrapped function.
            object.__setattr__(self, "__wrapped__", local)

    def _get_current_object(self):
        """Return the current object.  This is useful if you want the real
        object behind the proxy at a time for performance reasons or because
        you want to pass the object into a different context.
        """
        if not hasattr(self.__local, "__release_local__"):
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError("no object bound to %s" % self.__name__)

可以看到LocalProxy通過是_get_current_object來獲取代理的對象,然后執(zhí)行相應(yīng)的操作即可壁酬。對proxy執(zhí)行的任意操作次酌,都是直接通過被代理對象執(zhí)行的恨课。為了保證代理對象可以直接進(jìn)行操作,LocalProxy重載了所有的基本方法岳服,這樣就可以隨意對proxy對象執(zhí)行操作剂公。
那么為什么需要LocalProxy來代理Local或LocalStack對象呢?

LocalProxy的作用

下面看一個(gè)例子吊宋,直接使用LocalStack纲辽。

from werkzeug.local import LocalStack
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})


def get_user():
    return user_stack.pop()


# 直接調(diào)用函數(shù)獲取user對象
user = get_user()
print(user['name'])
print(user['name'])

結(jié)果

John
John

使用LocalProxy

from werkzeug.local import LocalStack, LocalProxy
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})


def get_user():
    return user_stack.pop()


# 使用 LocalProxy
user = LocalProxy(get_user)
print(user['name'])
print(user['name'])

結(jié)果

John
Bob

結(jié)果顯而易見,直接使用LocalStack對象璃搜,user一旦賦值就無法再動(dòng)態(tài)更新了拖吼,而使用Proxy,每次調(diào)用操作符这吻,都會(huì)重新獲取user吊档,從而實(shí)現(xiàn)了動(dòng)態(tài)更新user的效果。

而在_get_current_object方法中可以看到唾糯,對于可執(zhí)行對象或方法怠硼,就是直接執(zhí)行獲取可執(zhí)行對象或方法對應(yīng)的返回值。而對于Local對象移怯,則是獲取name作為屬性的值香璃。但是要注意的是,所有的獲取都是在執(zhí)行操作的時(shí)候獲取的舟误,這樣就可以隨著程序運(yùn)行動(dòng)態(tài)更新葡秒。同樣也可以解釋了上面的例子中,為方法get_user創(chuàng)建的LocalProxy類型的proxy_user嵌溢,可以兩次執(zhí)行proxy_user[‘name’]獲取到不同值了眯牧,因?yàn)閮纱螆?zhí)行時(shí),都會(huì)通過_get_current_object執(zhí)行g(shù)et_user方法堵腹,兩次執(zhí)行的結(jié)果不同炸站,返回的值也就不同了。

了解了Local疚顷,LocalProxy旱易,LocalStack,接下來看一下上下文腿堤。

什么是上下文

上下文多用于文章中阀坏,代表的一個(gè)整體環(huán)境,比如一篇文章笆檀,我們可以說下文中忌堂,訪問到下文所陳述的內(nèi)容,也可以說上文中酗洒,訪問到上文中的內(nèi)容士修,而我們這篇文章中每一段文字所代表的意思枷遂,都是要根據(jù)我們的上下文來決定的,因?yàn)槟汶S便拿出來一句話不去結(jié)合整體的語境去理解出來的意思肯定不是準(zhǔn)確的棋嘲,所以酒唉,我們這篇文章的上下文就是我們整篇的中心思想。

這是文章中的上下文沸移,那么程序中的上下文是什么痪伦?

程序中的上下文代表了程序當(dāng)下所運(yùn)行的環(huán)境,存儲了一些程序運(yùn)行的信息雹锣。比如在程序中我們所寫的函數(shù)大都不是單獨(dú)完整的网沾,在使用一個(gè)函數(shù)完成自身功能的時(shí)候,很可能需要同其他的部分進(jìn)行交互蕊爵,需要其他外部環(huán)境變量的支持辉哥,上下文就是給外部環(huán)境的變量賦值,使函數(shù)能正確運(yùn)行攒射。

Flask中的上下文

flask中有兩個(gè)上下文证薇,請求上下文和應(yīng)用上下文。

請求上下文

在 flask 中匆篓,可以直接在視圖函數(shù)中使用 request 這個(gè)對象進(jìn)行獲取相關(guān)數(shù)據(jù),而 request 就是請求上下文的對象寇窑,保存了當(dāng)前本次請求的相關(guān)數(shù)據(jù)鸦概。請求上下文對象有:request、session甩骏。

  • request
    封裝了HTTP請求的內(nèi)容窗市,針對的是http請求。舉例:user = request.args.get('user')饮笛,獲取的是get請求的參數(shù)咨察。
  • session
    用來記錄請求會(huì)話中的信息,針對的是用戶信息福青。舉例:session['name'] = user.id摄狱,可以記錄用戶信息。還可以通過session.get('name')獲取用戶信息无午。

應(yīng)用上下文

flask 應(yīng)用程序運(yùn)行過程中媒役,保存的一些配置信息,比如程序名宪迟、數(shù)據(jù)庫連接酣衷、應(yīng)用信息等。應(yīng)用上下文對象有:current_app次泽,g

  • current_app
    應(yīng)用程序上下文,用于存儲應(yīng)用程序中的變量穿仪,可以通過current_app.name打印當(dāng)前app的名稱席爽,也可以在current_app中存儲一些變量,例如:
    應(yīng)用的啟動(dòng)腳本是哪個(gè)文件啊片,啟動(dòng)時(shí)指定了哪些參數(shù)
    加載了哪些配置文件只锻,導(dǎo)入了哪些配置
    連了哪個(gè)數(shù)據(jù)庫
    有哪些public的工具類、常量
    應(yīng)用跑再哪個(gè)機(jī)器上钠龙,IP多少炬藤,內(nèi)存多大
  • g變量
    g 作為 flask 程序全局的一個(gè)臨時(shí)變量,充當(dāng)者中間媒介的作用,我們可以通過它傳遞一些數(shù)據(jù),g 保存的是當(dāng)前請求的全局變量碴里,不同的請求會(huì)有不同的全局變量沈矿。

current_app 的生命周期最長,只要當(dāng)前程序?qū)嵗€在運(yùn)行咬腋,都不會(huì)失效羹膳。
request 和 g 的生命周期為一次請求期間,當(dāng)請求處理完成后根竿,生命周期也就完結(jié)了陵像。

curren_app,g,request,session都是線程隔離的,我們可通過源碼發(fā)現(xiàn)寇壳。

上下文的定義

Flask上下文定義在globals.py上

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app


# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))

這里定義了兩個(gè)LocalStack棧醒颖,_request_ctx_stack是請求上下文棧,_app_ctx_stack是應(yīng)用上下文棧壳炎,curren_app,g,request,session都是LocalStack棧頂元素泞歉。

上下文處理流程

在上一篇中(Flask的工作原理)我們看到wsgi_app中會(huì)創(chuàng)建上下文環(huán)境,調(diào)用 ctx.push() 函數(shù)將上下文信息壓棧匿辩。

 def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                ctx.push()
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

ctx = self.request_context(environ)實(shí)際上是實(shí)例化一個(gè)RequestContext對象腰耙。

class RequestContext(object):

    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = None
        try:
            self.url_adapter = app.create_url_adapter(self.request)
        except HTTPException as e:
            self.request.routing_exception = e
        self.flashes = None
        self.session = session

    def push(self):
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, "exc_clear"):
            sys.exc_clear()

        _request_ctx_stack.push(self)

 
        if self.session is None:
            session_interface = self.app.session_interface
            self.session = session_interface.open_session(self.app, self.request)

            if self.session is None:
                self.session = session_interface.make_null_session(self.app)

        if self.url_adapter is not None:
            self.match_request()

    def pop(self, exc=_sentinel):
       

    def auto_pop(self, exc):


    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.auto_pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)

ctx.push()是創(chuàng)建上下文環(huán)境,把該請求的 ApplicationContext 和 RequestContext 有關(guān)的信息保存到對應(yīng)的棧上铲球。

到了這里Flask上下文基本明確了挺庞,每次有請求過來的時(shí)候,flask 會(huì)先創(chuàng)建當(dāng)前線程或者進(jìn)程需要處理的兩個(gè)重要上下文對象稼病,把它們保存到隔離的棧里面选侨,這樣視圖函數(shù)進(jìn)行處理的時(shí)候就能直接從棧上獲取這些信息。

結(jié)合上篇文章溯饵,F(xiàn)lask整個(gè)流程就很明確了侵俗。


一個(gè)線程同時(shí)只處理一個(gè)請求,那么 _req_ctx_stack和 _app_ctx_stack肯定都是只有一個(gè)棧頂元素的丰刊,為什么還要棧這種數(shù)據(jù)結(jié)構(gòu)隘谣?
每個(gè)請求同時(shí)擁有這兩個(gè)上下文信息,為什么要把 request context 和 application context 分開?

為什么要使用棧這種數(shù)據(jù)結(jié)構(gòu)

因?yàn)镕lask可以有多個(gè)應(yīng)用寻歧,也就是在一個(gè) Python 進(jìn)程中掌栅,可以擁有多個(gè)應(yīng)用,如果是多個(gè) app码泛,那么棧頂存放的是當(dāng)前活躍的 request猾封,也就是說使用棧是為了獲取當(dāng)前的活躍 request 對象。

為什么要拆分請求上下文和應(yīng)用上下文

為了靈活度噪珊,為了可以讓我們單獨(dú)創(chuàng)建兩個(gè)上下文晌缘,以便應(yīng)付不同的場景。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痢站,一起剝皮案震驚了整個(gè)濱河市帽芽,隨后出現(xiàn)的幾起案子冻押,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艾少,死亡現(xiàn)場離奇詭異啤覆,居然都是意外死亡帐要,警方通過查閱死者的電腦和手機(jī)怠李,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來朱庆,“玉大人盛泡,你說我怎么就攤上這事∮榧眨” “怎么了饭于?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長维蒙。 經(jīng)常有香客問我,道長果覆,這世上最難降的妖魔是什么颅痊? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮局待,結(jié)果婚禮上斑响,老公的妹妹穿的比我還像新娘。我一直安慰自己钳榨,他們只是感情好舰罚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著薛耻,像睡著了一般营罢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天饲漾,我揣著相機(jī)與錄音蝙搔,去河邊找鬼。 笑死考传,一個(gè)胖子當(dāng)著我的面吹牛吃型,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播僚楞,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼勤晚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泉褐?” 一聲冷哼從身側(cè)響起赐写,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兴枯,沒想到半個(gè)月后血淌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡财剖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年悠夯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躺坟。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沦补,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咪橙,到底是詐尸還是另有隱情夕膀,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布美侦,位于F島的核電站产舞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏菠剩。R本人自食惡果不足惜易猫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望具壮。 院中可真熱鬧准颓,春花似錦、人聲如沸棺妓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怜跑。三九已至样勃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背彤灶。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工看幼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人幌陕。 一個(gè)月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓诵姜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親搏熄。 傳聞我的和親對象是個(gè)殘疾皇子棚唆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350