netty 拆、粘包

定義

TCP是個(gè)“流”協(xié)議,所謂流,就是沒(méi)有界限的一串?dāng)?shù)據(jù)。大家可以想象河里的流水,它們是連成一片的,其間并沒(méi)有分界線。TCP底層并不了解上層業(yè)務(wù)數(shù)據(jù)的具體含義,它會(huì)根據(jù)TCP緩沖區(qū)的實(shí)際情況進(jìn)行包的劃分,所以在業(yè)務(wù)上認(rèn)為,一個(gè)完整的包可能會(huì)被TCP拆分成多個(gè)包進(jìn)行發(fā)送,也有可能把多個(gè)小的包封裝成一個(gè)大的數(shù)據(jù)包發(fā)送,這就是所謂的TCP粘包和拆包問(wèn)題。

TCP粘包/拆包問(wèn)題說(shuō)明
假設(shè)客戶端分別發(fā)送了兩個(gè)數(shù)據(jù)包D1和D2給服務(wù)端,由于服務(wù)端一次讀取的字節(jié)數(shù)是不定的,故可能存在以下四種情況。
(1) 服務(wù)端分兩次讀取到了兩個(gè)獨(dú)立的數(shù)據(jù)包,分別是D1和D2,沒(méi)有粘包和拆包;
(2) 服務(wù)端一次接收到了兩個(gè)數(shù)據(jù)包,D1和D2粘合在一起,被稱為TCP粘包;
(3) 服務(wù)端分兩次讀取到了兩個(gè)數(shù)據(jù)包,第一次讀取到了完整的D1包和D2包的部分內(nèi)容,第二次讀取到了D2包的剩余內(nèi)容,這被稱為TCP拆包;
(4) 服務(wù)端分兩次讀取到了兩個(gè)數(shù)據(jù)包,第一次讀取到了D1包的部分內(nèi)容D1_1,第二次讀取到了D1包的剩余內(nèi)容D1_2和D2包的整包。

如果此時(shí)服務(wù)端TCP接收滑窗非常小,而數(shù)據(jù)包D1和D2比較大,很有可能會(huì)發(fā)生第5種可能,即服務(wù)端分多次才能將D1和D2包接收完全,期間發(fā)生多次拆包。

緣由

問(wèn)題產(chǎn)生的原因有三個(gè),分別如下:

  1. 應(yīng)用程序write寫入的字節(jié)大小大于套接口發(fā)送緩沖區(qū)大小;
  2. 進(jìn)行MSS大小的TCP分段;
  3. 以太網(wǎng)幀的payload大于MTU進(jìn)行IP分片;


    image.png

由于底層的TCP無(wú)法理解上層的業(yè)務(wù)數(shù)據(jù),所以在底層是無(wú)法保證數(shù)據(jù)包不被拆分和重組的,這個(gè)問(wèn)題只能通過(guò)上層的應(yīng)用協(xié)議棧設(shè)計(jì)來(lái)解決

TCP 是一個(gè)面向字節(jié)流的協(xié)議,它是性質(zhì)是流式的,所以它并沒(méi)有分段。就像水流一樣,你沒(méi)法知道什么時(shí)候開(kāi)始,什么時(shí)候結(jié)束。所以他會(huì)根據(jù)當(dāng)前的套接字緩沖區(qū)的情況進(jìn)行拆包或是粘包。

首先,人的正常思維習(xí)慣是,數(shù)據(jù)傳輸應(yīng)該是基于報(bào)文的,人們不論是對(duì)話還是寫信,肯定是一句一句的話或者一封一封的信,這就是所謂的報(bào)文。而 TCP 是什么東西呢?TCP 是一種流式協(xié)議,簡(jiǎn)單說(shuō),你使用它的時(shí)候,根本就沒(méi)有所謂的報(bào)文,無(wú)論是聊天說(shuō)的話還是發(fā)的圖片,對(duì)于 TCP 來(lái)說(shuō),通通都是沒(méi)有邊界的,TCP 根本不認(rèn)識(shí)這些東西,不知道你按換行時(shí)是一句話,你發(fā)的圖片是一張圖片,所有的東西都是數(shù)據(jù)流沒(méi)有任何邊界。這顯然不符合人的正常思維,是扭曲的。而無(wú)論是人的思維,還是實(shí)際的東西,一定是要有邊界的。這就有矛盾了。所以,對(duì)于程序員,在使用 TCP 之前,你必須進(jìn)行報(bào)文協(xié)議設(shè)計(jì)。要么你使用別人設(shè)計(jì)好的報(bào)文協(xié)議,要么你自己設(shè)計(jì)。如果你自己不設(shè)計(jì),也不使用別人設(shè)計(jì)好的,那么你肯定錯(cuò)了,一定會(huì)遇到所謂的粘包和拆包。

