WebSocket

WebSocket 機制

WebSocket 是 HTML5 一種新的協議。它實現了瀏覽器與服務器全雙工通信,能更好的節省服務器資源和帶寬并達到實時通訊,它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸數據,但是它和 HTTP 最大不同是:

  • WebSocket 是一種雙向通信協議,在建立連接后,WebSocket 服務器和 Browser/Client Agent 都能主動的向對方發送或接收數據,就像 Socket 一樣;
  • WebSocket 需要類似 TCP 的客戶端和服務器端通過握手連接,連接成功后才能相互通信。

相對于傳統 HTTP 每次請求-應答都需要客戶端與服務端建立連接的模式,WebSocket 是類似 Socket 的 TCP 長連接的通訊模式,一旦 WebSocket 連接建立后,后續數據都以幀序列的形式傳輸。在客戶端斷開 WebSocket 連接或 Server 端斷掉連接前,不需要客戶端和服務端重新發起連接請求。在海量并發及客戶端與服務器交互負載流量大的情況下,極大的節省了網絡帶寬資源的消耗,有明顯的性能優勢,且客戶端發送和接受消息是在同一個持久連接上發起,實時性優勢明顯。

握手的實現

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

告訴Apache、Nginx等服務器:注意啦,窩發起的是Websocket協議,快點幫我找到對應的助理處理;

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
#首先,Sec-WebSocket-Key 是一個Base64 encode的值,這個是瀏覽器隨機生成的
 告訴服務器:泥煤,不要忽悠窩,我要驗證尼是不是真的是Websocket助理。

Sec-WebSocket-Protocol: chat, superchat
#Sec_WebSocket-Protocol 是一個用戶定義的字符串,用來區分同URL下,不同的服務所需要的協議。
  簡單理解:今晚我要服務A,別搞錯啦~
Sec-WebSocket-Version: 13
#Sec-WebSocket-Version 是告訴服務器所使用的Websocket Draft(協議版本)
 在最初的時候,Websocket協議還在 Draft 階段,各種奇奇怪怪的協議都有

然后服務器會返回下列東西,表示已經接受到請求, 成功建立Websocket啦!

HTTP/1.1 101 Switching Protocols
#這里開始就是HTTP最后負責的區域了,告訴客戶,我已經成功切換協議啦~

Upgrade: websocket
Connection: Upgrade
#告訴客戶端即將升級的是Websocket協議,而不是mozillasocket,lurnarsocket或者shitsocket。
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
#Sec-WebSocket-Accept 這個則是經過服務器確認,并且加密過后Sec-WebSocket-Key。
#服務器:好啦好啦,知道啦,給你看我的ID CARD來證明行了吧。

Sec-WebSocket-Protocol: chat
#后面的,Sec-WebSocket-Protocol 則是表示最終使用的協議。

1.WebSocket 客戶端連接報文

<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost:8080
Sec-WebSocket-Version: 13</pre>

可以看到,客戶端發起的 WebSocket 連接報文類似傳統 HTTP 報文,”Upgrade:websocket”參數值表明這是 WebSocket 類型請求,“Sec-WebSocket-Key”是 WebSocket 客戶端發送的一個 base64 編碼的密文,要求服務端必須返回一個對應加密的“Sec-WebSocket-Accept”應答,否則客戶端會拋出“Error during WebSocket handshake”錯誤,并關閉連接。

服務端收到報文后返回的數據格式類似:

2.WebSocket 服務端響應報文

<pre class="displaycode" style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; box-sizing: border-box;">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=</pre>

“Sec-WebSocket-Accept”的值是服務端采用與客戶端一致的密鑰計算出來后返回客戶端的,“HTTP/1.1 101 Switching Protocols”表示服務端接受 WebSocket 協議的客戶端連接,經過這樣的請求-響應處理后,客戶端服務端的 WebSocket 連接握手成功, 后續就可以進行 TCP 通訊了。

WebSocket 服務端支持

