Erlang 源码之OTP通用服务器行为模式分析(1)

Erlang并发基于Actor并发模型,容错 Actor之间的通信是异步的,发送方只管发送,不关心超时以及错误,这些都由框架层和独立的错误处理机制接管。Erlang/OTP 是Actor模型的标杆,OTP行为模式的理解非常像面向对象语言的接口,OTP行为模式分成通用部分和具体实现部分,行为模式包括gen_server, gen_statem, gen_event, gen_fsm, supervisor,下面分析它们实现的原理,


可以看到gen_event, gen_fsm, gen_statem, gen_event,依赖于gen这个通用模块,这种关于类似于

sequenceDiagram
调用者->>行为模块: 请求
行为模块->>调用者: 响应
行为模块->>gen: 请求
gen-->>调用者: 响应
下面分析gen源代码
  • gen暴露的方法,
-export([start/5, start/6, debug_options/2, hibernate_after/1,
     name/1, unregister_name/1, get_proc_name/1, get_parent/0,
     call/3, call/4, reply/2, stop/1, stop/3]).

-export([init_it/6, init_it/7]).

-export([format_status_header/2]).

首先看start方法实现代码块

-spec start(module(), linkage(), emgr_name(), module(), term(), options()) ->
    start_ret().

start(GenMod, LinkP, Name, Mod, Args, Options) ->
    case where(Name) of
    undefined ->
        do_spawn(GenMod, LinkP, Name, Mod, Args, Options);
    Pid ->
        {error, {already_started, Pid}}
    end.

-spec start(module(), linkage(), module(), term(), options()) -> start_ret().

start(GenMod, LinkP, Mod, Args, Options) ->
    do_spawn(GenMod, LinkP, Mod, Args, Options).

%%-----------------------------------------------------------------
%% Spawn the process (and link) maybe at another node.
%% If spawn without link, set parent to ourselves 'self'!!!
%%-----------------------------------------------------------------
do_spawn(GenMod, link, Mod, Args, Options) ->
    Time = timeout(Options),
    proc_lib:start_link(?MODULE, init_it,
            [GenMod, self(), self(), Mod, Args, Options], 
            Time,
            spawn_opts(Options));
do_spawn(GenMod, _, Mod, Args, Options) ->
    Time = timeout(Options),
    proc_lib:start(?MODULE, init_it,
           [GenMod, self(), self, Mod, Args, Options], 
           Time,
           spawn_opts(Options)).

do_spawn(GenMod, link, Name, Mod, Args, Options) ->
    Time = timeout(Options),
    proc_lib:start_link(?MODULE, init_it,
            [GenMod, self(), self(), Name, Mod, Args, Options],
            Time,
            spawn_opts(Options));
do_spawn(GenMod, _, Name, Mod, Args, Options) ->
    Time = timeout(Options),
    proc_lib:start(?MODULE, init_it,
           [GenMod, self(), self, Name, Mod, Args, Options], 
           Time,
           spawn_opts(Options)).
-type linkage()    :: 'link' | 'nolink'.
-type emgr_name()  :: {'local', atom()}
                    | {'global', term()}
                    | {'via', Module :: module(), Name :: term()}.

-type start_ret()  :: {'ok', pid()} | 'ignore' | {'error', term()}.

-type debug_flag() :: 'trace' | 'log' | 'statistics' | 'debug'
                    | {'logfile', string()}.
-type option()     :: {'timeout', timeout()}
            | {'debug', [debug_flag()]}
            | {'hibernate_after', timeout()}
            | {'spawn_opt', [proc_lib:spawn_option()]}.
-type options()    :: [option()].

第一个start传入6个参数,GenMod表示属于哪个行为,LinkP表示否启动Link监控进程,link方式可以建立进程之间的双向链接关系,当其中一个进程退出时,另一个进程会收到该进程退出的消息,Name表示进程名称,Mod表示模块名称,Args表示多余参数,Options表示是否debug hibernate spawn_otp某一选项,
第一个start方法进行了where方法判断 判断是否已经存在,如果存在返回改模块已经启动。没有启动则进入do_spwan方法中

where({global, Name}) -> global:whereis_name(Name);
where({via, Module, Name}) -> Module:whereis_name(Name);
where({local, Name})  -> whereis(Name).

