ESP32學(xué)習(xí)筆記(11)——UDP服務(wù)端

一西轩、TCP與UDP優(yōu)缺點(diǎn)

1悬蔽、TCP面向連接(如打電話要先撥號建立連接);UDP是無連接的扯躺,即發(fā)送數(shù)據(jù)之前不需要建立連接。

2蝎困、TCP提供可靠的服務(wù)录语。也就是說,通過TCP連接傳送的數(shù)據(jù)禾乘,無差錯澎埠,不丟失,不重復(fù)始藕,且按序到達(dá);UDP盡最大努力交付蒲稳,即不保證可靠交付氮趋。
TCP通過校驗(yàn)和,重傳控制弟塞,序號標(biāo)識凭峡,滑動窗口、確認(rèn)應(yīng)答實(shí)現(xiàn)可靠傳輸决记。如丟包時的重發(fā)控制,還可以對次序亂掉的分包進(jìn)行順序控制倍踪。

3系宫、UDP具有較好的實(shí)時性,工作效率比TCP高建车,適用于對高速傳輸和實(shí)時性有較高的通信或廣播通信扩借。

4、每一條TCP連接只能是點(diǎn)到點(diǎn)的;UDP支持一對一缤至,一對多潮罪,多對一和多對多的交互通信。

5领斥、TCP對系統(tǒng)資源要求較多嫉到,UDP對系統(tǒng)資源要求較少。

二月洛、概述

ESP-IDF使用開源 lwIP輕量級的TCP / IP堆棧何恶。ESP-IDF版本lwIP(esp-lwip)與上游項(xiàng)目相比有一些修改和補(bǔ)充。

ESP-IDF支持以下功能 lwIP TCP / IP堆棧功能:

BSD套接字API

BSD套接字API是一個通用的跨平臺TCP / IP套接字API细层,該API起源于UNIX的Berkeley標(biāo)準(zhǔn)發(fā)行版,但現(xiàn)在已在POSIX規(guī)范的一部分中進(jìn)行了標(biāo)準(zhǔn)化唬涧。BSD套接字有時稱為POSIX套接字或Berkeley套接字疫赎。

正如ESP-IDF中實(shí)施的那樣,lwIP支持BSD套接字API的所有常用用法碎节。

ESP-IDF 編程指南——ESP-NETIF
ESP-IDF 編程指南——lwIP

三捧搞、API說明

