docker源碼學(xué)習(xí)-docker deamon (1)

談到docker源碼蛾绎,其實網(wǎng)上有很多的源碼的分析的文章撩扒,也看過一些大牛寫的docker源碼解讀的文章,收獲很大逛尚。我之前也想去看docker的源碼垄惧,但是當我把源碼下載下來一看,源碼太多了绰寞,不知道該從何處下手到逊,看一個功能點的代碼,看完之后只知道個大概滤钱,不久就忘記的一干二凈觉壶,在加上docker現(xiàn)在版本更新飛速,所以就更跟不上那個腳步了件缸。但是我又一直想看看docker的源碼铜靶,所以,我就想了個辦法他炊,既然你版本更新太快争剿,那我就從你docker的第一個版本看起,一看總共才20幾個go文件痊末,頓時壓力沒有那么大了蚕苇。。舌胶。

docker版本:v0.1.0

總結(jié):

docker v0.1.0版本完全是基于lxc來實現(xiàn)的捆蜀。

docker 啟動

先檢查docker可執(zhí)行文件的絕對路徑是否在/sbin/init目錄下已經(jīng)存在

如果在,則設(shè)置docker容器啟動之前的環(huán)境

做如下操作:設(shè)置網(wǎng)絡(luò):添加路由幔嫂,切換用戶辆它,啟動docker程序

```

func main() {

if docker.SelfPath() == "/sbin/init" {

// Running in init mode

docker.SysInit()

return

}

// FIXME: Switch d and D ? (to be more sshd like)

fl_daemon := flag.Bool("d", false, "Daemon mode")

fl_debug := flag.Bool("D", false, "Debug mode")

flag.Parse()

rcli.DEBUG_FLAG = *fl_debug

if *fl_daemon {

if flag.NArg() != 0 {

flag.Usage()

return

}

if err := daemon(); err != nil {

log.Fatal(err)

}

} else {

if err := runCommand(flag.Args()); err != nil {

log.Fatal(err)

}

}

}

```

如果不存在則根據(jù)參入的命令行參數(shù):去選擇是啟動docker deamon 還是執(zhí)行 docker cli 的命令調(diào)用

如果是deamon (-d)則檢查是否有多余的參數(shù),如果有則退出履恩,顯示幫助信息

如果沒有則啟動docker deamon锰茉,在來看看docker deamon 啟動過程,具體干了些什么事情切心。

看看daemon的實現(xiàn):

主要是創(chuàng)建一個server對象飒筑, 然后通過這個server創(chuàng)建tcp服務(wù)端:其中最主要的是

在這個是在server的創(chuàng)建,創(chuàng)建過程比較復(fù)雜點绽昏,然后是在服務(wù)啟動tcp監(jiān)聽哪里协屡,用到了反射技術(shù),通過把對用的docker 的cmd

命令和對應(yīng)的server的方法對應(yīng)上全谤,然后通過方法名稱獲取對應(yīng)的方法執(zhí)行對應(yīng)命令的方法肤晓,從而相應(yīng)對應(yīng)的tcp客戶端發(fā)送的命令

創(chuàng)建server的詳細過程歸納如下:創(chuàng)建server實質(zhì)就是創(chuàng)建runtime對象,runtime對象中封裝了所有docker

daemon運行時所需要的所有的信息,在創(chuàng)建runtime時补憾,首先會在

/var/lib/docker目錄下創(chuàng)建對應(yīng)的文件:containers漫萄,graph文件夾,然后創(chuàng)建對應(yīng)的鏡像tag存儲對象盈匾,通過名為lxcbr0的卡的網(wǎng)絡(luò)創(chuàng)建網(wǎng)絡(luò)管理腾务,最后創(chuàng)建dockerhub的認證對象AuthConfig,至此server對象創(chuàng)建完畢削饵。其中最復(fù)雜的就是網(wǎng)絡(luò)管理的創(chuàng)建岩瘦。

下面我們來先看看一個完整的server對象的創(chuàng)建,到底干了些什么事情葵孤;

```

func daemon() error {

service, err := docker.NewServer()

if err != nil {

return err

}

return rcli.ListenAndServe("tcp", "127.0.0.1:4242", service)

}

```

##server的創(chuàng)建