do_spwan根据不同的参数,调用proc_lib:start_link或者proc_lib:start,proc_lib用来同步启动符合OTP设计原则的进程,start_link带了进程监控,proc_lib:start_link和proc_lib:spawn_link的不同之处在于,前者是同步创建子进程,后者是异步创建子进程,proc_lib:start_link调用后会阻塞,直到子进程初始化完毕,调用proc_lib:init_ack后才返回。而proc_lib:spawn_link一调用就会立即返回子进程ID,启动完do_spwan将回调启动init_it方法

init_it(GenMod, Starter, Parent, Mod, Args, Options) ->
    init_it2(GenMod, Starter, Parent, self(), Mod, Args, Options).

init_it(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
    case register_name(Name) of
    true ->
        init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options);
    {false, Pid} ->
        proc_lib:init_ack(Starter, {error, {already_started, Pid}})
    end.

init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
    GenMod:init_it(Starter, Parent, Name, Mod, Args, Options).

init_it方法,进行Name注册进程,如果注册失败 则返回,成功则启动行为模块中的init_it方法。

  • debug_options方法
debug_options(Name, Opts) ->
    case lists:keyfind(debug, 1, Opts) of
    {_,Options} ->
        try sys:debug_options(Options)
        catch _:_ ->
            error_logger:format(
              "~tp: ignoring erroneous debug options - ~tp~n",
              [Name,Options]),
            []
        end;
    false ->
        []
    end.
  • call方法
call(Process, Label, Request) -> 
    call(Process, Label, Request, ?default_timeout).

%% Optimize a common case.
call(Process, Label, Request, Timeout) when is_pid(Process),
  Timeout =:= infinity orelse is_integer(Timeout) andalso Timeout >= 0 ->
    do_call(Process, Label, Request, Timeout);
call(Process, Label, Request, Timeout)
  when Timeout =:= infinity; is_integer(Timeout), Timeout >= 0 ->
    Fun = fun(Pid) -> do_call(Pid, Label, Request, Timeout) end,
    do_for_proc(Process, Fun).

do_call(Process, Label, Request, Timeout) when is_atom(Process) =:= false ->
    Mref = erlang:monitor(process, Process),
    erlang:send(Process, {Label, {self(), Mref}, Request}, [noconnect]),

    receive
        {Mref, Reply} ->
            erlang:demonitor(Mref, [flush]),
            {ok, Reply};
        {'DOWN', Mref, _, _, noconnection} ->
            Node = get_node(Process),
            exit({nodedown, Node});
        {'DOWN', Mref, _, _, Reason} ->
            exit(Reason)
    after Timeout ->
            erlang:demonitor(Mref, [flush]),
            exit(timeout)
    end.
    do_for_proc(Pid, Fun) when is_pid(Pid) ->
    Fun(Pid);
%% Local by name
do_for_proc(Name, Fun) when is_atom(Name) ->
    case whereis(Name) of
    Pid when is_pid(Pid) ->
        Fun(Pid);
    undefined ->
        exit(noproc)
    end;
%% Global by name
do_for_proc(Process, Fun)
  when ((tuple_size(Process) == 2 andalso element(1, Process) == global)
    orelse
      (tuple_size(Process) == 3 andalso element(1, Process) == via)) ->
    case where(Process) of
    Pid when is_pid(Pid) ->
        Node = node(Pid),
        try Fun(Pid)
        catch
        exit:{nodedown, Node} ->
            %% A nodedown not yet detected by global,
            %% pretend that it was.
            exit(noproc)
        end;
    undefined ->
        exit(noproc)
    end;
%% Local by name in disguise
do_for_proc({Name, Node}, Fun) when Node =:= node() ->
    do_for_proc(Name, Fun);
%% Remote by name
do_for_proc({_Name, Node} = Process, Fun) when is_atom(Node) ->
    if
    node() =:= nonode@nohost ->
        exit({nodedown, Node});
    true ->
        Fun(Process)
    end.

将监控Process进程,并向该进程发送消息,收到消息,则取消监控。do_for_proc调用Fun函数传入Pid,

  • reply发送消息给客户端
reply({To, Tag}, Reply) ->
    Msg = {Tag, Reply},
    try To ! Msg catch _:_ -> Msg end.
gen_server(通用客户端-服务端)源码分析

暴露的方法:

%% API
-export([start/3, start/4,
     start_link/3, start_link/4,
     stop/1, stop/3,
     call/2, call/3,
     cast/2, reply/2,
     abcast/2, abcast/3,
     multi_call/2, multi_call/3, multi_call/4,
     enter_loop/3, enter_loop/4, enter_loop/5, wake_hib/6]).

