Mac&iOS Socket

大綱

一.Socket簡介

二.BSD Socket編程準(zhǔn)備

  • 1.地址
  • 2.端口
  • 3.網(wǎng)絡(luò)字節(jié)序
  • 4.半相關(guān)與全相關(guān)
  • 5.網(wǎng)絡(luò)編程模型

三.socket接口編程示例

四.使用select

五.使用kqueue

六.使用流

轉(zhuǎn)載

一.Socket簡介

在UNIX系統(tǒng)中,萬物皆文件(Everything is a file)。所有的IO操作都可以看作對文件的IO操作光酣,都遵循著這樣的操作模式:打開 -> 讀/寫 -> 關(guān)閉吩愧,打開操作(如open函數(shù))獲取“文件”使用權(quán),返回文件描述符棺妓,后繼的操作都通過這個文件描述符來進(jìn)行价涝。很多系統(tǒng)調(diào)用都依賴于文件描述符,它是一個無符號整數(shù),每一個用戶進(jìn)程都對應(yīng)著一個文件描述符表验烧,通過文件描述符就可以找到對應(yīng)文件的信息板驳。 在類UNIX平臺上,對于控制臺的標(biāo)準(zhǔn)輸入輸出以及標(biāo)準(zhǔn)錯誤輸出都有對應(yīng)的文件描述符碍拆,分別為0,1,2若治。它們定義在 unistd.h中

#define  STDIN_FILENO   0   /* standard input file descriptor */
#define STDOUT_FILENO   1   /* standard output file descriptor */
#define STDERR_FILENO   2   /* standard error file descriptor */ 

UNIX內(nèi)核加入TCP/IP協(xié)議的時候,便在系統(tǒng)中引入了一種新的IO操作感混,只不過由于網(wǎng)絡(luò)連接的不可靠性端幼,所以網(wǎng)絡(luò)IO比本地設(shè)備的IO復(fù)雜很多。這一系列的接口叫做BSD Socket API,當(dāng)初由伯克利大學(xué)研發(fā)弧满,最終成為網(wǎng)絡(luò)開發(fā)接口的標(biāo)準(zhǔn)婆跑。 網(wǎng)絡(luò)通信從本質(zhì)上講也是進(jìn)程間通信,只是這兩個進(jìn)程一般在網(wǎng)絡(luò)中不同計算機(jī)上庭呜。當(dāng)然Socket API其實(shí)也提供了專門用于本地IPC的使用方式:UNIX Domain Socket滑进,這個這里就不細(xì)說了。本文所講的Socket如無例外募谎,均是說的Internet Socket扶关。

在本地的進(jìn)程中,每一個進(jìn)程都可以通過PID來標(biāo)識数冬,對于網(wǎng)絡(luò)上的一個計算機(jī)中的進(jìn)程如何標(biāo)識呢节槐?網(wǎng)絡(luò)中的計算機(jī)可以通過一個IP地址進(jìn)行標(biāo)識,一個計算機(jī)中的某個進(jìn)程則可以通過一個無符號整數(shù)(端口號)來標(biāo)識吉执,所以一個網(wǎng)絡(luò)中的進(jìn)程可以通過IP地址+端口號的方式進(jìn)行標(biāo)識疯淫。

二.BSD Socket編程準(zhǔn)備

1.地址

在程序中,我們?nèi)绾伪4嬉粋€地址呢戳玫?在 <sys/socket.h>中的sockaddr便是描述socket地址的結(jié)構(gòu)體類型.

/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
    __uint8_t   sa_len;     /* total length */
    sa_family_t sa_family;  /* [XSI] address family */
    char        sa_data[14];    /* [XSI] addr value (actually larger) */
};

為了方便設(shè)置用語網(wǎng)絡(luò)通信的socket地址熙掺,引入了sockaddr_in結(jié)構(gòu)體(對于UNIX Domain Socket則對應(yīng)sockaddr_un)

/*
 * Socket address, internet style.
 */
struct sockaddr_in {
    __uint8_t   sin_len;
    sa_family_t sin_family;
    in_port_t   sin_port;//得是網(wǎng)絡(luò)字節(jié)序
    struct  in_addr sin_addr;//in_addr存在的原因則是歷史原因,其實(shí)質(zhì)是代表一個IP地址的32位整數(shù)
    char        sin_zero[8];//bzero之咕宿,純粹是為了兼容sockaddr
};

在實(shí)際編程的時候币绩,經(jīng)常需要將sockaddr_in強(qiáng)制轉(zhuǎn)換成sockaddr類型。

2.端口

說到端口我們經(jīng)常會聯(lián)想到硬件府阀,在網(wǎng)絡(luò)編程中的端口其實(shí)是一個標(biāo)識而已缆镣,或者說是系統(tǒng)的資源而已。系統(tǒng)提供了端口分配和管理的機(jī)制试浙。

3.網(wǎng)絡(luò)字節(jié)序