```

func NewServer() (*Server, error) {

rand.Seed(time.Now().UTC().UnixNano())

//檢查程序運行的arch是否是amd64

if runtime.GOARCH != "amd64" {

log.Fatalf("The docker runtime currently only supports amd64 (not %s).

This will change in the future. Aborting.", runtime.GOARCH)

}

//新建運行時環(huán)境担钮;這是重點,runtime中實現(xiàn)的docker的所有命令行中所有命令的api尤仍。

runtime, err := NewRuntime()

if err != nil {

return nil, err

}

srv := &Server{

runtime: runtime,

}

return srv, nil

}

```

對應(yīng)的NewRuntime()的方法:

```

func NewRuntime() (*Runtime, error) {

return NewRuntimeFromDirectory("/var/lib/docker")

}

```

對應(yīng)的NewRuntimeFromDirectory()的方法:

```

func NewRuntimeFromDirectory(root string) (*Runtime, error) {

//創(chuàng)建/var/lib/docker/containers文件夾

runtime_repo := path.Join(root, "containers")

if err := os.MkdirAll(runtime_repo, 0700); err != nil &&

!os.IsExist(err) {

return nil, err

}//這個判斷的意思是:創(chuàng)建這個文件夾箫津,如果報錯,并且錯誤信息不是 文件見已經(jīng)存在宰啦,則返回

//創(chuàng)建/var/lib/docker/graph目錄苏遥,同事創(chuàng)建Graph對象

g, err := NewGraph(path.Join(root, "graph"))

if err != nil {

return nil, err

}

/*

func NewGraph(root string) (*Graph, error) {

abspath, err := filepath.Abs(root)

if err != nil {

return nil, err

}

// Create the root directory if it doesn't exists

if err := os.Mkdir(root, 0700); err != nil && !os.IsExist(err)

{

return nil, err

}

return &Graph{

Root: abspath,

}, nil

}

*/

///var/lib/docker/repositories文件夾和Graph對象創(chuàng)建TagStore

repositories, err := NewTagStore(path.Join(root, "repositories"), g)

if err != nil {

return nil, fmt.Errorf("Couldn't create Tag store: %s", err)

}

//通過名為lxcbr0的卡的網(wǎng)絡(luò)創(chuàng)建網(wǎng)絡(luò)管理

netManager, err := newNetworkManager(networkBridgeIface)

在看看網(wǎng)絡(luò)管理的創(chuàng)建,實質(zhì)上通過指定名為lxcbr0的的網(wǎng)絡(luò)接口來實現(xiàn)的赡模,一個網(wǎng)絡(luò)管理的實例其實包括了:網(wǎng)橋名字田炭,ip網(wǎng)絡(luò),ip分配器,端口分配器,端口映射器檀训,那么在實例化一個網(wǎng)絡(luò)管理的時候茬腿,實質(zhì)上就是要將這些屬性全部賦值蚀浆,ip分配器實質(zhì)上就是一個ip地址的chanle,里面的ip地址是通過lxcbr0接口的ip

和對應(yīng)的網(wǎng)關(guān)mask計算得到的子網(wǎng)ip。端口分配器,實質(zhì)就是一個在存放了指定范圍49153~65535個int的chanle景用,端口映射器實際上就是一個設(shè)置iptalbe和清楚iptable的一個方法。

/*

type NetworkManager struct {

bridgeIface? string? //網(wǎng)橋名字

bridgeNetwork *net.IPNet //ip網(wǎng)絡(luò)

ipAllocator? *IPAllocator

//ip分配器惭蹂,就是我們在創(chuàng)建容器的時候給容器分配ip的伞插,其實就是一個包含了網(wǎng)卡的ip和網(wǎng)關(guān),同時有又一個ip池

portAllocator *PortAllocator //端口分配器 就是我們在創(chuàng)建容器的時候給容器分配端口的

portMapper? ? *PortMapper

}

func newNetworkManager(bridgeIface string) (*NetworkManager, error) {

addr, err := getIfaceAddr(bridgeIface) //獲取指定lxcbr0網(wǎng)卡的ip

if err != nil {

return nil, err

}

network := addr.(*net.IPNet)

ipAllocator, err := newIPAllocator(network)

/*

type IPAllocator struct {

network *net.IPNet

queue? chan (net.IP)

}

通過網(wǎng)卡lxcbr0的第一個ip和網(wǎng)關(guān)mask 得到當前這個網(wǎng)卡下的所有子網(wǎng)ip 并且封裝成一個ip分配器(IPAllocator)

func newIPAllocator(network *net.IPNet) (*IPAllocator, error) {

alloc := &IPAllocator{

network: network,

}

if err := alloc.populate(); err != nil {

return nil, err

}

return alloc, nil

}

func (alloc *IPAllocator) populate() error {

firstIP, _ := networkRange(alloc.network)

size, err := networkSize(alloc.network.Mask)

if err != nil {

return err

}

// The queue size should be the network size - 3

// -1 for the network address, -1 for the broadcast address and

// -1 for the gateway address

alloc.queue = make(chan net.IP, size-3)

for i := int32(1); i < size-1; i++ {

ipNum, err := ipToInt(firstIP)

if err != nil {

return err

}

ip, err := intToIp(ipNum + int32(i))

if err != nil {

return err

}

// Discard the network IP (that's the host IP address)

if ip.Equal(alloc.network.IP) {

continue

}

alloc.queue <- ip

}

return nil

}

*/

if err != nil {

return nil, err

}

//創(chuàng)建端口分配器(實質(zhì)是以一個存放49153~65535)個端口的int chanle

portAllocator, err := newPortAllocator(portRangeStart, portRangeEnd)

if err != nil {

return nil, err

}

/*

const (

networkBridgeIface = "lxcbr0"

portRangeStart? ? = 49153

portRangeEnd? ? ? = 65535

)

type PortAllocator struct {

ports chan (int)

}

func newPortAllocator(start, end int) (*PortAllocator, error) {

allocator := &PortAllocator{}

allocator.populate(start, end)

return allocator, nil

}

*/

//端口映射器通過設(shè)置iptables規(guī)則來處理將外部端口映射到容器盾碗。 它跟蹤所有映射媚污,并能夠隨意取消映射

portMapper, err := newPortMapper()

/*

type PortMapper struct {

mapping map[int]net.TCPAddr

}

func newPortMapper() (*PortMapper, error) {

mapper := &PortMapper{}

if err := mapper.cleanup(); err != nil {

return nil, err

}

if err := mapper.setup(); err != nil {

return nil, err

}

return mapper, nil

}

func (mapper *PortMapper) cleanup() error {

// Ignore errors - This could mean the chains were never set up

iptables("-t", "nat", "-D", "PREROUTING", "-j", "DOCKER")

iptables("-t", "nat", "-D", "OUTPUT", "-j", "DOCKER")

iptables("-t", "nat", "-F", "DOCKER")

iptables("-t", "nat", "-X", "DOCKER")

mapper.mapping = make(map[int]net.TCPAddr)

return nil

}

func (mapper *PortMapper) setup() error {

if err := iptables("-t", "nat", "-N", "DOCKER"); err != nil {

return errors.New("Unable to setup port networking: Failed to

create DOCKER chain")

}

if err := iptables("-t", "nat", "-A", "PREROUTING", "-j",

"DOCKER"); err != nil {

return errors.New("Unable to setup port networking: Failed to

inject docker in PREROUTING chain")

}

if err := iptables("-t", "nat", "-A", "OUTPUT", "-j", "DOCKER");

err != nil {

return errors.New("Unable to setup port networking: Failed to

inject docker in OUTPUT chain")

}

return nil

}

func iptables(args ...string) error {

if err := exec.Command("/sbin/iptables", args...).Run(); err != nil

{

return fmt.Errorf("iptables failed: iptables %v",

strings.Join(args, " "))

}

return nil

}

*/

manager := &NetworkManager{

bridgeIface:? bridgeIface,

bridgeNetwork: network,

ipAllocator:? ipAllocator,

portAllocator: portAllocator,

portMapper:? ? portMapper,

}

return manager, nil

}

*/

if err != nil {

return nil, err

}

//加載/var/lib/docker/.dockercfg生成對應(yīng)的auth對象

/*

type AuthConfig struct {

Username string `json:"username"`

Password string `json:"password"`

Email? ? string `json:"email"`

rootPath string `json:-`

}

*/

authConfig, err := auth.LoadConfig(root)

if err != nil && authConfig == nil {

// If the auth file does not exist, keep going

return nil, err

}

runtime := &Runtime{

root:? ? ? ? ? root,

repository:? ? runtime_repo,

containers:? ? list.New(),

networkManager: netManager,

graph:? ? ? ? ? g,

repositories:? repositories,

authConfig:? ? authConfig,

}

if err := runtime.restore(); err != nil {

return nil, err

}

return runtime, nil

/*

//讀取/var/lib/docker/containers目錄下的所有文件夾(實際就是所有之前運行過的容器的目錄,目錄名為對應(yīng)容器的id)

func (runtime *Runtime) restore() error {

dir, err := ioutil.ReadDir(runtime.repository)

if err != nil {

return err

}

for _, v := range dir {

id := v.Name()

container, err := runtime.Load(id)

if err != nil {

Debugf("Failed to load container %v: %v", id, err)

continue

}

Debugf("Loaded container %v", container.Id)

}

return nil

}

//load的實現(xiàn)流程如下:通過獲取對應(yīng)容器id目錄下的config.json文件數(shù)據(jù)來實力話一個container對象廷雅,

func (runtime *Runtime) Load(id string) (*Container, error) {

container := &Container{root: runtime.containerRoot(id)}

if err := container.FromDisk(); err != nil {

return nil, err

}

//最后檢查config.json(實際就是對應(yīng)容器的容器信息文件)是否被更改過

if container.Id != id {

return container, fmt.Errorf("Container %s is stored at %s",

container.Id, id)

}

if err := runtime.Register(container); err != nil {

return nil, err

}

return container, nil

}

//最后將加載的容器注冊到runtime中的容器list中去耗美,具體流程如下

func (runtime *Runtime) Register(container *Container) error {

//先檢查runtime中的容器list中是否存在氢伟,存在則提示錯誤

if container.runtime != nil || runtime.Exists(container.Id) {

return fmt.Errorf("Container is already loaded")

}

//檢查容器id是否為空 如果為空則說明容器是錯誤的,則退出返回錯誤

if err := validateId(container.Id); err != nil {

return err

}

//設(shè)置容器的runtime

container.runtime = runtime

//設(shè)置容器的狀態(tài)以及容器的標準輸出幽歼,輸出,錯誤流谬盐,然后將標準輸出和錯誤寫入磁盤指定的文件中

// Setup state lock (formerly in newState()

lock := new(sync.Mutex)

container.State.stateChangeLock = lock

container.State.stateChangeCond = sync.NewCond(lock)

// Attach to stdout and stderr

container.stderr = newWriteBroadcaster()

container.stdout = newWriteBroadcaster()

// Attach to stdin

if container.Config.OpenStdin {

container.stdin, container.stdinPipe = io.Pipe()

} else {

container.stdinPipe = NopWriteCloser(ioutil.Discard) // Silently

drop stdin

}

// Setup logging of stdout and stderr to disk

if err := runtime.LogToDisk(container.stdout,

container.logPath("stdout")); err != nil {

return err

}

if err := runtime.LogToDisk(container.stderr,

container.logPath("stderr")); err != nil {

return err

}

// done

//將container 加入到runtime中的容器list的最后

runtime.containers.PushBack(container)

return nil

}

*/

```

