Socket接口使用方法

Linux内核net/socket.c定义了一套socket的操作api。图1展示了socket层所处与TCP/IP协议栈之上和应用层之下。

socket接口层

socket()函数

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

int socket(int domain, int type, int protocol);

参数:

  • domain: 即协议域,又称为协议族(family)。

    常用的协议组有:

    • AF_INET(ipv4地址(32位的)与端口号(16位的)的组合)
    • AF_INET6( IPv6 的地址族)
    • AF_LOCAL(或称AF_UNIX,Unix域socket)(用一个绝对路径名作为地址)
    • AF_ROUTE
  • type: 指定socket类型。

    常用的socket类型有:

    • SOCK_STREAM
    • SOCK_DGRAM
    • SOCK_RAW
    • SOCK_PACKET
    • SOCK_SEQPACKET等等
  • protocol:指定协议。

    常用的协议有:

    • IPPROTO_TCP(TCP传输协议)
    • IPPTOTO_UDP(UDP传输协议)
    • IPPROTO_SCTP(STCP传输协议)
    • IPPROTO_TIPC等(TIPC传输协议)

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

bind()函数

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd: 即socket描述字,它是通过socket()函数创建了,唯一标识一个socketbind()函数就是将给这个描述字绑定一个名字。

  • addr: 一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:

    struct sockaddr_in {
      short int sin_family; /* 通信类型 */
      unsigned short int sin_port; /* 端口 */
      struct in_addr sin_addr; /* Internet 地址 */
      unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
    
    };
    /* Internet address. */
    struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
    };
    

    注意,原来的sockaddr的格式是(16个字节):

    struct sockaddr {
      unsigned short sa_family; /* 地址家族, AF_xxx */
      char sa_data[14]; /*14字节协议地址*/
    };
    

    sockaddr_in这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero()memset() 来全部置零。 同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也可以被指向结构体sockaddr并且代替它。这样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,并且在最后转换。

    ipv6对应的是:

    struct sockaddr_in6 { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
    };
    
    struct in6_addr { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
    };
    

    Unix域对应的是:

    #define UNIX_PATH_MAX    108
    
    struct sockaddr_un { 
        sa_family_t sun_family;               /* AF_UNIX */ 
        char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
    };
    
  • addrlen:地址的长度。

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-EndianLittle-Endian的定义如下:

a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket

在网络编程中,能够转换两种类型: short (两个字节)和 long (四个字节)。这个函 数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 "h" 表示 "本机 (host)",接着是 "to",然后用 "n" 表 示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。

这里有:

  • htons()--"Host to Network Short"
  • htonl()--"Host to Network Long"
  • ntohs()--"Network to Host Short"
  • ntohl()--"Network to Host Long"

为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?

答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。

处理IP地址

    listenAddr.sin_addr.s_addr = INADDR_ANY;
    // listenAddr.sin_addr.s_addr = inet_addr(IP);
    // inet_pton(AF_INET,IP,&listenAddr.sin_addr);

listen()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket

int listen(int sockfd, int backlog);

参数:

  • sockfd::即socket描述字,唯一的id。
  • backlog:相应socket可以排队的最大连接个数。

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect()函数

客户端调用connect()发出连接请求,服务器端就会接收到这个请求。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:socket描述符。
  • addr:服务器的socket地址。
  • addrlen:socket地址的长度

再一次,你应该检查 connect() 的返回值--它在错误的时候返回-1,并 设置全局错误变量 errno。 同时,你可能看到,我没有调用 bind()。因为我不在乎本地的端口号。 我只关心我要去那。内核将为我选择一个合适的端口号,而我们所连接的 地方也自动地获得这些信息。一切都不用担心。

客户端通过调用connect函数来建立与TCP服务器的连接。

accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd:服务器的socket描述字。
  • addr:指向struct sockaddr *的指针,用于返回客户端的协议地址。
  • addrlen:协议地址的长度。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

send() and recv()函数

两个函数用于流式套接字或者数据报套接字的通讯

send()函数

int send(int sockfd, const void *msg, int len, int flags);

参数:

  • sockfd:想要发送数据的套接字描述符。
  • msg:指向发送的消息数据的指针。
  • flags:一般设置为0.

send() 返回实际发送的数据的字节数--它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是 发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送其它的数据。但是这里也 有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。

recv() 函数

