Netty之旅二:口口相傳的高性能Netty到底是什么?

d0iosx.png

高清思維導(dǎo)圖原件(xmind/pdf/jpg)可以關(guān)注公眾號:一枝花算不算浪漫 回復(fù)netty01即可。

前言

上一篇文章講了NIO相關(guān)的知識點(diǎn)脑题,相比于傳統(tǒng)IONIO已經(jīng)做得很優(yōu)雅了铜靶,為什么我們還要使用Netty叔遂?

上篇文章最后留了很多坑,講了NIO使用的弊端争剿,也是為了引出Netty而設(shè)立的已艰,這篇文章我們就來好好揭開Netty的神秘面紗。

本篇文章的目的很簡單秒梅,希望看過后你能看懂Netty的示例代碼旗芬,針對于簡單的網(wǎng)絡(luò)通信,自己也能用Netty手寫一個開發(fā)應(yīng)用出來捆蜀!

一個簡單的Netty示例

以下是一個簡單聊天室Server端的程序,代碼參考自:http://www.imooc.com/read/82/article/2166

代碼有點(diǎn)長,主要核心代碼是在main()方法中辆它,這里代碼也希望大家看懂誊薄,后面也會一步步剖析。

PS:我是用mac系統(tǒng)锰茉,直接在終端輸入telnet 127.0.0.1 8007 即可啟動一個聊天框呢蔫,如果提示找不到telnet命令,可以通過brew進(jìn)行安裝飒筑,具體步驟請自行百度片吊。

/**
 * @Description netty簡易聊天室
 *
 * @Author 一枝花算不算浪漫
 * @Date 2020/8/10 6:52 上午
 */
public final class NettyChatServer {

    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // 1. EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 2. 服務(wù)端引導(dǎo)器
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 3. 設(shè)置線bootStrap信息
            serverBootstrap.group(bossGroup, workerGroup)
                    // 4. 設(shè)置ServerSocketChannel的類型
                    .channel(NioServerSocketChannel.class)
                    // 5. 設(shè)置參數(shù)
                    .option(ChannelOption.SO_BACKLOG, 100)
                    // 6. 設(shè)置ServerSocketChannel對應(yīng)的Handler,只能設(shè)置一個
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 7. 設(shè)置SocketChannel對應(yīng)的Handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            // 可以添加多個子Handler
                            p.addLast(new LoggingHandler(LogLevel.INFO));
                            p.addLast(new ChatNettyHandler());
                        }
                    });

            // 8. 綁定端口
            ChannelFuture f = serverBootstrap.bind(PORT).sync();
            // 9. 等待服務(wù)端監(jiān)聽端口關(guān)閉协屡,這里會阻塞主線程
            f.channel().closeFuture().sync();
        } finally {
            // 10. 優(yōu)雅地關(guān)閉兩個線程池
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private static class ChatNettyHandler extends SimpleChannelInboundHandler<ByteBuf> {
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            System.out.println("one conn active: " + ctx.channel());
            // channel是在ServerBootstrapAcceptor中放到EventLoopGroup中的
            ChatHolder.join((SocketChannel) ctx.channel());
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
            byte[] bytes = new byte[byteBuf.readableBytes()];
            byteBuf.readBytes(bytes);
            String content = new String(bytes, StandardCharsets.UTF_8);
            System.out.println(content);

            if (content.equals("quit\r\n")) {
                ctx.channel().close();
            } else {
                ChatHolder.propagate((SocketChannel) ctx.channel(), content);
            }
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            System.out.println("one conn inactive: " + ctx.channel());
            ChatHolder.quit((SocketChannel) ctx.channel());
        }
    }

    private static class ChatHolder {
        static final Map<SocketChannel, String> USER_MAP = new ConcurrentHashMap<>();

        /**
         * 加入群聊
         */
        static void join(SocketChannel socketChannel) {
            // 有人加入就給他分配一個id
            String userId = "用戶"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
            send(socketChannel, "您的id為:" + userId + "\n\r");

            for (SocketChannel channel : USER_MAP.keySet()) {
                send(channel, userId + " 加入了群聊" + "\n\r");
            }

            // 將當(dāng)前用戶加入到map中
            USER_MAP.put(socketChannel, userId);
        }

        /**
         * 退出群聊
         */
        static void quit(SocketChannel socketChannel) {
            String userId = USER_MAP.get(socketChannel);
            send(socketChannel, "您退出了群聊" + "\n\r");
            USER_MAP.remove(socketChannel);

            for (SocketChannel channel : USER_MAP.keySet()) {
                if (channel != socketChannel) {
                    send(channel, userId + " 退出了群聊" + "\n\r");
                }
            }
        }

        /**
         * 擴(kuò)散說話的內(nèi)容
         */
        public static void propagate(SocketChannel socketChannel, String content) {
            String userId = USER_MAP.get(socketChannel);
            for (SocketChannel channel : USER_MAP.keySet()) {
                if (channel != socketChannel) {
                    send(channel, userId + ": " + content);
                }
            }
        }

        /**
         * 發(fā)送消息
         */
        static void send(SocketChannel socketChannel, String msg) {
            try {
                ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
                ByteBuf writeBuffer = allocator.buffer(msg.getBytes().length);
                writeBuffer.writeCharSequence(msg, Charset.defaultCharset());
                socketChannel.writeAndFlush(writeBuffer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
dkeb0s.png

代碼有點(diǎn)長俏脊,執(zhí)行完的效果如上圖所示,下面所有內(nèi)容都是圍繞著如何看懂以及如何寫出這樣的代碼來展開的肤晓,希望你看完 也能輕松手寫Netty服務(wù)端代碼~爷贫。通過簡單demo開發(fā)讓大家體驗(yàn)了Netty實(shí)現(xiàn)相比NIO確實(shí)要簡單的多,但優(yōu)點(diǎn)不限于此补憾,只需要知道選擇Netty就對了漫萄。

Netty核心組件

對應(yīng)著文章開頭的思維導(dǎo)圖,我們知道Netty的核心組件主要有:

  • Bootstrap && ServerBootstrap
  • EventLoopGroup
  • EventLoop
  • ByteBuf
  • Channel
  • ChannelHandler
  • ChannelFuture
  • ChannelPipeline
  • ChannelHandlerContext

類圖如下:

dk8ZC9.png

Bootstrap & ServerBootstrap

一看到BootStrap大家就應(yīng)該想到啟動類盈匾、引導(dǎo)類這樣的詞匯腾务,之前分析過EurekaServer項(xiàng)目啟動類時(shí)介紹過EurekaBootstrap, 他的作用就是上下文初始化削饵、配置初始化岩瘦。

Netty中我們也有類似的類,BootstrapServerBootstrap它們都是Netty程序的引導(dǎo)類葵孤,主要用于配置各種參數(shù)担钮,并啟動整個Netty服務(wù),我們看下文章開頭的示例代碼:

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)      
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline p = ch.pipeline();
                p.addLast(new LoggingHandler(LogLevel.INFO));
                p.addLast(new ChatNettyHandler());
            }
        });

