Apache Thrift - 可伸縮的跨語言服務開發框架

前言:

目前流行的服務調用方式有很多種,例如基于 SOAP 消息格式的 Web Service,基于 JSON 消息格式的 RESTful 服務等。其中所用到的數據傳輸方式包括 XML,JSON 等,然而 XML 相對體積太大,傳輸效率低,JSON 體積較小,新穎,但還不夠完善。本文將介紹由 Facebook 開發的遠程服務調用框架 Apache Thrift,它采用接口描述語言定義并創建服務,支持可擴展的跨語言服務開發,所包含的代碼生成引擎可以在多種語言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等創建高效的、無縫的服務,其傳輸數據采用二進制格式,相對 XML 和 JSON 體積更小,對于高并發、大數據量和多語言的環境更有優勢。本文將詳細介紹 Thrift 的使用,并且提供豐富的實例代碼加以解釋說明,幫助使用者快速構建服務。

一個簡單的 Thrift 實例

本文首先介紹一個簡單的 Thrift 實現實例,使讀者能夠快速直觀地了解什么是 Thrift 以及如何使用 Thrift 構建服務。

創建一個簡單的服務 Hello。首先根據 Thrift 的語法規范編寫腳本文件 Hello.thrift,代碼如下:

清單 1. Hello.thrift

namespace java service.demo

service Hello{

string helloString(1:string para)

i32 helloInt(1:i32 para)

bool helloBoolean(1:bool para)

void helloVoid()

string helloNull()

}

其中定義了服務 Hello 的五個方法,每個方法包含一個方法名,參數列表和返回類型。每個參數包括參數序號,參數類型以及參數名。 Thrift 是對 IDL(Interface Definition Language) 描述性語言的一種具體實現。因此,以上的服務描述文件使用 IDL 語法編寫。使用 Thrift 工具編譯 Hello.thrift,就會生成相應的 Hello.java 文件。該文件包含了在 Hello.thrift 文件中描述的服務 Hello 的接口定義,即 Hello.Iface 接口,以及服務調用的底層通信細節,包括客戶端的調用邏輯 Hello.Client 以及服務器端的處理邏輯 Hello.Processor,用于構建客戶端和服務器端的功能。

創建 HelloServiceImpl.java 文件并實現 Hello.java 文件中的 Hello.Iface 接口,代碼如下:

清單 2. HelloServiceImpl.java

package service.demo;

import org.apache.thrift.TException;

public class HelloServiceImpl implements Hello.Iface {

@Override

public boolean helloBoolean(boolean para) throws TException {

return para;

}

@Override

public int helloInt(int para) throws TException {

try {

Thread.sleep(20000);

} catch (InterruptedException e) {

e.printStackTrace();

}

return para;

}

@Override

public String helloNull() throws TException {

return null;

}

@Override

public String helloString(String para) throws TException {

return para;

}

@Override

public void helloVoid() throws TException {

System.out.println("Hello World");

}

}

創建服務器端實現代碼,將 HelloServiceImpl 作為具體的處理器傳遞給 Thrift 服務器,代碼如下:

清單 3. HelloServiceServer.java

package service.server;

import org.apache.thrift.TProcessor;

import org.apache.thrift.protocol.TBinaryProtocol;

import org.apache.thrift.protocol.TBinaryProtocol.Factory;

import org.apache.thrift.server.TServer;

import org.apache.thrift.server.TThreadPoolServer;

import org.apache.thrift.transport.TServerSocket;

import org.apache.thrift.transport.TTransportException;

import service.demo.Hello;

import service.demo.HelloServiceImpl;

public class HelloServiceServer {

/**

* 啟動 Thrift 服務器

* @param args

*/

public static void main(String[] args) {

try {

// 設置服務端口為 7911

TServerSocket serverTransport = new TServerSocket(7911);

// 設置協議工廠為 TBinaryProtocol.Factory

Factory proFactory = new TBinaryProtocol.Factory();

// 關聯處理器與 Hello 服務的實現

TProcessor processor = new Hello.Processor(new HelloServiceImpl());

TServer server = new TThreadPoolServer(processor, serverTransport,

proFactory);

System.out.println("Start server on port 7911...");

server.serve();

} catch (TTransportException e) {

e.printStackTrace();

}

}

}

創建客戶端實現代碼,調用 Hello.client 訪問服務端的邏輯實現,代碼如下:

清單 4. HelloServiceClient.java

package service.client;

import org.apache.thrift.TException;

