go源碼解析之TCP連接(一)——Listen

tcp連接的一生系列基于go源碼1.16.5

端口是如何監(jiān)聽的

首先奉上net文檔中第一個映入眼簾的example

ln, err := net.Listen("tcp", ":8080")
if err != nil {
        // handle error
}
for {
        conn, err := ln.Accept()
        if err != nil {
                // handle error
        }
        go handleConnection(conn)
}

下面我們通過逐行跟蹤源碼铃将,來看開啟監(jiān)聽的過程:

1. net.Listen

src\net\dial.go

func Listen(network, address string) (Listener, error) {
    var lc ListenConfig
    return lc.Listen(context.Background(), network, address)
}

這個監(jiān)聽方法,其中network可以是tcp邑遏、tcp4另玖、tcp6困曙、unix、unixpacket谦去,我們通常傳入tcp即代表監(jiān)聽tcp連接慷丽,包括ipv4和ipv6,其他類型不在我們的介紹范圍鳄哭,包括udp本文也不討論要糊。address是監(jiān)聽的地址,ip:port格式妆丘,如果不指定port锄俄,將由系統(tǒng)自動分配一個端口。

ListenConfig的struct體如下:
src\net\dial.go

type ListenConfig struct {
    Control func(network, address string, c syscall.RawConn) error
    KeepAlive time.Duration
}

其中Control是一個方法變量飘痛,根據(jù)注釋珊膜,這個方法會在連接創(chuàng)建之后并將連接綁定到操作系統(tǒng)之前調(diào)用容握,相當(dāng)于是提供給用戶層的一個連接創(chuàng)建的回調(diào)方法宣脉,至于它的用處和調(diào)用時機(jī),隨著后續(xù)更深層的代碼分析再做進(jìn)一步介紹剔氏。
KeepAlive塑猖,應(yīng)該和內(nèi)核參數(shù)/proc/sys/net/ipv4/tcp_keepalive_time竹祷、tcp_keepalive_intvl、tcp_keepalive_probes是相同的作用羊苟,但是根據(jù)注釋說明塑陵,0是開啟,負(fù)數(shù)是關(guān)閉蜡励,沒有說明正數(shù)的作用令花。后續(xù)用到再研究。

2.ListenConfig的Listen方法

src\net\dial.go

func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
    addrs, err := DefaultResolver.resolveAddrList(ctx, "listen", network, address, nil)
    ...
        sl := &sysListener{
        ListenConfig: *lc,
        network:      network,
        address:      address,
    }
    var l Listener
    la := addrs.first(isIPv4)
    switch la := la.(type) {
    case *TCPAddr:
        l, err = sl.listenTCP(ctx, la)
        ...
    }
    ...
    return l, nil
}

其中...代表省略的一些細(xì)節(jié)處理或者是無關(guān)分支凉倚,后續(xù)也都會以這種方式貼代碼兼都。

ListenConfig的Listen方法同樣是傳入了network和address,ctx是上層傳入的context.Background()稽寒。返回值是Listener類型和error扮碧,其中的Listener其實(shí)是一個接口類型,具體接口定義如下:
src\net\net.go

type Listener interface {
    Accept() (Conn, error) //等待并返回建立成功的連接
    Close() error //關(guān)閉監(jiān)聽
    Addr() Addr //監(jiān)聽地址
}

我們再看ListenConfig的Listen方法的邏輯杏糙,第一行對傳入的地址進(jìn)行了解析慎王,轉(zhuǎn)換成了下層可用的地址格式。緊接著生成了一個sysListener的變量宏侍,sysListener的作用很簡單赖淤,它的存在就是為了構(gòu)造各種類型的實(shí)現(xiàn)了Listener接口的監(jiān)聽器,因此它的所有的方法都是listenXXX负芋,XXX則代表網(wǎng)絡(luò)協(xié)議類型漫蛔,例如這里的listenTCP,還有l(wèi)istenUDP等等旧蛾。

sysListener.listenTCP

繼續(xù)看代碼莽龟,下面的switch case我們不管,直接看case是TCPAddr的情況锨天,調(diào)用了sysListener的listenTCP方法毯盈,方法中代碼如下:

src\net\tcpsock_posix.go

func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
    fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
    if err != nil {
        return nil, err
    }
    return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

