grpc實戰(zhàn)及基本原理

rpc框架原理

image.png

RPC 框架的目標就是讓遠程服務調(diào)用更加簡單庸毫、透明,RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)偷霉、序列化方式(XML/Json/ 二進制)和通信細節(jié)。服務調(diào)用者可以像調(diào)用本地接口一樣調(diào)用遠程的服務提供者,而不需要關心底層通信細節(jié)和調(diào)用過程习勤。

grpc框架

image.png

grpc

grpc是google開源的一個高性能、跨語言的rpc框架焙格,基于http2協(xié)議图毕,基于protobuf3.x, 基于netty4.x+, grpc與thrift眷唉、avro-rpc等其實在原理上沒有太大區(qū)別予颤,grpc并沒有太多突破性的創(chuàng)新。

對于開發(fā)者而言開發(fā)grpc程序:

  1. 需要使用protobuf定義接口冬阳,即.proto文件
  2. 使用compile工具生成特定語言的執(zhí)行代碼蛤虐,比如java c/c++、python等摩泪,類似thrift笆焰,為了解決跨語言問題
  3. 啟動一個server端,server端通過偵聽指定的port见坑,來等待client連接請求嚷掠,通常使用netty來構(gòu)建捏检,grpc內(nèi)置了netty的支持
  4. 啟動一個或多個client端,client也是基于netty不皆,Client通過與server建立TCP長連接贯城,并發(fā)送請求,Request與Response均被封裝成HTTP2的stream Frame霹娄,通過Netty Channel進行交互能犯。

實例說明

  1. proto文件
    grpc并沒有創(chuàng)造新的序列化協(xié)議,而是使用已有的protobuf犬耻,基于protobuf來聲明數(shù)據(jù)模型和rpc接口服務踩晶。 接下來,我們設計一個sayHello接口枕磁,我們將數(shù)據(jù)模型和RPC接口分別保存在兩個文件中渡蜻。

1)TestModel.proto

syntax = "proto3";  
package com.test.grpc;  
option java_package = "com.test.grpc.service.model";  
message TestRequest{  
    string name  = 1;  
    int32 id    = 2;  
}  
message TestResponse{  
    string message = 1;  
}  
 

2)TestService.proto

syntax = "proto3";  
package com.test.grpc;  
option java_package = "com.test.grpc.service";  
import "TestModel.proto";  
service TestRpcService{  
    rpc sayHello(TestRequest) returns (TestResponse);  
}  

2、生成JAVA代碼

生成代碼计济,我們最好借助于maven插件茸苇,可以在pom文件中增加如下信息:

<pluginRepositories><!-- 插件庫 -->  
    <pluginRepository>  
        <id>protoc-plugin</id>  
        <url>https://dl.bintray.com/sergei-ivanov/maven/</url>  
    </pluginRepository>  
</pluginRepositories>  
<build>  
    <extensions>  
        <extension>  
            <groupId>kr.motd.maven</groupId>  
            <artifactId>os-maven-plugin</artifactId>  
            <version>1.4.0.Final</version>  
        </extension>  
    </extensions>  
    <plugins>  
        <plugin>  
            <groupId>com.google.protobuf.tools</groupId>  
            <artifactId>maven-protoc-plugin</artifactId>  
            <version>0.4.4</version>  
            <configuration>  
                <protocArtifact>com.google.protobuf:protoc:3.0.0-beta-2:exe:${os.detected.classifier}</protocArtifact>  
                <pluginId>grpc-java</pluginId>  
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>  
            </configuration>  
            <executions>  
                <execution>  
                    <goals>  
                        <goal>compile</goal>  
                        <goal>compile-custom</goal>  
                    </goals>  
                </execution>  
            </executions>  
        </plugin>  
    </plugins>  
</build>  

然后只需要執(zhí)行“mvn compile”指令即可,此后我們會在項目的target目錄下看到生成的classes文件

3沦寂、開發(fā)Server端服務
//server端實現(xiàn)類,擴展原有接口  
public class TestServiceImpl implements TestRpcServiceGrpc.TestRpcService {  
  
