Netty筆記之七:Google Protobuf與Netty結(jié)合

背景

學(xué)過java的都使用過RMI框架(remote method invocation),遠(yuǎn)程方法調(diào)用,比如A,B二個(gè)服務(wù)器锹引,A調(diào)用B服務(wù)器上的方法就像調(diào)用本地方法一樣,但是本質(zhì)上是跨機(jī)器的調(diào)用了辛蚊,A機(jī)器將調(diào)用的方法名粤蝎,參數(shù)通過字節(jié)碼的形式傳輸?shù)紹這臺(tái)機(jī)器上,B這臺(tái)機(jī)器將這些字節(jié)碼轉(zhuǎn)換成對B機(jī)器上具體方法的調(diào)用袋马,并將相應(yīng)的返回值序列化成二進(jìn)制數(shù)據(jù)傳輸?shù)紸服務(wù)器上。

RPC(Remote Procedure Call)其實(shí)和rmi及其類似秸应,RPC與RMI框架對比的優(yōu)勢就是好多RPC框架都是跨語言的虑凛。

RMI只針對java碑宴,A,B服務(wù)都使用java編寫桑谍。幾乎所有的RPC框架都存在代碼生成延柠,自動(dòng)代碼屏蔽了底層序列化通信等各種細(xì)節(jié)的處理,使得用戶(開發(fā)者)可以像調(diào)用本地方法一樣調(diào)用遠(yuǎn)程的方法锣披。一般這種自動(dòng)生成的代碼在客戶端我們稱為stub贞间,服務(wù)端我們稱為skeleton。

序列化與反序列化技術(shù)雹仿,也稱為編碼與解碼技術(shù)增热,比如我們本篇博客討論的Google Protobuf,和marshalling等技術(shù)胧辽。

從廣義上來講峻仇,webservice也可以稱為RPC框架,但是相比于其他的RPC框架來說邑商,webservice的性能稍微差點(diǎn)摄咆,因?yàn)闆Q定一個(gè)rpc性能的優(yōu)秀與否在于其底層對象編解碼性能。RPC一般都是基于socket協(xié)議傳輸?shù)娜硕希鴚ebservice基于http傳輸?shù)目源樱瑂ocket協(xié)議的性能也要高于http協(xié)議傳輸數(shù)據(jù)。所以恶迈,一般在公司內(nèi)部各個(gè)微服務(wù)之間的服務(wù)調(diào)用都使用RPC框架多一點(diǎn)涩金,因?yàn)樵谛阅苌系目紤],而我們總所周知的dubbo雖然也算是RPC框架蝉绷,但其實(shí)并不支持多語言鸭廷。

什么是protocol buffers?

Protocol buffers是谷歌的語言中立,平臺(tái)中立的熔吗,可擴(kuò)展機(jī)制的序列化數(shù)據(jù)結(jié)構(gòu)框架-可以看作是xml辆床,但是體積更小,傳輸速率更快桅狠,使用更加簡單讼载。一旦你定義了你的數(shù)據(jù)格式,你可以使用生成源代碼去輕松地從各種數(shù)據(jù)流讀和寫你的結(jié)構(gòu)化數(shù)據(jù)并且使用不同的語言中跌。protobuf有2.0版本和3.0版本咨堤,3.0版本十grpc框架的基礎(chǔ)

Protocol buffers目前支持Java, Python, Objective-C, 和C++生成代碼。新的proto3語言版本漩符,你可以使用Go, JavaNano, Ruby, 和 C#一喘。

為什么使用Protocol buffers

使用一個(gè)簡單的可以從一個(gè)文件中去讀寫人員聯(lián)系信息"地址簿"程序。每個(gè)在地址簿的人有姓名,id凸克,郵箱地址和一個(gè)聯(lián)系人電話號(hào)碼屬性议蟆。

你如何序列化和檢索這樣的結(jié)構(gòu)化數(shù)據(jù)? 有幾種方法來解決這個(gè)問題:
使用java原生的序列化萎战。這是一種默認(rèn)的方式因?yàn)槭莾?nèi)嵌于java語言的咐容,但是有一大堆眾所周知的問題(參考Effective Java這本書),并且你不能將數(shù)據(jù)分享于C++和Python應(yīng)用(也就是不能跨語言)蚂维。

還可以將數(shù)據(jù)項(xiàng)編碼為單個(gè)字符串的ad-hoc方式 - 例如將4個(gè)ints編碼為“12:3:-23:67”戳粒。 這是一個(gè)簡單而靈活的方法,盡管它需要編寫一次性編碼和解析代碼虫啥,并且解析具有很小的運(yùn)行時(shí)成本蔚约。 這最適合編碼非常簡單的數(shù)據(jù)。