BootstrapServerBootstrap是針對于ClientServer端定義的兩套啟動類尤仍,區(qū)別如下:

  • Bootstrap是客戶端引導(dǎo)類箫津,而ServerBootstrap是服務(wù)端引導(dǎo)類。
  • Bootstrap通常使用connect()方法連接到遠(yuǎn)程的主機(jī)和端口宰啦,作為一個TCP客戶端苏遥。
  • ServerBootstrap通常使用bind()方法綁定本地的端口,等待客戶端來連接赡模。
  • ServerBootstrap可以處理Accept事件田炭,這里面childHandler是用來處理Channel請求的,我們可以查看chaildHandler()方法的注解:

[圖片上傳失敗...(image-2595fc-1598167949595)]

  • Bootstrap客戶端引導(dǎo)只需要一個EventLoopGroup漓柑,但是一個ServerBootstrap通常需要兩個(上面的boosGroupworkerGroup)教硫。

EventLoopGroup && EventLoop

EventLoopGroupEventLoop這兩個類名稱定義的很奇怪叨吮,對于初學(xué)者來說往往無法通過名稱來了解其中的含義,包括我也是這樣瞬矩。

EventLoopGroup 可以理解為一個線程池茶鉴,對于服務(wù)端程序,我們一般會綁定兩個線程池景用,一個用于處理 Accept 事件涵叮,一個用于處理讀寫事件,看下EventLoop系列的類目錄:

dU4Roj.png

通過上面的類圖伞插,我們才恍然大悟割粮,我的親娘咧,這不就是一個線程池嘛媚污?(名字氣的犄角拐彎的真是難認(rèn))

EventLoopGroupEventLoop的集合舀瓢,一個EventLoopGroup 包含一個或者多個EventLoop。我們可以將EventLoop看做EventLoopGroup線程池中的一個個工作線程杠步。

至于這里為什么要用到兩個線程池氢伟,具體的其實(shí)可以參考Reactor設(shè)計(jì)模式,這里暫時(shí)不做過多的講解幽歼。

  • 一個 EventLoopGroup 包含一個或多個 EventLoop 朵锣,即 EventLoopGroup : EventLoop = 1 : n
  • 一個 EventLoop 在它的生命周期內(nèi),只能與一個 Thread 綁定甸私,即 EventLoop : Thread = 1 : 1
  • 所有有 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理诚些,從而保證線程安全,即 Thread : EventLoop = 1 : 1
  • 一個 Channel 在它的生命周期內(nèi)只能注冊到一個 EventLoop 上皇型,即 Channel : EventLoop = n : 1
  • 一個 EventLoop 可被分配至一個或多個 Channel 诬烹,即 EventLoop : Channel = 1 : n

當(dāng)一個連接到達(dá)時(shí),Netty 就會創(chuàng)建一個 Channel弃鸦,然后從 EventLoopGroup 中分配一個 EventLoop 來給這個 Channel 綁定上绞吁,在該 Channel 的整個生命周期中都是有這個綁定的 EventLoop 來服務(wù)的。

ByteBuf

Java NIO中我們有 ByteBuffer緩沖池唬格,對于它的操作我們應(yīng)該印象深刻家破,往Buffer中寫數(shù)據(jù)時(shí)我們需要關(guān)注寫入的位置,切換成讀模式時(shí)我們還要切換讀寫狀態(tài)购岗,不然將會出現(xiàn)大問題汰聋。

針對于NIO中超級難用的Buffer類, Netty 提供了ByteBuf來替代喊积。ByteBuf聲明了兩個指針:一個讀指針烹困,一個寫指針,使得讀寫操作進(jìn)行分離乾吻,簡化buffer的操作流程髓梅。

dkQocV.png

另外Netty提供了發(fā)幾種ByteBuf的實(shí)現(xiàn)以供我們選擇拟蜻,ByteBuf可以分為:

  • PooledUnpooled 池化和非池化
  • Heap 和 Direct,堆內(nèi)存和堆外內(nèi)存女淑,NIO中創(chuàng)建Buffer也可以指定
  • Safe 和 Unsafe瞭郑,安全和非安全
dkJ9TU.png

