iOS GCDAsyncSocket源码分析(一)

序言

上一篇文章文章中,简单介绍了GCDAsyncSocket的使用,socket创建、连接、发送消息、接收消息、关闭socket、粘包分包、以及心跳包机制。并且立下了一个flag,所以在这篇文章,将带来GCDAsyncSocket的源码分析,看看在GCDAsyncSocket中是如何运用原生代码并封装起来的,在简单实现的原生代码基础上,他又做了什么样的操作。

我们还是按照创建socket、连接socket、发送消息、接收消息、关闭socket的顺序,一步一步深入了解GCDAsyncSocket。

1. GCDAsyncSocket初始化

在开始之前,GCDAsyncSocket.m中声明了许许多多的成员变量,先看看都是啥。

@implementation GCDAsyncSocket
{
    //flags,当前正在做操作的标识符
    uint32_t flags;
    uint16_t config;
    
    //代理
    __weak id<GCDAsyncSocketDelegate> delegate;
    //代理回调的queue
    dispatch_queue_t delegateQueue;
    
    //本地IPV4Socket
    int socket4FD;
    //本地IPV6Socket
    int socket6FD;
    //unix域的套接字 // 进程通讯  locahost VS 127.0.0.1
    int socketUN;
    //unix域 服务端 url
    NSURL *socketUrl;
    //状态Index
    int stateIndex;
    
    //本机的IPV4地址  --- 地址host interface
    NSData * connectInterface4;
    //本机的IPV6地址
    NSData * connectInterface6;
    //本机unix域地址
    NSData * connectInterfaceUN;
    
    //这个类的对Socket的操作都在这个queue中,串行
    dispatch_queue_t socketQueue;
    
    // 源 ---> mergdata  get_data buffer tls ssl CFStream
    // data
    dispatch_source_t accept4Source;
    dispatch_source_t accept6Source;
    dispatch_source_t acceptUNSource;
    
    //连接timer,GCD定时器 重连
    dispatch_source_t connectTimer;
    dispatch_source_t readSource;
    dispatch_source_t writeSource;
    dispatch_source_t readTimer;
    dispatch_source_t writeTimer;
   
    //读写数据包数组 类似queue,最大限制为5个包 - FIFO
    NSMutableArray *readQueue;
    NSMutableArray *writeQueue;
    
    //当前正在读写数据包
    GCDAsyncReadPacket *currentRead;
    GCDAsyncWritePacket *currentWrite;
    //当前socket未获取完的数据大小
    unsigned long socketFDBytesAvailable;
    
    //全局公用的提前缓冲区
    GCDAsyncSocketPreBuffer *preBuffer;
        
#if TARGET_OS_IPHONE
    CFStreamClientContext streamContext;
    //读的数据流  ----  c
    CFReadStreamRef readStream;
    //写的数据流
    CFWriteStreamRef writeStream;
#endif
    //SSL上下文,用来做SSL认证
    SSLContextRef sslContext;
    
    //全局公用的SSL的提前缓冲区
    GCDAsyncSocketPreBuffer *sslPreBuffer;
    size_t sslWriteCachedLength;
    
    //记录SSL读取数据错误
    OSStatus sslErrCode;
    //记录SSL握手的错误
    OSStatus lastSSLHandshakeError;
    
    //socket队列的标识key -- key - queue
    void *IsOnSocketQueueOrTargetQueueKey;
    
    id userData;
    
    //连接备选服务端地址的延时 (另一个IPV4或IPV6)
    NSTimeInterval alternateAddressDelay;
}

创建函数

self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];

这个init方法最终将会来到,在这个方法里,socketQueue传值为NULL,所以后面如果有sq的部分可以先行跳过,等梳理完了,再去看看这个sq具体都干了啥。

- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
    if((self = [super init]))
    {
        delegate = aDelegate;
        delegateQueue = dq;
        
         //这个宏是在sdk6.0之后才有的,如果是之前的,则OS_OBJECT_USE_OBJC为0,!0即执行if语句
        //对6.0的适配,如果是6.0以下,则去retain release,6.0之后ARC也管理了GCD
        //作者很细
        #if !OS_OBJECT_USE_OBJC
        
        if (dq) dispatch_retain(dq);
        #endif
        
        //创建socket,先都置为 -1 , 代表socket默认创建失败
        //本机的ipv4
        socket4FD = SOCKET_NULL;
        //ipv6
        socket6FD = SOCKET_NULL;
        //应该是UnixSocket
        socketUN = SOCKET_NULL;
        //url
        socketUrl = nil;
        //状态
        stateIndex = 0;
        //这里并没有sq,可以选择跳过
        if (sq)
        {
            //如果scoketQueue是global的,则报错。断言必须要一个非并行queue。
            NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
                     @"The given socketQueue parameter must not be a concurrent queue.");
            NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
                     @"The given socketQueue parameter must not be a concurrent queue.");
            NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                     @"The given socketQueue parameter must not be a concurrent queue.");
            //拿到scoketQueue
            socketQueue = sq;
            //iOS6之下retain
            #if !OS_OBJECT_USE_OBJC
            dispatch_retain(sq);
            #endif
        }
        else
        {
            //没有的话创建一个socketQueue,  名字为:GCDAsyncSocket,NULL = 串行
            socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
        }
        
        //比如原来为   0X123 -> NULL 变成  0X222->0X123->NULL
        //自己的指针等于自己原来的指针,成二级指针了  看了注释是为了以后省略&,让代码更可读?
        //这里不懂作者的用意,继续往下看
        IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
        
        
        void *nonNullUnusedPointer = (__bridge void *)self;
        
        //dispatch_queue_set_specific给当前队里加一个标识 dispatch_get_specific当前线程取出这个标识,判断是不是在这个队列
        dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
        //读的数组 
        readQueue = [[NSMutableArray alloc] initWithCapacity:5];
        currentRead = nil;
        
        //写的数组
        writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
        currentWrite = nil;
        
        //缓冲区 设置大小为 4kb
        preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
    
#pragma mark alternateAddressDelay??
        //交替地址延时?? wtf 应该是用来给备用地址的
        alternateAddressDelay = 0.3;
    }
    return self;
}

看完这段代码...懵逼。只是一些初始化操作。本来还以为create()会在这里面呢,很无奈啊,哎,先不管了,继续往下看吧。

2. GCDAsyncSocket Connect

外层调用

[self.socket connectToHost:@"127.0.0.1" onPort:8090 withTimeout:-1 error:&error];

底层最终会来到这里,每个方法都好长啊 - - 。这里的inInterface传入的是nil,所以,跟上面那个方法的sq一样,如果有遇到可以选择跳过。