需要回调的方法为: init, handle_call, handle_cast, handle_info, handle_continue, terminate,
code_change,format_status,其中init, hand_call, hand_cast, terminate必须回调,

  • 启动方法:
start(Mod, Args, Options) ->
    gen:start(?MODULE, nolink, Mod, Args, Options).

start(Name, Mod, Args, Options) ->
    gen:start(?MODULE, nolink, Name, Mod, Args, Options).

start_link(Mod, Args, Options) ->
    gen:start(?MODULE, link, Mod, Args, Options).

start_link(Name, Mod, Args, Options) ->
    gen:start(?MODULE, link, Name, Mod, Args, Options).

其中start_link实际上启动了一个带监控的进程, 进入gen:start, 在进入回调gen_server中的init_it方法,init其中调用Mod中的init方法进行操作。

init_it(Starter, self, Name, Mod, Args, Options) ->
    init_it(Starter, self(), Name, Mod, Args, Options);
init_it(Starter, Parent, Name0, Mod, Args, Options) ->
    Name = gen:name(Name0),
    Debug = gen:debug_options(Name, Options),
    HibernateAfterTimeout = gen:hibernate_after(Options),

    case init_it(Mod, Args) of
    {ok, {ok, State}} ->
        proc_lib:init_ack(Starter, {ok, self()}),       
        loop(Parent, Name, State, Mod, infinity, HibernateAfterTimeout, Debug);
    {ok, {ok, State, Timeout}} ->
        proc_lib:init_ack(Starter, {ok, self()}),       
        loop(Parent, Name, State, Mod, Timeout, HibernateAfterTimeout, Debug);
    {ok, {stop, Reason}} ->
        %% For consistency, we must make sure that the
        %% registered name (if any) is unregistered before
        %% the parent process is notified about the failure.
        %% (Otherwise, the parent process could get
        %% an 'already_started' error if it immediately
        %% tried starting the process again.)
        gen:unregister_name(Name0),
        proc_lib:init_ack(Starter, {error, Reason}),
        exit(Reason);
    {ok, ignore} ->
        gen:unregister_name(Name0),
        proc_lib:init_ack(Starter, ignore),
        exit(normal);
    {ok, Else} ->
        Error = {bad_return_value, Else},
        proc_lib:init_ack(Starter, {error, Error}),
        exit(Error);
    {'EXIT', Class, Reason, Stacktrace} ->
        gen:unregister_name(Name0),
        proc_lib:init_ack(Starter, {error, terminate_reason(Class, Reason, Stacktrace)}),
        erlang:raise(Class, Reason, Stacktrace)
    end.
init_it(Mod, Args) ->
    try
    {ok, Mod:init(Args)}
    catch
    throw:R -> {ok, R};
    Class:R:S -> {'EXIT', Class, R, S}
    end.

init_it完成之后判断结果的值,成功之后,进入loop方法,这个方法是,预先处理hand_call, hand_cast这些回调方法,当cast call请求后 可以 回调响应的hand_call hand_cast方法。

loop(Parent, Name, State, Mod, {continue, Continue} = Msg, HibernateAfterTimeout, Debug) ->
    Reply = try_dispatch(Mod, handle_continue, Continue, State),
    case Debug of
    [] ->
        handle_common_reply(Reply, Parent, Name, undefined, Msg, Mod,
                HibernateAfterTimeout, State);
    _ ->
        Debug1 = sys:handle_debug(Debug, fun print_event/3, Name, Msg),
        handle_common_reply(Reply, Parent, Name, undefined, Msg, Mod,
                HibernateAfterTimeout, State, Debug1)
    end;

loop(Parent, Name, State, Mod, hibernate, HibernateAfterTimeout, Debug) ->
    proc_lib:hibernate(?MODULE,wake_hib,[Parent, Name, State, Mod, HibernateAfterTimeout, Debug]);

loop(Parent, Name, State, Mod, infinity, HibernateAfterTimeout, Debug) ->
    receive
        Msg ->
            decode_msg(Msg, Parent, Name, State, Mod, infinity, HibernateAfterTimeout, Debug, false)
    after HibernateAfterTimeout ->
        loop(Parent, Name, State, Mod, hibernate, HibernateAfterTimeout, Debug)
    end;

loop(Parent, Name, State, Mod, Time, HibernateAfterTimeout, Debug) ->
    Msg = receive
          Input ->
            Input
      after Time ->
          timeout
      end,
    decode_msg(Msg, Parent, Name, State, Mod, Time, HibernateAfterTimeout, Debug, false).