    @Override  
    public void sayHello(TestModel.TestRequest request, StreamObserver<TestModel.TestResponse> responseObserver) {  
        String result = request.getName() + request.getId();  
        TestModel.TestResponse response = TestModel.TestResponse.newBuilder().setMessage(result).build();  
        responseObserver.onNext(response);  
        responseObserver.onCompleted();  
    }  
}  
public class TestServer {  
  
    public static void main(String[] args) throws Exception{  
  
        ServerImpl server = NettyServerBuilder.forPort(50010).addService(TestRpcServiceGrpc.bindService(new TestServiceImpl())).build();  
        server.start();  
        server.awaitTermination();//阻塞直到退出  
    }  
}  

4传藏、開發(fā)Client端

public class TestClient {  
  
    private final TestRpcServiceGrpc.TestRpcServiceBlockingStub client;  
    public TestClient(String host,int port) {  
        ManagedChannel channel =  NettyChannelBuilder.forAddress(host, port).usePlaintext(true).build();  
        client = TestRpcServiceGrpc.newBlockingStub(channel).withDeadlineAfter(60000, TimeUnit.MILLISECONDS);  
    }  
  
    public String sayHello(String name,Integer id) {  
        TestModel.TestRequest request = TestModel.TestRequest.newBuilder().setId(id).setName(name).build();  
        TestModel.TestResponse response = client.sayHello(request);  
        return response.getMessage();  
    }  
}  

原理解析

GRPC的Client與Server腻暮,均通過Netty Channel作為數(shù)據(jù)通信,序列化漩氨、反序列化則使用Protobuf西壮,每個請求都將被封裝成HTTP2的Stream遗增,在整個生命周期中叫惊,客戶端Channel應該保持長連接,而不是每次調(diào)用重新創(chuàng)建Channel做修、響應結(jié)束后關閉Channel(即短連接霍狰、交互式的RPC),目的就是達到鏈接的復用饰及,進而提高交互效率蔗坯。

1、Server端
gRPC 服務端創(chuàng)建采用 Build 模式燎含,對底層服務綁定宾濒、transportServer 和 NettyServer 的創(chuàng)建和實例化做了封裝和屏蔽,讓服務調(diào)用者不用關心 RPC 調(diào)用細節(jié)屏箍,整體上分為三個過程:

創(chuàng)建 Netty HTTP/2 服務端绘梦;
將需要調(diào)用的服務端接口實現(xiàn)類注冊到內(nèi)部的 Registry 中橘忱,RPC 調(diào)用時,可以根據(jù) RPC 請求消息中的服務定義信息查詢到服務接口實現(xiàn)類卸奉;
創(chuàng)建 gRPC Server钝诚,它是 gRPC 服務端的抽象,聚合了各種 Listener榄棵,用于 RPC 消息的統(tǒng)一調(diào)度和處理凝颇。

image.png

我們通常使用NettyServerBuilder,即IO處理模型基于Netty疹鳄,將來可能會支持其他的IO模型拧略。Netty Server的IO模型簡析:

1)創(chuàng)建ServerBootstrap,設定BossGroup與workerGroup線程池

2)注冊childHandler瘪弓,用來處理客戶端鏈接中的請求成幀

3)bind到指定的port辑鲤,即內(nèi)部初始化ServerSocketChannel等月褥,開始偵聽和接受客戶端鏈接。

4)BossGroup中的線程用于accept客戶端鏈接导狡,并轉(zhuǎn)發(fā)(輪訓)給workerGroup中的線程。

5)workerGroup中的特定線程用于初始化客戶端鏈接走贪,初始化pipeline和handler坠狡,并將其注冊到worker線程的selector上(每個worker線程持有一個selector逃沿,不共享)

6)selector上發(fā)生讀寫事件后凯亮,獲取事件所屬的鏈接句柄假消,然后執(zhí)行handler(inbound),同時進行拆封package亿傅,handler執(zhí)行完畢后葵擎,數(shù)據(jù)寫入通過酬滤,由outbound handler處理(封包)通過鏈接發(fā)出盯串。    注意每個worker線程上的數(shù)據(jù)請求是隊列化的体捏。

GRPC而言几缭,只是對Netty Server的簡單封裝年栓,底層使用了PlaintextHandler某抓、Http2ConnectionHandler的相關封裝等否副。