一般的程序員當(dāng)然不愿意自己設(shè)計(jì),一是沒(méi)能力,二是不需要重新發(fā)明。那么設(shè)計(jì)完之后呢,必然是實(shí)現(xiàn)。因?yàn)樽陨硭降膯?wèn)題,還是會(huì)遇到 TCP 粘包和拆包問(wèn)題。所以,我的建議是,不要自己去實(shí)現(xiàn),應(yīng)該去使用別人已經(jīng)寫好的代碼。

下圖展示了一個(gè) TCP 協(xié)議傳輸?shù)倪^(guò)程:

image.png

發(fā)送端的字節(jié)流都會(huì)先傳入緩沖區(qū),再通過(guò)網(wǎng)絡(luò)傳入到接收端的緩沖區(qū)中,最終由接收端獲取。

當(dāng)我們發(fā)送兩個(gè)完整包到接收端的時(shí)候:

image.png

正常情況會(huì)接收到兩個(gè)完整的報(bào)文。

但也有以下的情況:

image.png

接收到的是一個(gè)報(bào)文,它是由發(fā)送的兩個(gè)報(bào)文組成的,這樣對(duì)于應(yīng)用程序來(lái)說(shuō)就很難處理了(這樣稱為粘包)。

image.png

還有可能出現(xiàn)上面這樣的雖然收到了兩個(gè)包,但是里面的內(nèi)容卻是互相包含,對(duì)于應(yīng)用來(lái)說(shuō)依然無(wú)法解析(拆包)

接下來(lái)講一講如果自己直接使用 TCP 的接口,為什么會(huì)遇到粘包和拆包問(wèn)題呢?TCP 的接口簡(jiǎn)單說(shuō)只有兩個(gè):

send(data, length);
recv(buff);

不懂的人會(huì)基于人的正常思維想當(dāng)然地認(rèn)為 send() 就是發(fā)送報(bào)文,recv() 就是接收?qǐng)?bào)文。但是,前面已經(jīng)提到了,TCP 是沒(méi)有所謂的報(bào)文邊界的,所以你錯(cuò)了。為什么錯(cuò)?因?yàn)?send() 和 recv() 不是一一對(duì)應(yīng)的。也就是說(shuō),send() 的調(diào)用次數(shù)和 recv() 的調(diào)用次數(shù)是獨(dú)立的,有時(shí)是相等的(你在自己機(jī)器上和內(nèi)網(wǎng)測(cè)試時(shí)基本都是相等的),有時(shí)是不相等的(互聯(lián)網(wǎng)上非常容易出現(xiàn))。

現(xiàn)在明白了吧,TCP 的 send() 和 recv() 不是一一對(duì)應(yīng)的,理所當(dāng)然會(huì)粘應(yīng)用層的包,拆應(yīng)用層的包。明白了之后怎么解決?簡(jiǎn)單,用別人封裝好的代碼。或者,你自己慢慢琢磨去吧!
正確地從TCP socket中讀取報(bào)文的代碼必須長(zhǎng)這個(gè)樣子,如果不長(zhǎng)這個(gè)樣子,就是錯(cuò)的!

char recv_buf[];
Buffer buffer;
// 網(wǎng)絡(luò)循環(huán):必須在一個(gè)循環(huán)中讀取網(wǎng)絡(luò),因?yàn)榫W(wǎng)絡(luò)數(shù)據(jù)是源源不斷的。
while(1){
    // 從TCP流中讀取不定長(zhǎng)度的一段流數(shù)據(jù),不能保證讀到的數(shù)據(jù)是你期望的長(zhǎng)度
    tcp.read(recv_buf);
    // 將這段流數(shù)據(jù)和之前收到的流數(shù)據(jù)拼接到一起
    buffer.append(recv_buf);
    // 解析循環(huán):必須在一個(gè)循環(huán)中解析報(bào)文,避免所謂的粘包
    while(1){
        // 嘗試解析報(bào)文
        msg = parse(buffer);
        if(!msg){
            // 報(bào)文還沒(méi)有準(zhǔn)備好,糟糕,我們遇到拆包了!跳出解析循環(huán),繼續(xù)讀網(wǎng)絡(luò)。
            break;
        }
        // 將解析過(guò)的報(bào)文對(duì)應(yīng)的流數(shù)據(jù)清除
        buffer.remove(msg.length);
        // 業(yè)務(wù)處理
        process(msg);
    }
}