對于這么多種創(chuàng)建Buffer的方式該怎么選擇呢辜御?Netty也為我們處理好了鸭你,我們可以直接使用(真是暖男Ntetty):

ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
ByteBuf buffer = allocator.buffer(length);

使用這種方式,Netty將最大努力的使用池化擒权、Unsafe袱巨、對外內(nèi)存的方式為我們創(chuàng)建buffer。

Channel

提起Channel并不陌生碳抄,上一篇講NIO的三大組件提到過愉老,最常見的就是java.nio.SocketChanneljava.nio.ServerSocketChannel,他們用于非阻塞的I/0操作剖效。類似于NIOChannel嫉入,Netty提供了自己的Channel和其子類實(shí)現(xiàn),用于異步I/0操作和其他相關(guān)的操作璧尸。

Netty 中, Channel 是一個 Socket 連接的抽象, 它為用戶提供了關(guān)于底層 Socket 狀態(tài)(是否是連接還是斷開) 以及對 Socket 的讀寫等操作咒林。每當(dāng) Netty 建立了一個連接后, 都會有一個對應(yīng)的 Channel 實(shí)例。并且爷光,有父子channel的概念垫竞。 服務(wù)器連接監(jiān)聽的channel ,也叫 parent channel蛀序。 對應(yīng)于每一個 Socket 連接的channel欢瞪,也叫 child channel

既然channel 是 Netty 抽象出來的網(wǎng)絡(luò) I/O 讀寫相關(guān)的接口徐裸,為什么不使用JDK NIO 原生的 Channel 而要另起爐灶呢遣鼓,主要原因如下:

  • JDKSocketChannelServersocketChannel沒有統(tǒng)一的 Channel 接口供業(yè)務(wù)開發(fā)者使用,對一于用戶而言重贺,沒有統(tǒng)一的操作視圖骑祟,使用起來并不方便。
  • JDKSocketChannelScrversockctChannel的主要職責(zé)就是網(wǎng)絡(luò) I/O 操作檬姥,由于他們是SPI 類接口曾我,由具體的虛擬機(jī)廠家來提供,所以通過繼承 SPI 功能直接實(shí)現(xiàn) ServersocketChannelSocketChannel 來擴(kuò)展其工作量和重新Channel 功類是差不多的健民。
  • Netty 的 ChannelPipeline Channel 需要夠跟 Netty 的整體架構(gòu)融合在一起抒巢,例如 I/O 模型、基的定制模型秉犹,以及基于元數(shù)據(jù)描述配置化的 TCP 參數(shù)等蛉谜,這些JDK SocketChannelServersocketChannel都沒有提供稚晚,需要重新封裝。
  • 自定義的 Channel 型诚,功實(shí)現(xiàn)更加靈活客燕。

基于上述 4 原因,它的設(shè)計(jì)原理比較簡單狰贯, Netty 重新設(shè)計(jì)了 Channel 接口也搓,并且給予了很多不同的實(shí)現(xiàn)。但是功能卻比較繁雜涵紊,主要的設(shè)計(jì)理念如下:

  • Channel 接口層傍妒,相關(guān)聯(lián)的其他操作封裝起來,采用 Facade 模式進(jìn)行統(tǒng)一封裝摸柄,將網(wǎng)絡(luò) I/O 操作颤练、網(wǎng)絡(luò) I/O 統(tǒng)一對外提供。
  • Channel 接口的定義盡量大而全驱负,統(tǒng)一的視圖嗦玖,由不同子類實(shí)現(xiàn)不同的功能,公共功能在抽象父類中實(shí)現(xiàn)跃脊,最大程度上實(shí)現(xiàn)接口的重用宇挫。
  • 具體實(shí)現(xiàn)采用聚合而非包含的方式,將相關(guān)的功類聚合在 Channel中匾乓,由 Channel 統(tǒng)一負(fù)責(zé)分配和調(diào)度捞稿,功能實(shí)現(xiàn)更加靈活。

Channel的實(shí)現(xiàn)類非常多拼缝,繼承關(guān)系復(fù)雜娱局,從學(xué)習(xí)的角度我們抽取最重要的兩個 NioServerSocketChannelNioSocketChannel

服務(wù)端 NioServerSocketChannel的繼承關(guān)系類圖如下:

dUn8G4.png

客戶端 NioSocketChannel的繼承關(guān)系類圖如下:

dUnJz9.png

后面文章源碼系列會具體分析咧七,這里就不進(jìn)一步闡述分析了衰齐。

ChannelHandler

ChannelHandlerNetty中最常用的組件。ChannelHandler 主要用來處理各種事件继阻,這里的事件很廣泛耻涛,比如可以是連接、數(shù)據(jù)接收瘟檩、異常抹缕、數(shù)據(jù)轉(zhuǎn)換等。

ChannelHandler 有兩個核心子類 ChannelInboundHandlerChannelOutboundHandler墨辛,其中 ChannelInboundHandler 用于接收卓研、處理入站( Inbound )的數(shù)據(jù)和事件,而 ChannelOutboundHandler 則相反,用于接收奏赘、處理出站( Outbound )的數(shù)據(jù)和事件寥闪。

dkJAp9.png

ChannelInboundHandler

ChannelInboundHandler處理入站數(shù)據(jù)以及各種狀態(tài)變化,當(dāng)Channel狀態(tài)發(fā)生改變會調(diào)用ChannelInboundHandler中的一些生命周期方法.這些方法與Channel的生命密切相關(guān)。

入站數(shù)據(jù),就是進(jìn)入socket的數(shù)據(jù)磨淌。下面展示一些該接口的生命周期API