廠商 應用服務器 備注
IBM WebSphere WebSphere 8.0 以上版本支持,7.X 之前版本結合 MQTT 支持類似的 HTTP 長連接
甲骨文 WebLogic WebLogic 12c 支持,11g 及 10g 版本通過 HTTP Publish 支持類似的 HTTP 長連接
微軟 IIS IIS 7.0+支持
Apache Tomcat Tomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通過自定義 API 支持
。。 Jetty Jetty 7.0+支持

WebSocket 客戶端支持

瀏覽器 支持情況
Chrome Chrome version 4+支持
Firefox Firefox version 5+支持
IE IE version 10+支持
Safari IOS 5+支持
Android Brower Android 4.5+支持

WebSocket 事件

事件 事件處理程序 描述
open Socket.onopen 建立 socket 連接時觸發這個事件。
message Socket.onmessage 客戶端從服務器接收數據時觸發。
error Socket.onerror 連接發生錯誤時觸發。
close Socket.onclose 連接被關閉時觸發

WebSocket 實現

接收和發送數據

var wss = new WebSocket('wss://example.com/socket');
ws.binaryType = "arraybuffer"; 
// 接收數據
wss.onmessage = function(msg) {
if(msg.data instanceof ArrayBuffer) {
processArrayBuffer(msg.data);
} else {
processText(msg.data);
}
}
// 發送數據
ws.onopen = function () {
socket.send("Hello server!"); 
socket.send(JSON.stringify({'msg': 'payload'}));

var buffer = new ArrayBuffer(128);
socket.send(buffer);

var intview = new Uint32Array(buffer);
socket.send(intview);

var blob = new Blob([buffer]);
socket.send(blob); 
}

數據格式

WebSocket 提供的信道是全雙工的,在同一個TCP 連接上,可以雙向傳輸文本信息和二進制數據,通過數據幀中的一位(bit)來區分二進制或者文本。WebSocket 只提供了最基礎的文本和二進制數據傳輸功能,如果需要傳輸其他類型的數據,就需要通過額外的機制進行協商。WebSocket 中的send( ) 方法是異步的:提供的數據會在客戶端排隊,而函數則立即返回。在傳輸大文件時,不要因為回調已經執行,就錯誤地以為數據已經發送出去了,數據很可能還在排隊。要監控在瀏覽器中排隊的數據量,可以查詢套接字的bufferedAmount 屬性:

var ws = new WebSocket('wss://example.com/socket');
ws.onopen = function () {
subscribeToApplicationUpdates(function(evt) { 
if (ws.bufferedAmount == 0) 
ws.send(evt.data); 
});
};

在以往使用HTTP 或XHR 協議來傳輸數據時,它們可以通過每次請求和響應的HTTP 首部來溝通元數據,以進一步確定傳輸的數據格式,而WebSocket 并沒有提供等價的機制。上文已經提到WebSocket只提供最基礎的文本和二進制數據傳輸,對消息的具體內容格式是未知的。因此,如果WebSocket需要溝通關于消息的元數據,客戶端和服務器必須達成溝通這一數據的子協議,進而間接地實現其他格式數據的傳輸。下面是一些可能策略的介紹:

客戶端和服務器可以提前確定一種固定的消息格式,比如所有通信都通過 JSON編碼的消息或者某種自定義的二進制格式進行,而必要的元數據作為這種數據結構的一個部分;
如果客戶端和服務器要發送不同的數據類型,那它們可以確定一個雙方都知道的消息首部,利用它來溝通說明信息或有關凈荷的其他解碼信息;
混合使用文本和二進制消息可以溝通凈荷和元數據,比如用文本消息實現 HTTP首部的功能,后跟包含應用凈荷的二進制消息。

WebSocket構造器方法如下所示:

WebSocket WebSocket(
in DOMString url, // 表示要連接的URL。這個URL應該為響應WebSocket的地址。
in optional DOMString protocols // 可以是一個單個的協議名字字符串或者包含多個協議名字字符串的數組。默認設為一個空字符串。
);