對(duì)于具體的解析報(bào)文這樣的問(wèn)題只能通過(guò)上層的應(yīng)用來(lái)解決,常見(jiàn)的方式有:

  • 在報(bào)文末尾增加換行符表明一條完整的消息,這樣在接收端可以根據(jù)這個(gè)換行符來(lái)判斷消息是否完整。
  • 將消息分為消息頭、消息體。可以在消息頭中聲明消息的長(zhǎng)度,根據(jù)這個(gè)長(zhǎng)度來(lái)獲取報(bào)文(比如 808 協(xié)議)。
  • 規(guī)定好報(bào)文長(zhǎng)度,不足的空位補(bǔ)齊,取的時(shí)候按照長(zhǎng)度截取即可。

以上的這些方式我們?cè)?Netty 的 pipline 中里加入對(duì)應(yīng)的解碼器都可以手動(dòng)實(shí)現(xiàn)。
但其實(shí) Netty 已經(jīng)幫我們做好了,完全可以開(kāi)箱即用。

LineBasedFrameDecoder 可以基于換行符解決。
DelimiterBasedFrameDecoder可基于分隔符解決。
FixedLengthFrameDecoder可指定長(zhǎng)度解決。

字符串拆、粘包

簡(jiǎn)單的來(lái)說(shuō):
多次發(fā)送較少內(nèi)容,會(huì)發(fā)生粘包現(xiàn)象。
單次發(fā)送內(nèi)容過(guò)多,會(huì)發(fā)生拆包現(xiàn)象

下面來(lái)模擬一下最簡(jiǎn)單的字符串傳輸。

public class NettyClientHandler extends ChannelInboundHandlerAdapter{  
    
    private byte[] req;  
      
    private int counter;   
      
    public NettyClientHandler() {  
        req = ("書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!").getBytes();
    }  
      
    @Override  
    public void channelActive(ChannelHandlerContext ctx) throws Exception {  
        ByteBuf message = null;  
        //會(huì)發(fā)生粘包現(xiàn)象
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }  
      
    @Override  
    public void channelRead(ChannelHandlerContext ctx, Object msg)  
        throws Exception {  
        String buf = (String) msg;  
        System.out.println("現(xiàn)在 : " + buf + " ; 條數(shù)是 : "+ ++counter);  
    }  
  
    @Override  
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {  
        ctx.close();  
    }  
}

書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!發(fā)送一百次,就會(huì)發(fā)送粘包現(xiàn)象;
server端

public class NettyServerHandler extends ChannelInboundHandlerAdapter{  
    private int counter;  
    @Override  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
          
        String body = (String)msg;  
        System.out.println("接受的數(shù)據(jù)是: " + body + ";條數(shù)是: " + ++counter);  
    }  
    @Override  
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
        cause.printStackTrace();  
        ctx.close();  
    }  
}  
服務(wù)端啟動(dòng)成功,端口為 :2345
接受的數(shù)據(jù)是: 書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí);條數(shù)是: 1
接受的數(shù)據(jù)是: 方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,;條數(shù)是: 2
接受的數(shù)據(jù)是: 事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò);條數(shù)是: 3
接受的數(shù)據(jù)是: 不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!?;條數(shù)是: 4
接受的數(shù)據(jù)是: ?到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!書(shū)到用時(shí)方恨少,事非經(jīng)過(guò)不知難!;條數(shù)是: 5

可以看到 ctx.writeAndFlush(message) 在client了調(diào)用了100次,也即100次send,但是在server端 read只調(diào)用了5次,這也驗(yàn)證了前面講的send和receive次數(shù)不一致問(wèn)題。

而將《春江花月夜》和《行路難》發(fā)送一次就會(huì)發(fā)送拆包現(xiàn)象;

