tomcat组件及原理详细分析

查看端口号连接数

netstat -nat  |  grep 8080   //查看8080端口的连接数

查看java进程id

ps -ef | grep java

查看进程下有多少线程

ps   -o nlwp pid   //nlwp=number of light weight process

获取真正在running的线程数量

ps -eLo pid,stat | grep 27989 | grep running | wc -l //ps -eLo pid,stat是一个命令,可以找出所有线程

tomcat组件分析


tomcat组件

层次关系:

<server> 顶级组件,位于整个配置的最外层
            <service> 容器类组件
                <connect/> 
                <engine>
                    <host>
                        <realm/>
                        <valve/>
                        <context>
                        </context>
                    </host> 
                    <host/>
                </engine>
            </service>
        </server>

server组件:(位于整个配置的最外层)
让Tomcat启动一个server实例,并监听8005端口以接受shutdown命令(默认:SHUTDOWN字符)
如果在同一个物理机上启动多个server实例,则需要配置不同的端口号
service组件:容器类组件
一个server下可以有多个service
将connector关联至Engine,一个service下只有一个Engine却可以有多个Connector,每个Connector通过特定的端口和协议接受请求,并将请求转发到关联的引擎进行处理。
Connector组件:
参数和实际配置

进入tomcat的请求可以根据tomcat的工作方式分为两种
1.tomcat作用应用程序服务器,请求来自前端的web服务器,这可能是apache,IIS,Nginx等
2.tomcat作为独立服务器,请求来自web浏览器
tomcat应该考虑工作情形并未相应情形下的请求分别定义好需要的连接器才能正确接收来自客户端的请求,一个engine可以有一个或者多个connector,以适应多种请求方式,  
定义连接器时可以配置的属性非常多,常见如下:
1.address:指链接器监听的地址,默认所有地址,即0.0.0.0
2.maxThread:支持最大的连接并发数,默认200
3.port:监听的端口,默认为0
4.protocol:连接器使用的协议,默认HTTP/1.1 ,定义AJP协议时通常为AJP/1.3
5.redirectPort:如果某连接器支持的协议是HTTP,如果接受到https时,则转发至此属性定义的端口
6.connectTimeout: 等待客户端发送请求的超时时间,单位ms,默认1分钟
7.enableLookups: 默认为true,Web应用程序可以通过Web容器提供的request.getRemoteHost()方法获得访问Web应用客户的IP地址和名称,但是这样会消耗Web容器的资源,并且还需要通过IP地址和DNS服务器反查用户的名字。因此当系统上线时,可以将这个属性关闭,从而减少资源消耗,那么Web应用也就只能记录下IP地址。修改的属性是enableLoopups="false"
8.acceptCount:设置等待队列的最大长度,通常在tomcat所有等待线程均处于繁忙状态时,新发来的请求将被放置在等待队列中
下面定义了一个多属性的SSL连接器:
debug="0" 不启动调试模式 ;
<Connector port="8443" maxThreads="150" minSpareThreads="25" maxSpareThread="75" enableLookups="false"
    acceptCount="100" debug="0" scheme="HTTPs" secure="true" clientAuth="false" sslProtocol="TLS"/>

Engine组件:
Engine是servlet处理器的一个实例,即servlet引擎,默认定义在server.xml中的catalina . engine需要defaultHost属性来为其定义一个接受所有未明确定义虚拟主机的请求的host组件,定义如下:
<Engine defaultHost="localhost">
defaultHost:
tomcat支持基于FQDN的虚拟主机,这些虚拟主机可以通过在Engine容器中定义不同的host组件来实现;但是如果引擎收到一个发往非明确定义的虚拟主机请求时则需要将此请求发往一个默认的虚拟主机进行处理。因此,在Engine中定义的多个虚拟主机host中至少要有一个跟defaultHost定义的主机名同名
Engine中可以包含Realm、Host、Listener和valve子容器

简单的Engine配置样例:
<Engine defaultHost="localhost">
    <Host name="localhost" appBase="webapps" unpackWARS="true" autoDeploy="true" xmlValidation="false"  xmlNamespaceAware="false">
        <context path="" docBase="/ROOT" reloadable="true" crossContext="true"/>
    </Host>
</Engine>   

Host组件:

位于Engine容器中,用于接受请求并进行相应处理的主机或虚拟主机,定义如下:
appBase:相对路径,相对catalina home路径
<Host name="localhost" appBase="webapps" unpackWARS="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">

context组件
context在某些意义上类似apache中的路径别名,一个context别名用于标识tomcat实例中的一个web应用程序,如下面的定义:
<context path="" docBase="/web/webapps"/>
<Context path="/bbs" docBase="/web/threads/bbs" reloadable="true"></Context>
<Context path="/chat" docBase="/web/chat"/>
<Context path="/darian" docBase="darian"></Context>
在Tomcat中,每一个context定义也可以使用一个单独的XML文件进行,其文件的目录为$CATALINA_HOME/conf/。可以用于Context中的XML元素有Loader,Manager,Realm,Resources和WatchedResource。
常用属性定义:
1)docBase:相应的web程序存放的位置,也可以使用相对路径,其实路径为此context所属Host中appBase定义的路径;切记,docBase的路径名不能与相应host中appBase中定义的路径名有包含关系。比如,appBase为deploy,而docBase绝对不能为deploy-bbs类的名字
2)path:相对此web服务器根路径而言的URI,如果为"",则表示为此webapp的根路径
3)reloadable:默认为false,是否容许重新加载此context相关的web应用程序的类
问题点:在tomcat加载变化代码的时候有可能会出现内存溢出,tomcat服务不正常等异常
path和docBase容易混淆:
path是指url访问的时候设置的路由,docBase是指对应的url能访问的资源存放的位置

mac上操作实例:

<Host name="localhost"  appBase="www/webapps"
            unpackWARs="true" autoDeploy="true">
        <Context path="test" docBase="ROOT" reloadable="true"/> <!-- 新增-->
        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
说明:
访问的url:http://localhost:8080/test/ 
或者http://localhost:8080 或者 http://localhost:8080/test/index.jsp
index.jsp存放的位置:/Users/penny/download/tomcat/www/webapps/ROOT/index.jsp
docBase:ROOT,相对于所在Host的appBase路径(www/webapps)
访问url:localhost:8080/test====localhost:8080+path(Context中的path值)

Cluster
专用于配置Tomcat集群的元素,可用于Engine和Host容器中。在用于Engine容器中时,Engine中的所有Host均支持集群功能。在Cluster元素中,需要直接定义一个Manager元素,这个Manager元素有一个其值为org.apache.catalina.ha.session.DeltaManager或org.apache.catalina.ha.session.BackupManager的className属性。同时,Cluster中还需要分别定义一个Channel和ClusterListener元素。

Channel 用于Cluster中给集群中同一组中的节点定义通信“信道”。Channel中需要至少定义Membership、Receiver和Sender三个元素,此外还有一个可选元素Interceptor。

Membership 用于Channel中配置同一通信信道上节点集群组中的成员情况,即监控加入当前集群组中的节点并在各节点间传递心跳信息,而且可以在接收不到某成员的心跳信息时将其从集群节点中移除。Tomcat中Membership的实现是org.apache.catalina.tribes.membership.McastService。

Sender 用于Channel中配置“复制信息”的发送器,实现发送需要同步给其它节点的数据至集群中的其它节点。发送器不需要属性的定义,但可以在其内部定义一个Transport元素。

Transport 用于Sender内部,配置数据如何发送至集群中的其它节点。Tomcat有两种Transport的实现: 1) PooledMultiSender基于Java阻塞式IO,可以将一次将多个信息并发发送至其它节点,但一次只能传送给一个节点。 2)PooledParallelSener 基于Java非阻塞式IO,即NIO,可以一次发送多个信息至一个或多个节点。

Receiver 用于Channel定义某节点如何从其它节点的Sender接收复制数据,Tomcat中实现的接收方式有两种BioReceiver和NioReceiver。

Tomcat配置文件:
server.xml
context.xml:为部署此tomcat实例上所有的web应用程序提供的默认配置文件,每个webapp都可以使用独有的context.xml,通常放置于META-INFO子目录中;常用于定义会话管理器、realm以及jdbc等

web.xml
为部署至此tomcat上的所有实例上所有的web应用程序提供默认部署描述符:通常用于为webapp提供基本的servlet定义和MIME映射表等
通常有2个存放位置:

$CATALINA_BASE/conf/web.xml和每个Web应用程序(/WEB-INFO/web.xml)