dUntMR.png

當(dāng)某個 ChannelInboundHandler的實(shí)現(xiàn)重寫 channelRead()方法時(shí)疲憋,它將負(fù)責(zé)顯式地釋放與池化的 ByteBuf 實(shí)例相關(guān)的內(nèi)存。 Netty 為此提供了一個實(shí)用方法ReferenceCountUtil.release()梁只。

@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ReferenceCountUtil.release(msg);
    }
}

這種方式還挺繁瑣的,Netty提供了一個SimpleChannelInboundHandler,重寫channelRead0()方法,就可以在調(diào)用過程中會自動釋放資源.

public class SimpleDiscardHandler
    extends SimpleChannelInboundHandler<Object> {
    @Override
    public void channelRead0(ChannelHandlerContext ctx,
                                    Object msg) {
            // 不用調(diào)用ReferenceCountUtil.release(msg)也會釋放資源
    }
}

ChannelOutboundHandler

出站操作和數(shù)據(jù)將由 ChannelOutboundHandler 處理缚柳。它的方法將被 ChannelChannelPipeline以及 ChannelHandlerContext 調(diào)用敛纲。
ChannelOutboundHandler 的一個強(qiáng)大的功能是可以按需推遲操作或者事件喂击,這使得可以通過一些復(fù)雜的方法來處理請求。例如淤翔, 如果到遠(yuǎn)程節(jié)點(diǎn)的寫入被暫停了, 那么你可以推遲沖刷操作并在稍后繼續(xù)佩谷。

d0PxbT.png

ChannelPromiseChannelFuture: ChannelOutboundHandler中的大部分方法都需要一個ChannelPromise參數(shù)旁壮, 以便在操作完成時(shí)得到通知。 ChannelPromiseChannelFuture的一個子類谐檀,其定義了一些可寫的方法抡谐,如setSuccess()setFailure(),從而使ChannelFuture不可變桐猬。

ChannelHandlerAdapter

ChannelHandlerAdapter顧名思義,就是handler的適配器麦撵。你需要知道什么是適配器模式,假設(shè)有一個A接口溃肪,我們需要A的subclass實(shí)現(xiàn)功能,但是B類中正好有我們需要的功能,不想復(fù)制粘貼B中的方法和屬性了,那么可以寫一個適配器類Adpter繼承B實(shí)現(xiàn)A潘酗,這樣一來Adapter是A的子類并且能直接使用B中的方法模软,這種模式就是適配器模式。

就比如Netty中的SslHandler類厨钻,想使用ByteToMessageDecoder中的方法進(jìn)行解碼扼雏,但是必須是ChannelHandler子類對象才能加入到ChannelPipeline中,通過如下簽名和其實(shí)現(xiàn)細(xì)節(jié)(SslHandler實(shí)現(xiàn)細(xì)節(jié)就不貼了)就能夠作為一個handler去處理消息了夯膀。

public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundHandler

ChannelHandlerAdapter提供了一些實(shí)用方法isSharable()如果其對應(yīng)的實(shí)現(xiàn)被標(biāo)注為Sharable诗充, 那么這個方法將返回 true, 表示它可以被添加到多個 ChannelPipeline中 诱建。如果想在自己的ChannelHandler中使用這些適配器類蝴蜓,只需要擴(kuò)展他們,重寫那些想要自定義的方法即可涂佃。

ChannelPipeline

每一個新創(chuàng)建的 Channel 都將會被分配一個新的 ChannelPipeline励翼。這項(xiàng)關(guān)聯(lián)是永久性的蜈敢; Channel 既不能附加另外一個 ChannelPipeline,也不能分離其當(dāng)前的汽抚。在 Netty 組件的生命周期中抓狭,這是一項(xiàng)固定的操作,不需要開發(fā)人員的任何干預(yù)造烁。

Netty 的 ChannelHandler 為處理器提供了基本的抽象否过, 目前你可以認(rèn)為每個 ChannelHandler 的實(shí)例都類似于一種為了響應(yīng)特定事件而被執(zhí)行的回調(diào)。從應(yīng)用程序開發(fā)人員的角度來看惭蟋, 它充當(dāng)了所有處理入站和出站數(shù)據(jù)的應(yīng)用程序邏輯的攔截載體苗桂。ChannelPipeline提供了 ChannelHandler 鏈的容器,并定義了用于在該鏈上傳播入站和出站事件流的 API告组。當(dāng) Channel 被創(chuàng)建時(shí)煤伟,它會被自動地分配到它專屬的 ChannelPipeline

ChannelHandler 安裝到 ChannelPipeline 中的過程如下所示:

  • 一個ChannelInitializer的實(shí)現(xiàn)被注冊到了ServerBootstrap
  • 當(dāng) ChannelInitializer.initChannel()方法被調(diào)用時(shí)木缝,ChannelInitializer將在 ChannelPipeline中安裝一組自定義的 ChannelHandler
  • ChannelInitializer 將它自己從 ChannelPipeline中移除
dkJuTO.png

如上圖所示:這是一個同時(shí)具有入站和出站 ChannelHandlerChannelPipeline的布局便锨,并且印證了我們之前的關(guān)于 ChannelPipeline主要由一系列的 ChannelHandler 所組成的說法。 ChannelPipeline還提供了通過 ChannelPipeline 本身傳播事件的方法我碟。如果一個入站事件被觸發(fā)放案,它將被從 ChannelPipeline的頭部開始一直被傳播到 Channel Pipeline 的尾端。

