Tomcat架構(gòu)入門-請求轉(zhuǎn)換

作為Java開發(fā)人員來說Tomcat應(yīng)該在熟悉不過了,自己最近空閑時(shí)間比較多蚯撩,前天突然很好奇到底用戶的請求是怎么被轉(zhuǎn)化成HttpServletRequest,所以自己想著看看源碼,但是因?yàn)榭紤]到一直以來自己對 Tomcat的架構(gòu)并不熟悉,所以我決定在看源碼前好好的熟悉下Tomcat的架構(gòu)挪哄,看看Tomcat都有哪些組件以及它們的作用分別是什么,這個(gè)過程我主要是參考官方文檔琉闪,另外就是Tomcat的源碼迹炼。

一、Tomcat的組成

關(guān)于Tomcat的架構(gòu)其實(shí)可以通過Tomcat下的server.xml做一個(gè)基本了解,在面試過程中有時(shí)候也會問到server.xml一些內(nèi)容斯入,包括Tomcat的優(yōu)化其實(shí)很多都是在這里完成的砂碉,所以需要引起重視。

<!--?Prevent?memory?leaks?due?to?use?of?particular?java/javax?APIs-->

type="org.apache.catalina.UserDatabase"

description="User?database?that?can?be?updated?and?saved"

factory="org.apache.catalina.users.MemoryUserDatabaseFactory"

pathname="conf/tomcat-users.xml"/>

connectionTimeout="20000"

redirectPort="8443"/>

resourceName="UserDatabase"/>

unpackWARs="true"autoDeploy="true">

prefix="localhost_access_log"suffix=".txt"

pattern="%h?%l?%u?%t?&quot;%r&quot;?%s?%b"/>

然后通過網(wǎng)上的一張架構(gòu)圖來了解各個(gè)層級之間的關(guān)系

tomcat整體架構(gòu).jpg

二刻两、核心組件

結(jié)合這server.xml和上面這張圖增蹭,來簡單的了解一下各個(gè)組件。

Server是Tomcat的頂層組件闹伪。在server.xml中Server元素表示整個(gè)Catalina Servlet容器沪铭。因此壮池,它必須是server.xml配置文件中的單個(gè)最外層元素偏瓤,簡單一點(diǎn)可以將Server看做是Tomcat服務(wù)器本身,Server管理著整個(gè)Tomcat生命周期椰憋。一個(gè)Server內(nèi)可以包了多個(gè)listener厅克,可以包含一個(gè)或多個(gè)Service。

Service是Tomcat的另一個(gè)頂層組件橙依,Service元素由一個(gè)或多個(gè)Connector組件的組合证舟,這些Connector共享一個(gè)Engine組件以處理傳入的請求。Service的功能就是對外提供服務(wù)窗骑。

接下來是Tomcat的幾個(gè)比較重要的組件:

Connector顧名思義就是連接器女责,它連接的是用戶請求和容器,即用戶的請求到達(dá)Connector后创译,由它將用戶請求進(jìn)行包裝抵知,然后傳遞給具體的容器Engine進(jìn)行處理,之后再通過Connector將結(jié)果進(jìn)行包裝轉(zhuǎn)換傳遞給用戶软族。Connector功能就是處理用戶的請求和響應(yīng)刷喜。Service中的Engine對外是不可見的,所有與Engine的交互必須先經(jīng)過Connector處理立砸。目前Tomcat有三種Connector掖疮,它們主要區(qū)別在于支持的協(xié)議不同。最常見的就是支持http/1.1颗祝,另外還有支持http/2以及ajp(Apache Jserv Protocol浊闪,一種二進(jìn)制協(xié)議)。關(guān)于這個(gè)三種連接器上相關(guān)的屬性建議看下官方文檔螺戳,因?yàn)閮?nèi)容還是比較多的搁宾。在對Tomcat進(jìn)行優(yōu)化事很多時(shí)候都會需要修改Connector上的相關(guān)參數(shù),比如最大連接數(shù)温峭、最大線程數(shù)量等等猛铅。所以這部分內(nèi)容是非常重要的,也是核心凤藏。

Engine代表的處理請求的入口奸忽。它接受并處理來自Connector的所有請求堕伪,并將完成的響應(yīng)返回給連接器,最終傳輸回客戶端栗菜。

Host是Engine的子容器欠雌,一個(gè)Engine內(nèi)可以有一個(gè)或多個(gè)Host,它表示的一個(gè)虛擬主機(jī)疙筹,