wake_hib(Parent, Name, State, Mod, HibernateAfterTimeout, Debug) ->
    Msg = receive
          Input ->
          Input
      end,
    decode_msg(Msg, Parent, Name, State, Mod, hibernate, HibernateAfterTimeout, Debug, true).

decode_msg(Msg, Parent, Name, State, Mod, Time, HibernateAfterTimeout, Debug, Hib) ->
    case Msg of
    {system, From, Req} ->
        sys:handle_system_msg(Req, From, Parent, ?MODULE, Debug,
                  [Name, State, Mod, Time, HibernateAfterTimeout], Hib);
    {'EXIT', Parent, Reason} ->
        terminate(Reason, ?STACKTRACE(), Name, undefined, Msg, Mod, State, Debug);
    _Msg when Debug =:= [] ->
        handle_msg(Msg, Parent, Name, State, Mod, HibernateAfterTimeout);
    _Msg ->
        Debug1 = sys:handle_debug(Debug, fun print_event/3,
                      Name, {in, Msg}),
        handle_msg(Msg, Parent, Name, State, Mod, HibernateAfterTimeout, Debug1)
    end.

将进入handle_msg 方法,hangle_msg将处理回调方法,
例如:

try_handle_call(Mod, Msg, From, State) ->
    try
    {ok, Mod:handle_call(Msg, From, State)}
    catch
    throw:R ->
        {ok, R};
    Class:R:Stacktrace ->
        {'EXIT', Class, R, Stacktrace}
    end.

call是同步调用,cast异步调用。
三个handle开头的回调函数对应着三种不同的使用服务器的方式。如下:

gen_server:call ------------- handle_call/3
gen_server:cast ------------- handle_cast/2
用!向服务进程发消息 ------------- handle_info/2
call是有返回值的调用;cast是无返回值的调用,即通知;而直接向服务器进程发的 消息则由handle_info处理。

  • call
    call对应的回调函数handle_call/3在正常情况下的返回值是{reply, Reply, NewState},使用call要小心的是,两个服务器进程不能互相call,不然会死锁。gen_server将调用Module:handle_call/3来处理该请求。
    其中ServerRef 可以是:
    • 进程ID,
    • 进程名:如果gen_server被注册为本地进程的话;
    • {Name,Node}:如果gen_server在其它结点注册的话;
    • {global,GlobalName}:如果gen_server注册为全局进程的话
    call源码如下
call(Name, Request) ->
    case catch gen:call(Name, '$gen_call', Request) of
    {ok,Res} ->
        Res;
    {'EXIT',Reason} ->
        exit({Reason, {?MODULE, call, [Name, Request]}})
    end.

call(Name, Request, Timeout) ->
    case catch gen:call(Name, '$gen_call', Request, Timeout) of
    {ok,Res} ->
        Res;
    {'EXIT',Reason} ->
        exit({Reason, {?MODULE, call, [Name, Request, Timeout]}})
    end.
  • cast
    cast是没有返回值的调用,实际是往这个Pid发送消息。它是一个“异步”的调用,调用后会直接收到 ok,无需等待回调函数执行完毕,直接发送消息,无需返回消息,gen_server将调用Module:handle_cast/2来处理请求。
cast({global,Name}, Request) ->
    catch global:send(Name, cast_msg(Request)),
    ok;
cast({via, Mod, Name}, Request) ->
    catch Mod:send(Name, cast_msg(Request)),
    ok;
cast({Name,Node}=Dest, Request) when is_atom(Name), is_atom(Node) -> 
    do_cast(Dest, Request);
cast(Dest, Request) when is_atom(Dest) ->
    do_cast(Dest, Request);
cast(Dest, Request) when is_pid(Dest) ->
    do_cast(Dest, Request).

do_cast(Dest, Request) -> 
    do_send(Dest, cast_msg(Request)),
    ok.
do_send(Dest, Msg) ->
    try erlang:send(Dest, Msg)
    catch
        error:_ -> ok
    end,
    ok.
  • enter_loop方法
enter_loop(Mod, Options, State) ->
    enter_loop(Mod, Options, State, self(), infinity).

enter_loop(Mod, Options, State, ServerName = {Scope, _})
  when Scope == local; Scope == global ->
    enter_loop(Mod, Options, State, ServerName, infinity);

enter_loop(Mod, Options, State, ServerName = {via, _, _}) ->
    enter_loop(Mod, Options, State, ServerName, infinity);

enter_loop(Mod, Options, State, Timeout) ->
    enter_loop(Mod, Options, State, self(), Timeout).