1)bossEventLoopGroup:如果沒指定备禀,默認為一個static共享的對象痹届,即JVM內(nèi)所有的NettyServer都使用同一個Group蚕捉,默認線程池大小為1。

2)workerEventLoopGroup:如果沒指定为严,默認為一個static共享的對象第股,線程池大小為coreSize * 2夕吻。這兩個對象采用默認值并不會帶來問題涉馅;

3)channelType:默認為NioServerSocketChannel黄虱,通常我們采用默認值晤揣;當然你也可以開發(fā)自己的類朱灿。如果此值為NioServerSocketChannel滞诺,則開啟keepalive习霹,同時設定SO_BACKLOG為128淋叶;BACKLOG就是系統(tǒng)底層已經(jīng)建立引入鏈接但是尚未被accept的Socket隊列的大小煞檩,在鏈接密集型(特別是短連接)時斟湃,如果隊列超過此值注暗,新的創(chuàng)建鏈接請求將會被拒絕
sysctl -a|grep tcp_keepalive  
net.ipv4.tcp_keepalive_time = 60  ##單位:秒  
net.ipv4.tcp_keepalive_probes = 9  
net.ipv4.tcp_keepalive_intvl = 75 ##單位:秒  
可以在/etc/sysctl.conf查看和修改相關值  
tcp_keepalive_time:最后一個實際數(shù)據(jù)包發(fā)送完畢后捆昏,首個keepalive探測包發(fā)送的時間。  
如果首個keepalive包探測成功寇仓,那么鏈接會被標記為keepalive(首先TCP開啟了keepalive)  
此后此參數(shù)將不再生效焚刺,而是使用下述的2個參數(shù)繼續(xù)探測  
tcp_keepalive_intvl:此后乳愉,無論通道上是否發(fā)生數(shù)據(jù)交換蔓姚,keepalive探測包發(fā)送的時間間隔  
tcp_keepalive_probes:在斷定鏈接失效之前坡脐,嘗試發(fā)送探測包的次數(shù)备闲;  
如果都失敗恬砂,則斷定鏈接已關閉。  
4)followControlWindow:流量控制的窗口大小梧奢,單位:字節(jié)亲轨,默認值為1M希柿,HTTP2中的“Flow Control”特性曾撤;連接上,已經(jīng)發(fā)送尚未ACK的數(shù)據(jù)幀大小巫湘,比如window大小為100K装悲,且winow已滿,每次向Client發(fā)送消息時尚氛,如果客戶端反饋ACK(攜帶此次ACK數(shù)據(jù)的大芯髡铩),window將會減掉此大性乃弧属瓣;每次向window中添加亟待發(fā)送的數(shù)據(jù)時,window增加讯柔;如果window中的數(shù)據(jù)已達到限定值抡蛙,它將不能繼續(xù)添加數(shù)據(jù),只能等待Client端ACK捣炬。

5)maxConcurrentCallPerConnection:每個connection允許的最大并發(fā)請求數(shù),默認值為Integer.MAX_VALUE;如果此連接上已經(jīng)接受但尚未響應的streams個數(shù)達到此值,新的請求將會被拒絕。為了避免TCP通道的過度擁堵,我們可以適度調(diào)整此值化撕,以便Server端平穩(wěn)處理热芹,畢竟buffer太多的streams會對server的內(nèi)存造成巨大壓力

6)maxMessageSize:每次調(diào)用允許發(fā)送的最大數(shù)據(jù)量,默認為100M。

7)maxHeaderListSize:每次調(diào)用允許發(fā)送的header的最大條數(shù)捌刮,GRPC中默認為8192俄认。

gRPC 的請求消息由 Netty HTTP/2 協(xié)議棧接入钾埂,通過 gRPC 注冊的 Http2FrameListener,將解碼成功之后的 HTTP Header 和 HTTP Body 發(fā)送到 gRPC 的 NettyServerHandler 中妓湘,實現(xiàn)基于 HTTP/2 的 RPC 請求消息接入鬼佣。

image.png
image.png

GRPC Server端翁狐,還有一個最終要的方法:addService