##創(chuàng)建tcp服務(wù)端:

創(chuàng)建一個監(jiān)聽 接受tcp的請求甸私,為每個請求開啟一個單獨的攜程處理請求 ,如果有請求到來則進行處理

```

func ListenAndServe(proto, addr string, service Service) error {

//創(chuàng)建一個監(jiān)聽

listener, err := net.Listen(proto, addr)

if err != nil {

return err

}

log.Printf("Listening for RCLI/%s on %s\n", proto, addr)

defer listener.Close()

for {

//接受tcp的請求

if conn, err := listener.Accept(); err != nil {

return err

} else {

go func() {

if DEBUG_FLAG {

CLIENT_SOCKET = conn

}

//如果有請求到來則進行處理

if err := Serve(conn, service); err != nil {

log.Printf("Error: " + err.Error() + "\n")

fmt.Fprintf(conn, "Error: "+err.Error()+"\n")

}

conn.Close()

}()

}

}

return nil

}

//獲取請求中的參數(shù)然后調(diào)用call飞傀,call的一系列調(diào)度過程如下

func Serve(conn io.ReadWriter, service Service) error {

r := bufio.NewReader(conn)

var args []string

if line, err := r.ReadString('\n'); err != nil {

return err

} else if err := json.Unmarshal([]byte(line), &args); err != nil {

return err

} else {

return call(service, ioutil.NopCloser(r), conn, args...)

}

return nil

}

func call(service Service, stdin io.ReadCloser, stdout io.Writer, args

...string) error {

return LocalCall(service, stdin, stdout, args...)

}

```