接受的數(shù)據(jù)是: 春江潮水連海平,海上明月共潮生。  滟滟隨波千萬(wàn)里,何處春江無(wú)月明!  江流宛轉(zhuǎn)繞芳甸,月照花林皆似霰;    空里流霜不覺(jué)飛,汀上白沙看不見(jiàn)。    江天一色無(wú)纖塵,皎皎空中孤月輪。  江畔何人初見(jiàn)月?江月何年初照人?  人生代代無(wú)窮已,江月年年望相似。    不知江月待何人,但見(jiàn)長(zhǎng)江送流水。    白云一片去悠悠,青楓浦上不勝愁。    誰(shuí)家今夜扁舟子?何處相思明月樓?    可憐樓上月徘徊,應(yīng)照離人妝鏡臺(tái)。    玉戶簾中卷不去,搗衣砧上拂還來(lái)。    此時(shí)相望不相聞,愿逐月華流照君。    鴻雁長(zhǎng)飛光不度,魚(yú)龍潛躍水成文。    昨夜閑潭夢(mèng)落花,可憐春半不還家。    江水流春去欲盡,江潭落月復(fù)西斜。    斜月沉沉藏海霧,碣石瀟湘無(wú)限路。    不知乘月幾人歸,落月?lián)u情滿江樹(shù)。 噫吁嚱,危乎高哉!蜀道之難,難于上青天!蠶叢及魚(yú)鳧,開(kāi)國(guó)何茫然!爾來(lái)四萬(wàn)八千歲,不與秦塞通人煙?;條數(shù)是: 1
接受的數(shù)據(jù)是: ? 西當(dāng)太白有鳥(niǎo)道,可以橫絕峨眉巔。地崩山摧壯士死,然后天梯石棧相鉤連。上有六龍回日之高標(biāo),下有沖波逆折之回川。 黃鶴之飛尚不得過(guò),猿猱欲度愁攀援。青泥何盤盤,百步九折縈巖巒。捫參歷井仰脅息,以手撫膺坐長(zhǎng)嘆。 問(wèn)君西游何時(shí)還?畏途巉巖不可攀。但見(jiàn)悲鳥(niǎo)號(hào)古木,雄飛雌從繞林間。又聞子規(guī)啼夜月,愁空山。 蜀道之難,難于上青天,使人聽(tīng)此凋朱顏!連峰去天不盈尺,枯松倒掛倚絕壁。飛湍瀑流爭(zhēng)喧豗,砯崖轉(zhuǎn)石萬(wàn)壑雷。 其險(xiǎn)也如此,嗟爾遠(yuǎn)道之人胡為乎來(lái)哉!劍閣崢嶸而崔嵬,一夫當(dāng)關(guān),萬(wàn)夫莫開(kāi)。 所守或匪親,化為狼與豺。朝避猛虎,夕避長(zhǎng)蛇;磨牙吮血,殺人如麻。錦城雖云樂(lè),不如早還家。 蜀道之難,難于上青天,側(cè)身西望長(zhǎng)咨嗟!
;條數(shù)是: 2

實(shí)際只發(fā)送了一次,但receive了兩次,發(fā)生了拆包行為。

該怎么解決呢?這便可采用之前提到的 LineBasedFrameDecoder 利用換行符解決。LineBasedFrameDecoder 解碼器使用非常簡(jiǎn)單,只需要在 pipline 鏈條上添加即可。

//字符串解析,換行防拆包
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder())

構(gòu)造函數(shù)中傳入了 1024 是指報(bào)的長(zhǎng)度最大不超過(guò)這個(gè)值,具體可以看下文的源碼分析。

注意,由于 LineBasedFrameDecoder 解碼器是通過(guò)換行符來(lái)判斷的,所以在發(fā)送時(shí),一條完整的消息需要加上 \n。

LineBasedFrameDecoder 的原理

image.png

從這個(gè)邏輯中可以看出就是尋找報(bào)文中是否包含換行符,并進(jìn)行相應(yīng)的截取。

由于是通過(guò)緩沖區(qū)讀取的,所以即使這次沒(méi)有換行符的數(shù)據(jù),只要下一次的報(bào)文存在換行符,上一輪的數(shù)據(jù)也不會(huì)丟。

為什么要粘包拆包

為什么要粘包
在用戶數(shù)據(jù)量非常小的情況下,極端情況下,一個(gè)字節(jié),該TCP數(shù)據(jù)包的有效載荷非常低,傳遞100字節(jié)的數(shù)據(jù),需要100次TCP傳送,100次ACK,在應(yīng)用及時(shí)性要求不高的情況下,將這100個(gè)有效數(shù)據(jù)拼接成一個(gè)數(shù)據(jù)包,那會(huì)縮短到一個(gè)TCP數(shù)據(jù)包,以及一個(gè)ack,有效載荷提高了,帶寬也節(jié)省了。

非極端情況,有可能兩個(gè)數(shù)據(jù)包拼接成一個(gè)數(shù)據(jù)包,也有可能一個(gè)半的數(shù)據(jù)包拼接成一個(gè)數(shù)據(jù)包,也有可能兩個(gè)半的數(shù)據(jù)包拼接成一個(gè)數(shù)據(jù)包。

為什么要拆包
拆包和粘包是相對(duì)的,一端粘了包,另外一端就需要將粘過(guò)的包拆開(kāi),舉個(gè)栗子,發(fā)送端將三個(gè)數(shù)據(jù)包粘成兩個(gè)TCP數(shù)據(jù)包發(fā)送到接收端,接收端就需要根據(jù)應(yīng)用協(xié)議將兩個(gè)數(shù)據(jù)包重新組裝成三個(gè)數(shù)據(jù)包. 還有一種情況就是用戶數(shù)據(jù)包超過(guò)了mss(最大報(bào)文長(zhǎng)度),那么這個(gè)數(shù)據(jù)包在發(fā)送的時(shí)候必須拆分成幾個(gè)數(shù)據(jù)包,接收端收到之后需要將這些數(shù)據(jù)包粘合起來(lái)之后,再拆開(kāi)。