談網(wǎng)絡(luò)字節(jié)序(Endianness)之前我們先說說什么是字節(jié)序董瞻。字節(jié)序又叫端序,就是指計算機(jī)中存放 多字節(jié)數(shù)據(jù)的字節(jié)的順序。典型的就是數(shù)據(jù)存放在內(nèi)存中或者網(wǎng)絡(luò)傳輸時的字節(jié)的順序钠糊。常用的字節(jié)序有大端序(big-endian)挟秤,小端序(litle-endian,另還有不常見的混合序middle-endian)。不同的CPU可能會使用不同的字節(jié)序抄伍,如X86艘刚,PDP-11等處理器為小端序,Motorola 6800,PowerPC 970等使用的是大端序截珍。小端序是指低字節(jié)位存放在內(nèi)存地址的低端攀甚,高端序是指高位字節(jié)存放在內(nèi)存的低端。 舉個例子來說明什么是大端序和小端序: 比如一個4字節(jié)的整數(shù) 16進(jìn)制形式為 0x12345678岗喉,最左邊是高位秋度。
大端序
低位

高位
12
34
56
78
小端序
低位

高位
78
56
34
12
TCP/IP 各層協(xié)議將字節(jié)序使用的是大端序,我們把TCP/IP協(xié)議中使用的字節(jié)序稱之為網(wǎng)絡(luò)字節(jié)序沈堡。 編程的時候可以使用定義在sys/_endian.h中的相關(guān)的接口進(jìn)行本地字節(jié)序和網(wǎng)絡(luò)字節(jié)序的互轉(zhuǎn)静陈。

#define ntohs(x)    __DARWIN_OSSwapInt16(x) // 16位整數(shù) 網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)主機(jī)字節(jié)序
#define htons(x)    __DARWIN_OSSwapInt16(x) // 16位整數(shù) 主機(jī)字節(jié)序轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序

#define ntohl(x)    __DARWIN_OSSwapInt32(x)  //32位整數(shù) 網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)主機(jī)字節(jié)序
#define htonl(x)    __DARWIN_OSSwapInt32(x) //32位整數(shù) 主機(jī)字節(jié)序轉(zhuǎn)網(wǎng)絡(luò)字節(jié)序

以上聲明中 n代表netwrok燕雁, h代表host 诞丽,s代表short,l代表long
如果數(shù)據(jù)是單字節(jié)的話拐格,則其沒有字節(jié)序的說法了僧免。

4.半相關(guān)與全相關(guān)

半相關(guān)(half-association)是指一個三元組 (協(xié)議,本地IP地址,本地端口),通過這個三元組就可以唯一標(biāo)識一個網(wǎng)絡(luò)中的進(jìn)程,一般用于listening socket。但是實(shí)際進(jìn)行通信的過程捏浊,至少需要兩個進(jìn)程懂衩,且它們所使用的協(xié)議必須一致,所以一個完成的網(wǎng)絡(luò)通信至少需要一個五元組表示(協(xié)議,本地地址,本地端口,遠(yuǎn)端地址,遠(yuǎn)端端口)金踪,這樣的五元組叫做全相關(guān)浊洞。

5.網(wǎng)絡(luò)編程模型

網(wǎng)絡(luò)存在的本質(zhì)其實(shí)就是網(wǎng)絡(luò)中個體之間的在某個領(lǐng)域的信息存在不對等性,所以一般情況下總有一些個體為另一些個體提供服務(wù)胡岔。提供服務(wù)器的我們把它叫做服務(wù)器法希,接受服務(wù)的叫做客戶端。所以在網(wǎng)絡(luò)編程中靶瘸,也存在服務(wù)器端和客戶端之分苫亦。

三.BSD Socket編程詳解

下面的例子是一個簡單的一對一聊天的程序,分服務(wù)器和客戶端怨咪,且發(fā)送消息和接受消息次序固定屋剑。

Server端代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

int main (int argc, const char * argv[])
{
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;//Address families AF_INET互聯(lián)網(wǎng)地址簇
    server_addr.sin_port = htons(11332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero),8);
    
    //創(chuàng)建socket
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM 有連接
    if (server_socket == -1) {
        perror("socket error");
        return 1;
    }
    
    //綁定socket:將創(chuàng)建的socket綁定到本地的IP地址和端口,此socket是半相關(guān)的诗眨,只是負(fù)責(zé)偵聽客戶端的連接請求唉匾,并不能用于和客戶端通信
    int bind_result = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (bind_result == -1) {
        perror("bind error");
        return 1;
    }
    
    //listen偵聽 第一個參數(shù)是套接字,第二個參數(shù)為等待接受的連接的隊列的大小匠楚,在connect請求過來的時候,完成三次握手后先將連接放到這個隊列中巍膘,直到被accept處理卫病。如果這個隊列滿了,且有新的連接的時候典徘,對方可能會收到出錯信息蟀苛。
    if (listen(server_socket, 5) == -1) {
        perror("listen error");
        return 1;
    }

    struct sockaddr_in client_address;
    socklen_t address_len;
    int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &address_len);
    //返回的client_socket為一個全相關(guān)的socket,其中包含client的地址和端口信息逮诲,通過client_socket可以和客戶端進(jìn)行通信帜平。
    if (client_socket == -1) {
        perror("accept error");
        return -1;
    }
    
    char recv_msg[1024];
    char reply_msg[1024];
    
    while (1) {
        bzero(recv_msg, 1024);
        bzero(reply_msg, 1024);
        
        printf("reply:");
        scanf("%s",reply_msg);
        send(client_socket, reply_msg, 1024, 0);
        
        long byte_num = recv(client_socket,recv_msg,1024,0);
        recv_msg[byte_num] = '\0';
        printf("client said:%s\n",recv_msg);

    }
    
    return 0;
}