import org.apache.thrift.protocol.TBinaryProtocol;

import org.apache.thrift.protocol.TProtocol;

import org.apache.thrift.transport.TSocket;

import org.apache.thrift.transport.TTransport;

import org.apache.thrift.transport.TTransportException;

import service.demo.Hello;

public class HelloServiceClient {

/**

* 調用 Hello 服務

* @param args

*/

public static void main(String[] args) {

try {

// 設置調用的服務地址為本地,端口為 7911

TTransport transport = new TSocket("localhost", 7911);

transport.open();

// 設置傳輸協議為 TBinaryProtocol

TProtocol protocol = new TBinaryProtocol(transport);

Hello.Client client = new Hello.Client(protocol);

// 調用服務的 helloVoid 方法

client.helloVoid();

transport.close();

} catch (TTransportException e) {

e.printStackTrace();

} catch (TException e) {

e.printStackTrace();

}

}

}

代碼編寫完后運行服務器,再啟動客戶端調用服務 Hello 的方法 helloVoid,在服務器端的控制臺窗口輸出“Hello World”(helloVoid 方法實現在控制臺打印字符串,沒有返回值,所以客戶端調用方法后沒有返回值輸出,讀者可以自己嘗試其他有返回值方法的調用,其結果可以打印在客戶端的控制臺窗口 )。

Thrift 架構

Thrift 包含一個完整的堆棧結構用于構建客戶端和服務器端。下圖描繪了 Thrift 的整體架構。


圖 1. 架構圖

如圖所示,圖中黃色部分是用戶實現的業務邏輯,褐色部分是根據 Thrift 定義的服務接口描述文件生成的客戶端和服務器端代碼框架,紅色部分是根據 Thrift 文件生成代碼實現數據的讀寫操作。紅色部分以下是 Thrift 的傳輸體系、協議以及底層 I/O 通信,使用 Thrift 可以很方便的定義一個服務并且選擇不同的傳輸協議和傳輸層而不用重新生成代碼。

Thrift 服務器包含用于綁定協議和傳輸層的基礎架構,它提供阻塞、非阻塞、單線程和多線程的模式運行在服務器上,可以配合服務器 / 容器一起運行,可以和現有的 J2EE 服務器 /Web 容器無縫的結合。

服務端和客戶端具體的調用流程如下:


圖 2. Server 端啟動、服務時序圖(查看大圖)

該圖所示是 HelloServiceServer 啟動的過程以及服務被客戶端調用時,服務器的響應過程。從圖中我們可以看到,程序調用了 TThreadPoolServer 的 serve 方法后,server 進入阻塞監聽狀態,其阻塞在 TServerSocket 的 accept 方法上。當接收到來自客戶端的消息后,服務器發起一個新線程處理這個消息請求,原線程再次進入阻塞狀態。在新線程中,服務器通過 TBinaryProtocol 協議讀取消息內容,調用 HelloServiceImpl 的 helloVoid 方法,并將結果寫入 helloVoid_result 中傳回客戶端。


圖 3. Client 端調用服務時序圖(查看大圖)


該圖所示是 HelloServiceClient 調用服務的過程以及接收到服務器端的返回值后處理結果的過程。從圖中我們可以看到,程序調用了 Hello.Client 的 helloVoid 方法,在 helloVoid 方法中,通過 send_helloVoid 方法發送對服務的調用請求,通過 recv_helloVoid 方法接收服務處理請求后返回的結果。

數據類型

Thrift 腳本可定義的數據類型包括以下幾種類型:

基本類型:

bool:布爾值,true 或 false,對應 Java 的 boolean

byte:8 位有符號整數,對應 Java 的 byte

i16:16 位有符號整數,對應 Java 的 short

i32:32 位有符號整數,對應 Java 的 int

i64:64 位有符號整數,對應 Java 的 long

double:64 位浮點數,對應 Java 的 double

string:未知編碼文本或二進制字符串,對應 Java 的 String

結構體類型:

struct:定義公共的對象,類似于 C 語言中的結構體定義,在 Java 中是一個 JavaBean

容器類型:

list:對應 Java 的 ArrayList

set:對應 Java 的 HashSet

map:對應 Java 的 HashMap

異常類型:

exception:對應 Java 的 Exception

服務類型:

service:對應服務的類

協議