將數(shù)據(jù)序列化為XML孝鹊。 這種方法可能非常有吸引力炊琉,因?yàn)閄ML是(可能的)人類可讀的,并且有很多語言的綁定庫又活。 如果您想與其他應(yīng)用程序/項(xiàng)目共享數(shù)據(jù)苔咪,這可能是一個(gè)很好的選擇。 然而柳骄,XML浪費(fèi)性能团赏,編碼/解碼可能會(huì)對應(yīng)用程序造成巨大的性能損失。 另外耐薯,檢索XML DOM樹比在一般類中簡單的字段檢索要復(fù)雜得多舔清。

Protocol buffers是靈活,高效曲初,自動(dòng)化的解決方案來解決這個(gè)問題体谒。 使用Protocol buffers,您可以編寫一個(gè).proto描述您希望存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)臼婆。 Protocol buffers編譯器創(chuàng)建一個(gè)實(shí)現(xiàn)自動(dòng)編碼和解析協(xié)議緩沖區(qū)數(shù)據(jù)的類抒痒,并使用高效的二進(jìn)制格式。 生成的類為組成Protocol buffers的字段提供getter和setter颁褂。

使用Protobuf編寫一個(gè)編碼解碼的最簡單程序

  • 在 .proto結(jié)尾的文件中定義消息格式故响。
  • 使用protocol buffers編譯器將 .proto結(jié)尾的文件生成對應(yīng)語言的源代碼(本demo使用java編譯器)。
  • 使用Java protocol buffer API 去讀寫消息颁独。

定義一個(gè)Student.proto文件

syntax ="proto2";

package com.zhihao.miao.protobuf;

//optimize_for 加快解析的速度
option optimize_for = SPEED;
option java_package = "com.zhihao.miao.protobuf";
option java_outer_classname="DataInfo";

message Student{
    required string name = 1;
    optional int32 age = 2;
    optional string address = 3;
}

在Java項(xiàng)目中彩届,除非你已經(jīng)明確指定了java_package,否則package 用作Java的包名誓酒。即使您提供java_package樟蠕,您仍然應(yīng)該定義一個(gè)package,以避免在Protocol Buffers名稱空間和非Java語言中的名稱沖突。

在package的定義之后坯墨,我們可以看到兩個(gè)定義的java選項(xiàng):java_packagejava_outer_classname寂汇。java_package指定您生成的類應(yīng)該存放的Java包名稱病往。 如果沒有明確指定它捣染,將會(huì)使用package定義的name作為包名,但這些名稱通常不是適合的Java包名稱(因?yàn)樗鼈兺ǔ2灰杂蛎_頭)停巷。 java_outer_classname選項(xiàng)定義應(yīng)該包含此文件中所有類的類名耍攘。 如果你不明確地給出一個(gè)java_outer_classname,它將通過將文件名轉(zhuǎn)換為駝峰的方式來生成畔勤。 例如蕾各,默認(rèn)情況下,“my_proto.proto”將使用“MyProto”作為外部類名稱庆揪。

每個(gè)元素上的“= 1”式曲,“= 2”標(biāo)記標(biāo)識(shí)字段在二進(jìn)制編碼中使用的唯一“標(biāo)簽”。你可以將經(jīng)常使用或者重復(fù)的字段標(biāo)注成1-15缸榛,因?yàn)樵谶M(jìn)行編碼的時(shí)候因?yàn)樯僖粋€(gè)字節(jié)進(jìn)行編碼吝羞,所以效率更高。