Client端代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

int main (int argc, const char * argv[])
{
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(11332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero),8);
    
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket error");
        return 1;
    }
    char recv_msg[1024];
    char reply_msg[1024];
    
    if (connect(server_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in))==0)     {
    //connect 成功之后,其實(shí)系統(tǒng)將你創(chuàng)建的socket綁定到一個系統(tǒng)分配的端口上梅鹦,且其為全相關(guān)裆甩,包含服務(wù)器端的信息,可以用來和服務(wù)器端進(jìn)行通信齐唆。
        while (1) {
            bzero(recv_msg, 1024);
            bzero(reply_msg, 1024);
            long byte_num = recv(server_socket,recv_msg,1024,0);
            recv_msg[byte_num] = '\0';
            printf("server said:%s\n",recv_msg);
            
            printf("reply:");
            scanf("%s",reply_msg);
            if (send(server_socket, reply_msg, 1024, 0) == -1) {
                perror("send error");
            }
        }
        
    }
    
    // insert code here...
    printf("Hello, World!\n");
    return 0;
}

上面的服務(wù)器端和客戶端連接成功之后打開的端口的情況是怎么樣的呢嗤栓? * 服務(wù)器端 ,存在一個用于listen的半相關(guān)的socket,一個用于和客戶端進(jìn)行通信的全相關(guān)的socket
服務(wù)器端進(jìn)程打開文件
服務(wù)器端進(jìn)程打開文件

* 客戶端 存在一個用于和服務(wù)器端進(jìn)行通信的全相關(guān)的socket
客戶端進(jìn)程打開文件
由于accept只運(yùn)行了一次箍邮,所以服務(wù)器端一次只能和一個客戶端進(jìn)行通信茉帅,且使用的send和recv方法都是阻塞的,所以上面這個例子存在一個問題就是服務(wù)器端客戶端連接成功之后锭弊,發(fā)送堪澎,接受,發(fā)送味滞,接受的次序就被固定了樱蛤。比如服務(wù)器端發(fā)送消息之后就等客戶端發(fā)送消息了,沒有接受到客戶端的消息之前服務(wù)器端是沒有辦法發(fā)送消息的剑鞍。使用select這個這個系統(tǒng)調(diào)用可以解決上面的問題昨凡。

四.使用select select這個系統(tǒng)調(diào)用,是一種多路復(fù)用IO方案蚁署,可以同時對多個文件描述符進(jìn)行監(jiān)控便脊,從而知道哪些文件描述符可讀,可寫或者出錯形用,不過select方法是阻塞的就轧,可以設(shè)定超時時間。 select使用的步驟如下:

  • 1.創(chuàng)建一個fd_set變量(fd_set實(shí)為包含了一個整數(shù)數(shù)組的結(jié)構(gòu)體)田度,用來存放所有的待檢查的文件描述符
  • 2.清空fd_set變量妒御,并將需要檢查的所有文件描述符加入fd_set
  • 3.調(diào)用select。若返回-1镇饺,則說明出錯;返回0,則說明超時乎莉,返回正數(shù),則為發(fā)生狀態(tài)變化的文件描述符的個數(shù)
  • 4.若select返回大于0,則依次查看哪些文件描述符變的可讀,并對它們進(jìn)行處理
  • 5.返回步驟2惋啃,開始新一輪的檢測 若上面的聊天程序使用select進(jìn)行改寫哼鬓,則是下面這樣的