Thrift 可以讓用戶選擇客戶端與服務端之間傳輸通信協議的類別,在傳輸協議上總體劃分為文本 (text) 和二進制 (binary) 傳輸協議,為節約帶寬,提高傳輸效率,一般情況下使用二進制類型的傳輸協議為多數,有時還會使用基于文本類型的協議,這需要根據項目 / 產品中的實際需求。常用協議有以下幾種:

TBinaryProtocol —— 二進制編碼格式進行數據傳輸

使用方法如清單 3 和清單 4 所示。

TCompactProtocol —— 高效率的、密集的二進制編碼格式進行數據傳輸

構建 TCompactProtocol 協議的服務器和客戶端只需替換清單 3 和清單 4 中 TBinaryProtocol 協議部分即可,替換成如下代碼:

清單 5. 使用 TCompactProtocol 協議構建的 HelloServiceServer.java

TCompactProtocol.Factory proFactory = new TCompactProtocol.Factory();

清單 6. 使用 TCompactProtocol 協議的 HelloServiceClient.java

TCompactProtocol protocol = new TCompactProtocol(transport);

TJSONProtocol —— 使用 JSON 的數據編碼協議進行數據傳輸

構建 TJSONProtocol 協議的服務器和客戶端只需替換清單 3 和清單 4 中 TBinaryProtocol 協議部分即可,替換成如下代碼:

清單 7. 使用 TJSONProtocol 協議構建的 HelloServiceServer.java

TJSONProtocol.Factory proFactory = new TJSONProtocol.Factory();

清單 8. 使用 TJSONProtocol 協議的 HelloServiceClient.java

TJSONProtocol protocol = new TJSONProtocol(transport);

TSimpleJSONProtocol —— 只提供 JSON 只寫的協議,適用于通過腳本語言解析

傳輸層

常用的傳輸層有以下幾種:

TSocket —— 使用阻塞式 I/O 進行傳輸,是最常見的模式

使用方法如清單 4 所示。

TFramedTransport —— 使用非阻塞方式,按塊的大小進行傳輸,類似于 Java 中的 NIO

若使用 TFramedTransport 傳輸層,其服務器必須修改為非阻塞的服務類型,客戶端只需替換清單 4 中 TTransport 部分,代碼如下,清單 9 中 TNonblockingServerTransport 類是構建非阻塞 socket 的抽象類,TNonblockingServerSocket 類繼承 TNonblockingServerTransport

清單 9. 使用 TFramedTransport 傳輸層構建的 HelloServiceServer.java

TNonblockingServerTransport serverTransport;

serverTransport = new TNonblockingServerSocket(10005);

Hello.Processor processor = new Hello.Processor(new HelloServiceImpl());

TServer server = new TNonblockingServer(processor, serverTransport);

System.out.println("Start server on port 10005 ...");

server.serve();

清單 10. 使用 TFramedTransport 傳輸層的 HelloServiceClient.java

TTransport transport = new TFramedTransport(new TSocket("localhost", 10005));

TNonblockingTransport —— 使用非阻塞方式,用于構建異步客戶端

使用方法請參考 Thrift 異步客戶端構建

服務端類型

常見的服務端類型有以下幾種:

TSimpleServer —— 單線程服務器端使用標準的阻塞式 I/O

代碼如下:

清單 11. 使用 TSimpleServer 服務端構建的 HelloServiceServer.java

TServerSocket serverTransport = new TServerSocket(7911);

TProcessor processor = new Hello.Processor(new HelloServiceImpl());

TServer server = new TSimpleServer(processor, serverTransport);

System.out.println("Start server on port 7911...");

server.serve();

客戶端的構建方式可參考清單 4。

TThreadPoolServer —— 多線程服務器端使用標準的阻塞式 I/O

使用方法如清單 3 所示。

TNonblockingServer —— 多線程服務器端使用非阻塞式 I/O

使用方法請參考 Thrift 異步客戶端構建

Thrift 異步客戶端構建

Thrift 提供非阻塞的調用方式,可構建異步客戶端。在這種方式中,Thrift 提供了新的類 TAsyncClientManager 用于管理客戶端的請求,在一個線程上追蹤請求和響應,同時通過接口 AsyncClient 傳遞標準的參數和 callback 對象,服務調用完成后,callback 提供了處理調用結果和異常的方法。

首先我們看 callback 的實現:

清單 12.CallBack 的實現:MethodCallback.java

package service.callback;

import org.apache.thrift.async.AsyncMethodCallback;

public class MethodCallback implements AsyncMethodCallback {

Object response = null;

public Object getResult() {

// 返回結果值

return this.response;

}

// 處理服務返回的結果值

@Override

public void onComplete(Object response) {

this.response = response;

}

// 處理調用服務過程中出現的異常

@Override

public void onError(Throwable throwable) {

}

}

