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:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" 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阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容