通過上述WebSocket構造器方法的第二個參數,客戶端可以在初次連接握手時,可以告知服務器自己支持哪種協議。如下所示:

var ws = new WebSocket('wss://example.com/socket',['appProtocol', 'appProtocol-v2']);

ws.onopen = function () {
if (ws.protocol == 'appProtocol-v2') { 
...
} else {
...
}
}

如上所示,WebSocket 構造函數接受了一個可選的子協議名字的數組,通過這個數組,客戶端可以向服務器通告自己能夠理解或希望服務器接受的協議。當服務器接收到該請求后,會根據自身的支持情況,返回相應信息。

有支持的協議,則子協議協商成功,觸發客戶端的onopen回調,應用可以查詢WebSocket 對象上的protocol 屬性,從而得知服務器選定的協議;
沒有支持的協議,則協商失敗,觸發onerror 回調,連接斷開。

協議

WS與WSS

WebSocket 資源URI采用了自定義模式:ws 表示純文本通信;
wss 表示使用加密信道通信(TCP+TLS);

WebSocket 的連接協議也可以用于瀏覽器之外的場景,可以通過非HTTP協商機制交換數據。考慮到這一點,HyBi Working Group 就選擇采用了自定義的URI模式:

ws協議:普通請求,占用與http相同的80端口;
wss協議:基于SSL的安全傳輸,占用與tls相同的443端口。
各自的URI如下:

ws-URI = "ws:" "http://" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "http://" host [ ":" port ] path [ "?" query ]

數據成幀

WebSocket 使用了自定義的二進制分幀格式,把每個應用消息切分成一或多個幀,發送到目的地之后再組裝起來,等到接收到完整的消息后再通知接收端。基本的成幀協議定義了幀類型有操作碼、有效載荷的長度,指定位置的Extension data和Application data,統稱為Payload data,保留了一些特殊位和操作碼供后期擴展。在打開握手完成后,終端發送一個關閉幀之前的任何時間里,數據幀可能由客戶端或服務器的任何一方發送。

幀:最小的通信單位,包含可變長度的幀首部和凈荷部分,凈荷可能包含完整或部分應用消息。
消息:一系列幀,與應用消息對等。

是否把消息分幀由客戶端和服務器實現決定,應用并不需要關注WebSocket幀和如何分幀,因為客戶端(如瀏覽器)和服務端為完成該工作。那么客戶端和服務端是按照什么規則進行分幀的呢?RFC 6455規定的分幀規則如下:

1.一個未分幀的消息包含單個幀,FIN設置為1,opcode非0。
2.一個分幀了的消息包含:開始于:單個幀,FIN設為0,opcode非0;后接 :0個或多個幀,FIN設為0,opcode設為0;終結于:單個幀,FIN設為1,opcode設為0。一個分幀了消息在概念上等價于一個未分幀的大消息,它的有效載荷長度等于所有幀的有效載荷長度的累加;然而,有擴展時,這可能不成立,因為擴展定義了出現的Extension data的解釋。例如,Extension data可能只出現在第一幀,并用于后續的所有幀,或者Extension data出現于所有幀,且只應用于特定的那個幀。在缺少Extension data時,下面的示例示范了分幀如何工作。舉例:如一個文本消息作為三個幀發送,第一幀的opcode是0x1,FIN是0,第二幀的opcode是0x0,FIN是0,第三幀的opcode是0x0,FIN是1。
3.控制幀可能被插入到分幀了消息中,控制幀必須不能被分幀。如果控制幀不能插入,例如,如果是在一個大消息后面,ping的延遲將會很長。因此要求處理消息幀中間的控制幀。
4.消息的幀必須以發送者發送的順序傳遞給接受者。
5.一個消息的幀必須不能交叉在其他幀的消息中,除非有擴展能夠解釋交叉。
6.一個終端必須能夠處理消息幀中間的控制幀。
7.一個發送者可能對任意大小的非控制消息分幀。
8.客戶端和服務器必須支持接收分幀和未分幀的消息。
9.由于控制幀不能分幀,中間設施必須不嘗試改變控制幀。
10.中間設施必須不修改消息的幀,如果保留位的值已經被使用,且中間設施不明白這些值的含義。