你可能會說矫俺, 從事件途經(jīng) ChannelPipeline的角度來看吱殉, ChannelPipeline的頭部和尾端取決于該事件是入站的還是出站的。然而 Netty 總是將 ChannelPipeline的入站口(圖 的左側(cè))作為頭部厘托,而將出站口(該圖的右側(cè))作為尾端友雳。
當(dāng)你完成了通過調(diào)用 ChannelPipeline.add*()方法將入站處理器( ChannelInboundHandler)和 出 站 處 理 器 ( ChannelOutboundHandler ) 混 合 添 加 到 ChannelPipeline之 后 , 每 一 個ChannelHandler 從頭部到尾端的順序位置正如同我們方才所定義它們的一樣催烘。因此沥阱,如果你將圖 6-3 中的處理器( ChannelHandler)從左到右進(jìn)行編號,那么第一個被入站事件看到的 ChannelHandler 將是1伊群,而第一個被出站事件看到的 ChannelHandler將是 5考杉。

ChannelPipeline 傳播事件時(shí),它會測試 ChannelPipeline 中的下一個 Channel?Handler 的類型是否和事件的運(yùn)動方向相匹配舰始。如果不匹配崇棠, ChannelPipeline 將跳過該ChannelHandler 并前進(jìn)到下一個,直到它找到和該事件所期望的方向相匹配的為止丸卷。 (當(dāng)然枕稀, ChannelHandler也可以同時(shí)實(shí)現(xiàn)ChannelInboundHandler接口和 ChannelOutboundHandler 接口。)

修改ChannelPipeline

修改指的是添加或刪除ChannelHandler,見代碼示例:

ChannelPipeline pipeline = ..;
FirstHandler firstHandler = new FirstHandler();
// 先添加一個Handler到ChannelPipeline中
pipeline.addLast("handler1", firstHandler);
// 這個Handler放在了first,意味著放在了handler1之前
pipeline.addFirst("handler2", new SecondHandler());
// 這個Handler被放到了last,意味著在handler1之后
pipeline.addLast("handler3", new ThirdHandler());
...
// 通過名稱刪除
pipeline.remove("handler3");
// 通過對象刪除
pipeline.remove(firstHandler);
// 名稱"handler2"替換成名稱"handler4",并切handler2的實(shí)例替換成了handler4的實(shí)例
pipeline.replace("handler2", "handler4", new ForthHandler());

ChannelPipeline的出入站API

入站API所示:

[圖片上傳失敗...(image-6037f5-1598167949595)]

出站API所示:

dUndZ6.png

ChannelPipeline 這個組件上面所講的大致只需要記住這三點(diǎn)即可:

  • ChannelPipeline 保存了與 Channel 相關(guān)聯(lián)的 ChannelHandler
  • ChannelPipeline可以根據(jù)需要,通過添加或者刪除 ChannelHandler 來動態(tài)地修改
  • ChannelPipeline有著豐富的API用以被調(diào)用萎坷,以響應(yīng)入站和出站事件

ChannelHandlerContext

當(dāng) ChannelHandler 被添加到 ChannelPipeline 時(shí)凹联,它將會被分配一個 ChannelHandlerContext ,它代表了 ChannelHandlerChannelPipeline 之間的綁定哆档。ChannelHandlerContext 的主要功能是管理它所關(guān)聯(lián)的ChannelHandler和在同一個 ChannelPipeline 中的其他ChannelHandler之間的交互蔽挠。

如果調(diào)用ChannelChannelPipeline上的方法,會沿著整個ChannelPipeline傳播,如果調(diào)用ChannelHandlerContext上的相同方法,則會從對應(yīng)的當(dāng)前ChannelHandler進(jìn)行傳播。

ChannelHandlerContext API如下表所示:

dUn0IO.png
  • ChannelHandlerContextChannelHandler之間的關(guān)聯(lián)(綁定)是永遠(yuǎn)不會改變的瓜浸,所以緩存對它的引用是安全的澳淑;
  • 如同在本節(jié)開頭所解釋的一樣,相對于其他類的同名方法插佛,ChannelHandlerContext的方法將產(chǎn)生更短的事件流杠巡, 應(yīng)該盡可能地利用這個特性來獲得最大的性能。

ChannelHandler雇寇、ChannelPipeline的關(guān)聯(lián)使用

dUnDiD.png

ChannelHandlerContext訪問channel

ChannelHandlerContext ctx = ..;
// 獲取channel引用
Channel channel = ctx.channel();
// 通過channel寫入緩沖區(qū)
channel.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));

ChannelHandlerContext訪問ChannelPipeline

ChannelHandlerContext ctx = ..;
// 獲取ChannelHandlerContext
ChannelPipeline pipeline = ctx.pipeline();
// 通過ChannelPipeline寫入緩沖區(qū)
pipeline.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));
dUnrJe.png

有時(shí)候我們不想從頭傳遞數(shù)據(jù),想跳過幾個handler,從某個handler開始傳遞數(shù)據(jù).我們必須獲取目標(biāo)handler之前的handler關(guān)聯(lián)的ChannelHandlerContext氢拥。

ChannelHandlerContext ctx = ..;
// 直接通過ChannelHandlerContext寫數(shù)據(jù),發(fā)送到下一個handler
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
dUnyzd.png

好了,ChannelHandlerContext的基本使用應(yīng)該掌握了,但是你真的理解ChannelHandlerContext,ChannelPipelineChannelhandler之間的關(guān)系了嗎?不理解也沒關(guān)系谢床,因?yàn)樵创a以后會幫你理解的更為深刻兄一。