以下 BSD Socket 接口位于 lwip/lwip/src/include/lwip/sockets.h

  • socket()
  • bind()
  • accept()
  • shutdown()
  • getpeername()
  • getsockopt()setsockopt()(請參閱套接字選項(xiàng)
  • close()(通過虛擬文件系統(tǒng)組件
  • read()钓株,readv()实牡,write()writev()(經(jīng)由虛擬文件系統(tǒng)部件
  • recv()轴合,recvmsg()创坞,recvfrom()
  • send()sendmsg()受葛,sendto()
  • select()(通過虛擬文件系統(tǒng)組件
  • poll()(注意:在ESP-IDF上题涨,poll()是通過內(nèi)部調(diào)用select來實(shí)現(xiàn)的偎谁,因此,select()如果有可用的方法選擇纲堵,建議直接使用巡雨。)
  • fcntl()(請參閱fcntl

非標(biāo)準(zhǔn)功能:

  • ioctl()(請參閱ioctls

四、UDP服務(wù)端

4.1 主要流程

4.1.1 第一步:新建socket

int addr_family = 0;
int ip_protocol = 0;

addr_family = AF_INET;
ip_protocol = IPPROTO_IP;

int sock =  socket(addr_family, SOCK_DGRAM, ip_protocol);
if(sock < 0) 
{
    ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
}

4.1.2 第二步:配置服務(wù)器信息

#define UDP_PORT        3333                 // UDP服務(wù)器端口號

struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr.sin_port = htons(UDP_PORT);

4.1.3 第三步:綁定地址

int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if(err < 0)
{
    ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
    close(sock);
}
ESP_LOGI(TAG, "Socket bound, port %d", PORT);

4.1.4 第四步:接收數(shù)據(jù)

char rx_buffer[128];
char host_ip[] = HOST_IP_ADDR;

while(1)
{
    ······
    // 清空緩存
    memset(rx_buffer, 0, sizeof(rx_buffer));

    struct sockaddr_in source_addr; // Large enough for both IPv4 or IPv6
    socklen_t socklen = sizeof(source_addr);
    int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer), 0, (struct sockaddr *)&source_addr, &socklen);

    // Error occurred during receiving
    if(len < 0) 
    {
        ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
        break;
    }
    // Data received
    else 
    {
        ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip);
        ESP_LOGI(TAG, "%s", rx_buffer);
    }
}
close(sock);

4.1.5 第五步:發(fā)送數(shù)據(jù)

static const char *payload = "Message from ESP32 ";

int err = sendto(sock, payload, strlen(payload), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if(err < 0) 
{
    ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
    close(sock);
}

4.2 配置SSID和密碼連接WIFI創(chuàng)建UDP服務(wù)端

UDP Server類似


使用 esp-idf\examples\protocols\sockets\udp_server 中的例程

/* BSD Socket API Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>

#define PORT CONFIG_EXAMPLE_PORT

static const char *TAG = "example";

static void udp_server_task(void *pvParameters)
{
    char rx_buffer[128];
    char addr_str[128];
    int addr_family = (int)pvParameters;
    int ip_protocol = 0;
    struct sockaddr_in6 dest_addr;

    while (1) {

        if (addr_family == AF_INET) {
            struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
            dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
            dest_addr_ip4->sin_family = AF_INET;
            dest_addr_ip4->sin_port = htons(PORT);
            ip_protocol = IPPROTO_IP;
        } else if (addr_family == AF_INET6) {
            bzero(&dest_addr.sin6_addr.un, sizeof(dest_addr.sin6_addr.un));
            dest_addr.sin6_family = AF_INET6;
            dest_addr.sin6_port = htons(PORT);
            ip_protocol = IPPROTO_IPV6;
        }

        int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
        if (sock < 0) {
            ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Socket created");

#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
        if (addr_family == AF_INET6) {
            // Note that by default IPV6 binds to both protocols, it is must be disabled
            // if both protocols used at the same time (used in CI)
            int opt = 1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
        }
#endif

        int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if (err < 0) {
            ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
        }
        ESP_LOGI(TAG, "Socket bound, port %d", PORT);

        while (1) {

            ESP_LOGI(TAG, "Waiting for data");
            struct sockaddr_in6 source_addr; // Large enough for both IPv4 or IPv6
            socklen_t socklen = sizeof(source_addr);
            int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);

            // Error occurred during receiving
            if (len < 0) {
                ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
                break;
            }
            // Data received
            else {
                // Get the sender's ip address as string
                if (source_addr.sin6_family == PF_INET) {
                    inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
                } else if (source_addr.sin6_family == PF_INET6) {
                    inet6_ntoa_r(source_addr.sin6_addr, addr_str, sizeof(addr_str) - 1);
                }

                rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string...
                ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
                ESP_LOGI(TAG, "%s", rx_buffer);

                int err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr));
                if (err < 0) {
                    ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
                    break;
                }
            }
        }

        if (sock != -1) {
            ESP_LOGE(TAG, "Shutting down socket and restarting...");
            shutdown(sock, 0);
            close(sock);
        }
    }
    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    ESP_ERROR_CHECK(example_connect());

#ifdef CONFIG_EXAMPLE_IPV4
    xTaskCreate(udp_server_task, "udp_server", 4096, (void*)AF_INET, 5, NULL);
#endif
#ifdef CONFIG_EXAMPLE_IPV6
    xTaskCreate(udp_server_task, "udp_server", 4096, (void*)AF_INET6, 5, NULL);
#endif

}

idf.py menuconfig 配置服務(wù)器端口


配置SSID和密碼



然后 idf.py flash 編譯下載

查看打酉:



4.3 作為AP創(chuàng)建UDP服務(wù)端

根據(jù) esp-idf\examples\protocols\sockets\udp_server 中的例程修改

/* BSD Socket API Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"

#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>

#define PORT CONFIG_EXAMPLE_PORT

#define EXAMPLE_ESP_WIFI_SSID      "ESP32_TEST"
#define EXAMPLE_ESP_WIFI_PASS      "12345678"
#define EXAMPLE_ESP_WIFI_CHANNEL   1
#define EXAMPLE_MAX_STA_CONN       4

static const char *TAG = "example";

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                                    int32_t event_id, void* event_data)
{
    if (event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
                 MAC2STR(event->mac), event->aid);
    } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
                 MAC2STR(event->mac), event->aid);
    }
}

void wifi_init_softap(void)
{
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_ap();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
            .channel = EXAMPLE_ESP_WIFI_CHANNEL,
            .password = EXAMPLE_ESP_WIFI_PASS,
            .max_connection = EXAMPLE_MAX_STA_CONN,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK
        },
    };
    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL);
}

static void udp_server_task(void *pvParameters)
{
    char rx_buffer[128];
    char addr_str[128];
    int addr_family = (int)pvParameters;
    int ip_protocol = 0;
    struct sockaddr_in6 dest_addr;

    while (1) {

        if (addr_family == AF_INET) {
            struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
            dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
            dest_addr_ip4->sin_family = AF_INET;
            dest_addr_ip4->sin_port = htons(PORT);
            ip_protocol = IPPROTO_IP;
        } else if (addr_family == AF_INET6) {
            bzero(&dest_addr.sin6_addr.un, sizeof(dest_addr.sin6_addr.un));
            dest_addr.sin6_family = AF_INET6;
            dest_addr.sin6_port = htons(PORT);
            ip_protocol = IPPROTO_IPV6;
        }

        int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
        if (sock < 0) {
            ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
            break;
        }
        ESP_LOGI(TAG, "Socket created");

#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
        if (addr_family == AF_INET6) {
            // Note that by default IPV6 binds to both protocols, it is must be disabled
            // if both protocols used at the same time (used in CI)
            int opt = 1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
        }
#endif

        int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if (err < 0) {
            ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
        }
        ESP_LOGI(TAG, "Socket bound, port %d", PORT);

        while (1) {

            ESP_LOGI(TAG, "Waiting for data");
            struct sockaddr_in6 source_addr; // Large enough for both IPv4 or IPv6
            socklen_t socklen = sizeof(source_addr);
            int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);

            // Error occurred during receiving
            if (len < 0) {
                ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
                break;
            }
            // Data received
            else {
                // Get the sender's ip address as string
                if (source_addr.sin6_family == PF_INET) {
                    inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
                } else if (source_addr.sin6_family == PF_INET6) {
                    inet6_ntoa_r(source_addr.sin6_addr, addr_str, sizeof(addr_str) - 1);
                }

                rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string...
                ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
                ESP_LOGI(TAG, "%s", rx_buffer);

                int err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr));
                if (err < 0) {
                    ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
                    break;
                }
            }
        }

        if (sock != -1) {
            ESP_LOGE(TAG, "Shutting down socket and restarting...");
            shutdown(sock, 0);
            close(sock);
        }
    }
    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    //ESP_ERROR_CHECK(esp_netif_init());
    //ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
     * Read "Establishing Wi-Fi or Ethernet Connection" section in
     * examples/protocols/README.md for more information about this function.
     */
    //ESP_ERROR_CHECK(example_connect());
    wifi_init_softap();

#ifdef CONFIG_EXAMPLE_IPV4
    xTaskCreate(udp_server_task, "udp_server", 4096, (void*)AF_INET, 5, NULL);
#endif
#ifdef CONFIG_EXAMPLE_IPV6
    xTaskCreate(udp_server_task, "udp_server", 4096, (void*)AF_INET6, 5, NULL);
#endif

}

idf.py menuconfig 配置服務(wù)器端口


然后 idf.py flash 編譯下載

查看打宇硗:




? 由 Leung 寫于 2021 年 5 月 6 日

? 參考:第十七章 ESP32的UDP廣播
    樂鑫Esp32學(xué)習(xí)之旅⑧ esp32上實(shí)現(xiàn)本地 UDP 客戶端和服務(wù)端角色,在局域網(wǎng)內(nèi)實(shí)現(xiàn)通訊
    ESP32UDP 通信

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茂附,一起剝皮案震驚了整個濱河市正蛙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌营曼,老刑警劉巖乒验,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蒂阱,居然都是意外死亡锻全,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門录煤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳄厌,“玉大人,你說我怎么就攤上這事辐赞〔壳蹋” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵响委,是天一觀的道長新思。 經(jīng)常有香客問我,道長赘风,這世上最難降的妖魔是什么夹囚? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮邀窃,結(jié)果婚禮上荸哟,老公的妹妹穿的比我還像新娘。我一直安慰自己瞬捕,他們只是感情好鞍历,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肪虎,像睡著了一般劣砍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扇救,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天刑枝,我揣著相機(jī)與錄音香嗓,去河邊找鬼。 笑死装畅,一個胖子當(dāng)著我的面吹牛靠娱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播掠兄,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼像云,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚂夕?” 一聲冷哼從身側(cè)響起苫费,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎双抽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闲礼,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牍汹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了柬泽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慎菲。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锨并,靈堂內(nèi)的尸體忽然破棺而出露该,到底是詐尸還是另有隱情,我是刑警寧澤第煮,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布解幼,位于F島的核電站,受9級特大地震影響包警,放射性物質(zhì)發(fā)生泄漏撵摆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一害晦、第九天 我趴在偏房一處隱蔽的房頂上張望特铝。 院中可真熱鬧,春花似錦壹瘟、人聲如沸鲫剿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灵莲。三九已至,卻和暖如春澄者,著一層夾襖步出監(jiān)牢的瞬間笆呆,已是汗流浹背请琳。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赠幕,地道東北人俄精。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像榕堰,于是被迫代替她去往敵國和親竖慧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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