根據(jù)參數(shù)是否有值來執(zhí)行不同方法皇型,如果沒有參數(shù),則執(zhí)行runtime的help方法砸烦,也就是我們通常輸入docker

這個命令看到的那些heilp的信息弃鸦,如果有參數(shù),在進行參數(shù)的處理幢痘,處理邏輯:獲取第二個參數(shù)唬格,就是docker

后的命令,然后獲取命令之后的所有參數(shù)颜说,進行整條命令的打印日志輸出购岗,之后再通過cmd命令和反射技術(shù)去找到對應(yīng)的cmd所對應(yīng)的方法,最后找到方法將參數(shù)傳入方法门粪,執(zhí)行cmd對應(yīng)的方法喊积,結(jié)構(gòu)返回connect中。至此整個deamon啟動玄妈,到處理具體的api請求全部完成

```

func LocalCall(service Service, stdin io.ReadCloser, stdout io.Writer,

args ...string) error {

if len(args) == 0 {

args = []string{"help"}

}

flags := flag.NewFlagSet("main", flag.ContinueOnError)

flags.SetOutput(stdout)

flags.Usage = func() { stdout.Write([]byte(service.Help())) }

if err := flags.Parse(args); err != nil {

return err

}

cmd := flags.Arg(0)

log.Printf("%s\n",

strings.Join(append(append([]string{service.Name()}, cmd),

flags.Args()[1:]...), " "))

if cmd == "" {

cmd = "help"

}

method := getMethod(service, cmd)

if method != nil {

return method(stdin, stdout, flags.Args()[1:]...)

}

return errors.New("No such command: " + cmd)

}

func getMethod(service Service, name string) Cmd {

if name == "help" {

return func(stdin io.ReadCloser, stdout io.Writer, args ...string)

error {

if len(args) == 0 {

stdout.Write([]byte(service.Help()))

} else {

if method := getMethod(service, args[0]); method == nil {

return errors.New("No such command: " + args[0])

} else {

method(stdin, stdout, "--help")

}

}

return nil

}

}

methodName := "Cmd" + strings.ToUpper(name[:1]) +

strings.ToLower(name[1:])

method, exists := reflect.TypeOf(service).MethodByName(methodName)

if !exists {

return nil

}

return func(stdin io.ReadCloser, stdout io.Writer, args ...string)

error {

ret := method.Func.CallSlice([]reflect.Value{

reflect.ValueOf(service),

reflect.ValueOf(stdin),

reflect.ValueOf(stdout),

reflect.ValueOf(args),

})[0].Interface()

if ret == nil {

return nil

}

return ret.(error)

}

}

*/

}

```