核心組件之間的關(guān)系

  • 一個 Channel對應(yīng)一個 ChannelPipeline
  • 一個 ChannelPipeline 包含一條雙向的 ChannelHandlerContext
  • 一個 ChannelHandlerContext中包含一個ChannelHandler
  • 一個 Channel會綁定到一個EventLoop
  • 一個 NioEventLoop 維護(hù)了一個 Selector(使用的是 Java 原生的 Selector)
  • 一個 NioEventLoop 相當(dāng)于一個線程

粘包拆包問題

粘包拆包問題是處于網(wǎng)絡(luò)比較底層的問題,在數(shù)據(jù)鏈路層识腿、網(wǎng)絡(luò)層以及傳輸層都有可能發(fā)生。我們?nèi)粘5木W(wǎng)絡(luò)應(yīng)用開發(fā)大都在傳輸層進(jìn)行造壮,由于UDP有消息保護(hù)邊界渡讼,不會發(fā)生粘包拆包問題,而因此粘包拆包問題只發(fā)生在TCP協(xié)議中耳璧。具體講TCP是個”流"協(xié)議成箫,只有流的概念,沒有包的概念旨枯,對于業(yè)務(wù)上層數(shù)據(jù)的具體含義和邊界并不了解蹬昌,它只會根據(jù)TCP緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分。所以在業(yè)務(wù)上認(rèn)為攀隔,一個完整的包可能會被TCP拆分成多個包進(jìn)行發(fā)送皂贩,也有可能把多個小的包封裝成一個大的數(shù)據(jù)包發(fā)送,這就是所謂的TCP粘包和拆包問題昆汹。

問題舉例說明

下面針對客戶端分別發(fā)送了兩個數(shù)據(jù)表Packet1Packet2給服務(wù)端的時(shí)候明刷,TCP粘包和拆包會出現(xiàn)的情況進(jìn)行列舉說明:

(1)第一種情況,服務(wù)端分兩次正常收到兩個獨(dú)立數(shù)據(jù)包满粗,即沒有發(fā)生拆包和粘包的現(xiàn)象辈末;

dUncQA.png

(2)第二種情況,接收端只收到一個數(shù)據(jù)包,由于TCP是不會出現(xiàn)丟包的挤聘,所以這一個數(shù)據(jù)包中包含了客戶端發(fā)送的兩個數(shù)據(jù)包的信息轰枝,這種現(xiàn)象即為粘包。這種情況由于接收端不知道這兩個數(shù)據(jù)包的界限组去,所以對于服務(wù)接收端來說很難處理鞍陨。

dUn2Lt.png

(3)第三種情況,服務(wù)端分兩次讀取到了兩個數(shù)據(jù)包添怔,第一次讀取到了完整的Packet1Packet2包的部分內(nèi)容湾戳,第二次讀取到了Packet2的剩余內(nèi)容,這被稱為TCP拆包广料;

d0Pq8s.png

(4)第四種情況砾脑,服務(wù)端分兩次讀取到了兩個數(shù)據(jù)包,第一次讀取到了部分的Packet1內(nèi)容艾杏,第二次讀取到了Packet1剩余內(nèi)容和Packet2的整包韧衣。

dUn5FS.png

如果此時(shí)服務(wù)端TCP接收滑窗非常小,而數(shù)據(jù)包Packet1Packet2比較大购桑,很有可能服務(wù)端需要分多次才能將兩個包接收完全畅铭,期間發(fā)生多次拆包。以上列舉情況的背后原因分別如下:

  1. 應(yīng)用程序?qū)懭氲臄?shù)據(jù)大于套接字緩沖區(qū)大小勃蜘,這將會發(fā)生拆包硕噩。
  2. 應(yīng)用程序?qū)懭霐?shù)據(jù)小于套接字緩沖區(qū)大小,網(wǎng)卡將應(yīng)用多次寫入的數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上缭贡,這將會發(fā)生粘包炉擅。
  3. 進(jìn)行MSS(最大報(bào)文長度)大小的TCP分段,當(dāng)TCP報(bào)文長度-TCP頭部長度>MSS的時(shí)候?qū)l(fā)生拆包阳惹。
  4. 接收方法不及時(shí)讀取套接字緩沖區(qū)數(shù)據(jù)谍失,這將發(fā)生粘包。

如何基于Netty處理粘包莹汤、拆包問題

由于底層的TCP無法理解上層的業(yè)務(wù)數(shù)據(jù)快鱼,所以在底層是無法保證數(shù)據(jù)包不被拆分和重組的,這個問題只能通過上層的應(yīng)用協(xié)議棧設(shè)計(jì)來解決纲岭,根據(jù)業(yè)界的主流協(xié)議的解決方案抹竹,可以歸納如下:

  1. 消息定長,例如每個報(bào)文的大小為固定長度200字節(jié)荒勇,如果不夠柒莉,空位補(bǔ)空格;
  2. 在包尾增加回車換行符進(jìn)行分割沽翔,例如FTP協(xié)議兢孝;
  3. 將消息分為消息頭和消息體窿凤,消息頭中包含表示消息總長度的字段,通常設(shè)計(jì)思路為消息頭的第一個字段使用int32來表示消息的總長度跨蟹;
  4. 更復(fù)雜的應(yīng)用層協(xié)議雳殊。

之前Netty示例中其實(shí)并沒有考慮讀半包問題,這在功能測試往往沒有問題窗轩,但是一旦請求數(shù)過多或者發(fā)送大報(bào)文之后夯秃,就會存在該問題。如果代碼沒有考慮痢艺,往往就會出現(xiàn)解碼錯位或者錯誤仓洼,導(dǎo)致程序不能正常工作,下面看看Netty是如何根據(jù)主流的解決方案進(jìn)行抽象實(shí)現(xiàn)來幫忙解決這一問題的堤舒。