服務(wù)器端

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define BACKLOG 5 //完成三次握手但沒有accept的隊列的長度
#define CONCURRENT_MAX 8 //應(yīng)用層同時可以處理的連接
#define SERVER_PORT 11332
#define BUFFER_SIZE 1024
#define QUIT_CMD ".quit"
int client_fds[CONCURRENT_MAX];
int main (int argc, const char * argv[])
{
    char input_msg[BUFFER_SIZE];
    char recv_msg[BUFFER_SIZE];   
    //本地地址
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero),8);
    //創(chuàng)建socket
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock_fd == -1) {
        perror("socket error");
        return 1;
    }
    //綁定socket
    int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (bind_result == -1) {
        perror("bind error");
        return 1;
    }
    //listen
    if (listen(server_sock_fd, BACKLOG) == -1) {
        perror("listen error");
        return 1;
    }
    //fd_set
    fd_set server_fd_set;
    int max_fd = -1;
    struct timeval tv;
    tv.tv_sec = 20;
    tv.tv_usec = 0;
    while (1) {
        FD_ZERO(&server_fd_set);
        //標(biāo)準(zhǔn)輸入
        FD_SET(STDIN_FILENO, &server_fd_set);
        if (max_fd < STDIN_FILENO) {
            max_fd = STDIN_FILENO;
        }
        //服務(wù)器端socket
        FD_SET(server_sock_fd, &server_fd_set);
        if (max_fd < server_sock_fd) {
            max_fd = server_sock_fd;
        }
        //客戶端連接
        for (int i = 0; i < CONCURRENT_MAX; i++) {
            if (client_fds[i]!=0) {
                FD_SET(client_fds[i], &server_fd_set);
                
                if (max_fd < client_fds[i]) {
                    max_fd = client_fds[i];
                }
            }
        }
        int ret = select(max_fd+1, &server_fd_set, NULL, NULL, &tv);
        if (ret < 0) {
            perror("select 出錯\n");
            continue;
        }else if(ret == 0){
            printf("select 超時\n");
            continue;
        }else{
            //ret為未狀態(tài)發(fā)生變化的文件描述符的個數(shù)
            if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
                //標(biāo)準(zhǔn)輸入
                bzero(input_msg, BUFFER_SIZE);
                fgets(input_msg, BUFFER_SIZE, stdin);
                //輸入 ".quit" 則退出服務(wù)器
                if (strcmp(input_msg, QUIT_CMD) == 0) {
                    exit(0);
                }
                for (int i=0; i<CONCURRENT_MAX; i++) {
                    if (client_fds[i]!=0) {
                        send(client_fds[i], input_msg, BUFFER_SIZE, 0);
                    }
                }
            }
            if (FD_ISSET(server_sock_fd, &server_fd_set)) {
                //有新的連接請求
                struct sockaddr_in client_address;
                socklen_t address_len;
                int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
                if (client_socket_fd > 0) {
                    int index = -1;
                    for (int i = 0; i < CONCURRENT_MAX; i++) {
                        if (client_fds[i] == 0) {
                            index = i;
                            client_fds[i] = client_socket_fd;
                            break;
                        }
                    }
                    if (index >= 0) {
                        printf("新客戶端(%d)加入成功 %s:%d \n",index,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                    }else{
                        bzero(input_msg, BUFFER_SIZE);
                        strcpy(input_msg, "服務(wù)器加入的客戶端數(shù)達(dá)到最大值,無法加入!\n");
                        send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
                        printf("客戶端連接數(shù)達(dá)到最大值,新客戶端加入失敗 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                    }
                }
            }
            for (int i = 0; i <CONCURRENT_MAX; i++) {
                if (client_fds[i]!=0) {
                    if (FD_ISSET(client_fds[i], &server_fd_set)) {
                        //處理某個客戶端過來的消息
                        bzero(recv_msg, BUFFER_SIZE);
                        long byte_num = recv(client_fds[i],recv_msg,BUFFER_SIZE,0);
                        if (byte_num > 0) {
                            if (byte_num > BUFFER_SIZE) {
                                byte_num = BUFFER_SIZE;
                            }
                            recv_msg[byte_num] = '\0';
                            printf("客戶端(%d):%s\n",i,recv_msg);
                        }else if(byte_num < 0){
                            printf("從客戶端(%d)接受消息出錯.\n",i);
                        }else{
                            FD_CLR(client_fds[i], &server_fd_set);
                            client_fds[i] = 0;
                            printf("客戶端(%d)退出了\n",i);
                        }
                    }
                }
            }
        }
    }
    return 0;
}

客戶端

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#define BUFFER_SIZE 1024

int main (int argc, const char * argv[])
{
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(11332);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero),8);
    
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock_fd == -1) {
        perror("socket error");
        return 1;
    }
    char recv_msg[BUFFER_SIZE];
    char input_msg[BUFFER_SIZE];
    
    if (connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in))==0) {
        fd_set client_fd_set;
        struct timeval tv;
        tv.tv_sec = 20;
        tv.tv_usec = 0;

        
        while (1) {
            FD_ZERO(&client_fd_set);
            FD_SET(STDIN_FILENO, &client_fd_set);
            FD_SET(server_sock_fd, &client_fd_set);
            
            int ret = select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
            if (ret < 0 ) {
                printf("select 出錯!\n");
                continue;
            }else if(ret ==0){
                printf("select 超時!\n");
                continue;
            }else{
                if (FD_ISSET(STDIN_FILENO, &client_fd_set)) {
                    bzero(input_msg, BUFFER_SIZE);
                    fgets(input_msg, BUFFER_SIZE, stdin);
                    if (send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1) {
                        perror("發(fā)送消息出錯!\n");
                    }
                }
                
                if (FD_ISSET(server_sock_fd, &client_fd_set)) {
                    bzero(recv_msg, BUFFER_SIZE);
                    long byte_num = recv(server_sock_fd,recv_msg,BUFFER_SIZE,0);
                    if (byte_num > 0) {
                        if (byte_num > BUFFER_SIZE) {
                            byte_num = BUFFER_SIZE;
                        }
                        recv_msg[byte_num] = '\0';
                        printf("服務(wù)器:%s\n",recv_msg);
                    }else if(byte_num < 0){
                        printf("接受消息出錯!\n");
                    }else{
                        printf("服務(wù)器端退出!\n");
                        exit(0);
                    }

                }
            }
        }
        
    }
    
    return 0;
}

當(dāng)然select也有其局限性边灭。當(dāng)fd_set中的文件描述符較少异希,或者大都數(shù)文件描述符都比較活躍的時候,select的效率還是不錯的绒瘦。Mac系統(tǒng)中已經(jīng)定義了fd_set 最大可以容納的文件描述符的個數(shù)為1024