**最后做個總結(jié)**:

docker

daemon是運行過程如下:首先在添加網(wǎng)橋的默認路由乾吻,切換用戶權(quán)限等操作,然后再在/var/lib/docker目錄下創(chuàng)建containers拟蜻,graph等文件夾(如果已經(jīng)存在則將containers中的所有容器加載到runtime(我認為runtime實際就是docker-deamon的運行時環(huán)境)運行時環(huán)境中)绎签,然后創(chuàng)建image的tag存儲,然后創(chuàng)建對應(yīng)的網(wǎng)絡(luò)管理器瞭郑,最后加載認證文件辜御,最后啟動tpc服務(wù)監(jiān)聽處理請求。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屈张,一起剝皮案震驚了整個濱河市擒权,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阁谆,老刑警劉巖碳抄,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異场绿,居然都是意外死亡剖效,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來璧尸,“玉大人咒林,你說我怎么就攤上這事∫猓” “怎么了垫竞?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蛀序。 經(jīng)常有香客問我欢瞪,道長,這世上最難降的妖魔是什么徐裸? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任遣鼓,我火速辦了婚禮,結(jié)果婚禮上重贺,老公的妹妹穿的比我還像新娘骑祟。我一直安慰自己,他們只是感情好气笙,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布曾我。 她就那樣靜靜地躺著,像睡著了一般健民。 火紅的嫁衣襯著肌膚如雪抒巢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天秉犹,我揣著相機與錄音蛉谜,去河邊找鬼。 笑死崇堵,一個胖子當著我的面吹牛型诚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸳劳,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼狰贯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了赏廓?” 一聲冷哼從身側(cè)響起涵紊,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎幔摸,沒想到半個月后摸柄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡既忆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年驱负,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗦玖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡跃脊,死狀恐怖宇挫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酪术,我是刑警寧澤捞稿,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站拼缝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏彰亥。R本人自食惡果不足惜咧七,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望任斋。 院中可真熱鬧继阻,春花似錦、人聲如沸废酷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澈蟆。三九已至墨辛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間趴俘,已是汗流浹背睹簇。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寥闪,地道東北人太惠。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像疲憋,于是被迫代替她去往敵國和親凿渊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

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