如代碼所示,onComplete 方法接收服務處理后的結果,此處我們將結果 response 直接賦值給 callback 的私有屬性 response。onError 方法接收服務處理過程中拋出的異常,此處未對異常進行處理。

創建非阻塞服務器端實現代碼,將 HelloServiceImpl 作為具體的處理器傳遞給異步 Thrift 服務器,代碼如下:

清單 13.HelloServiceAsyncServer.java

package service.server;

import org.apache.thrift.server.TNonblockingServer;

import org.apache.thrift.server.TServer;

import org.apache.thrift.transport.TNonblockingServerSocket;

import org.apache.thrift.transport.TNonblockingServerTransport;

import org.apache.thrift.transport.TTransportException;

import service.demo.Hello;

import service.demo.HelloServiceImpl;

public class HelloServiceAsyncServer {

/**

* 啟動 Thrift 異步服務器

* @param args

*/

public static void main(String[] args) {

TNonblockingServerTransport serverTransport;

try {

serverTransport = new TNonblockingServerSocket(10005);

Hello.Processor processor = new Hello.Processor(

new HelloServiceImpl());

TServer server = new TNonblockingServer(processor, serverTransport);

System.out.println("Start server on port 10005 ...");

server.serve();

} catch (TTransportException e) {

e.printStackTrace();

}

}

}

HelloServiceAsyncServer 通過 java.nio.channels.ServerSocketChannel 創建非阻塞的服務器端等待客戶端的連接。

創建異步客戶端實現代碼,調用 Hello.AsyncClient 訪問服務端的邏輯實現,將 MethodCallback 對象作為參數傳入調用方法中,代碼如下:

清單 14.HelloServiceAsyncClient.java

package service.client;

import java.io.IOException;

import org.apache.thrift.async.AsyncMethodCallback;

import org.apache.thrift.async.TAsyncClientManager;

import org.apache.thrift.protocol.TBinaryProtocol;

import org.apache.thrift.protocol.TProtocolFactory;

import org.apache.thrift.transport.TNonblockingSocket;

import org.apache.thrift.transport.TNonblockingTransport;

import service.callback.MethodCallback;

import service.demo.Hello;

public class HelloServiceAsyncClient {

/**

* 調用 Hello 服務

* @param args

*/

public static void main(String[] args) throws Exception {

try {

TAsyncClientManager clientManager = new TAsyncClientManager();

TNonblockingTransport transport = new TNonblockingSocket(

"localhost", 10005);

TProtocolFactory protocol = new TBinaryProtocol.Factory();

Hello.AsyncClient asyncClient = new Hello.AsyncClient(protocol,

clientManager, transport);

System.out.println("Client calls .....");

MethodCallback callBack = new MethodCallback();

asyncClient.helloString("Hello World", callBack);

Object res = callBack.getResult();

while (res == null) {

res = callBack.getResult();

}

System.out.println(((Hello.AsyncClient.helloString_call) res)

.getResult());

} catch (IOException e) {

e.printStackTrace();

}

}

}

HelloServiceAsyncClient 通過 java.nio.channels.Socketchannel 創建異步客戶端與服務器建立連接。在本文中異步客戶端通過以下的循環代碼實現了同步效果,讀者可去除這部分代碼后再運行對比。

清單 15. 異步客戶端實現同步效果代碼段

Object res = callBack.getResult();

// 等待服務調用后的返回結果

while (res == null) {

res = callBack.getResult();

}

通過與清單 9 和清單 10 的代碼比較,我們可以構建一個 TNonblockingServer 服務類型的服務端,在客戶端構建一個 TFramedTransport 傳輸層的同步客戶端和一個 TNonblockingTransport 傳輸層的異步客戶端,那么一個服務就可以通過一個 socket 端口提供兩種不同的調用方式。有興趣的讀者可以嘗試一下。

常見問題

NULL 問題

我們在對服務的某個方法調用時,有時會出現該方法返回 null 值的情況,在 Thrift 中,直接調用一個返回 null 值的方法會拋出 TApplicationException 異常。在清單 2 中,HelloServiceImpl 里實現了 helloNull 方法,返回 null 值,我們在 HelloServiceClient.java 中加入調用該方法的代碼,出現如下圖所示的異常:


圖 4. TApplicationException 異常