//sys/_structs.h
    #define __DARWIN_FD_SETSIZE 1024
    /////////////////////////////////////////////
    //Kernel.framework sys/select.h
    #define FD_SETSIZE  __DARWIN_FD_SETSIZE

每一次select 調(diào)用的時候称簿,都涉及到user space和kernel space的內(nèi)存拷貝,且會對fd_set中的所有文件描述符進(jìn)行遍歷惰帽,如果所有的文件描述符均不滿足憨降,且沒有超時,則當(dāng)前進(jìn)程便開始睡眠该酗,直到超時或者有文件描述符狀態(tài)發(fā)生變化授药。當(dāng)文件描述符數(shù)量較大的時候,將耗費(fèi)大量的CPU時間呜魄。所以后來有新的方案出現(xiàn)了悔叽,如windows2000引入的IOCP,Linux Kernel 2.6中成熟的epoll耕赘,F(xiàn)reeBSD4.x引入的kqueue骄蝇。

五.使用kqueue Mac是基于BSD的內(nèi)核

所使用的是kqueue(kernel event notification mechanism,詳細(xì)內(nèi)容可以Mac中 man 2 kqueue)操骡,kqueue比select先進(jìn)的地方就在于使用事件觸發(fā)的機(jī)制,且其調(diào)用無需每次對所有的文件描述符進(jìn)行遍歷赚窃,返回的時候只返回需要處理的事件册招,而不像select中需要自己去一個個通過FD_ISSET檢查。 kqueue默認(rèn)的觸發(fā)方式是level 水平觸發(fā)勒极,可以通過設(shè)置event的flag為EV_CLEAR 使得這個事件變?yōu)檫呇赜|發(fā),可能epoll的觸發(fā)方式無法細(xì)化到單個event是掰,需要查證。 kqueue中涉及兩個系統(tǒng)調(diào)用辱匿,kqueue()和kevent()

  • kqueue() 創(chuàng)建kernel級別的事件隊列键痛,并返回隊列的文件描述符
  • kevent() 往事件隊列中加入訂閱事件,或者返回相關(guān)的事件數(shù)組 kqueue使用的流程一般如下:
  • 創(chuàng)建kqueue
  • 創(chuàng)建struct kevent變量(注意這里的kevent是結(jié)構(gòu)體類型名)匾七,可以通過EV_SET這個宏提供的快捷方式進(jìn)行創(chuàng)建
  • 通過kevent系統(tǒng)調(diào)用將創(chuàng)建好的kevent結(jié)構(gòu)體變量加入到kqueue隊列中絮短,完成對指定文件描述符的事件的訂閱
  • 通過kevent系統(tǒng)調(diào)用獲取滿足條件的事件隊列,并對每一個事件進(jìn)行處理
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/event.h>
#include <sys/types.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define BACKLOG 5 //完成三次握手但沒有accept的隊列的長度
#define CONCURRENT_MAX 8 //應(yīng)用層同時可以處理的連接
#define SERVER_PORT 11332
#define BUFFER_SIZE 1024
#define QUIT_CMD ".quit"
int client_fds[CONCURRENT_MAX];
struct kevent events[10];//CONCURRENT_MAX + 2
int main (int argc, const char * argv[])
{
    char input_msg[BUFFER_SIZE];
    char recv_msg[BUFFER_SIZE];
    //本地地址
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero),8);
    //創(chuàng)建socket
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock_fd == -1) {
        perror("socket error");
        return 1;
    }
    //綁定socket
    int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (bind_result == -1) {
        perror("bind error");
        return 1;
    }
    //listen
    if (listen(server_sock_fd, BACKLOG) == -1) {
        perror("listen error");
        return 1;
    }
    struct timespec timeout = {10,0};
    //kqueue
    int kq = kqueue();
    if (kq == -1) {
        perror("創(chuàng)建kqueue出錯!\n");
        exit(1);
    }
    struct kevent event_change;
    EV_SET(&event_change, STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, NULL);
    kevent(kq, &event_change, 1, NULL, 0, NULL);
    EV_SET(&event_change, server_sock_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
    kevent(kq, &event_change, 1, NULL, 0, NULL);
    while (1) {
        int ret = kevent(kq, NULL, 0, events, 10, &timeout);
        if (ret < 0) {
            printf("kevent 出錯!\n");
            continue;
        }else if(ret == 0){
            printf("kenvent 超時!\n");
            continue;
        }else{
            //ret > 0 返回事件放在events中
            for (int i = 0; i < ret; i++) {
                struct kevent current_event = events[i];
                //kevent中的ident就是文件描述符
                if (current_event.ident == STDIN_FILENO) {
                    //標(biāo)準(zhǔn)輸入
                    bzero(input_msg, BUFFER_SIZE);
                    fgets(input_msg, BUFFER_SIZE, stdin);
                    //輸入 ".quit" 則退出服務(wù)器
                    if (strcmp(input_msg, QUIT_CMD) == 0) {
                        exit(0);
                    }
                    for (int i=0; i<CONCURRENT_MAX; i++) {
                        if (client_fds[i]!=0) {
                            send(client_fds[i], input_msg, BUFFER_SIZE, 0);
                        }
                    }
                }else if(current_event.ident == server_sock_fd){
                    //有新的連接請求
                    struct sockaddr_in client_address;
                    socklen_t address_len;
                    int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
                    if (client_socket_fd > 0) {
                        int index = -1;
                        for (int i = 0; i < CONCURRENT_MAX; i++) {
                            if (client_fds[i] == 0) {
                                index = i;
                                client_fds[i] = client_socket_fd;
                                break;
                            }
                        }
                        if (index >= 0) {
                            EV_SET(&event_change, client_socket_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
                            kevent(kq, &event_change, 1, NULL, 0, NULL);
                            printf("新客戶端(fd = %d)加入成功 %s:%d \n",client_socket_fd,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                        }else{
                            bzero(input_msg, BUFFER_SIZE);
                            strcpy(input_msg, "服務(wù)器加入的客戶端數(shù)達(dá)到最大值,無法加入!\n");
                            send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
                            printf("客戶端連接數(shù)達(dá)到最大值昨忆,新客戶端加入失敗 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                        }
                    }
                }else{
                    //處理某個客戶端過來的消息
                    bzero(recv_msg, BUFFER_SIZE);
                    long byte_num = recv((int)current_event.ident,recv_msg,BUFFER_SIZE,0);
                    if (byte_num > 0) {
                        if (byte_num > BUFFER_SIZE) {
                            byte_num = BUFFER_SIZE;
                        }
                        recv_msg[byte_num] = '\0';
                        printf("客戶端(fd = %d):%s\n",(int)current_event.ident,recv_msg);
                    }else if(byte_num < 0){
                        printf("從客戶端(fd = %d)接受消息出錯.\n",(int)current_event.ident);
                    }else{
                        EV_SET(&event_change, current_event.ident, EVFILT_READ, EV_DELETE, 0, 0, NULL);
                        kevent(kq, &event_change, 1, NULL, 0, NULL);
                        close((int)current_event.ident);
                        for (int i = 0; i < CONCURRENT_MAX; i++) {
                            if (client_fds[i] == (int)current_event.ident) {
                                client_fds[i] = 0;
                                break;
                            }
                        }
                        printf("客戶端(fd = %d)退出了\n",(int)current_event.ident);
                    }
                }
            }
        }
    }
    return 0;
}

其實(shí)kqueue的應(yīng)用場景非常的廣闊丁频,可以監(jiān)控文件系統(tǒng)中文件的變化(對文件變化的事件可以粒度非常的細(xì),具體可以查看kqueue的手冊),監(jiān)控系統(tǒng)進(jìn)程的生命周期席里。GCD的事件處理便是建立在kqueue之上的叔磷。

六.使用Streams

使用Objective-C的一大優(yōu)點(diǎn)便是面向?qū)ο缶幊蹋沟眠壿嫵橄蟮酶觾?yōu)美奖磁,更加符合人類思維改基。 一開始說過,無論是對于文件的操作或者對于網(wǎng)絡(luò)的操作咖为,本質(zhì)上都是IO操作寥裂,無非寫數(shù)據(jù)和讀數(shù)據(jù),可以對這種輸入輸出進(jìn)行抽象案疲,抽象成輸入流和輸出流封恰, 從輸入流中讀取數(shù)據(jù),往輸出流中寫數(shù)據(jù)褐啡。 Cocoa中的NSInputStream和NSOutputStream便是輸入流和輸出流的抽象诺舔,它們的實(shí)現(xiàn)分別基于CoreFoundation中的CFReadStream和CFWriteStream。 輸入輸出流對runloop有很好的支持备畦。 NSInputStream和CFReadStream以及NSOutputStream和CFWriteStream之間可以通過 "toll-free bridging"實(shí)現(xiàn)無縫的類型轉(zhuǎn)換低飒。 CoreFoundation中的CFStream提供了輸入輸出流和CFSocket綁定的函數(shù)。 這樣便可以通過輸入輸出流和遠(yuǎn)端進(jìn)行通信了懂盐。 首先通過XCode創(chuàng)建一個Foundation(C的也行褥赊,但是你得將main.c 改成main.m)的命令行項目. 創(chuàng)建一個ChatServer的類,包含一個run的方法莉恼。在Cocoa的程序中有一點(diǎn)是和C語言不同的拌喉,你無需自己去寫一個死循環(huán)充當(dāng)runloop,框架本身就對runloop進(jìn)行了支持俐银,需要做的就是將事件源加入到當(dāng)前線程的runloop中尿背,然后啟動runloop。 所以在run方法中捶惜,創(chuàng)建好用于偵聽連接請求的socket田藐,socket有對應(yīng)的處理連接accept的回調(diào)函數(shù),以及把它封裝成runloop的輸入源吱七,加入到當(dāng)前runloop汽久。 我們還得從標(biāo)準(zhǔn)輸入獲取需要發(fā)送消息,所以使用了CFFileDescriptor踊餐,它是文件描述符的objc的封裝景醇,加入了runloop的支持,通過它可以將標(biāo)準(zhǔn)輸入以輸入源的方法加入到當(dāng)前runloop市袖,當(dāng)標(biāo)準(zhǔn)輸入緩沖區(qū)有數(shù)據(jù)可讀的時候啡直,設(shè)置好的回調(diào)函數(shù)便會被調(diào)用烁涌。 最后啟動runloop。
ChatServer中的run方法

- (BOOL)run:(NSError **)error{
    BOOL successful = YES;
    CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};
    _socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, 
                             IPPROTO_TCP, 
                             kCFSocketAcceptCallBack,
                             (CFSocketCallBack)&SocketConnectionAcceptedCallBack,
                             &socketCtxt);
    if (NULL == _socket) {
        if (nil != error) {
            *error = [[NSError alloc] 
                      initWithDomain:ServerErrorDomain
                      code:kServerNoSocketsAvailable
                      userInfo:nil];
        }
        successful = NO;
    }
    if(YES == successful) {
        // enable address reuse
        int yes = 1;
        setsockopt(CFSocketGetNative(_socket), 
                   SOL_SOCKET, SO_REUSEADDR,
                   (void *)&yes, sizeof(yes));
        uint8_t packetSize = 128;
        setsockopt(CFSocketGetNative(_socket),
                   SOL_SOCKET, SO_SNDBUF,
                   (void *)&packetSize, sizeof(packetSize));
        setsockopt(CFSocketGetNative(_socket),
                   SOL_SOCKET, SO_RCVBUF,
                   (void *)&packetSize, sizeof(packetSize));
        struct sockaddr_in addr4;
        memset(&addr4, 0, sizeof(addr4));
        addr4.sin_len = sizeof(addr4);
        addr4.sin_family = AF_INET;
        addr4.sin_port = htons(CHAT_SERVER_PORT); 
        addr4.sin_addr.s_addr = htonl(INADDR_ANY);
        NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
        if (kCFSocketSuccess != CFSocketSetAddress(_socket, (CFDataRef)address4)) {
            if (error) *error = [[NSError alloc] 
                                 initWithDomain:ServerErrorDomain
                                 code:kServerCouldNotBindToIPv4Address
                                 userInfo:nil];
            if (_socket) CFRelease(_socket);
            _socket = NULL;
            successful = NO;
        } else {
            // now that the binding was successful, we get the port number 
            NSData *addr = [(NSData *)CFSocketCopyAddress(_socket) autorelease];
            memcpy(&addr4, [addr bytes], [addr length]);
            self.port = ntohs(addr4.sin_port);
            // 將socket 輸入源加入到當(dāng)前的runloop
            CFRunLoopRef cfrl = CFRunLoopGetCurrent();
            CFRunLoopSourceRef source4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
            CFRunLoopAddSource(cfrl, source4, kCFRunLoopDefaultMode);
            CFRelease(source4);             
            //標(biāo)準(zhǔn)輸入酒觅,當(dāng)在命令行中輸入時撮执,回調(diào)函數(shù)便會被調(diào)用
            CFFileDescriptorContext context = {0,self,NULL,NULL,NULL};
            CFFileDescriptorRef stdinFDRef = CFFileDescriptorCreate(kCFAllocatorDefault, STDIN_FILENO, true, FileDescriptorCallBack, &context);
            CFFileDescriptorEnableCallBacks(stdinFDRef,kCFFileDescriptorReadCallBack);
            CFRunLoopSourceRef stdinSource = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, stdinFDRef, 0);
            CFRunLoopAddSource(cfrl, stdinSource, kCFRunLoopDefaultMode);
            CFRelease(stdinSource);
            CFRelease(stdinFDRef); 
            CFRunLoopRun();
        }
    }
    return successful;
}