在遵循了上述分幀規則之后,一個消息的所有幀屬于同樣的類型,由第一個幀的opcdoe指定。由于控制幀不能分幀,消息的所有幀的類型要么是文本、二進制數據或保留的操作碼中的一個。

協議擴展

從上述的數據分幀格式可以知道,有很多擴展位預留,WebSocket 規范允許對協議進行擴展,可以使用這些預留位在基本的WebSocket 分幀層之上實現更多的功能。
下面是負責制定WebSocket 規范的HyBi Working Group進行的兩項擴展:

多路復用擴展(A Multiplexing Extension for WebSockets):這個擴展可以將WebSocket 的邏輯連接獨立出來,實現共享底層的TCP 連接。每個WebSocket 連接都需要一個專門的TCP 連接,這樣效率很低。多路復用擴展解決了這個問題。它使用“信道ID”擴展每個WebSocket 幀,從而實現多個虛擬的WebSocket 信道共享一個TCP 連接。
壓縮擴展(Compression Extensions for WebSocket):給WebSocket 協議增加了壓縮功能。基本的WebSocket 規范沒有壓縮數據的機制或建議,每個幀中的凈荷就是應用提供的凈荷。雖然這對優化的二進制數據結構不是問題,但除非應用實現自己的壓縮和解壓縮邏輯,否則很多情況下都會造成傳輸載荷過大的問題。實際上,壓縮擴展就相當于HTTP 的傳輸編碼協商。
要使用擴展,客戶端必須在第一次的Upgrade 握手中通知服務器,服務器必須選擇并確認要在商定連接中使用的擴展。

升級協商

從上面的介紹可知,WebSocket具有很大的靈活性,提供了很多強大的特性:基于消息的通信、自定義的二進制分幀層、子協議協商、可選的協議擴展等等。上面也講到,客戶端和服務端需先通過HTTP方式協商適當的參數后才可建立連接,完成協商之后,所有信息的發送和接收不再和HTTP相關,全由WebSocket自身的機制處理。當然,完成最初的連接參數協商并非必須使用HTTP協議,它只是一種實現方案,可以有其他選擇。但使用HTTP協議完成最初的協商,有以下好處:讓WebSockets 與現有HTTP 基礎設施兼容:WebSocket 服務器可以運行在80 和443 端口上,這通常是對客戶端唯一開放的端口;可以重用并擴展HTTP 的Upgrade 流,為其添加自定義的WebSocket 首部,以完成協商。
在協商過程中,用到的一些頭域如下:

Sec-WebSocket-Version:客戶端發送,表示它想使用的WebSocket 協議版本(13表示RFC 6455)。如果服務器不支持這個版本,必須回應自己支持的版本。

Sec-WebSocket-Key:客戶端發送,自動生成的一個鍵,作為一個對服務器的“挑戰”,以驗證服務器支持請求的協議版本;

Sec-WebSocket-Accept:服務器響應,包含Sec-WebSocket-Key 的簽名值,證明它支持請求的協議版本;

Sec-WebSocket-Protocol:用于協商應用子協議:客戶端發送支持的協議列表,服務器必須只回應一個協議名;

Sec-WebSocket-Extensions:用于協商本次連接要使用的WebSocket 擴展:客戶端發送支持的擴展,服務器通過返回相同的首部確認自己支持一或多個擴展。

在進行HTTP Upgrade之前,客戶端會根據給定的URI、子協議、擴展和在瀏覽器情況下的origin,先打開一個TCP連接,隨后再發起升級協商。

升級協商具體如下:

GET /socket HTTP/1.1 // 請求的方法必須是GET,HTTP版本必須至少是1.1
Host: thirdparty.com
Origin: http://example.com
Connection: Upgrade 
Upgrade: websocket // 請求升級到WebSocket 協議
Sec-WebSocket-Version: 13 // 客戶端使用的WebSocket 協議版本
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 自動生成的鍵,以驗證服務器對協議的支持,其值必須是nonce組成的隨機選擇的16字節的被base64編碼后的值
Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 // 可選的應用指定的子協議列表
Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension // 可選的客戶端支持的協議擴展列表,指示了客戶端希望使用的協議級別的擴展

在安全工程中,Nonce是一個在加密通信只能使用一次的數字。在認證協議中,它往往是一個隨機或偽隨機數,以避免重放攻擊。Nonce也用于流密碼以確保安全。如果需要使用相同的密鑰加密一個以上的消息,就需要Nonce來確保不同的消息與該密鑰加密的密鑰流不同。

與瀏覽器中客戶端發起的任何連接一樣,WebSocket 請求也必須遵守同源策略:瀏覽器會自動在升級握手請求中追加Origin 首部,遠程服務器可能使用CORS 判斷接受或拒絕跨源請求。要完成握手,服務器必須返回一個成功的“Switching Protocols”(切換協議)響應,具體如下:

HTTP/1.1 101 Switching Protocols // 101 響應碼確認升級到WebSocket 協議
Upgrade: websocket
Connection: Upgrade
Access-Control-Allow-Origin: http://example.com // CORS 首部表示選擇同意跨源連接
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 簽名的鍵值驗證協議支持
Sec-WebSocket-Protocol: appProtocol-v2 // 服務器選擇的應用子協議
Sec-WebSocket-Extensions: x-custom-extension // 服務器選擇的WebSocket 擴展

服務器端該怎么使用WebSocket

服務器端使用WebSocket需引入相關的模塊,目前比較流行的是socket.io和ws

ws

服務端:

var express = require('express');
var http = require('http');
var WebSocket = require('ws');

var app = express();
app.get('/', function(req, res, next){
        res.sendFile(__dirname + '/index.html');
});
var server = http.createServer(app);
var wss = new WebSocket.Server({ server });
wss.on('connection', function(ws){
        ws.on('message', function(message, flag){
                if (ws.readyState === WebSocket.OPEN){
                        //你的操作
                }
        });
        ws.send('something');
});
server.listen(3000, function(){
        console.log('listening on port:3000');
});

下面簡單說明一下,搭配上述服務器我還引入了express模塊,其實ws模塊是可以單獨工作的,只不過我的項目都是建立在express開發框架上的,express怎么使用網上教程也蠻多的,大家想知道具體用法可自行百度。上面的代碼可以稱作模板式的代碼,以后大家想搭建一個WebSocket服務的話,只需復制一下,然后修改對應的參數即可

客戶端:

let ws  = new WebSocket('ws://10.11.10.66:3000');
ws.addEventListener('open', event => {
        console.log(ws.readyState);
        ws.addEventListener('message', (event, flags) => {
          console.log(event.data);
              ws.send('ssss');
    });
    ws.addEventListener('close', event => {
            console.log('client notified websocket has closed', event.data);
    });
});
ws.addEventListener('error', event => {
        console.log('error', event.data);
});

socket.io

Socket.IO是一個開源的WebSocket庫,包括了客戶端的js和服務器端的nodejs。官方地址:http://socket.io
它通過Node.js實現WebSocket服務端,同時也提供客戶端JS庫。Socket.IO支持以事件為基礎的實時雙向通訊,它可以工作在任何平臺、瀏覽器或移動設備。

Socket.IO支持4種協議:WebSocket、htmlfile、xhr-polling、jsonp-polling,它會自動根據瀏覽器選擇適合的通訊方式,從而讓開發者可以聚焦到功能的實現而不是平臺的兼容性,同時Socket.IO具有不錯的穩定性和性能。