在此之前蛇耀,我們需要介紹一下bindService方法崎脉,每個GRPC生成的service代碼中都有此方法,它以硬編碼的方式遍歷此service的方法列表灶体,將每個方法的調(diào)用過程都與“被代理實例”綁定蝎抽,這個模式有點類似于靜態(tài)代理破花,比如調(diào)用sayHello方法時捂寿,其實內(nèi)部直接調(diào)用“被代理實例”的sayHello方法(參見MethodHandler.invoke方法杠愧,每個方法都有一個唯一的index待榔,通過硬編碼方式執(zhí)行)bindService方法的最終目的是創(chuàng)建一個ServerServiceDefinition對象,這個對象內(nèi)部位置一個map流济,key為此Service的方法的全名(fullname,{package}.{service}.{method}),value就是此方法的GRPC封裝類(ServerMethodDefinition)腌闯。

/** Definition of a service to be exposed via a Server. */
public final class ServerServiceDefinition {
  /** Convenience that constructs a {@link ServiceDescriptor} simultaneously. */
  public static Builder builder(String serviceName) {
    return new Builder(serviceName);
  }

  public static Builder builder(ServiceDescriptor serviceDescriptor) {
    return new Builder(serviceDescriptor);
  }

  private final ServiceDescriptor serviceDescriptor;
  private final Map<String, ServerMethodDefinition<?, ?>> methods;
源碼分析:
public final class TestRpcServiceGrpc {
private static final int METHODID_SAY_HELLO = 0;  
private static class MethodHandlers<Req, Resp> implements  
      ... {  
    private final TestRpcService serviceImpl;//實際被代理實例  
    private final int methodId;  
  
    public MethodHandlers(TestRpcService serviceImpl, int methodId) {  
      this.serviceImpl = serviceImpl;  
      this.methodId = methodId;  
    }  
  
    @java.lang.SuppressWarnings("unchecked")  
    public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {  
      switch (methodId) {  
        case METHODID_SAY_HELLO:        //通過方法的index來判定具體需要代理那個方法  
          serviceImpl.sayHello((com.test.grpc.service.model.TestModel.TestRequest) request,  
              (io.grpc.stub.StreamObserver<com.test.grpc.service.model.TestModel.TestResponse>) responseObserver);  
          break;  
        default:  
          throw new AssertionError();  
      }  
    }  
    ....  
  }  
  
  public static io.grpc.ServerServiceDefinition bindService(  
      final TestRpcService serviceImpl) {  
    return io.grpc.ServerServiceDefinition.builder(SERVICE_NAME)  
        .addMethod(  
          METHOD_SAY_HELLO,  
          asyncUnaryCall(  
            new MethodHandlers<  
              com.test.grpc.service.model.TestModel.TestRequest,  
              com.test.grpc.service.model.TestModel.TestResponse>(  
                serviceImpl, METHODID_SAY_HELLO)))  
        .build();  
  }  
}

addService方法將會把service保存在內(nèi)部的一個map中绳瘟,key為serviceName(即{package}.{service}),value就是上述bindService生成的對象

那么究竟Server端是如何解析RPC過程的?Client在調(diào)用時會將調(diào)用的service名稱 + method信息保存在一個GRPC“保留”的header中姿骏,那么Server端即可通過獲取這個特定的header信息糖声,就可以得知此stream需要請求的service、以及其method分瘦,那么接下來只需要從上述提到的map中找到service蘸泻,然后找到此method,直接代理調(diào)用即可嘲玫。執(zhí)行結(jié)果在Encoder之后發(fā)送給Client悦施。

因為是map存儲,所以我們需要在定義.proto文件時去团,盡可能的指定package信息抡诞,以避免因為service過多導致名稱可能重復的問題。

Client端

我們使用ManagedChannelBuilder來創(chuàng)建客戶端channel,ManagedChannel是客戶端最核心的類土陪,它表示邏輯上的一個channel昼汗;底層持有一個物理的transport(TCP通道,參見NettyClientTransport)鬼雀,并負責維護此transport的活性顷窒;即在RPC調(diào)用的任何時機,如果檢測到底層transport處于關閉狀態(tài)(terminated)源哩,將會嘗試重建transport

通常情況下鞋吉,我們不需要在RPC調(diào)用結(jié)束后就關閉Channel出刷,Channel可以被一直重用,直到Client不再需要請求位置或者Channel無法真的異常中斷而無法繼續(xù)使用坯辩。當然馁龟,為了提高Client端application的整體并發(fā)能力,我們可以使用連接池模式漆魔,即創(chuàng)建多個ManagedChannel坷檩,然后使用輪訓、隨機等算法改抡,在每次RPC請求時選擇一個Channel即可矢炼。

每個Service客戶端,都生成了2種stub:BlockingStub和FutureStub阿纤;這兩個Stub內(nèi)部調(diào)用過程幾乎一樣句灌,唯一不同的是BlockingStub的方法直接返回Response Model,而FutureStub返回一個Future對象欠拾。BlockingStub內(nèi)部也是基于Future機制胰锌,只是封裝了阻塞等待的過程:

 /**
   * Executes a unary call and blocks on the response.
   *
   * @return the single response message.
   */
  public static <ReqT, RespT> RespT blockingUnaryCall(
      Channel channel, MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, ReqT param) {
    ThreadlessExecutor executor = new ThreadlessExecutor();
    ClientCall<ReqT, RespT> call = channel.newCall(method, callOptions.withExecutor(executor));
    try {
        //也是基于Future  
      ListenableFuture<RespT> responseFuture = futureUnaryCall(call, param);
      //阻塞過程  
      while (!responseFuture.isDone()) {
        try {
          executor.waitAndDrain();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw Status.CANCELLED.withCause(e).asRuntimeException();
        }
      }
      return getUnchecked(responseFuture);
    } catch (Throwable t) {
      call.cancel(null, t);
      throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
    }
  }

創(chuàng)建一個Stub的成本是非常低的,我們可以在每次請求時都通過channel創(chuàng)建新的stub藐窄,這并不會帶來任何問題(只不過是創(chuàng)建了大量對象)资昧;其實更好的方式是,我們應該使用一個Stub發(fā)送多次請求荆忍,即Stub也是可以重用的格带;直到Stub上的狀態(tài)異常而無法使用。

最常見的異常刹枉,就是“io.grpc.StatusRuntimeException: DEADLINE_EXCEEDED”叽唱,即表示DEADLINE時間過期,我們可以為每個Stub配置deadline時間微宝,那么如果此stub被使用的時長超過此值(不是空閑的時間)棺亭,將不能再發(fā)送請求,此時我們應該創(chuàng)建新的Stub芥吟。

很多人想盡辦法來使用“withDeadlineAfter”方法來實現(xiàn)一些奇怪的事情侦铜,此參數(shù)的主要目的就是表明:此stub只能被使用X時長,此后將不能再進行請求钟鸵,應該被釋放钉稍。所以,它并不能實現(xiàn)類似于“keepAlive”的語義棺耍,即使我們需要keepAlive贡未,也應該在Channel級別,而不是在一個Stub上。

如果你使用了連接池俊卤,那么其實連接池不應該關注DEADLINE的錯誤嫩挤,只要Channel本身沒有terminated即可;就把這個問題交給調(diào)用者處理消恍。如果你也對Stub使用了對象池岂昭,那么你就可能需要關注這個情況了,你不應該向調(diào)用者返回一個“DEADLINE”的stub狠怨,或者如果調(diào)用者發(fā)現(xiàn)了DEADLINE约啊,你的對象池應該能夠移除它。

  • 實例化ManagedChannel佣赖,此channel可以被任意多個Stub實例引用恰矩;如上文說述,我們可以通過創(chuàng)建Channel池憎蛤,來提高application整體的吞吐能力外傅。此Channel實例,不應該被shutdown俩檬,直到Client端停止服務萎胰;在任何時候,特別是創(chuàng)建Stub時豆胸,我們應該判定Channel的狀態(tài)奥洼。
synchronized (this) {  
    if (channel.isShutdown() || channel.isTerminated()) {  
        channel = ManagedChannelBuilder.forAddress(poolConfig.host, poolConfig.port).usePlaintext(true).build();  
    }  
    //new Stub  
}  
  
//或者  
ManagedChannel channel = (ManagedChannel)client.getChannel();  
if(channel.isShutdown() || channel.isTerminated()) {  
    client = createBlockStub();  
}  
client.sayHello(...)  

因為Channel是可以多路復用,所以我們用Pool機制(比如commons-pool)也可以實現(xiàn)連接池晚胡,只是這種池并非完全符合GRPC/HTTP2的設計語義,因為GRPC允許一個Channel上連續(xù)發(fā)送多個Requests(然后一次性接收多個Responses)嚼沿,而不是“交互式”的Request-Response模式估盘,當然這么使用并不會有任何問題。