當(dāng)有客戶端連接請求過來時, SocketConnectionAcceptedCallBack這個回調(diào)函數(shù)會被調(diào)用舷丹,根據(jù)新的全相關(guān)的socket抒钱,生成輸入輸出流,并設(shè)置輸入輸出流的delegate方法颜凯,將其添加到當(dāng)前的runloop谋币,這樣流中有數(shù)據(jù)過來的時候,delegate方法會被調(diào)用症概。 SocketConnectionAcceptedCallBack函數(shù)

static void SocketConnectionAcceptedCallBack(CFSocketRef socket, 
                                             CFSocketCallBackType type, 
                                             CFDataRef address, 
                                             const void *data, void *info) {
    ChatServer *theChatServer = (ChatServer *)info;
    if (kCFSocketAcceptCallBack == type) { 
        // 摘自kCFSocketAcceptCallBack的文檔蕾额,New connections will be automatically accepted and the callback is called with the data argument being a pointer to a CFSocketNativeHandle of the child socket. This callback is usable only with listening sockets.
        CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
        // create the read and write streams for the connection to the other process
        CFReadStreamRef readStream = NULL;
        CFWriteStreamRef writeStream = NULL;
        CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle,
                                     &readStream, &writeStream);
        if(NULL != readStream && NULL != writeStream) {
            CFReadStreamSetProperty(readStream, 
                                    kCFStreamPropertyShouldCloseNativeSocket,
                                    kCFBooleanTrue);
            CFWriteStreamSetProperty(writeStream, 
                                     kCFStreamPropertyShouldCloseNativeSocket,
                                     kCFBooleanTrue);
            NSInputStream *inputStream = (NSInputStream *)readStream;//toll-free bridging
            NSOutputStream *outputStream = (NSOutputStream *)writeStream;//toll-free bridging
            inputStream.delegate = theChatServer;
            [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [inputStream open];
            outputStream.delegate = theChatServer;
            [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [outputStream open];
            Client *aClient = [[Client alloc] init];
            aClient.inputStream = inputStream;
            aClient.outputStream = outputStream;
            aClient.sock_fd = nativeSocketHandle;
            [theChatServer.clients setValue:aClient  
                                     forKey:[NSString stringWithFormat:@"%d",inputStream]];
            NSLog(@"有新客戶端(sock_fd=%d)加入",nativeSocketHandle);
        } else {
            close(nativeSocketHandle);
        }
        if (readStream) CFRelease(readStream);
        if (writeStream) CFRelease(writeStream);
    }
}

當(dāng)客戶端有數(shù)據(jù)傳過來時,相應(yīng)的NSInputStream的delegate方法被調(diào)用

- (void) stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode {
    switch (eventCode) {
        case NSStreamEventOpenCompleted: {
            break;
        }
        case NSStreamEventHasBytesAvailable: {
            Client *client = [self.clients objectForKey:[NSString stringWithFormat:@"%d",stream]];
            NSMutableData *data = [NSMutableData data];
            uint8_t *buf = calloc(128, sizeof(uint8_t));
            NSUInteger len = 0;
            while([(NSInputStream*)stream hasBytesAvailable]) {
                len = [(NSInputStream*)stream read:buf maxLength:128];
                if(len > 0) {
                    [data appendBytes:buf length:len];
                }
            }
            free(buf);
            if ([data length] == 0) {
                //客戶端退出
                NSLog(@"客戶端(sock_fd=%d)退出",client.sock_fd);
                [self.clients removeObjectForKey:[NSString stringWithFormat:@"%d",stream]];
                close(client.sock_fd);
            }else{
                NSLog(@"收到客戶端(sock_fd=%d)消息:%@",client.sock_fd,[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
            }
            break;
        }
        case NSStreamEventHasSpaceAvailable: {
            break;
        }
        case NSStreamEventEndEncountered: {
            break;
        }
        case NSStreamEventErrorOccurred: {
            break;
        }
        default:
            break;
    }
}

當(dāng)在debug窗口中輸入內(nèi)容并回車時彼城,標(biāo)準(zhǔn)輸入緩沖區(qū)中便有數(shù)據(jù)了诅蝶,這個時候回調(diào)函數(shù)FileDescriptorCallBack將被調(diào)用,處理標(biāo)準(zhǔn)輸入募壕。

static void FileDescriptorCallBack(CFFileDescriptorRef f,
                                   CFOptionFlags callBackTypes,
                                   void *info){
    int fd = CFFileDescriptorGetNativeDescriptor(f);
    ChatServer *theChatServer = (ChatServer *)info;
    if (fd == STDIN_FILENO) {
        NSData *inputData = [[NSFileHandle fileHandleWithStandardInput] availableData];
        NSString *inputString = [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] autorelease];
        NSLog(@"準(zhǔn)備發(fā)送消息:%@",inputString);
        for (Client *client in [theChatServer.clients allValues]) {
            [client.outputStream write:[inputData bytes] maxLength:[inputData length]];
        }
        //處理完數(shù)據(jù)之后必須重新Enable 回調(diào)函數(shù)
        CFFileDescriptorEnableCallBacks(f,kCFFileDescriptorReadCallBack);
    }
}

服務(wù)器端

創(chuàng)建Socket
將Socket和本地的地址端口綁定
開始進(jìn)行偵聽
握手成功调炬,接受請求,得到一個新的Socket舱馅,通過它可以和客戶端進(jìn)行通信

客戶端

創(chuàng)建一個Socket和服務(wù)器的地址并通過它們向服務(wù)器發(fā)送連接請求
連接成功缰泡,客戶端的Socket會綁定到系統(tǒng)分配的一個端口上,并可以通過它和服務(wù)器端進(jìn)行通信

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末代嗤,一起剝皮案震驚了整個濱河市棘钞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌资溃,老刑警劉巖武翎,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溶锭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)符隙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門趴捅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霹疫,你說我怎么就攤上這事拱绑。” “怎么了丽蝎?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵猎拨,是天一觀的道長膀藐。 經(jīng)常有香客問我,道長红省,這世上最難降的妖魔是什么额各? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮吧恃,結(jié)果婚禮上虾啦,老公的妹妹穿的比我還像新娘。我一直安慰自己痕寓,他們只是感情好傲醉,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呻率,像睡著了一般硬毕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礼仗,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天吐咳,我揣著相機(jī)與錄音,去河邊找鬼藐守。 笑死挪丢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卢厂。 我是一名探鬼主播乾蓬,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼慎恒!你這毒婦竟也來了晦炊?” 一聲冷哼從身側(cè)響起赂蠢,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后达椰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡度迂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年敞临,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片外盯。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡摘盆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饱苟,到底是詐尸還是另有隱情孩擂,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布箱熬,位于F島的核電站类垦,受9級特大地震影響狈邑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蚤认,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一米苹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烙懦,春花似錦驱入、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至掩缓,卻和暖如春雪情,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背你辣。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工巡通, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舍哄。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓宴凉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親表悬。 傳聞我的和親對象是個殘疾皇子弥锄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

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