Tomcat在部署一个应用程序时(包括重启和重新载入),会首先读取$CATALINA_BASE/conf/web.xml,然后再读取WEB-INFO/web.xml

catalina.policy:当基于-security选项启动tomcat实例时会读取此配置文件
catalina.properties:java属性定义文件,设定类加载器路径、安全包列表、调整性能的参数信息
logging.properties:定义日志相关的配置信息,例如:日志级别、文件路径
webapps体系结构:
webapp有特定组织格式,是一种层次型目录结构;通常包含servlet代码;jsp页面文件、类文件、部署描述符文件等等,一般会打包成格式文档
/:web应用程序的根目录,相对于应用程序而言,即 ROOT目录
/WEB-INF:此webapp的私有资源目录,通常web.xml和context.xml文件均放置于此处
/WEB-INF/classes:此webapp自有的类
/WEB-INF/lib:此webapp自有的能够被打包成jar格式的类
/META-INF:不同应用程序不同

webapp的归档格式:
EJB类的归档的扩展名为.jar
web应用程序归档扩展名为.war
资源适配器类的文件扩展名为.rar
企业级应用程序的扩展名为.ear
web服务的扩展名为.ear或.war
Tomcat的http连接器:
类型有3种:
1.基于java的http/1.1连接器
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
2.基于java的高性能NIO http/1.1连接器
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true">
3.基于c/c++研发的native apr http/1.1连接器 <connector port="8080"
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol" maxThreads="150" SSLEnabled="true" >
从操作系统级别来解决异步的IO问题,大幅度的提高性能.。必须要安装apr和native,直接启动就支持apr

两种代理方式
································································································
LAMT方式:利用apache进行反向代理
AJP是为Tomcat与HTTP服务器之间通信而定制的协议,能提供较高的通信速度和效率。如果tomcat前端放的是apache的时候,会使用到AJP这个连接器
apache(mod_jk,ajp)+tomcat(ajp connector)
apache(mod_proxy,(http,https,ajp))+tomcat(http,https,ajp)

LNMT:利用nginx进行反向代理

nginx安装路径:cd /usr/local/etc/nginx/
一、安装

执行如下命令
brew search nginx
brew install nginx
安装完以后,可以在终端输出的信息里看到一些配置路径:
/usr/local/etc/nginx/nginx.conf (配置文件路径)
/usr/local/var/www (服务器默认路径)
/usr/local/Cellar/nginx/1.8.0 (安装路径)

启动:
目录:/usr/local/Cellar/nginx/1.15.2/bin 
./nginx -c /usr/local/etc/nginx/nginx.conf

停止:
在终端中输入以下几种命令都可以停止
kill -QUIT  15800 (从容的停止,即不会立刻停止)
Kill -TERM  15800 (立刻停止)
Kill -INT  15800  (和上面一样,也是立刻停止)

重启:
在启动nginx之前,需要先验证在配置文件的正确性
1.  /usr/local/Cellar/nginx/1.8.0/bin/nginx -t -c /usr/local/etc/nginx/nginx.conf
2.  ./nginx -s reload

ln -s之后的命令
nginx -t -c /usr/local/ngConfig/nginx.conf 检查配置
nginx -s reload 重启


nginx的意义:
在做反向代理时,静态内容由前端去获取,动态内容由后端去获取

#静态的文件在/web/htdocs目录下获取
        location / {
            root   /web/htdocs;
            index  index.html index.htm;
            #proxy_pass http://127.0.0.1:8080;
        }
#jsp、do结尾的请求,代理到127.0.0.1:8080端口
        location ~* /.(jsp|do)$ {
            proxy_pass http://127.0.0.1:8080;
        }

#jpg|jpeg|exe|gif|rar等文件可以配置到另外一台服务器上取      
        location ~* /.(jpg|jpeg|exe|gif|rar)$ {
            proxy_pass http://127.0.0.1:8182;
        }


nginx+tomcat(http,https)
nginx+tomcat...(多个tomcat)

nginx代理多个tomcat示例如下:

    http{
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
          upstream tomcat {
                server 127.0.0.1:8080;
          }
          server {
                 listen       88;
                server_name  localhost;
                location ~* \.(jsp|do)$ {
                        proxy_pass http://tomcat;
                }
  }
}