- (BOOL)connectToHost:(NSString *)inHost
               onPort:(uint16_t)port
         viaInterface:(NSString *)inInterface
          withTimeout:(NSTimeInterval)timeout
                error:(NSError **)errPtr
{
    //LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) -- 跟踪当前行为
    LogTrace();
    
    //拿到host ,copy防止值被修改
    NSString *host = [inHost copy];
    //interface?接口?先不管 反正是nil
    NSString *interface = [inInterface copy];
    
    //声明两个__block的临时变量
    __block BOOL result = NO;
    //error信息
    __block NSError *preConnectErr = nil;
    
    //gcdBlock ,都包裹在自动释放池中 :
    // 1: 大量临时变量 connect : 重连
    // 2: 自定义线程管理 : nsoperation
    // 3: 非UI 命令 工具
    dispatch_block_t block = ^{ @autoreleasepool {
        
        // Check for problems with host parameter
        // 翻译:检查host参数 是否存在问题
        if ([host length] == 0)
        {
            NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
            preConnectErr = [self badParamError:msg];
            
            // 其实就是return,大牛的代码真是充满逼格 - ret
            // 里面有注释,有想法的可以自己去看看,大概意思就是
            // 可以让这个return能更快的被read,后面还有很多地方被调用到
            return_from_block;
        }
        
        //一个前置的检查,如果没通过返回,这q个检查里,如果interface有值,则会将本机的IPV4 IPV6的 address设置上。
        // 参数 : 指针 操作同一片内存空间
        // 因为interface 是nil,所以不会执行return
        if (![self preConnectWithInterface:interface error:&preConnectErr])
        {
            return_from_block;
        }

        // We've made it past all the checks.我们已经检查了所有参数
        // It's time to start the connection process.是时候开始连接了
        //flags 做或等运算。 flags标识为开始Socket连接
        flags |= kSocketStarted;
        
        //又是一个{}? 只是为了标记么?
        LogVerbose(@"Dispatching DNS lookup...");
        
        //很可能给我们的服务端的参数是一个可变字符串
        //所以我们需要copy,在Block里同步的执行
        //这种基于Block的异步查找,不需要担心它被改变
        //copy,防止改变
        NSString *hostCpy = [host copy];
        
        //拿到状态 初始化的时候 stateIndex = 0
        int aStateIndex = stateIndex;
        __weak GCDAsyncSocket *weakSelf = self;
        
        //获取全局并发Queue 
        dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //异步执行,这里的autoreleasepool 跟上面的一样,可以往上翻
        dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
            //忽视循环引用,牛逼
        #pragma clang diagnostic push
        #pragma clang diagnostic warning "-Wimplicit-retain-self"
            
            //查找错误
            NSError *lookupErr = nil;
            //server地址数组(包含IPV4 IPV6的地址  sockaddr_in6、sockaddr_in类型)
            NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr];
            
            //strongSelf
            __strong GCDAsyncSocket *strongSelf = weakSelf;
            
            //完整Block安全形态,在加个if
            if (strongSelf == nil) return_from_block;
            
            //如果有错
            if (lookupErr)
            {
                //用cocketQueue
                dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                    //一些错误处理,清空一些数据等等
                    [strongSelf lookup:aStateIndex didFail:lookupErr];
                }});
            }
            //正常
            else
            {
                
                NSData *address4 = nil;
                NSData *address6 = nil;
                //遍历地址数组
                for (NSData *address in addresses)
                {
                    //判断address4为空,且address为IPV4
                    if (!address4 && [[self class] isIPv4Address:address])
                    {
                        address4 = address;
                    }
                    //判断address6为空,且address为IPV6
                    else if (!address6 && [[self class] isIPv6Address:address])
                    {
                        address6 = address;
                    }
                }
                //异步去发起
                dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
                    // 方法名大概是说,address4 address6 两个地址都成功获取到了。
                    [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];
                }});
            }
            
        #pragma clang diagnostic pop
        }});
        
        
        //开启连接超时
        [self startConnectTimeout:timeout];
        
        result = YES;
    }};
    //在socketQueue中执行这个Block
    if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
        block();
    //否则同步的调起这个queue去执行
    else
        dispatch_sync(socketQueue, block);
    
    //如果有错误,赋值错误
    if (errPtr) *errPtr = preConnectErr;
    //把连接是否成功的result返回
    return result;
}

这个connect跟想的也不太一样,并没有熟悉的connect(),有毒。但是!还知道这个方法里都干了啥呢。

[strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6];

一探究竟!

- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
{
    LogTrace();
    
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    //至少有一个server地址
    NSAssert(address4 || address6, @"Expected at least one valid address");
    
    //如果状态不一致,说明断开连接
    if (aStateIndex != stateIndex)
    {
        LogInfo(@"Ignoring lookupDidSucceed, already disconnected");
        
        // The connect operation has been cancelled.
        // That is, socket was disconnected, or connection has already timed out.
        return;
    }
    
    // Check for problems
    //分开判断。
    BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
    BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
    
    if (isIPv4Disabled && (address6 == nil))
    {
        NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address.";
        
        [self closeWithError:[self otherError:msg]];
        return;
    }
    
    if (isIPv6Disabled && (address4 == nil))
    {
        NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address.";
        
        [self closeWithError:[self otherError:msg]];
        return;
    }
    
    // Start the normal connection process
    
    NSError *err = nil;
    //调用连接方法,如果失败,则错误返回
    if (![self connectWithAddress4:address4 address6:address6 error:&err])
    {
        [self closeWithError:err];
    }
}

咦,好像有点苗头,看作者悄咪咪的都干了些啥。

    if (![self connectWithAddress4:address4 address6:address6 error:&err])

继续点进去看看

- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr
{
    LogTrace();
    
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
    
    //输出了两个地址的信息
    LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]);
    LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]);
    
    //判断是否倾向于IPV6
    BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
    
    // Create and bind the sockets
    
    //如果有IPV4地址
    if (address4)
    {
        LogVerbose(@"Creating IPv4 socket");
        // 咦?这不是创建吗,瞧瞧我发现了啥。
        socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr];
    }
    //如果有IPV6地址,同上
    if (address6)
    {
        LogVerbose(@"Creating IPv6 socket");
        
        socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr];
    }
    
    //如果都为空,直接返回
    if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL)
    {
        return NO;
    }
    
    //主选socketFD,备选alternateSocketFD
    int socketFD, alternateSocketFD;
    //主选地址和备选地址
    NSData *address, *alternateAddress;
    
    //IPV6
    if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL)
    {
        socketFD = socket6FD;
        alternateSocketFD = socket4FD;
        address = address6;
        alternateAddress = address4;
    }
    //主选IPV4
    else
    {
        socketFD = socket4FD;
        alternateSocketFD = socket6FD;
        address = address4;
        alternateAddress = address6;
    }
    //拿到当前状态
    int aStateIndex = stateIndex;

    // 我去,这不是连接吗?都悄咪咪的把创建跟连接放在这个方法里了,糟老头子坏得很。
    [self connectSocket:socketFD address:address stateIndex:aStateIndex];
    
    //如果有备选地址
    if (alternateAddress)
    {
        //延迟去连接备选的地址
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{
            [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex];
        });
    }
    
    return YES;
}

作者是真的皮啊,把这么重要的方法,放在一个if里面?骚还是你骚啊。
总算是找到创建跟连接了,说什么也要点进去看看吧。
先看创建

//创建Socket
- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr
{
    // 注意
    // 这个connectInterface 创建socketFD4跟6时,分别是传入了connectInterface4与connectInterface6
    // 这两个值,在preConnectWithInterface时,如果interface不为空,就会赋值,但是interface一直是nil,所以
    // connectInterface4与connectInterface6 都是nil
    
    
    // 创建socket,用的SOCK_STREAM TCP流
    // 总算是看到了熟悉的东西
    int socketFD = socket(family, SOCK_STREAM, 0);
    //如果创建失败 SOCKET_NULL = -1
    if (socketFD == SOCKET_NULL)
    {
        if (errPtr)
            *errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
        
        return socketFD;
    }
    
    //和connectInterface绑定,由于connectInterface 是nil 所以这个方法会放回YES,
    //所以不会走进去
    if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr])
    {
        //绑定失败,直接关闭返回
        [self closeSocket:socketFD];
        
        return SOCKET_NULL;
    }
    
    // Prevent SIGPIPE signals
    //防止终止进程的信号?
    int nosigpipe = 1;
    //SO_NOSIGPIPE是为了避免网络错误,而导致进程退出。用这个来避免系统发送signal
    //setsockopt()函数,用于任意类型、任意状态套接口的设置选项值。百度百科有详解
    setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
    
    return socketFD;
}

再来就是连接socket

- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex
{
    //已连接,关闭连接返回
    if (self.isConnected)
    {
        [self closeSocket:socketFD];
        return;
    }
    
    // Start the connection process in a background queue
    //开始连接过程,在后台queue中
    __weak GCDAsyncSocket *weakSelf = self;
    
    //获取到全局Queue
    dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //新线程
    dispatch_async(globalConcurrentQueue, ^{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"
        //调用connect方法,该函数阻塞线程,所以要异步新线程
        //客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
        int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
        
        //老样子,安全判断
        __strong GCDAsyncSocket *strongSelf = weakSelf;
        if (strongSelf == nil) return_from_block;
        
        //在socketQueue中,开辟线程
        dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool {
            //如果状态为已经连接,关闭连接返回
            if (strongSelf.isConnected)
            {
                [strongSelf closeSocket:socketFD];
                // 又是这个装逼写法
                return_from_block;
            }
            
            //说明连接成功
            if (result == 0)
            {
                //关闭掉另一个没用的socket
                [self closeUnusedSocket:socketFD];
                //调用didConnect,生成stream,改变状态等等!
                [strongSelf didConnect:aStateIndex];
            }
            //连接失败
            else
            {
                //关闭当前socket
                [strongSelf closeSocket:socketFD];
                
                // If there are no more sockets trying to connect, we inform the error to the delegate
                //返回连接错误的error
                if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL)
                {
                    NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"];
                    [strongSelf didNotConnect:aStateIndex error:error];
                }
            }
        }});
        
#pragma clang diagnostic pop
    });
    //输出正在连接中
    LogVerbose(@"Connecting...");
}

至此,我们就看到了socket的创建跟连接的实现原理,接下来说读写操作。
由于篇幅问题这里另起一篇文章看这里

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

推荐阅读更多精彩内容