如下表所示色建,Netty為了找出消息的邊界,采用封幀方式:

方式 解碼 編碼
固定長度 FixedLengthFrameDecoder 簡單
分隔符 DelimiterBasedFrameDecoder 簡單
專門的 length 字段 LengthFieldBasedFrameDecoder LengthFieldPrepender

注意到舌缤,Netty提供了對應(yīng)的解碼器來解決對應(yīng)的問題箕戳,有了這些解碼器,用戶不需要自己對讀取的報(bào)文進(jìn)行人工解碼国撵,也不需要考慮TCP的粘包和半包問題陵吸。為什么這么說呢?下面列舉一個包尾增加分隔符的例子:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: wuxiaofei
 * @Date: 2020/8/15 0015 19:15
 * @Version: 1.0
 * @Description:入站處理器
 */
@ChannelHandler.Sharable
public class DelimiterServerHandler extends ChannelInboundHandlerAdapter {

    private AtomicInteger counter = new AtomicInteger(0);
    private AtomicInteger completeCounter = new AtomicInteger(0);

    /*** 服務(wù)端讀取到網(wǎng)絡(luò)數(shù)據(jù)后的處理*/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf)msg;
        String request = in.toString(CharsetUtil.UTF_8);
        System.out.println("Server Accept["+request
                +"] and the counter is:"+counter.incrementAndGet());
        String resp = "Hello,"+request+". Welcome to Netty World!"
                + DelimiterEchoServer.DELIMITER_SYMBOL;
        ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));
    }

    /*** 服務(wù)端讀取完成網(wǎng)絡(luò)數(shù)據(jù)后的處理*/
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx)
            throws Exception {
        ctx.fireChannelReadComplete();
        System.out.println("the ReadComplete count is "
                +completeCounter.incrementAndGet());
    }

    /*** 發(fā)生異常后的處理*/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;

import java.net.InetSocketAddress;

/**
 * @Author: wuxiaofei
 * @Date: 2020/8/15 0015 19:17
 * @Version: 1.0
 * @Description:服務(wù)端
 */
public class DelimiterEchoServer {

    public static final String DELIMITER_SYMBOL = "@~";
    public static final int PORT = 9997;

    public static void main(String[] args) throws InterruptedException {
        DelimiterEchoServer delimiterEchoServer = new DelimiterEchoServer();
        System.out.println("服務(wù)器即將啟動");
        delimiterEchoServer.start();
    }

    public void start() throws InterruptedException {
        final DelimiterServerHandler serverHandler = new DelimiterServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();/*線程組*/
        try {
            ServerBootstrap b = new ServerBootstrap();/*服務(wù)端啟動必須*/
            b.group(group)/*將線程組傳入*/
                .channel(NioServerSocketChannel.class)/*指定使用NIO進(jìn)行網(wǎng)絡(luò)傳輸*/
                .localAddress(new InetSocketAddress(PORT))/*指定服務(wù)器監(jiān)聽端口*/
                /*服務(wù)端每接收到一個連接請求介牙,就會新啟一個socket通信壮虫,也就是channel,
                所以下面這段代碼的作用就是為這個子channel增加handle*/
                .childHandler(new ChannelInitializerImp());
            ChannelFuture f = b.bind().sync();/*異步綁定到服務(wù)器环础,sync()會阻塞直到完成*/
            System.out.println("服務(wù)器啟動完成旨指,等待客戶端的連接和數(shù)據(jù).....");
            f.channel().closeFuture().sync();/*阻塞直到服務(wù)器的channel關(guān)閉*/
        } finally {
            group.shutdownGracefully().sync();/*優(yōu)雅關(guān)閉線程組*/
        }
    }

    private static class ChannelInitializerImp extends ChannelInitializer<Channel> {

        @Override
        protected void initChannel(Channel ch) throws Exception {
            ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_SYMBOL
                    .getBytes());
            //服務(wù)端收到數(shù)據(jù)包后經(jīng)過DelimiterBasedFrameDecoder即分隔符基礎(chǔ)框架解碼器解碼為一個個帶有分隔符的數(shù)據(jù)包。
            ch.pipeline().addLast( new DelimiterBasedFrameDecoder(1024,
                    delimiter));
            ch.pipeline().addLast(new DelimiterServerHandler());
        }
    }

}

添加到ChannelPipelineDelimiterBasedFrameDecoder用于對使用分隔符結(jié)尾的消息進(jìn)行自動解碼喳整,當(dāng)然還有沒有用到的FixedLengthFrameDecoder用于對固定長度的消息進(jìn)行自動解碼等解碼器。正如上門的代碼使用案例裸扶,有了Netty提供的幾碼器可以輕松地完成對很多消息的自動解碼框都,而且不需要考慮TCP粘包/拆包導(dǎo)致的讀半包問題,極大地提升了開發(fā)效率呵晨。

Netty示例代碼詳解

相信看完上面的鋪墊魏保,你對Netty編碼有了一定的了解了,下面再來整體梳理一遍吧摸屠。

dVp7yn.png

1谓罗、設(shè)置EventLoopGroup線程組(Reactor線程組)

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

上面我們說過Netty中使用Reactor模式,bossGroup表示服務(wù)器連接監(jiān)聽線程組季二,專門接受 Accept 新的客戶端client 連接檩咱。另一個workerGroup表示處理每一連接的數(shù)據(jù)收發(fā)的線程組揭措,來處理消息的讀寫事件。

2刻蚯、服務(wù)端引導(dǎo)器

ServerBootstrap serverBootstrap = new ServerBootstrap();