enter_loop(Mod, Options, State, ServerName, Timeout) ->
    Name = gen:get_proc_name(ServerName),
    Parent = gen:get_parent(),
    Debug = gen:debug_options(Name, Options),
    HibernateAfterTimeout = gen:hibernate_after(Options),
    loop(Parent, Name, State, Mod, Timeout, HibernateAfterTimeout, Debug).

调用enter_loop将进入loop,使一个已存在的进程成为gen_server.函数不会返回,调用该函数的进程将进入gen_server的接收循环,并成为一个gen_server进程。调用进程必须是用proc_lib中的启动函数之一启动的。使用者需要负责对进程做所有初始化工作,包括给进程注册名字。当初始化过程非常复杂,而gen_server行为提供的初始化接口不能完成时,这个函数非就常有用。
Module,Options 和ServerName跟调用gen_server:start[link]/3,4时的意义一样。但是,如果指定了ServerName的话,调用进程必须在调用该函数前已经被注册为ServerName。
State 和Timeout跟Module:init/1返回值中的意义一样。并且回调模块不必导出init/1函数。
失败情况:调用进程不是通过proc_lib中的启动函数启动的,或者没用注册为ServerName

  • abcast 向指定的节点中所有注册为Name的gen_server发送异步请求。不管节点和gen_server Name是否存在,函数都立即返回.gen_server将调用Module:handle_cast/2来处理请求。
abcast(Name, Request) when is_atom(Name) ->
    do_abcast([node() | nodes()], Name, cast_msg(Request)).

abcast(Nodes, Name, Request) when is_list(Nodes), is_atom(Name) ->
    do_abcast(Nodes, Name, cast_msg(Request)).

do_abcast([Node|Nodes], Name, Msg) when is_atom(Node) ->
    do_send({Name,Node},Msg),
    do_abcast(Nodes, Name, Msg);
do_abcast([], _,_) -> abcast.
  • multi_call
    对所指定的多个节点中注册为Name的gen_server进程发送请求作同步调用,然后等待返回。gen_server将调用Module:handle_call/3来处理请求,函数返回一个元组{Replies,BadNodes},其中Replies是以{Node,Reply}为元素的列表,BadNodes是以下情况的节点列表:节点不存在,或以Name为名的gen_server在该节点不存在或无返回。Nodes是发送请求的目标节点名的列表,默认值是所有已知节点的列表[node()|nodes()].Name是每gen_servr在其本地注册的名字。
    Request 可以为任意项,其作为参数之一传递给Module:handle_call/3。
    Timeout为大于0的整数,指定了等待返回值的最长毫秒数,或者设为infinity让函数无限等待。默认值为infinity.如果在指定的时间内没有收到节点的返回
do_multi_call(Nodes, Name, Req, infinity) ->
    Tag = make_ref(),
    Monitors = send_nodes(Nodes, Name, Tag, Req),
    rec_nodes(Tag, Monitors, Name, undefined);
do_multi_call(Nodes, Name, Req, Timeout) ->
    Tag = make_ref(),
    Caller = self(),
    Receiver =
    spawn(
      fun() ->
          %% Middleman process. Should be unsensitive to regular
          %% exit signals. The sychronization is needed in case
          %% the receiver would exit before the caller started
          %% the monitor.
          process_flag(trap_exit, true),
          Mref = erlang:monitor(process, Caller),
          receive
              {Caller,Tag} ->
              Monitors = send_nodes(Nodes, Name, Tag, Req),
              TimerId = erlang:start_timer(Timeout, self(), ok),
              Result = rec_nodes(Tag, Monitors, Name, TimerId),
              exit({self(),Tag,Result});
              {'DOWN',Mref,_,_,_} ->
              %% Caller died before sending us the go-ahead.
              %% Give up silently.
              exit(normal)
          end
      end),
    Mref = erlang:monitor(process, Receiver),
    Receiver ! {self(),Tag},
    receive
    {'DOWN',Mref,_,_,{Receiver,Tag,Result}} ->
        Result;
    {'DOWN',Mref,_,_,Reason} ->
        %% The middleman code failed. Or someone did 
        %% exit(_, kill) on the middleman process => Reason==killed
        exit(Reason)
    end.

multi_call调用了do_multi_call,do_multi_call将启动启动会一个进程,去监控process,向Receiver发送消息,为了避免延时返回污染调用者的消息队列,有一个中间进程来做实际的调用。当延时返回发送结已终止进程时将被丢弃,

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

推荐阅读更多精彩内容