Context表示在特定虛擬主機(jī)中運(yùn)行的Web應(yīng)用程序富俄,每一個(gè)Web應(yīng)用程序都基于WAR文件或包含相應(yīng)解壓縮內(nèi)容的相應(yīng)目錄。每一個(gè)虛擬主機(jī)中可以包含多個(gè)Context而咆。

在Service中還有一個(gè)很重要的組件就是Executor霍比,Executor表示是Tomcat中的組件之間共享的線程池。?Tomcat已經(jīng)默認(rèn)為每個(gè)連接器都創(chuàng)建了一個(gè)線程池暴备。自定義線程池可以供Connector共享悠瞬,也可以和其他組件共享該線程池。

除了上面的核心組件之外涯捻,還有其他一些嵌套組件浅妆,比如server.xml中定義的Listener、Realm障癌、GlobalNamingResources等凌外。這里就不細(xì)述了。

下圖是一個(gè)用戶請求和響應(yīng)的流程示意圖涛浙,實(shí)際情況當(dāng)然要復(fù)雜許多康辑。

請求流程.jpg

三、請求轉(zhuǎn)換

下面主要看看Connector是如何將用戶請求一步一步進(jìn)行轉(zhuǎn)換的蝗拿,我們先看下對應(yīng)類的繼承體系

Connector.jpg

Connector封裝了兩個(gè)主要的成員變量晾捏,一個(gè)是ProtocolHandler,一個(gè)是Adapter哀托,前者根據(jù)不同的協(xié)議有不同的實(shí)現(xiàn)類型惦辛,我們以Http11NioProtocol為例,其類的繼承體系如下

Http11NioProtocol.jpg

ProtocolHandler主要是處理網(wǎng)絡(luò)連接,將字節(jié)流封裝成 Request對象,再將Request 適配成 Servlet 處理ServletRequest?對象這幾個(gè)動作钓账,用組件封裝起來了议慰,ProtocolHandler包括了三個(gè)組件:Endpoint这橙、Processor、Adapter。

而Endpoint的創(chuàng)建則在其默認(rèn)構(gòu)造函數(shù)實(shí)現(xiàn),以Http11NioProtocol為例剿另,其代碼如下:

publicHttp11NioProtocol(){

super(newNioEndpoint());

}

Endpoint主要用來處理底層的Socket網(wǎng)絡(luò)連接,在Endpoint具體實(shí)現(xiàn)類中里面有個(gè)SocketProcessor的內(nèi)部類,它負(fù)責(zé)將Endpoint接收到的Socket請求轉(zhuǎn)化成org.apache.coyote.Request請求雨女。

代碼如下:

protectedclassSocketProcessorextendsSocketProcessorBase{

publicSocketProcessor(SocketWrapperBase<NioChannel>?socketWrapper,?SocketEvent?event){

super(socketWrapper,?event);

}

@Override

protectedvoiddoRun(){

NioChannel?socket?=?socketWrapper.getSocket();

SelectionKey?key?=?socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector());

Poller?poller?=?NioEndpoint.this.poller;

if(poller?==null)?{

socketWrapper.close();

return;

}

try{

inthandshake?=?-1;

try{

if(key?!=null)?{

if(socket.isHandshakeComplete())?{

handshake?=0;

}elseif(event?==?SocketEvent.STOP?||?event?==?SocketEvent.DISCONNECT?||

event?==?SocketEvent.ERROR)?{

handshake?=?-1;

}else{

handshake?=?socket.handshake(key.isReadable(),?key.isWritable());

event?=?SocketEvent.OPEN_READ;

}

}

}catch(IOException?x)?{

handshake?=?-1;

if(log.isDebugEnabled())?log.debug("Error?during?SSL?handshake",x);

}catch(CancelledKeyException?ckx)?{

handshake?=?-1;

}

if(handshake?==0)?{

SocketState?state?=?SocketState.OPEN;

//?Process?the?request?from?this?socket

if(event?==null)?{

state?=?getHandler().process(socketWrapper,?SocketEvent.OPEN_READ);

}else{

state?=?getHandler().process(socketWrapper,?event);

}

if(state?==?SocketState.CLOSED)?{

poller.cancelledKey(key,?socketWrapper);

}

}elseif(handshake?==?-1)?{

poller.cancelledKey(key,?socketWrapper);

}elseif(handshake?==?SelectionKey.OP_READ){

socketWrapper.registerReadInterest();

}elseif(handshake?==?SelectionKey.OP_WRITE){

socketWrapper.registerWriteInterest();

}

}catch(CancelledKeyException?cx)?{

poller.cancelledKey(key,?socketWrapper);

}catch(VirtualMachineError?vme)?{

ExceptionUtils.handleThrowable(vme);

}catch(Throwable?t)?{

log.error(sm.getString("endpoint.processing.fail"),?t);

poller.cancelledKey(key,?socketWrapper);

}finally{

socketWrapper?=null;

event?=null;

//return?to?cache

if(running?&&?!paused?&&?processorCache?!=null)?{

processorCache.push(this);

}

}

}

}