socket.io封裝 了websocket,同時包含了其它的連接方式,比如Ajax。原因在于不是所有的瀏覽器 都支持websocket,通過socket.io的封裝 ,你不用關心里面用了什么連接方式。你在任何瀏覽器 里都可以使用socket.io來建立異步 的連接。socket.io包含了服務端 和客戶端的庫,如果在[瀏覽器] 中使用了socket.io的js,服務端 也必須同樣適用。如果你很清楚你需要的就是websocket,那可以直接使用websocket。
socket.io是一個WebSocket協議的實現,用它你可以進行websocket通信,這是應用層 node.js net.socket是系統socket接口,用它你可以操作linux socket,這是傳輸層
websocket協議本質上也是使用系統socket,它是把socket引入了http通信,也就是不使用80端口進行http通信。它的目的是建立全雙工的連接,可以用來解決服務器客戶端保持長連接的問題。

1.客戶端使用socket.io
去github clone socket.io的最新版本,或者直接飲用使用socket.io的CDN服務:

  <script src="http://cdn.socket.io/stable/socket.io.js"></script>
下面可以創建使用socket.io庫來創建客戶端js代碼了:

var socket = io.connect('http://localhost');
socket.on('news', function (data) {
 console.log(data);
 socket.emit('my other event', { my: 'data' });
});

socket.on是監聽,收到服務器端發來的news的內容,則運行function,其中data就是請求回來的數據,socket.emit是發送消息給服務器端的方法。
在使用Socket.IO類庫時,服務器端和客戶端之間除了可以互相發送消息之外,也可以使用socket端口對象的emit方法,互相發送事件。

socket.emit(event,data,[callback])

event表示:參數值為一個用于指定事件名的字符串。
data參數值:代表該事件中攜帶的數據。這個數據就是要發送給對方的數據。數據可以是字符串,也可以是對象。
callback參數:值為一個參數,用于指定一個當對方確定接收到數據時調用的回調函數。
一方使用emit發送事件后,另一方可以使用on,或者once方法,對該事件進行監聽。once和on不同的地方就是,once只監聽一次,會在回調函數執行完畢后,取消監聽。

socket.on(event,function(data,fn){})
socket.once(event,function(data,fn){})

2.服務端
emit的三個參數:首先是服務器端:

var socket = sio.listen(server);
socket.on('connection',function(socket)){
    console.log('客戶端已經建立');
    socket.emit('setName','Seven',function(data1,data2){
        console.log('客戶端傳來的數據1>'+data1);
        console.log('客戶端傳來的數據2>'+data2);
    });
    socket.on('disconnect',function(){
        console.log('客戶端連接斷開');
    })
};

再是客戶端:

<script src="/socket.io/socket.io.js"></script>
<script>
    var socket = io.connect();
    socket.on('setName',function(data,fn){
        console.log(data);
        //fn為當對方確認收到數據時,調用的回調函數
        fn("Jason","Jade");
    });
    
    socket.on('disconnect',function(){
        console.log("服務端斷開連接");
    });
</script>
  1. 房間
    房間是Socket.IO提供的一個非常好用的功能。房間相當于為指定的一些客戶端提供了一個命名空間,所有在房間里的廣播和通信都不會影響到房間以外的客戶端。

進入房間與離開房間
使用join()方法將socket加入房間:

io.on('connection', function(socket){
 socket.join('some room');
});

使用leave()方法離開房間:

socket.leave('some room');

在房間中發送消息
在某個房間中發送消息:

 io.to('some room').emit('some event');

to()方法用于在指定的房間中,對除了當前socket的其他socket發送消息。

  socket.broadcast.to('game').emit('message','nice game');

in()方法用于在指定的房間中,為房間中的所有有socket發送消息。

  io.sockets.in('game').emit('message','cool game');

當socket進入一個房間之后,可以通過以下兩種方式在房間里廣播消息:

// 向myroom廣播一個事件,提交者會被排除在外(即不會收到消息)
io.socket.on('connection',function (socket)){
      // 和下面對比,這里從客戶端的角度來提交事件
      socket.broadcast.to('my room').emit('event_name',data);
}

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

推薦閱讀更多精彩內容