集成所有配置绊含,用來啟動Netty服務(wù)端。

3炊汹、設(shè)置ServerBootstrap信息

serverBootstrap.group(bossGroup, workerGroup);

將兩個線程組設(shè)置到ServerBootstrap中躬充。

4、設(shè)置ServerSocketChannel類型

serverBootstrap.channel(NioServerSocketChannel.class);

設(shè)置通道的IO類型讨便,Netty不止支持Java NIO充甚,也支持阻塞式IO,例如OIOOioServerSocketChannel.class)

5霸褒、設(shè)置參數(shù)

serverBootstrap.option(ChannelOption.SO_BACKLOG, 100);

通過option()方法可以設(shè)置很多參數(shù)伴找,這里SO_BACKLOG標(biāo)識服務(wù)端接受連接的隊(duì)列長度,如果隊(duì)列已滿傲霸,客戶端連接將被拒絕疆瑰。默認(rèn)值,Windows為200昙啄,其他為128穆役,這里設(shè)置的是100。

6梳凛、設(shè)置Handler

serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));

設(shè)置 ServerSocketChannel對應(yīng)的Handler耿币,這里只能設(shè)置一個,它會在SocketChannel建立起來之前執(zhí)行韧拒。

7淹接、設(shè)置子Handler

serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline p = ch.pipeline();
        p.addLast(new LoggingHandler(LogLevel.INFO));
        p.addLast(new ChatNettyHandler());
    }
});

Netty中提供了一種可以設(shè)置多個Handler的途徑,即使用ChannelInitializer方式叛溢。ChannelPipelineNetty處理請求的責(zé)任鏈塑悼,這是一個ChannelHandler的鏈表,而ChannelHandler就是用來處理網(wǎng)絡(luò)請求的內(nèi)容的楷掉。

每一個channel厢蒜,都有一個處理器流水線。裝配child channel流水線烹植,調(diào)用childHandler()方法斑鸦,傳遞一個ChannelInitializer 的實(shí)例。

child channel 創(chuàng)建成功草雕,開始通道初始化的時(shí)候巷屿,在bootstrap啟動器中配置的ChannelInitializer 實(shí)例就會被調(diào)用。

這個時(shí)候墩虹,才真正的執(zhí)行去執(zhí)行 initChannel 初始化方法嘱巾,開始通道流水線裝配憨琳。

流水線裝配,主要是在流水線pipeline的后面浓冒,增加負(fù)責(zé)數(shù)據(jù)讀寫栽渴、處理業(yè)務(wù)邏輯的handler

處理器 ChannelHandler 用來處理網(wǎng)絡(luò)請求內(nèi)容稳懒,有ChannelInboundHandlerChannelOutboundHandler兩種闲擦,ChannlPipeline會從頭到尾順序調(diào)用ChannelInboundHandler處理網(wǎng)絡(luò)請求內(nèi)容,從尾到頭調(diào)用ChannelOutboundHandler處理網(wǎng)絡(luò)請求內(nèi)容

8场梆、綁定端口號

ChannelFuture f = serverBootstrap.bind(PORT).sync();

綁定端口號

9墅冷、等待服務(wù)端端口號關(guān)閉

f.channel().closeFuture().sync();

等待服務(wù)端監(jiān)聽端口關(guān)閉,sync()會阻塞主線程或油,內(nèi)部調(diào)用的是 Objectwait()方法

10寞忿、關(guān)閉EventLoopGroup線程組

bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

總結(jié)

這篇文章主要是從一個demo作為引子,然后介紹了Netty的包結(jié)構(gòu)顶岸、Reactor模型腔彰、編程規(guī)范等等,目的很簡單辖佣,希望你能夠讀懂這段demo并寫出來霹抛。

后面開始繼續(xù)Netty源碼解析部分,敬請期待卷谈。

參考資料

  1. 《Netty in Action》書籍
  2. 慕課Netty專欄
  3. 掘金閃電俠Netty小冊
  4. 芋道源碼Netty專欄
  5. Github[fork from krcys]

感謝Netty專欄作者們優(yōu)秀的文章內(nèi)容~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杯拐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子世蔗,更是在濱河造成了極大的恐慌端逼,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件污淋,死亡現(xiàn)場離奇詭異顶滩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)寸爆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門诲祸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人而昨,你說我怎么就攤上這事≌姨铮” “怎么了歌憨?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長墩衙。 經(jīng)常有香客問我务嫡,道長甲抖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任心铃,我火速辦了婚禮准谚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘去扣。我一直安慰自己柱衔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布愉棱。 她就那樣靜靜地躺著唆铐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奔滑。 梳的紋絲不亂的頭發(fā)上艾岂,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音朋其,去河邊找鬼王浴。 笑死,一個胖子當(dāng)著我的面吹牛梅猿,可吹牛的內(nèi)容都是我干的氓辣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼粒没,長吁一口氣:“原來是場噩夢啊……” “哼筛婉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起癞松,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤爽撒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后响蓉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硕勿,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年枫甲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了源武。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡想幻,死狀恐怖粱栖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脏毯,我是刑警寧澤闹究,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站食店,受9級特大地震影響渣淤,放射性物質(zhì)發(fā)生泄漏赏寇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一价认、第九天 我趴在偏房一處隱蔽的房頂上張望嗅定。 院中可真熱鬧,春花似錦用踩、人聲如沸渠退。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽智什。三九已至,卻和暖如春丁屎,著一層夾襖步出監(jiān)牢的瞬間荠锭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工晨川, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留证九,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓共虑,卻偏偏與公主長得像愧怜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妈拌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344