可見sysListener構(gòu)造了一個TCPListener并返回,看一下internetSocket病袄,internetSocket的作用是創(chuàng)建一個socket搂赋,TCPListener將使用這個socket來監(jiān)聽端口接收連接,下面看具體代碼:

src\net\ipsock_posix.go

func internetSocket(ctx context.Context, net string, laddr, raddr sockaddr, sotype, proto int, mode string, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
    if (runtime.GOOS == "aix" || runtime.GOOS == "windows" || runtime.GOOS == "openbsd") && mode == "dial" && raddr.isWildcard() {
        raddr = raddr.toLocal(net)
    }
    family, ipv6only := favoriteAddrFamily(net, laddr, raddr, mode)
    return socket(ctx, net, family, sotype, proto, ipv6only, laddr, raddr, ctrlFn)
}

這個方法的參數(shù)可真長益缠,我們對照方法調(diào)用一個個看一下:

  1. 參數(shù)1脑奠,ctx不說了
  2. 參數(shù)2,net幅慌,是我們最初傳入的network宋欺,即網(wǎng)絡(luò)協(xié)議類型,tcp、udp等
  3. 參數(shù)3齿诞,laddr是local address的縮寫酸休,即本地地址。我們構(gòu)建Listener需要傳入本地地址
  4. 參數(shù)4祷杈,raddr是remoe address的縮寫斑司,即遠(yuǎn)端地址。構(gòu)建Listener不需要遠(yuǎn)端地址但汞,當(dāng)連接到遠(yuǎn)端時需要raddr
  5. 參數(shù)5宿刮,sotype,傳入了syscall.SOCK_STREAM即代表進(jìn)行tcp監(jiān)聽私蕾,與之對應(yīng)的是SOCK_DGRAM
  6. 參數(shù)6糙置,proto,默認(rèn)0是目。
  7. 參數(shù)7谤饭,mode,傳入了listen懊纳,代表要建立的socket是監(jiān)聽socket
  8. 參數(shù)8揉抵,ctrlFn,這里就是上面ListenConfig的Controller屬性

方法的第一部分還是地址轉(zhuǎn)換嗤疯,第二部分的favoriteAddrFamily方法則是返回了支持的協(xié)議簇(AF_INET或者AF_INET6冤今,代表了ipv4和ipv6),第三部分則是socket方法的調(diào)用茂缚,它的入?yún)⒑蚷nternetSocket的基本一致戏罢,返回值是*netFD,而netFD則是對系統(tǒng)文件描述符(socket也有一個唯一的文件描述符fd與之對應(yīng))的包裝脚囊,下面我們看下socket方法中是怎么創(chuàng)建netFD的:

socket

src\net\sock_posix.go

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
    s, err := sysSocket(family, sotype, proto)
    if err != nil {
        return nil, err
    }
    if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {
        poll.CloseFunc(s)
        return nil, err
    }
    if fd, err = newFD(s, family, sotype, net); err != nil {
        poll.CloseFunc(s)
        return nil, err
    }

    if laddr != nil && raddr == nil {
        switch sotype {
        case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
            if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
                fd.Close()
                return nil, err
            }
            return fd, nil
                ...
    }
    if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil {
        fd.Close()
        return nil, err
    }
    return fd, nil
}

我們從上到下介紹每個方法調(diào)用的作用:

  1. sysSocket龟糕,顧名思義,它的作用是創(chuàng)建系統(tǒng)socket
  2. setDefaultSockopts悔耘,設(shè)置了socket的一些屬性讲岁,例如是否只支持ipv6
  3. newFD,對返回的系統(tǒng)fd進(jìn)行了包裝衬以,生成了本方法要返回的netFD
  4. if laddr != nil && raddr == nil缓艳,如果傳入了本地地址,沒有傳入遠(yuǎn)端地址看峻,則認(rèn)為新的socket是用來監(jiān)聽的阶淘,調(diào)用了netFD的listenStream進(jìn)行端口綁定,可以看到這里將ctrlFn(ListenConfig的Controller屬性)又一次傳入互妓,那么ListenConfig的Controller方法屬性是在socket創(chuàng)建之后執(zhí)行的溪窒,具體在什么操作之前分井,還需要進(jìn)一步跟代碼。
  5. fd.dial霉猛,是傳入了遠(yuǎn)端地址的情況,則認(rèn)為新的socket是用來connect的珠闰,dial進(jìn)行了連接惜浅。