為了處理返回 null 值情況,我們要捕獲該異常,并進行相應的處理,具體客戶端代碼實現如下:

清單 16. 處理服務返回值為 null 的代碼

package service.client;

import org.apache.thrift.TApplicationException;

import org.apache.thrift.TException;

import org.apache.thrift.protocol.TBinaryProtocol;

import org.apache.thrift.protocol.TProtocol;

import org.apache.thrift.transport.TSocket;

import org.apache.thrift.transport.TTransport;

import org.apache.thrift.transport.TTransportException;

import service.demo.Hello;

public class HelloServiceClient {

/**

* 調用 Hello 服務,并處理 null 值問題

* @param args

*/

public static void main(String[] args) {

try {

TTransport transport = new TSocket("localhost", 7911);

transport.open();

TProtocol protocol = new TBinaryProtocol(transport);

Hello.Client client = new Hello.Client(protocol);

System.out.println(client.helloNull());

transport.close();

} catch (TTransportException e) {

e.printStackTrace();

} catch (TException e) {

if (e instanceof TApplicationException

&& ((TApplicationException) e).getType() ==

TApplicationException.MISSING_RESULT) {

System.out.println("The result of helloNull function is NULL");

}

}

}

}

調用 helloNull 方法后,會拋出 TApplicationException 異常,并且異常種類為 MISSING_RESULT,本段代碼顯示,捕獲該異常后,直接在控制臺打印“The result of helloNull function is NULL”信息。

安裝部署

Apache Thrift 的官方網站為:http://thrift.apache.org/tutorial/,具體安裝步驟如下:

下載 thrift 源文件(http://svn.apache.org/repos/asf/thrift/tags/thrift-0.6.1/

將 thrift 源文件導入 eclipse,進入 /lib/java 目錄,使用 ant 編譯 build.xml 獲得 libthrift-0.6.1-snapshot.jar

將 libthrift-0.6.1-snapshot.jar、slf4j-api-1.5.8.jar、slf4j-log4j12-1.5.8.jar 和 log4j-1.2.14.jar 導入 eclipse 開發環境

下載 thrift 編譯工具,該工具可將 thrift 腳本文件編譯成 java 文件,下載地址:http://apache.etoak.com//thrift/0.6.0/thrift-0.6.1.exe

創建 Hello.thrift 腳本文件,具體代碼如上一章節所述,進入 thrift-0.6.1.exe 所在目錄,執行命令"thrift-0.6.1.exe -gen java x:\Hello.thrift",在當前運行盤符下,可看見 gen-java 目錄,進入目錄可看到生成的 Java 代碼。更多 thrift 的命令內容,請參考 thrift 自帶的 help 命令

編寫服務端和客戶端代碼,完成 thrift 的安裝和部署

基于 Apache Thrift 框架生成的服務包括客戶端和服務器端,具體的部署模式如下所示:


圖 5. 部署圖

從圖中我們可以看到,客戶端和服務器端部署時,需要用到公共的 jar 包和 java 文件,如圖“Common file”區域,其中 Hello.java 由 Hello.thrift 編譯而來。在服務器端,服務必須實現 Hello.Iface 接口,同時要包括服務器的啟動代碼 HelloServiceServer.java。在客戶端,包括客戶端調用服務的代碼 HelloServiceClient.java??蛻舳撕头掌魍ㄟ^ Hello.java 提供的 API 實現遠程服務調用。

總結

本文介紹了 Apache Thrift 的安裝部署和架構,并通過大量實例介紹了在不同情況下如何使用 Apache Thrift 來構建服務,同時著重介紹了 Thrift 異步客戶端的構建,希望能給讀者帶來一些幫助。

相關主題

Apache Thrift 官網:可下載 Thrift 工具和源碼。

Thrift Features and Non-features:Thrift 的功能特點和不足之處。

Apache Thrift 介紹:介紹 Thrift 架構、協議、傳輸層和服務端類型,并與其他構建服務的方法 ( 如:REST) 進行比較分析。

Thrift 的安裝部署:Thrift 的安裝部署說明

Thrift: Scalable Cross-Language Services Implementation:Thrift 官方文檔,詳細介紹 Thrift 的設計

Thrift API:關于 Apache Thrift 0.6.1 構建服務端和客戶端的 API 手冊

Thrift 實例:Thrift 的簡單應用實例

Fully async Thrift client in Java:關于 Thrift 異步客戶端的介紹

developerWorks Java 技術專區:這里有數百篇關于 Java 編程各個方面的文章。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容