int recv(int sockfd, void *buf, int len, unsigned int flags);

参数:

  • sockfd:要读的套接字描述符。
  • buf:读取数据的缓冲区。
  • len:缓冲区的最大长度。
  • flags:一般设置为0.

recv() 返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。

sendto() 和 recvfrom()函数

既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之 前需要什么信息呢? 不错,是目标地址!

sendto()函数

int sendto(int sockfd, const void *msg, int len, unsigned int flags,
            const struct sockaddr *to, int tolen);

除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。

recvfrom() 函数

int recvfrom(int sockfd, void *buf, int len, unsigned int flags,  
                struct sockaddr *from, int *fromlen);

from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。

recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。

close()函数

关闭文件描述符。

close(sockfd);

shutdown()函数

它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企 图都将返回错误信息。如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭,你可以使用:

int shutdown(int sockfd, int how);

参数:

  • sockfd:是你想要关闭的套接字文件描述符。
  • how:值是下面的其中之 一:
    • 0 – 不允许接受
    • 1 – 不允许发送
    • 2 – 不允许发送和接受(和 close() 一样)

shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可以使用它们的)。

getpeername()函数

函数 getpeername() 告诉你在连接的流式套接字上谁在另外一边。函数是这样的:

#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

参数:

  • sockfd:连接的流式套接字的描述符。
  • addr:是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的信息。
  • addrlen:是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)

函数在错误的时候返回 -1,设置相应的 errno。

一旦你获得它们的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 来打印或者获得更多的信息。但是你不能得到它的帐号

gethostname()函数

它返回你程序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你的机器的 IP 地址。

#include <unistd.h>
int gethostname(char *hostname, size_t size);

参数:

  • hostname:字符串指针,保存返回的主机名。
  • size:hostname 数组的字节长度。

函数调用成功时返回 0,失败时返回 -1,并设置 errno。

简单的实例

服务器demo代码server.c:

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

#define SERVER_PORT 10086     /*定义用户连接端口*/
#define MAX_CONNECT_NUM 10    /*多少等待连接控制*/
const char *IP = "127.0.0.1"; /*设定本地地址*/

void ServerDemo()
{
    int sockFd;                               //服务器的文件描述符
    struct sockaddr_in serverAddr;            // 服务器端的ip地址和端口即协议族
    sockFd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
    if (sockFd == -1)
    {
        printf("create Server Socket error!\n");
        exit(-1);
    }
    // 设置ip地址和协议族及端口
    serverAddr.sin_family = AF_INET;
    // serverAddr.sin_addr.s_addr = inet_addr(IP);
    inet_pton(AF_INET, IP, &serverAddr.sin_addr);
    serverAddr.sin_port = htons(SERVER_PORT);

    bzero(&serverAddr.sin_zero, sizeof(serverAddr.sin_zero));

    // 绑定,服务器需要绑定
    int bind_ret = bind(sockFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (bind_ret == -1)
    {
        printf("bind error!\n");
        if (errno == EACCES)
        {
            exit(1); // 没有权限
        }
        else if (errno == EADDRINUSE)
        {
            exit(2); // 端口被占用
        }
        else
        {
            exit(errno);
        }
    }

    // 监听
    int ret = listen(sockFd, MAX_CONNECT_NUM);
    if (ret == -1)
    {
        printf("listen error!\n");
        exit(-1);
    }

    // do once
    // 接收客户端的连接
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    int connectFd = accept(sockFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
    if (connectFd == -1)
    {
        printf("connect error!\n");
        printf("accept failed, errno:%d\n", errno);
    }
    else
    {
        //  接收一次消息
        char clientIp[INET_ADDRSTRLEN];
        printf("connected with %s:%d\n", inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN), ntohs(clientAddr.sin_port));
        char data[1024];
        recv(connectFd, data, sizeof(data), 0);
        printf("receive data: %s\n", data);
    }
    close(sockFd);  // 关闭套接字
}

int main()
{
    ServerDemo();
    return 0;
}

客户端demo代码client.c:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/wait.h>
#include <arpa/inet.h>

#define PORT 10086            /* 客户机连接远程主机的端口 */
#define MAXDATASIZE 100       /* 每次可以接收的最大字节 */
const char *IP = "127.0.0.1"; /* 设定连接IP地址为本地IP地址 */

void ClientDemo()
{
    int sockFd;
    // 创建客户端套接字
    sockFd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockFd == -1)
    {
        printf("create client socket error!\n");
        exit(-1);
    }
    // 设定需要连接的服务器的IP地址、地址族以及端口
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    // serverAddr.sin_addr.s_addr = inet_addr(IP);
    inet_pton(AF_INET, IP, &serverAddr.sin_addr);
    serverAddr.sin_port = htons(PORT);

    // 连接服务器
    int ret = connect(sockFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));

    if (ret == -1)
    {
        printf("connect failed, errno:%d\n", errno);
    }
    else
    {
        char data[] = "hello,world!";        // 定义缓冲区
        send(sockFd, data, sizeof(data), 0); // 发送数据
    }
    close(sockFd); // 关闭客户端套接字
}
int main()
{
    ClientDemo();
    return 0;
}