一個tcp的監(jiān)聽socket創(chuàng)建完成、進(jìn)行了端口綁定伏嗜,并將此socket的fd包裝成了netFD返回給調(diào)用者坛悉,沿著調(diào)用鏈一直向上返回到sysListener的listenTCP方法,為方便大家查看承绸,將上面貼過的代碼再次貼到這里:

src\net\tcpsock_posix.go

func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) {
    fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_STREAM, 0, "listen", sl.ListenConfig.Control)
    if err != nil {
        return nil, err
    }
    return &TCPListener{fd: fd, lc: sl.ListenConfig}, nil
}

中場小結(jié)

在繼續(xù)深入sysSocket裸影、setDefaultSockopts、newFD军熏、listenStream幾個方法之前轩猩,我們現(xiàn)在通過一張圖來回顧一下前面的調(diào)用過程

8424B70F-2273-4AE4-B0D3-1C6F9A19509C.png

到此為止,整個邏輯除了最下層的socket方法中略顯復(fù)雜杀糯,其他每個方法體都很小枪狂,但是調(diào)用鏈路還是比較長藕施,我們來簡單總結(jié)下每一層的代碼設(shè)計。

  1. net.Listen是整個鏈路的入口方法彤委,它創(chuàng)建了一個空的ListenConfig,并調(diào)用了ListenConfig的Listen方法
  2. ListenConfig或衡,它目前擁有兩個可選配置項(xiàng):Control和KeepAlive焦影。它將被作為配置數(shù)據(jù)傳遞給下游,設(shè)計成一個struct可以避免通過傳參的方式傳遞很多配置
  3. ListenConfig.Listen方法封断,將上層傳入的字符串類型的address轉(zhuǎn)換成下層使用的Addr數(shù)據(jù)斯辰,并通過判斷network的類型調(diào)用sysListener的不同的listen方法(listenTCP、listenUDP等)
  4. sysListener將ListenConfig坡疼、address椒涯、network作為自己的屬性,并實(shí)現(xiàn)了各種network的listen方法
  5. sysListener.listenTCP方法回梧,調(diào)用internetSocket方法废岂,并使用返回的netFD創(chuàng)建TCPListener
  6. internetSocket方法,是一個創(chuàng)建監(jiān)聽socket和connect socket(dial方法主動發(fā)起連接)的共用方法
  7. socket方法狱意,是unixsock和ipsock的共用方法湖苞,它首先創(chuàng)建了socket并為socket設(shè)置默認(rèn)屬性,再將返回的fd包裝成netFD详囤,最后使用此socket綁定端口或者進(jìn)行連接财骨。
  8. 最終將TCPListener返回給net.Listen的調(diào)用者镐作,調(diào)用者可以調(diào)用TCPListener的Accept方法開始接受連接請求,這一部分將在下一篇中介紹隆箩。

下面繼續(xù)介紹sysSocket该贾、setDefaultSockopts、newFD捌臊、listenStream幾個方法

sysSocket

老套路杨蛋,先祭出代碼:

src\net\sock_cloexec.go

func sysSocket(family, sotype, proto int) (int, error) {
    s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)

    ...
    
    return s, nil
}

中間省略部分是socketFunc報錯后的容錯處理,老版本內(nèi)核由于不支持創(chuàng)建socket時設(shè)置SOCK_NONBLOCK或者SOCK_CLOEXEC理澎,導(dǎo)致創(chuàng)建失敗逞力。省略部分進(jìn)行了容錯,先創(chuàng)建socket糠爬,再進(jìn)行socket屬性的設(shè)置寇荧。

在跟入socketFunc之前先介紹一下它的參數(shù):

  1. family是AF_INET或者AF_INET6,即ipv4或者ipv6
  2. sotype是SOCK_STREAM或者SOCK_DGRAM执隧,即tcp或者udp
  3. SOCK_NONBLOCK是將socket設(shè)置為非阻塞
  4. SOCK_CLOEXEC是將socket設(shè)置為close-on-exec
  5. proto默認(rèn)0

socketFunc是一個全局的方法變量揩抡,它的值如下:

src\net\hook_unix.go