拆包的原理
在沒(méi)有netty的情況下,用戶如果自己需要拆包,基本原理就是不斷從TCP緩沖區(qū)中讀取數(shù)據(jù),每次讀取完都需要判斷是否是一個(gè)完整的數(shù)據(jù)包

1.如果當(dāng)前讀取的數(shù)據(jù)不足以拼接成一個(gè)完整的業(yè)務(wù)數(shù)據(jù)包,那就保留該數(shù)據(jù),繼續(xù)從tcp緩沖區(qū)中讀取,直到得到一個(gè)完整的數(shù)據(jù)包

  1. 如果當(dāng)前讀到的數(shù)據(jù)加上已經(jīng)讀取的數(shù)據(jù)足夠拼接成一個(gè)數(shù)據(jù)包,那就將已經(jīng)讀取的數(shù)據(jù)拼接上本次讀取的數(shù)據(jù),夠成一個(gè)完整的業(yè)務(wù)數(shù)據(jù)包傳遞到業(yè)務(wù)邏輯,多余的數(shù)據(jù)仍然保留,以便和下次讀到的數(shù)據(jù)嘗試拼接.

netty 中的拆包也是如上這個(gè)原理,內(nèi)部會(huì)有一個(gè)累加器,每次讀取到數(shù)據(jù)都會(huì)不斷累加,然后嘗試對(duì)累加到的數(shù)據(jù)進(jìn)行拆包,拆成一個(gè)完整的業(yè)務(wù)數(shù)據(jù)包,這個(gè)基類叫做 ByteToMessageDecoder,下面我們先詳細(xì)分析下這個(gè)類

累加器
ByteToMessageDecoder 中定義了兩個(gè)累加器

public static final Cumulator MERGE_CUMULATOR = ...;
public static final Cumulator COMPOSITE_CUMULATOR = ...;

默認(rèn)情況下,會(huì)使用 MERGE_CUMULATOR

private Cumulator cumulator = MERGE_CUMULATOR;

MERGE_CUMULATOR 的原理是每次都將讀取到的數(shù)據(jù)通過(guò)內(nèi)存拷貝的方式,拼接到一個(gè)大的字節(jié)容器中,這個(gè)字節(jié)容器在 ByteToMessageDecoder中叫做 cumulation

ByteBuf cumulation;

下面我們看一下 MERGE_CUMULATOR 是如何將新讀取到的數(shù)據(jù)累加到字節(jié)容器里的

public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        ByteBuf buffer;
        if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                || cumulation.refCnt() > 1) {
            buffer = expandCumulation(alloc, cumulation, in.readableBytes());
        } else {
            buffer = cumulation;
        }
        buffer.writeBytes(in);
        in.release();
        return buffer;
}

netty 中ByteBuf的抽象,使得累加非常簡(jiǎn)單,通過(guò)一個(gè)簡(jiǎn)單的api調(diào)用 buffer.writeBytes(in); 便將新數(shù)據(jù)累加到字節(jié)容器中,為了防止字節(jié)容器大小不夠,在累加之前還進(jìn)行了擴(kuò)容處理

static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
        ByteBuf oldCumulation = cumulation;
        cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
        cumulation.writeBytes(oldCumulation);
        oldCumulation.release();
        return cumulation;
}

下面我們回到netty讀的主流程,目光集中在 channelRead 方法,channelRead方法是每次從TCP緩沖區(qū)讀到數(shù)據(jù)都會(huì)調(diào)用的方法,觸發(fā)點(diǎn)在AbstractNioByteChannel的read方法中,里面有個(gè)while循環(huán)不斷讀取,讀取到一次就觸發(fā)一次channelRead

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            ByteBuf data = (ByteBuf) msg;
            first = cumulation == null;
            if (first) {
                cumulation = data;
            } else {
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new DecoderException(t);
        } finally {
            if (cumulation != null && !cumulation.isReadable()) {
                numReads = 0;
                cumulation.release();
                cumulation = null;
            } else if (++ numReads >= discardAfterReads) {
                numReads = 0;
                discardSomeReadBytes();
            }

            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

可以分為以下幾個(gè)邏輯步驟
1.累加數(shù)據(jù)
2.將累加到的數(shù)據(jù)傳遞給業(yè)務(wù)進(jìn)行業(yè)務(wù)拆包
3.清理字節(jié)容器
4.傳遞業(yè)務(wù)數(shù)據(jù)包給業(yè)務(wù)解碼器處理

累加數(shù)據(jù)
如果當(dāng)前累加器沒(méi)有數(shù)據(jù),就直接跳過(guò)內(nèi)存拷貝,直接將字節(jié)容器的指針指向新讀取的數(shù)據(jù),否則,調(diào)用累加器累加數(shù)據(jù)至字節(jié)容器

ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
    cumulation = data;
} else {
    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}