代碼中g(shù)etHandler().process()方法會創(chuàng)建一個(gè)Processor對象谚攒,因?yàn)榇a較多,我只粘貼一部分:

publicSocketStateprocess(SocketWrapperBase<S>?wrapper,?SocketEvent?status){

if(wrapper?==null)?{

returnSocketState.CLOSED;

}

S?socket?=?wrapper.getSocket();

Processor?processor?=?connections.get(socket);

....

if(processor?==null)?{

String?negotiatedProtocol?=?wrapper.getNegotiatedProtocol();

if(negotiatedProtocol?!=null&&?negotiatedProtocol.length()?>0)?{

UpgradeProtocol?upgradeProtocol?=?getProtocol().getNegotiatedProtocol(negotiatedProtocol);

if(upgradeProtocol?!=null)?{

processor?=?upgradeProtocol.getProcessor(wrapper,?getProtocol().getAdapter());

}elseif(negotiatedProtocol.equals("http/1.1"))?{

}else{

if(getLog().isDebugEnabled())?{

getLog().debug(sm.getString("abstractConnectionHandler.negotiatedProcessor.fail",negotiatedProtocol));

}

returnSocketState.CLOSED;

}

}

}

if(processor?==null)?{

processor?=?recycledProcessors.pop();

if(getLog().isDebugEnabled())?{

getLog().debug(sm.getString("abstractConnectionHandler.processorPop",processor));

}

}

if(processor?==null)?{

processor?=?getProtocol().createProcessor();

register(processor);

}

//?省略代碼

......

}

上面的代碼在AbstractProtocol的內(nèi)部類ConnectionHandler中氛堕,其中socket變量其實(shí)是一個(gè)NioChannel實(shí)例馏臭。這個(gè)方法代碼中會根據(jù)不同情形創(chuàng)建相關(guān)的Processor。比如我這里會獲取相應(yīng)的AbstractHttp11Protocol然后調(diào)用其createProcessor()方法創(chuàng)建Http11Processor對象讼稚。

@Override

protectedProcessorcreateProcessor(){

Http11Processor?processor?=newHttp11Processor(this,?adapter);

returnprocessor;

}

而adapter其實(shí)在Connector初始化的時(shí)候就創(chuàng)建完成了括儒,并將其添加到protocolHandler中,下面是Connector的initInternal()方法:

@Override