  • 每個RPC方法的調(diào)用骡尽,比如sayHello遣妥,調(diào)用開始后,將會為每個調(diào)用請求創(chuàng)建一個ClientCall實例攀细,其內(nèi)部封裝了調(diào)用的方法箫踩、配置選項(headers)等。此后將會創(chuàng)建Stream對象谭贪,每個Stream都持有唯一的streamId境钟,它是Transport用于分揀Response的憑證。最終調(diào)用的所有參數(shù)都會被封裝在Stream中俭识。

  • 檢測DEADLINE慨削,是否已經(jīng)過期,如果過期,將使用FailingClientStream對象來模擬整個RPC過程缚态,當然請求不會通過通道發(fā)出磁椒,直接經(jīng)過異常流處理過程。

  • 然后獲取transport玫芦,如果此時檢測到transport已經(jīng)中斷浆熔,則重建transport。(自動重連機制桥帆,ClientCallImpl.start()方法)

  • 發(fā)送請求參數(shù)医增,即我們Request實例。一次RPC調(diào)用环葵,數(shù)據(jù)是分多次發(fā)送调窍,但是ClientCall在創(chuàng)建時已經(jīng)綁定到了指定的線程上,所以數(shù)據(jù)發(fā)送總是通過一個線程進行(不會亂序)

  • 將ClientCall實例置為halfClose张遭,即半關閉邓萨,并不是將底層Channel或者Transport半關閉,只是邏輯上限定此ClientCall實例上將不能繼續(xù)發(fā)送任何stream信息菊卷,而是等待Response

  • Netty底層IO將會對reponse數(shù)據(jù)流進行解包(Http2ConnectionDecoder),并根據(jù)streamId分揀Response缔恳,同時喚醒響應的ClientCalls阻塞

  • 如果是BlockingStub,則請求返回洁闰,如果響應中包含應用異常歉甚,則封裝后拋出;如果是網(wǎng)絡異常扑眉,則可能觸發(fā)Channel重建纸泄、Stream重置等。

實戰(zhàn): 構(gòu)建一個簡單的名稱解析服務

可參考本人githubgrpc hello world demo

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腰素,一起剝皮案震驚了整個濱河市聘裁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弓千,老刑警劉巖衡便,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異洋访,居然都是意外死亡镣陕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門姻政,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呆抑,“玉大人,你說我怎么就攤上這事扶歪±矸危” “怎么了摄闸?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長妹萨。 經(jīng)常有香客問我年枕,道長,這世上最難降的妖魔是什么乎完? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任熏兄,我火速辦了婚禮,結(jié)果婚禮上树姨,老公的妹妹穿的比我還像新娘摩桶。我一直安慰自己,他們只是感情好帽揪,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布硝清。 她就那樣靜靜地躺著,像睡著了一般转晰。 火紅的嫁衣襯著肌膚如雪芦拿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天查邢,我揣著相機與錄音蔗崎,去河邊找鬼。 笑死扰藕,一個胖子當著我的面吹牛缓苛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播邓深,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼未桥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芥备?” 一聲冷哼從身側(cè)響起钢属,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎门躯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酷师,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡讶凉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了山孔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懂讯。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖台颠,靈堂內(nèi)的尸體忽然破棺而出褐望,到底是詐尸還是另有隱情勒庄,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布瘫里,位于F島的核電站实蔽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谨读。R本人自食惡果不足惜局装,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劳殖。 院中可真熱鬧铐尚,春花似錦、人聲如沸哆姻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矛缨。三九已至爹脾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間劳景,已是汗流浹背誉简。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盟广,地道東北人闷串。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像筋量,于是被迫代替她去往敵國和親烹吵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

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