var (
    ...

    // Placeholders for socket system calls.
    socketFunc        func(int, int, int) (int, error)  = syscall.Socket
    connectFunc       func(int, syscall.Sockaddr) error = syscall.Connect
    listenFunc        func(int, int) error              = syscall.Listen
    getsockoptIntFunc func(int, int, int) (int, error)  = syscall.GetsockoptInt
)

可見除了socketFunc之外,還有connectFunc镀琉、listenFunc捅膘、getsockoptIntFunc,它們都是syscall包里的方法滚粟。

繼續(xù)跟入syscall.Socket:

src\syscall\syscall_unix.go

func Socket(domain, typ, proto int) (fd int, err error) {
    if domain == AF_INET6 && SocketDisableIPv6 {
        return -1, EAFNOSUPPORT
    }
    fd, err = socket(domain, typ, proto)
    return
}

src\syscall\zsyscall_linux_amd64.go

// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT

func socket(domain int, typ int, proto int) (fd int, err error) {
    r0, _, e1 := RawSyscall(SYS_SOCKET, uintptr(domain), uintptr(typ), uintptr(proto))
    fd = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

src\syscall\zsysnum_linux_amd64.go

const {
    ...
    SYS_SOCKET                 = 41
    ...
}

src\syscall\asm_linux_amd64.s

// func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    trap+0(FP), AX  // syscall entry
    SYSCALL
        ...

以上4段代碼邏輯都比較簡單寻仗,就是實(shí)現(xiàn)了一個socket的系統(tǒng)調(diào)用,最后的rawSyscall是使用匯編實(shí)現(xiàn)的一段系統(tǒng)調(diào)用方法凡壤,創(chuàng)建socket的系統(tǒng)調(diào)用號是SYS_SOCKET署尤。

setDefaultSockopts

老規(guī)矩,上代碼:

src\net\sockopt_linux.go

func setDefaultSockopts(s, family, sotype int, ipv6only bool) error {
    if family == syscall.AF_INET6 && sotype != syscall.SOCK_RAW {
        syscall.SetsockoptInt(s, syscall.IPPROTO_IPV6, syscall.IPV6_V6ONLY, boolint(ipv6only))
    }
    if (sotype == syscall.SOCK_DGRAM || sotype == syscall.SOCK_RAW) && family != syscall.AF_UNIX {
        // Allow broadcast.
        return os.NewSyscallError("setsockopt", syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1))
    }
    return nil
}

可見代碼在一定條件下設(shè)置了是否只允許ipv6亚侠。如果是udp的話曹体,還將socket設(shè)置為允許廣播。
syscall.SetsockoptInt方法同syscall.Socket方法硝烂,都是syscall中的系統(tǒng)調(diào)用箕别。

newFD

廢話不多說,上代碼:

src\net\fd_unix.go

func newFD(sysfd, family, sotype int, net string) (*netFD, error) {
    ret := &netFD{
        pfd: poll.FD{
            Sysfd:         sysfd,
            IsStream:      sotype == syscall.SOCK_STREAM,
            ZeroReadIsEOF: sotype != syscall.SOCK_DGRAM && sotype != syscall.SOCK_RAW,
        },
        family: family,
        sotype: sotype,
        net:    net,
    }
    return ret, nil
}

newFD方法將創(chuàng)建成功的系統(tǒng)fd包裝成了netFD滞谢,下面挑選幾個netFD的重要方法來了解它:

func (fd *netFD) Read(p []byte) (n int, err error)
func (fd *netFD) Write(p []byte) (nn int, err error)
func (fd *netFD) SetDeadline(t time.Time)
func (fd *netFD) SetReadDeadline(t time.Time)
func (fd *netFD) SetWriteDeadline(t time.Time)
func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error)
func (fd *netFD) accept() (netfd *netFD, err error)
func (fd *netFD) dial(ctx context.Context, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) error
func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error
func (fd *netFD) listenDatagram(laddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) error

netFD除了具有讀寫socket的方法串稀,還實(shí)現(xiàn)了listen、accept及dial方法狮杨。

fd.listenStream

socket創(chuàng)建成功后母截,進(jìn)而就是進(jìn)行端口綁定和監(jiān)聽,看代碼:

src\net\sock_posix.go

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
    
        ...
    
    if ctrlFn != nil {
        c, err := newRawConn(fd)
        if err != nil {
            return err
        }
        if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
            return err
        }
    }
    if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
        return os.NewSyscallError("bind", err)
    }
    if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
        return os.NewSyscallError("listen", err)
    }
    
        ...
    
    return nil
}

省略去了一些初始化和地址轉(zhuǎn)換的代碼橄教。

syscall.Bind又一個系統(tǒng)調(diào)用清寇,注意fd.pfd.Sysfd就是我們新創(chuàng)建的socket的fd喘漏,lsa則是我們最初傳入的ip:port經(jīng)過轉(zhuǎn)換后的地址,Bind將這個地址綁定到我們創(chuàng)建的socket上华烟。
listenFunc是一個方法變量翩迈,存儲各種操作系統(tǒng)的Listen方法:

src\net\hook_unix.go

listenFunc        func(int, int) error              = syscall.Listen

經(jīng)過Listen系統(tǒng)調(diào)用,我們的socket就被激活了盔夜,內(nèi)核將接收連接到此socket的連接請求负饲。下一步調(diào)用accept就可以取到連接請求的socket了。

呼呼??比吭,終于把端口綁定和監(jiān)聽的大體代碼流程捋完了∫涛校看下面這張圖衩藤,本文對應(yīng)到了TCP Server的監(jiān)聽socket創(chuàng)建和bind、listen涛漂,下一章將繼續(xù)介紹accept赏表。

socket.png

最后將開頭ListenConfig的Controller屬性的調(diào)用時機(jī)補(bǔ)上,netFD.listenStream方法中的ctrlFn就是這個屬性匈仗,可見它是在監(jiān)聽socket創(chuàng)建后瓢剿,bind調(diào)用之前被回調(diào)的。應(yīng)該是開放給應(yīng)用層個性化設(shè)置socket的屬性的悠轩。

最最后再把backlog說一下??间狂,在netFD.listenStream方法中的listenFunc(fd.pfd.Sysfd, backlog)這一行中的backlog參數(shù)控制著待處理連接隊(duì)列的長度,如果隊(duì)列已滿火架,新的連接請求將被忽略鉴象。backlog的值取自系統(tǒng)參數(shù)(linux系統(tǒng))/proc/sys/net/core/somaxconn,如果讀取失敗何鸡,默認(rèn)設(shè)置為128纺弊。如果值超過backlog可以存儲的最大值(內(nèi)核版本4.1以下backlog使用uint16存儲,高版本使用uint32存儲)骡男,將被設(shè)置為可存儲的最大值淆游。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市隔盛,隨后出現(xiàn)的幾起案子犹菱,更是在濱河造成了極大的恐慌,老刑警劉巖吮炕,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件已亥,死亡現(xiàn)場離奇詭異,居然都是意外死亡来屠,警方通過查閱死者的電腦和手機(jī)虑椎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門震鹉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捆姜,你說我怎么就攤上這事传趾。” “怎么了泥技?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵浆兰,是天一觀的道長。 經(jīng)常有香客問我珊豹,道長簸呈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任店茶,我火速辦了婚禮蜕便,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贩幻。我一直安慰自己轿腺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布丛楚。 她就那樣靜靜地躺著族壳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪趣些。 梳的紋絲不亂的頭發(fā)上仿荆,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機(jī)與錄音坏平,去河邊找鬼赖歌。 笑死,一個胖子當(dāng)著我的面吹牛功茴,可吹牛的內(nèi)容都是我干的庐冯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼坎穿,長吁一口氣:“原來是場噩夢啊……” “哼展父!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起玲昧,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤栖茉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后孵延,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吕漂,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年尘应,在試婚紗的時候發(fā)現(xiàn)自己被綠了惶凝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吼虎。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖苍鲜,靈堂內(nèi)的尸體忽然破棺而出思灰,到底是詐尸還是另有隱情,我是刑警寧澤混滔,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布洒疚,位于F島的核電站,受9級特大地震影響坯屿,放射性物質(zhì)發(fā)生泄漏油湖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一领跛、第九天 我趴在偏房一處隱蔽的房頂上張望乏德。 院中可真熱鬧,春花似錦隔节、人聲如沸鹅经。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贷痪,卻和暖如春幻妓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劫拢。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工肉津, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舱沧。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓妹沙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親熟吏。 傳聞我的和親對象是個殘疾皇子距糖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

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