將累加到的數(shù)據(jù)傳遞給業(yè)務(wù)進(jìn)行拆包
到這一步,字節(jié)容器里的數(shù)據(jù)已是目前未拆包部分的所有的數(shù)據(jù)了

CodecOutputList out = CodecOutputList.newInstance();
callDecode(ctx, cumulation, out);

callDecode 將嘗試將字節(jié)容器的數(shù)據(jù)拆分成業(yè)務(wù)數(shù)據(jù)包塞到業(yè)務(wù)數(shù)據(jù)容器out中

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    while (in.isReadable()) {
        // 記錄一下字節(jié)容器中有多少字節(jié)待拆
        int oldInputLength = in.readableBytes();
        decode(ctx, in, out);
        if (out.size() == 0) {
            // 拆包器未讀取任何數(shù)據(jù)
            if (oldInputLength == in.readableBytes()) {
                break;
            } else {
             // 拆包器已讀取部分?jǐn)?shù)據(jù),還需要繼續(xù)
                continue;
            }
        }

        if (oldInputLength == in.readableBytes()) {
            throw new DecoderException(
                    StringUtil.simpleClassName(getClass()) +
                    ".decode() did not read anything but decoded a message.");
        }

        if (isSingleDecode()) {
            break;
        }
    }
}

在解碼之前,先記錄一下字節(jié)容器中有多少字節(jié)待拆,然后調(diào)用抽象函數(shù) decode 進(jìn)行拆包

protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

netty中對(duì)各種用戶協(xié)議的支持就體現(xiàn)在這個(gè)抽象函數(shù)中,傳進(jìn)去的是當(dāng)前讀取到的未被消費(fèi)的所有的數(shù)據(jù),以及業(yè)務(wù)協(xié)議包容器,所有的拆包器最終都實(shí)現(xiàn)了該抽象方法

業(yè)務(wù)拆包完成之后,如果發(fā)現(xiàn)并沒(méi)有拆到一個(gè)完整的數(shù)據(jù)包,這個(gè)時(shí)候又分兩種情況

  1. 一個(gè)是拆包器什么數(shù)據(jù)也沒(méi)讀取,可能數(shù)據(jù)還不夠業(yè)務(wù)拆包器處理,直接break等待新的數(shù)據(jù)
  2. 拆包器已讀取部分?jǐn)?shù)據(jù),說(shuō)明解碼器仍然在工作,繼續(xù)解碼

業(yè)務(wù)拆包完成之后,如果發(fā)現(xiàn)已經(jīng)解到了數(shù)據(jù)包,但是,發(fā)現(xiàn)并沒(méi)有讀取任何數(shù)據(jù),這個(gè)時(shí)候就會(huì)拋出一個(gè)Runtime異常 DecoderException,告訴你,你什么數(shù)據(jù)都沒(méi)讀取,卻解析出一個(gè)業(yè)務(wù)數(shù)據(jù)包,這是有問(wèn)題的

3 清理字節(jié)容器

業(yè)務(wù)拆包完成之后,只是從字節(jié)容器中取走了數(shù)據(jù),但是這部分空間對(duì)于字節(jié)容器來(lái)說(shuō)依然保留著,而字節(jié)容器每次累加字節(jié)數(shù)據(jù)的時(shí)候都是將字節(jié)數(shù)據(jù)追加到尾部,如果不對(duì)字節(jié)容器做清理,那么時(shí)間一長(zhǎng)就會(huì)OOM
正常情況下,其實(shí)每次讀取完數(shù)據(jù),netty都會(huì)在下面這個(gè)方法中將字節(jié)容器清理,只不過(guò),當(dāng)發(fā)送端發(fā)送數(shù)據(jù)過(guò)快,channelReadComplete可能會(huì)很久才被調(diào)用一次

public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    numReads = 0;
    discardSomeReadBytes();
    if (decodeWasNull) {
        decodeWasNull = false;
        if (!ctx.channel().config().isAutoRead()) {
            ctx.read();
        }
    }
    ctx.fireChannelReadComplete();
}

如果一次數(shù)據(jù)讀取完畢之后(可能接收端一邊收,發(fā)送端一邊發(fā),這里的讀取完畢指的是接收端在某個(gè)時(shí)間不再接受到數(shù)據(jù)為止),發(fā)現(xiàn)仍然沒(méi)有拆到一個(gè)完整的用戶數(shù)據(jù)包,即使該channel的設(shè)置為非自動(dòng)讀取,也會(huì)觸發(fā)一次讀取操作 ctx.read(),該操作會(huì)重新向selector注冊(cè)op_read事件,以便于下一次能讀到數(shù)據(jù)之后拼接成一個(gè)完整的數(shù)據(jù)包