protectedvoidinitInternal()throwsLifecycleException{

super.initInternal();

.......

//?Initialize?adapter

adapter?=newCoyoteAdapter(this);

protocolHandler.setAdapter(adapter);

if(service?!=null)?{

protocolHandler.setUtilityExecutor(service.getServer().getUtilityExecutor());

}

......

另外需要關(guān)注的一點(diǎn)是創(chuàng)建Http11Processor的時(shí)候锐想,會調(diào)用其父類的構(gòu)造函數(shù):

publicHttp11Processor(AbstractHttp11Protocol<?>?protocol,?Adapter?adapter){

super(adapter);

this.protocol?=?protocol;

httpParser?=newHttpParser(protocol.getRelaxedPathChars(),

protocol.getRelaxedQueryChars());

inputBuffer?=newHttp11InputBuffer(request,?protocol.getMaxHttpHeaderSize(),

protocol.getRejectIllegalHeaderName(),?httpParser);

request.setInputBuffer(inputBuffer);

outputBuffer?=newHttp11OutputBuffer(response,?protocol.getMaxHttpHeaderSize());

response.setOutputBuffer(outputBuffer);

//?省略代碼

.....

}

而調(diào)用父類構(gòu)造函數(shù)的時(shí)候會創(chuàng)建兩個(gè)對象帮寻,Request和Response,對應(yīng)的具體類是org.apache.coyote.Request和org.apache.coyote.Response痛倚。這兩個(gè)對象下面還會使用到规婆,不要混淆了。

回歸到正題蝉稳,Processor創(chuàng)建完成后會調(diào)用它的process方法(AbstractProcessorLight.process)其方法內(nèi)部會調(diào)用具體的service方法,這里調(diào)用的是Http11Processor.service方法掘鄙,這個(gè)方法內(nèi)內(nèi)容很多耘戚,我也并沒有仔細(xì)的看,應(yīng)該是對org.apache.coyote.Request和org.apache.coyote.Response進(jìn)行了參數(shù)設(shè)置操漠,核心的地方在于調(diào)用adapter的service方法收津,代碼如下:

if(getErrorState().isIoAllowed())?{

try{

rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);

//?請求轉(zhuǎn)換request和response

getAdapter().service(request,?response);

if(keepAlive?&&?!getErrorState().isError()?&&?!isAsync()?&&

statusDropsConnection(response.getStatus()))?{

setErrorState(ErrorState.CLOSE_CLEAN,?null);

}

}catch(InterruptedIOException?e)?{

setErrorState(ErrorState.CLOSE_CONNECTION_NOW,?e);

}catch(HeadersTooLargeException?e)?{

log.error(sm.getString("http11processor.request.process"),?e);

if(response.isCommitted())?{

setErrorState(ErrorState.CLOSE_NOW,?e);

}else{

response.reset();

response.setStatus(500);

setErrorState(ErrorState.CLOSE_CLEAN,?e);

response.setHeader("Connection","close");//TODO:Remove

}

}catch(Throwable?t)?{

ExceptionUtils.handleThrowable(t);

log.error(sm.getString("http11processor.request.process"),?t);

response.setStatus(500);

setErrorState(ErrorState.CLOSE_CLEAN,?t);

getAdapter().log(request,?response,0);

}

}

這里會調(diào)用CoyoteAdapter的service方法,代碼如下:

@Override

publicvoidservice(org.apache.coyote.Request?req,?org.apache.coyote.Response?res)?throws?Exception

{

Request?request?=?(Request)?req.getNote(ADAPTER_NOTES);

Response?response?=?(Response)?res.getNote(ADAPTER_NOTES);

if(request?==null)?{

request?=?connector.createRequest();

request.setCoyoteRequest(req);

response?=?connector.createResponse();

response.setCoyoteResponse(res);

request.setResponse(response);

response.setRequest(request);

req.setNote(ADAPTER_NOTES,?request);

res.setNote(ADAPTER_NOTES,?response);

req.getParameters().setQueryStringCharset(connector.getURICharset());

}

if(connector.getXpoweredBy())?{

response.addHeader("X-Powered-By",?POWERED_BY);

}

booleanasync=false;

boolean?postParseSuccess?=false;

req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());

try{

postParseSuccess?=?postParseRequest(req,?request,?res,?response);

if(postParseSuccess)?{

request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());

connector.getService().getContainer().getPipeline().getFirst().invoke(

request,?response);

}

if(request.isAsync())?{

async=true;

ReadListener?readListener?=?req.getReadListener();

if(readListener?!=null&&?request.isFinished())?{

ClassLoader?oldCL?=null;

try{

oldCL?=?request.getContext().bind(false,null);

if(req.sendAllDataReadEvent())?{

req.getReadListener().onAllDataRead();

}

}finally{

request.getContext().unbind(false,?oldCL);

}

}

Throwable?throwable?=(Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

if(!request.isAsyncCompleting()?&&?throwable?!=null)?{

request.getAsyncContextInternal().setErrorState(throwable,true);

}

}else{

request.finishRequest();

response.finishResponse();

}

}catch(IOException?e)?{

//?Ignore

}finally{

AtomicBoolean?error?=newAtomicBoolean(false);

res.action(ActionCode.IS_ERROR,?error);

if(request.isAsyncCompleting()?&&?error.get())?{

res.action(ActionCode.ASYNC_POST_PROCESS,null);

async=false;

}

//?Access?log

if(!async&&?postParseSuccess)?{

Context?context?=?request.getContext();

Host?host?=?request.getHost();

longtime?=?System.currentTimeMillis()?-?req.getStartTime();

if(context?!=null)?{

context.logAccess(request,?response,?time,false);

}elseif(response.isError())?{

if(host?!=null)?{

host.logAccess(request,?response,?time,false);

}else{

connector.getService().getContainer().logAccess(request,?response,?time,false);

}

}

}

req.getRequestProcessor().setWorkerThreadName(null);

if(!async)?{

updateWrapperErrorCount(request,?response);

request.recycle();

response.recycle();

}

}

}