分别执行下面的命令:

gcc server.c -o server
gcc client.c -o client

执行

Server
client

<font color='pink'>下面是UDP通信的例子(UDP不存在服务器客户端的概念)</font>

udpListen.c

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

#define MYPORT 4950 /* the port users will be sending to */
#define MAXBUFLEN 100

const char *IP = "127.0.0.1";
void UDP_ListenDemo()
{
    int udpSockFd;
    struct sockaddr_in listenAddr; // 接收方地址
    struct sockaddr_in talkAddr;   // 发送方的地址

    udpSockFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (udpSockFd == -1)
    {
        printf("create udp sock error!\n");
        exit(-1);
    }

    listenAddr.sin_family = AF_INET;
    listenAddr.sin_addr.s_addr = INADDR_ANY;
    // listenAddr.sin_addr.s_addr = inet_addr(IP);
    // inet_pton(AF_INET,IP,&listenAddr.sin_addr);
    listenAddr.sin_port = htons(MYPORT);

    bzero(&listenAddr.sin_zero, sizeof(listenAddr.sin_zero));

    if (bind(udpSockFd, (struct sockaddr *)&listenAddr, sizeof(listenAddr)) == -1)
    {
        printf("bind error!\n");
        exit(-1);
    }

    socklen_t talkAddrLen = sizeof(talkAddr);
    char buf[MAXBUFLEN];
    int numbytes;
    numbytes = recvfrom(udpSockFd, buf, MAXBUFLEN, 0, (struct sockaddr *)(&talkAddr), &talkAddrLen);
    if (numbytes == -1
    {
        printf("revefrom error!\n");
        exit(-1);
    }

    printf("got packet from %s\n", inet_ntoa(talkAddr.sin_addr)); // 得到对方的ip地址
    printf("packet is %d bytes long\n", numbytes);
    buf[numbytes] = '\0';
    printf("packet contains \"%s\"\n", buf); //输出接收的内容
    close(udpSockFd);                        // 关闭UDP套接字
}
int main()
{
    UDP_ListenDemo();
    return 0;
}

udpTalk.c

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

#define MYPORT 4950
const char *IP = "127.0.0.1";

void UDP_TalkDemo()
{
    int udpTalkFd;
    struct sockaddr_in listenAddr;
    struct hostent *he;
    int numbytes;

    udpTalkFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (udpTalkFd == -1)
    {
        fprintf(stdout, "create udp sock error!\n");
        exit(-1);
    }

    listenAddr.sin_family = AF_INET;
    listenAddr.sin_addr.s_addr = inet_addr(IP);
    // inet_pton(AF_INET,IP,&listenAddr.sin_addr);
    listenAddr.sin_port = htons(MYPORT);
    bzero(&listenAddr.sin_zero, sizeof(listenAddr.sin_zero));

    char buf[] = "Hello!";
    numbytes = sendto(udpTalkFd, buf, sizeof(buf), 0, (struct sockaddr *)&listenAddr, sizeof(listenAddr));
    if (numbytes == -1)
    {
        printf("udp send error!\n");
        exit(-1);
    }
    printf("sent %d bytes to %s\n", numbytes, inet_ntoa(listenAddr.sin_addr));
    close(udpTalkFd);
}

int main()
{
    UDP_TalkDemo();
    return 0;
}

编写Makefile进行编译

test: udpListen.c
    gcc udpListen.c -o udpListen
    gcc udpTalk.c -o udpTalk

分别执行:

make test
./udpListen
./udpTalk

结果:

udpListen
udpTalk

参考:

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