所以為了防止發(fā)送端發(fā)送數(shù)據(jù)過(guò)快,netty會(huì)在每次讀取到一次數(shù)據(jù),業(yè)務(wù)拆包之后對(duì)字節(jié)字節(jié)容器做清理,清理部分的代碼如下

if (cumulation != null && !cumulation.isReadable()) {
    numReads = 0;
    cumulation.release();
    cumulation = null;
} else if (++ numReads >= discardAfterReads) {
    numReads = 0;
    discardSomeReadBytes();
}

如果字節(jié)容器當(dāng)前已無(wú)數(shù)據(jù)可讀取,直接銷毀字節(jié)容器,并且標(biāo)注一下當(dāng)前字節(jié)容器一次數(shù)據(jù)也沒(méi)讀取

4 傳遞業(yè)務(wù)數(shù)據(jù)包給業(yè)務(wù)解碼器處理
以上三個(gè)步驟完成之后,就可以將拆成的包丟到業(yè)務(wù)解碼器處理了,代碼如下

int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();

期間用一個(gè)成員變量 decodeWasNull 來(lái)標(biāo)識(shí)本次讀取數(shù)據(jù)是否拆到一個(gè)業(yè)務(wù)數(shù)據(jù)包,然后調(diào)用 fireChannelRead 將拆到的業(yè)務(wù)數(shù)據(jù)包都傳遞到后續(xù)的handler

static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
    for (int i = 0; i < numElements; i ++) {
        ctx.fireChannelRead(msgs.getUnsafe(i));
    }
}

這樣,就可以把一個(gè)個(gè)完整的業(yè)務(wù)數(shù)據(jù)包傳遞到后續(xù)的業(yè)務(wù)解碼器進(jìn)行解碼,隨后處理業(yè)務(wù)邏輯.

下面,以一個(gè)具體的例子來(lái)看看業(yè)netty自帶的拆包器是如何來(lái)拆包的

這個(gè)類叫做 LineBasedFrameDecoder,基于行分隔符的拆包器,TA可以同時(shí)處理 \n以及\r\n兩種類型的行分隔符,核心方法都在繼承的 decode 方法中

protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    Object decoded = decode(ctx, in);
    if (decoded != null) {
        out.add(decoded);
    }
}

1 找到換行符位置

final int eol = findEndOfLine(buffer);

private static int findEndOfLine(final ByteBuf buffer) {
    int i = buffer.forEachByte(ByteProcessor.FIND_LF);
    if (i > 0 && buffer.getByte(i - 1) == '\r') {
        i--;
    }
    return i;
}

ByteProcessor FIND_LF = new IndexOfProcessor((byte) '\n');

for循環(huán)遍歷,找到第一個(gè) \n 的位置,如果\n前面的字符為\r,那就返回\r的位置

2 非discarding模式的處理
接下來(lái),netty會(huì)判斷,當(dāng)前拆包是否屬于丟棄模式,用一個(gè)成員變量來(lái)標(biāo)識(shí)

private boolean discarding;

第一次拆包不在discarding模式,于是進(jìn)入以下環(huán)節(jié)

非discarding模式下找到行分隔符的處理

// 1.計(jì)算分隔符和包長(zhǎng)度
final ByteBuf frame;
final int length = eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

// 丟棄異常數(shù)據(jù)
if (length > maxLength) {
    buffer.readerIndex(eol + delimLength);
    fail(ctx, length);
    return null;
}

// 取包的時(shí)候是否包括分隔符
if (stripDelimiter) {
    frame = buffer.readRetainedSlice(length);
    buffer.skipBytes(delimLength);
} else {
    frame = buffer.readRetainedSlice(length + delimLength);
}
return frame;
  1. 首先,新建一個(gè)幀,計(jì)算一下當(dāng)前包的長(zhǎng)度和分隔符的長(zhǎng)度(因?yàn)橛袃煞N分隔符)
    2.然后判斷一下需要拆包的長(zhǎng)度是否大于該拆包器允許的最大長(zhǎng)度(maxLength),這個(gè)參數(shù)在構(gòu)造函數(shù)中被傳遞進(jìn)來(lái),如超出允許的最大長(zhǎng)度,就將這段數(shù)據(jù)拋棄,返回null
    3.最后,將一個(gè)完整的數(shù)據(jù)包取出,如果構(gòu)造本解包器的時(shí)候指定 stripDelimiter為false,即解析出來(lái)的包包含分隔符,默認(rèn)為不包含分隔符