required:必須提供該字段的值内颗,否則被認(rèn)為沒有初始化钧排。嘗試構(gòu)建一個(gè)未初始化的值被會(huì)拋出RuntimeException。解析一個(gè)為初始化的消息會(huì)拋出IOException均澳。除此之外與optional一樣恨溜。
optional:可以設(shè)置或不設(shè)置該字段。 如果未設(shè)置可選字段值找前,則使用默認(rèn)值糟袁。
repeated:字段可能重復(fù)任意次數(shù)(包括零)。 重復(fù)值的順序?qū)⒈A粼?code>protocol buffer中躺盛。 將重復(fù)的字段視為動(dòng)態(tài)大小的數(shù)組项戴。(本列子中沒有字段定義成repeated類型,定義成repeated類型其實(shí)就是java中List類型的字段颗品。

慎重使用required類型肯尺,將required類型的字段更改為optional會(huì)有一些問題,而將optional類型的字段更改為required類型躯枢,則沒有問題则吟。

編譯

使用protocol buffers編譯器將對應(yīng)的.proto文件編譯成對應(yīng)的類
關(guān)于編譯器的安裝,下載地址

下載頁面圖示

修改環(huán)境變量

?  vim .bash_profile
export PATH=/Users/naeshihiroshi/software/work/protoc-3.3.0-osx-x86_64/bin
?  source .bash_profile
?  which protoc
/Users/naeshihiroshi/software/work/protoc-3.3.0-osx-x86_64/bin/protoc

進(jìn)入項(xiàng)目目錄锄蹂,執(zhí)行編譯語句如下:

?  netty_lecture git:(master) ? protoc --java_out=src/main/java  src/protobuf/Student.proto   

--java_out后面第一個(gè)參數(shù)指定代碼的路徑氓仲,具體的包名在.proto文件中的java_package指定了,第二個(gè)指定要編譯的proto文件。

自動(dòng)生成的類名是DataInfo(在java_outer_classname中指定了)敬扛,自動(dòng)生成的類太長晰洒,這邊就不列出來了。

編寫序列化反序列化測試類

package com.zhihao.miao.protobuf;

//實(shí)際使用protobuf序列化框架客戶端將對象轉(zhuǎn)譯成字節(jié)數(shù)組啥箭,然后通過協(xié)議傳輸?shù)椒?wù)器端谍珊,服務(wù)器端可以是其他的語言框架(比如說python)將
//字節(jié)對象反編譯成java對象
public class ProtobuffTest {
    public static void main(String[] args) throws Exception{
        DataInfo.Student student = DataInfo.Student.newBuilder().
                setName("張三").setAge(20).setAddress("北京").build();

        //將對象轉(zhuǎn)譯成字節(jié)數(shù)組,序列化
        byte[] student2ByteArray = student.toByteArray();

        //將字節(jié)數(shù)組轉(zhuǎn)譯成對象,反序列化
        DataInfo.Student student2 = DataInfo.Student.parseFrom(student2ByteArray);

        System.out.println(student2.getName());
        System.out.println(student2.getAge());
        System.out.println(student2.getAddress());
    }
}

執(zhí)行測試類,控制臺(tái)打蛹苯摹:

張三
20
北京

Google Protobuf與netty結(jié)合

protobuf做為序列化的一種方式砌滞,序列化之后通過什么樣的載體在網(wǎng)絡(luò)中傳輸?

使用netty使得經(jīng)過protobuf序列化的對象可以通過網(wǎng)絡(luò)通信進(jìn)行客戶端和服務(wù)器的信息通信坏怪”慈螅客戶端使用protobuf將對象序列化成字節(jié)碼,而服務(wù)器端通過protobuf將對象反序列化成原本對象铝宵。

寫一個(gè)使用Protobuf作為序列化框架打掘,netty作為傳輸層的最簡單的demo,需求描述:

  • 客戶端傳遞一個(gè)User對象給服務(wù)端(User對象包括姓名鹏秋,年齡尊蚁,密碼)
  • 客戶端接收客戶端的User對象并且將其相應(yīng)的銀行賬戶等信息反饋給客戶端

定義的.proto文件如下:

syntax ="proto2";

package com.zhihao.miao.netty.sixthexample;

option optimize_for = SPEED;
option java_package = "com.zhihao.miao.test.day06";
option java_outer_classname="DataInfo";

message RequestUser{
    optional string user_name = 1;
    optional int32 age = 2;
    optional string password = 3;
}

message ResponseBank{
    optional string bank_no = 1;
    optional double money = 2;
    optional string bank_name=3;
}

使用Protobuf編譯器進(jìn)行編譯,生成DataInfo對象拼岳,

服務(wù)器端代碼:

package com.zhihao.miao.test.day06;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class ProtoServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ProtoServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}

服務(wù)端ProtoServerInitializer(初始化連接):

package com.zhihao.miao.test.day06;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;


public class ProtoServerInitializer extends ChannelInitializer<SocketChannel>{

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        //解碼器枝誊,通過Google Protocol Buffers序列化框架動(dòng)態(tài)的切割接收到的ByteBuf
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //服務(wù)器端接收的是客戶端RequestUser對象,所以這邊將接收對象進(jìn)行解碼生產(chǎn)實(shí)列
        pipeline.addLast(new ProtobufDecoder(DataInfo.RequestUser.getDefaultInstance()));
        //Google Protocol Buffers編碼器
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        //Google Protocol Buffers編碼器
        pipeline.addLast(new ProtobufEncoder());

        pipeline.addLast(new ProtoServerHandler());
    }
}

自定義服務(wù)端的處理器:

package com.zhihao.miao.test.day06;


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class ProtoServerHandler extends SimpleChannelInboundHandler<DataInfo.RequestUser> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DataInfo.RequestUser msg) throws Exception {
        System.out.println(msg.getUserName());
        System.out.println(msg.getAge());
        System.out.println(msg.getPassword());

        DataInfo.ResponseBank bank = DataInfo.ResponseBank.newBuilder().setBankName("中國工商銀行")
                .setBankNo("6222222200000000000").setMoney(560000.23).build();

        ctx.channel().writeAndFlush(bank);
    }
}

客戶端:

package com.zhihao.miao.test.day06;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class ProtoClient {

    public static void main(String[] args) throws Exception{
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new ProtoClientInitializer());

            ChannelFuture channelFuture = bootstrap.connect("localhost",8899).sync();
            channelFuture.channel().closeFuture().sync();

        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客戶端初始化連接(ProtoClientInitializer)惜纸,

package com.zhihao.miao.test.day06;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;

public class ProtoClientInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        //解碼器叶撒,通過Google Protocol Buffers序列化框架動(dòng)態(tài)的切割接收到的ByteBuf
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //將接收到的二進(jìn)制文件解碼成具體的實(shí)例,這邊接收到的是服務(wù)端的ResponseBank對象實(shí)列
        pipeline.addLast(new ProtobufDecoder(DataInfo.ResponseBank.getDefaultInstance()));
        //Google Protocol Buffers編碼器
        pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
        //Google Protocol Buffers編碼器
        pipeline.addLast(new ProtobufEncoder());

        pipeline.addLast(new ProtoClientHandler());
    }
}

自定義客戶端處理器:

package com.zhihao.miao.test.day06;


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class ProtoClientHandler extends SimpleChannelInboundHandler<DataInfo.ResponseBank> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DataInfo.ResponseBank msg) throws Exception {
        System.out.println(msg.getBankNo());
        System.out.println(msg.getBankName());
        System.out.println(msg.getMoney());
    }


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        DataInfo.RequestUser user = DataInfo.RequestUser.newBuilder()
                .setUserName("zhihao.miao").setAge(27).setPassword("123456").build();
        ctx.channel().writeAndFlush(user);
    }
}

運(yùn)行服務(wù)器端和客戶端耐版,服務(wù)器控制臺(tái)打屿艄弧:

七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x08c534f3, L:/127.0.0.1:8899 - R:/127.0.0.1:65448]
七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
zhihao.miao
27
123456

客戶端控制臺(tái)打印:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
6222222200000000000
中國工商銀行
560000.23

總結(jié)

本節(jié)我們使用Google Protobuf定義消息體格式粪牲,使用Netty作為網(wǎng)絡(luò)傳輸層框架古瓤。其實(shí)大多數(shù)RPC框架底層實(shí)現(xiàn)都是使用序列化框架和NIO通信框架進(jìn)行結(jié)合。下面還會(huì)學(xué)習(xí)基于Protobuf 3.0協(xié)議的Grpc框架(Google基于Protobuf 3.0協(xié)議的一個(gè)跨語言的rpc框架腺阳,更加深入的去了解rpc框架)落君。

參考資料

官方網(wǎng)站
指南
java指南

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亭引,隨后出現(xiàn)的幾起案子绎速,更是在濱河造成了極大的恐慌,老刑警劉巖焙蚓,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纹冤,死亡現(xiàn)場離奇詭異洒宝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)萌京,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門雁歌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人知残,你說我怎么就攤上這事靠瞎。” “怎么了橡庞?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵较坛,是天一觀的道長。 經(jīng)常有香客問我扒最,道長,這世上最難降的妖魔是什么华嘹? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任吧趣,我火速辦了婚禮,結(jié)果婚禮上耙厚,老公的妹妹穿的比我還像新娘强挫。我一直安慰自己,他們只是感情好薛躬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布俯渤。 她就那樣靜靜地躺著,像睡著了一般型宝。 火紅的嫁衣襯著肌膚如雪八匠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天趴酣,我揣著相機(jī)與錄音梨树,去河邊找鬼。 笑死岖寞,一個(gè)胖子當(dāng)著我的面吹牛抡四,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仗谆,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼指巡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了隶垮?” 一聲冷哼從身側(cè)響起藻雪,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎岁疼,沒想到半個(gè)月后阔涉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缆娃,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年瑰排,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贯要。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡椭住,死狀恐怖崇渗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情京郑,我是刑警寧澤宅广,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站些举,受9級特大地震影響跟狱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜户魏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一驶臊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叼丑,春花似錦关翎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至星立,卻和暖如春爽茴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贞铣。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工闹啦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人辕坝。 一個(gè)月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓窍奋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酱畅。 傳聞我的和親對象是個(gè)殘疾皇子琳袄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355

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