································································································
tomcat会话管理:
manager:
manager对象用于实现http会话管理的功能,tomcat6种有5种会话管理的manager的实现:
1.standardManager:
tomcat6的默认会话管理,用于非集群环境中对单个处于运行状态的tomcat实例进行管理,当tomcat关闭时,这些会话相关的数据会被写入到磁盘上一个名叫SESSION.ser的文件,并在Tomcat下次启动时读取此文件
2.persisentManager:
当一个会话长时间处于空闲状态时会被写入到swap会话对象,这对于内存资源比较吃进的应用比较有用
3.DeltaManager:
用于tomcat集群的管理,它通过将改变了会话数据同步给其他节点实现会话复制,这种实现会讲所有会话的改变同步给集群中每一个节点,也是在集群中使用最多的一种实现
4.BackupManager:
将某节点会话的改变同步给集群中的另一个节点,而非全部节点
5.SimpleTcpReplicationManager:
过于老旧

标准会话管理器(standard manager):
<Manager className="org.apache....StandardManager" maxInactiveInterval="7200"/>
默认保存在$CATALINA_HOME/work/Catalina/<hostname>/<webapp-name>/下的SESSIONS.ser文件

持久会话管理器(Persisent Manager):
将会话保存至持久存储中并且能在服务器意外终止后重新启动时加载这些会话信息。持久会话管理器容许将会话保存至文件存储(FIleStore)
或JDBC中
保存至文件中的实例:
<Manager className="org.apache....PersisentManager" maxInactiveInterval="7200" saveOnRestart="true">
<Store ClassName="org.apache...FIleStore" directory="/data/tomcat-serssions"/>
</Manager>

Tomcat连接相关参数总结

在Tomcat配置文件server.xml中的配置中
maxThreads 客户请求最大线程数
minSpareThreads    Tomcat初始化时创建的 socket 线程数
maxSpareThreads   Tomcat连接器的最大空闲 socket 线程数
enableLookups      若设为true, 则支持域名解析,可把 ip 地址解析为主机名
redirectPort        在需要基于安全通道的场合,把客户请求转发到基于SSL 的 redirectPort端口
acceptAccount       监听端口队列最大数,满了之后客户请求会被拒绝(不能小于maxSpareThreads  )
connectionTimeout   连接超时
minProcessors         服务器创建时的最小处理线程数
maxProcessors        服务器同时最大处理线程数
URIEncoding    URL统一编码
compression 打开压缩功能
compressionMinSize   启用压缩的输出内容大小,这里面默认为2KB
compressableMimeType 压缩类型
connectionTimeout 定义建立客户连接超时的时间. 如果为 -1, 表示不限制建立客户连接的时间

jvm相关
线程共享内存:
方法区:存储jvm加载的class文件、常量、静态变量、即时编译后的代码等
java堆:存储java的所有对象的实例、数组
线程私有内存:
程序计数器:每个线程有一个计数寄存器,存放线程运行的字节码地址
线程栈:线程私有,线程调用方法时进行入栈和出栈操作;每调用一个方法则在栈中生成一个新的栈帧
本地方法区:负责线程调用本地方法

jinfo的用法:
jinfo -flags pid 可以看tomcat启动参数,也可以看jvm相关信息
jinfo -flag <具体某一个参数> 可以看具体的jvm参数
jinfo -flag <+/-><name> +:开启,-:禁用
$CATALINA_BASE/bin/catalina.sh中设置JAVA_OPTS参数(jvm运行参数)

遗留问题:
tomcat的类加载,双亲委派等
Tomcat 的类加载器是怎么设计的?
首先,我们来问个问题:

Tomcat 如果使用默认的类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
  3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。

再看看我们的问题:Tomcat 如果使用默认的类加载机制行不行?
答案是不行的。为什么?我们看,第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。第三个问题和第一个问题一样。我们再看第四个问题,我们想我们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat 如何实现自己独特的类加载机制?
所以,Tomcat 是怎么实现的呢?牛逼的Tomcat团队已经设计好了。我们看看他们的设计图

tomcat类加载器

可以看到,前三个加载器和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

从图中的委派关系中可以看出:

CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能

如果tomcat的common classLoad 想加载web ClassLoad中的类该怎么办?

因此引入了线程上下文类加载器(Thread Context ClassLoader)。
这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。
如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范
围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器。

【参考博客】
1.http://blog.51cto.com/freeloda/1299644

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

推荐阅读更多精彩内容