2.2 非discarding模式下未找到分隔符的處理
沒(méi)有找到對(duì)應(yīng)的行分隔符,說(shuō)明字節(jié)容器沒(méi)有足夠的數(shù)據(jù)拼接成一個(gè)完整的業(yè)務(wù)數(shù)據(jù)包,進(jìn)入如下流程處理

final int length = buffer.readableBytes();
if (length > maxLength) {
    discardedBytes = length;
    buffer.readerIndex(buffer.writerIndex());
    discarding = true;
    if (failFast) {
        fail(ctx, "over " + discardedBytes);
    }
}
return null;

首先取得當(dāng)前字節(jié)容器的可讀字節(jié)個(gè)數(shù),接著,判斷一下是否已經(jīng)超過(guò)可允許的最大長(zhǎng)度,如果沒(méi)有超過(guò),直接返回null,字節(jié)容器中的數(shù)據(jù)沒(méi)有任何改變,否則,就需要進(jìn)入丟棄模式. 使用一個(gè)成員變量 discardedBytes 來(lái)表示已經(jīng)丟棄了多少數(shù)據(jù),然后將字節(jié)容器的讀指針移到寫指針,意味著丟棄這一部分?jǐn)?shù)據(jù),設(shè)置成員變量discarding為true表示當(dāng)前處于丟棄模式。

3 discarding模式

如果解包的時(shí)候處在discarding模式,也會(huì)有兩種情況發(fā)生

3.1 discarding模式下找到行分隔符

在discarding模式下,如果找到分隔符,那可以將分隔符之前的都丟棄掉

final int length = discardedBytes + eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
buffer.readerIndex(eol + delimLength);
discardedBytes = 0;
discarding = false;
if (!failFast) {
    fail(ctx, length);
}

計(jì)算出分隔符的長(zhǎng)度之后,直接把分隔符之前的數(shù)據(jù)全部丟棄,當(dāng)然丟棄的字符也包括分隔符,經(jīng)過(guò)這么一次丟棄,后面就有可能是正常的數(shù)據(jù)包,下一次解包的時(shí)候就會(huì)進(jìn)入正常的解包流程

3.2discarding模式下未找到行分隔符

這種情況比較簡(jiǎn)單,因?yàn)楫?dāng)前還在丟棄模式,沒(méi)有找到行分隔符意味著當(dāng)前一個(gè)完整的數(shù)據(jù)包還沒(méi)丟棄完,當(dāng)前讀取的數(shù)據(jù)是丟棄的一部分,所以直接丟棄

discardedBytes += buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());

簡(jiǎn)單說(shuō):LineBasedFrameDecoder的工作原理是它依次遍歷ByteBuf中的可讀字節(jié),判斷是否有“\n”或“\r\n”,如果有,就以此位置為結(jié)束位置,從可讀索引到結(jié)束位置區(qū)間的字節(jié)就組成了一行。它是以換行符為結(jié)束標(biāo)志的解碼器,支持?jǐn)y帶結(jié)束符或不攜帶結(jié)束符兩種解碼方式,同時(shí)支持配置單行的最大長(zhǎng)度。如果連接讀取到最大長(zhǎng)度后仍然沒(méi)有發(fā)現(xiàn)換行符,就會(huì)拋出異常,同時(shí)忽略掉之前讀到的異常碼流。防止由于數(shù)據(jù)報(bào)沒(méi)有攜帶換行符導(dǎo)致接收到 ByteBuf 無(wú)限制積壓,引起系統(tǒng)內(nèi)存溢出。

通常LineBasedFrameDecoder會(huì)和StringDecoder搭配使用。StringDecoder的功能非常簡(jiǎn)單,就是將接收到的對(duì)象轉(zhuǎn)換成字符串,然后繼續(xù)調(diào)用后面的Handler。LineBasedFrameDecoder+StringDecoder組合就是按行切換的文本解碼器,它本設(shè)計(jì)用來(lái)支持TCP的粘包和拆包。對(duì)于文本類協(xié)議的解析,文本換行解碼器非常實(shí)用,例如對(duì) HTTP 消息頭的解析、FTP 協(xié)議消息的解析等。

而DelimiterBasedFrameDecoder是分隔符解碼器,用戶可以指定消息結(jié)束的分隔符,它可以自動(dòng)完成以分隔符作為碼流結(jié)束標(biāo)識(shí)的消息的解碼。回車換行解碼器實(shí)際上是一種特殊的DelimiterBasedFrameDecoder解碼器。使用起來(lái)十分簡(jiǎn)單,只需要在ChannelPipeline 中添加即可。

小結(jié)

netty中的拆包過(guò)程其實(shí)是和你自己去拆包過(guò)程一樣,只不過(guò)TA將拆包過(guò)程中邏輯比較獨(dú)立的部分抽象出來(lái)變成幾個(gè)不同層次的類,方便各種協(xié)議的擴(kuò)展.

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