這個(gè)方法的入?yún)閛rg.apache.coyote.Request和org.apache.coyote.Response兩個(gè)變量浊伙,方法內(nèi)會根據(jù)入?yún)?chuàng)建出org.apache.catalina.connector.Request和org.apache.catalina.connector.Response撞秋,而這兩個(gè)對象分別繼承了HttpServletRequest和HttpServletResponse,也就是說實(shí)際上用戶請求在這里完成了轉(zhuǎn)換嚣鄙,變成了我們非常熟悉的HttpServletRequest和HttpServletResponse吻贿。

然后執(zhí)行下面這段代碼:

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);?

即通過Connector一步步將request和response丟給容器進(jìn)行處理,而且從方法名稱也可以看的出來哑子,實(shí)際上是由Pipeline進(jìn)行處理的舅列,這個(gè)就暫不深究了。也就是說到這里我們的HttpServletRequest和HttpServletResponse會被容器進(jìn)行處理了卧蜓,在Connector的流程執(zhí)行完成帐要,之后就是有容器進(jìn)行處理過程了,這里就不再繼續(xù)往下看了弥奸。

整個(gè)Connector執(zhí)行流程如下圖:

請求轉(zhuǎn)換流程圖.png

通過以上代碼和流程圖基本上就搞清楚了榨惠,用戶請求是如何變成我們熟悉的HttpServletRequest和HttpServletResponse的,當(dāng)然實(shí)際過程要復(fù)雜很多,我只是簡單的通過跟蹤代碼了解了大概的過程赠橙,具體代碼內(nèi)容并沒有詳細(xì)的去看伸蚯,后期如果有需要的話自己會挑選一部分進(jìn)行閱讀,比如今天的Connector這部分简烤。

本次的學(xué)習(xí)收獲主要有兩點(diǎn)一是Tomcat的架構(gòu)剂邮,即其組成及各組件的作用,當(dāng)然這部分沒有去深入學(xué)習(xí)横侦,二是請求轉(zhuǎn)換挥萌,即用戶請求過來之后是如何轉(zhuǎn)換成HttpServletRequest和HttpServletResponse的,這部分主要理清了大概的執(zhí)行流程枉侧,并簡單的跟蹤了代碼引瀑,如果后期有需要會在來具體的分析這部分代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末榨馁,一起剝皮案震驚了整個(gè)濱河市憨栽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翼虫,老刑警劉巖屑柔,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異珍剑,居然都是意外死亡掸宛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門招拙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唧瘾,“玉大人,你說我怎么就攤上這事别凤∈涡颍” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵规哪,是天一觀的道長求豫。 經(jīng)常有香客問我,道長由缆,這世上最難降的妖魔是什么注祖? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮均唉,結(jié)果婚禮上是晨,老公的妹妹穿的比我還像新娘。我一直安慰自己舔箭,他們只是感情好罩缴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布蚊逢。 她就那樣靜靜地躺著,像睡著了一般箫章。 火紅的嫁衣襯著肌膚如雪烙荷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天檬寂,我揣著相機(jī)與錄音终抽,去河邊找鬼。 笑死桶至,一個(gè)胖子當(dāng)著我的面吹牛昼伴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播镣屹,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼圃郊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了女蜈?” 一聲冷哼從身側(cè)響起持舆,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伪窖,沒想到半個(gè)月后逸寓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惰许,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年席覆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汹买。...
    茶點(diǎn)故事閱讀 39,932評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖聊倔,靈堂內(nèi)的尸體忽然破棺而出晦毙,到底是詐尸還是另有隱情,我是刑警寧澤耙蔑,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布见妒,位于F島的核電站,受9級特大地震影響甸陌,放射性物質(zhì)發(fā)生泄漏须揣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一钱豁、第九天 我趴在偏房一處隱蔽的房頂上張望耻卡。 院中可真熱鬧,春花似錦牲尺、人聲如沸卵酪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溃卡。三九已至溢豆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘸羡,已是汗流浹背漩仙。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留犹赖,地道東北人队他。 一個(gè)月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像冷尉,于是被迫代替她去往敵國和親漱挎。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評論 2 354

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