java实现websocket的五种方式

1. 前言

本文主要讲述在Java技术领域实现websocket服务的五种方式.

2. 第一种使用Java原生代码实现websocket

使用Java原生代码实现websocket服务的方法, 此方法需要引入一个第三方库java-websocket.jar. 截至目前2023/01/01最新版本为1.5.3.

项目源代码位于:

https://github.com/TooTallNate/Java-WebSocket

示例代码位于:

https://github.com/TooTallNate/Java-WebSocket/tree/master/src/main/example

2.1. 首先在项目中引入依赖

如果你的项目使用gradle作为管理工具, 可以添加以下gradle依赖


implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.3'

如果你的项目使用maven进行管理, 可以添加以下maven依赖

mven依赖

<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.5.3</version>
</dependency>

2.2. 创建WebsocketServer类


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

public class SocketServer extends WebSocketServer {

    public SocketServer(int port) throws UnknownHostException {
        super(new InetSocketAddress(port));
      }
    
      public SocketServer(InetSocketAddress address) {
        super(address);
      }

    @Override
    public void onOpen(WebSocket conn, ClientHandshake handshake) {
        conn.send("Welcome to the server!"); // This method sends a message to the new client
        broadcast("new connection: " + handshake
                .getResourceDescriptor()); // This method sends a message to all clients connected
        System.out.println(
                conn.getRemoteSocketAddress().getAddress().getHostAddress() + " entered the room!");

    }

    @Override
    public void onClose(WebSocket conn, int code, String reason, boolean remote) {
        broadcast(conn + " has left the room!");
        System.out.println(conn + " has left the room!");

    }

    @Override
    public void onMessage(WebSocket conn, String message) {

        broadcast(message);
        System.out.println(conn + ": " + message);
    }

    @Override
    public void onError(WebSocket conn, Exception ex) {
        ex.printStackTrace();
        if (conn != null) {
            // some errors like port binding failed may not be assignable to a specific
            // websocket
        }

    }

    @Override
    public void onStart() {
        System.out.println("Server started!");
        setConnectionLostTimeout(0);
        setConnectionLostTimeout(100);

    }

}

2.3. 启动SocketServer

我们以及创建好了SocketServer, 这个时候我们可以启动它了, 启动代码如下.


    public static void main(String[] args) throws InterruptedException, IOException {
        int port = 8887; // 843 flash policy port
       
        SocketServer s = new SocketServer(port);
        s.start();
        System.out.println("ChatServer started on port: " + s.getPort());
    
        BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));
        while (true) {
          String in = sysin.readLine();
          s.broadcast(in);
          if (in.equals("exit")) {
            s.stop(1000);
            break;
          }
        }
      }

写好main方法后, 我们可以启动它, 当控制台输出ChatServer started on port: 8887表示启动成功.

2.4. 测试web socket server

此时web socket server已经监听在了localhost:8887上. 我们可以使用websocket在线调试工具对其进行测试.

该工具主要是利用html5 的websocket去连接服务端的websocket,因此,
无论你是内网还是外网都可使用!

打开工具在输入框中输入 ws://localhost:8887点击连接, 既可以看到服务器端的反馈, 同时web socket server的控制台也会输出日志信息.

3. 使用Java原生+SpringBoot混合

在此种方式中, SocketServer依然使用原生的java代码编写, 但是SocketServer实例化过程由spring来管理.

此时我们需要引入spring-boot-starter-websocket, 上一节中的依赖包Java-WebSocket已经不需要了. 两种方式采用了不同的机制.

可以参考我的博文创建spring boot项目, 里面有详细讲解 - 如何手动创建一个springBoot项目

3.1. 引入依赖


plugins {
   
    id 'org.springframework.boot' version '2.7.7'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    
}

dependencies {
  
    implementation 'org.springframework.boot:spring-boot-starter-websocket'

}

此处我们需要在gradle配置文件的plugins闭包内添加两个plugins, 一个复制控制spring boot的版本, 一个负责管理依赖.

对于maven, 需要如下配置

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.7</version>
        <relativePath/> 
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
    </dependencies>

3.2. 创建ServerEndpoint

创建ServerEndpoint代码如下:


import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

@ServerEndpoint("/myWs")
@Component
public class WsServerEndpoint {

    /**
     * 连接成功
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("连接成功");
    }

    /**
     * 连接关闭
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        System.out.println("连接关闭");
    }

    /**
     * 接收到消息
     *
     * @param text
     */
    @OnMessage
    public String onMsg(String text) throws IOException {
        return "servet 发送:" + text;
    }
}

说明
这里有几个注解需要注意一下,首先是他们的包都在 javax.websocket下。并不是 spring 提供的,而 jdk 自带的,下面是他们的具体作用。

  1. @ServerEndpoint
    通过这个 spring boot 就可以知道你暴露出去的 ws 应用的路径,有点类似我们经常用的@RequestMapping。比如你的启动端口是 8080,而这个注解的值是 ws,那我们就可以通过 ws://127.0.0.1:8080/ws 来连接你的应用

  2. @OnOpen
    当 websocket 建立连接成功后会触发这个注解修饰的方法,注意它有一个 Session 参数

  3. @OnClose
    当 websocket 建立的连接断开后会触发这个注解修饰的方法,注意它有一个 Session 参数

  4. @OnMessage
    当客户端发送消息到服务端时,会触发这个注解修改的方法,它有一个 String 入参表明客户端传入的值

  5. @OnError
    当 websocket 建立连接时出现异常会触发这个注解修饰的方法,注意它有一个 Session 参数

  6. 服务器主动发送消息
    当服务器端要主动给客户端发送, 需要获取掉相应客户端与服务器端的session, 通过 session.getBasicRemote().sendText(), 将消息发送到前端.
    因此最好在onOpen方法中将session对象保存起来, 这样下次主动连接客户端时能找到相应的session对象.

3.3. 添加Spring配置

有了WsServerEndpoint后我们还要配置ServerEndpointExporter, 将Endpoint暴露出去让客户端来建立连接.

而配置ServerEndpointExporter的方式非常简单, 只需要创建一个ServerEndpointExporter bean即可, 它会去获取Spring上下文中所有的Endpoint示例, 完成endpoint的注册过程, 并监听在application.properties 的server.port 属性所指定的端口.


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
@EnableWebSocket
public class WebsocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpoint() {
        return new ServerEndpointExporter();
    } 

}

3.4. 启动应用程序并测试

我们只需要向一般的Spring boot应用一样启动它即可.



@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

测试, 我们依然使用websocket在线调试工具来测试, 详情可参考上一节中的介绍

与上一节稍微差异的地方是, 我们可以url中指定endpoint了 ws://127.0.0.1:8080/myWs

4. 使用SpringBoot实现websocket

4.1. 引入依赖

implementation 'org.springframework.boot:spring-boot-starter-websocket'

4.2. 实现类

HttpAuthHandler.java


import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.time.LocalDateTime;

@Component
public class HttpAuthHandler extends TextWebSocketHandler {

    /**
     * socket 建立成功事件
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Object sessionId = session.getAttributes().get("session_id");
        if (sessionId != null) {
            // 用户连接成功,放入在线用户缓存
            WsSessionManager.add(sessionId.toString(), session);
        } else {
            throw new RuntimeException("用户登录已经失效!");
        }
    }

    /**
     * 接收消息事件
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 获得客户端传来的消息
        String payload = message.getPayload();
        Object sessionId = session.getAttributes().get("session_id");
        System.out.println("server 接收到 " + sessionId + " 发送的 " + payload);
        session.sendMessage(new TextMessage("server 发送给 " + sessionId + " 消息 " + payload + " " + LocalDateTime.now().toString()));
    }

    /**
     * socket 断开连接时
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Object sessionId = session.getAttributes().get("session_id");
        if (sessionId != null) {
            // 用户退出,移除缓存
            WsSessionManager.remove(sessionId.toString());
        }
    }

    
}

WsSessionManager.java



import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

public class WsSessionManager {
    /**
     * 保存连接 session 的地方
     */
    private static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>();

    /**
     * 添加 session
     *
     * @param key
     */
    public static void add(String key, WebSocketSession session) {
        // 添加 session
        SESSION_POOL.put(key, session);
    }

    /**
     * 删除 session,会返回删除的 session
     *
     * @param key
     * @return
     */
    public static WebSocketSession remove(String key) {
        // 删除 session
        return SESSION_POOL.remove(key);
    }

    /**
     * 删除并同步关闭连接
     *
     * @param key
     */
    public static void removeAndClose(String key) {
        WebSocketSession session = remove(key);
        if (session != null) {
            try {
                // 关闭连接
                session.close();
            } catch (IOException e) {
                // todo: 关闭出现异常处理
                e.printStackTrace();
            }
        }
    }

    /**
     * 获得 session
     *
     * @param key
     * @return
     */
    public static WebSocketSession get(String key) {
        // 获得 session
        return SESSION_POOL.get(key);
    }
}



import java.util.Map;

import org.apache.logging.log4j.util.Strings;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

@Component
public class MyInterceptor implements HandshakeInterceptor {

    /**
     * 握手前
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("握手开始");
        String hostName = request.getRemoteAddress().getHostName();
        String sessionId = hostName+String.valueOf((int)(Math.random()*1000));
        if (Strings.isNotBlank(sessionId)) {
            // 放入属性域
            attributes.put("session_id", sessionId);
            System.out.println("用户 session_id " + sessionId + " 握手成功!");
            return true;
        }
        System.out.println("用户登录已失效");
        return false;
    }

    /**
     * 握手后
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        System.out.println("握手完成");
    }

}

4.3. Spring 配置



import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import DataLoader.ws.HttpAuthHandler;
import DataLoader.ws.MyInterceptor;

@Configuration
@EnableWebSocket
public class WebsocketConfig implements WebSocketConfigurer {
    
    @Autowired
    private HttpAuthHandler httpAuthHandler;
    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
                .addHandler(httpAuthHandler, "myWS")
                .addInterceptors(myInterceptor)
                .setAllowedOrigins("*");
    }

}

4.4. 启动与测试

启动代码如下:


import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {

            System.out.println("application started");

        };
    }
}

执行main方法启动应用程序

测试依然使用websocket在线调试工具

5. 使用TIO+SpringBoot实现websocket

以下是关于t-io的一些信息, 如果需要更详细的了解tio可以访问以下这些站点

sit link
t-io site https://www.t-io.org
t-io on gitee https://gitee.com/tywo45/t-io
t-io on github https://github.com/tywo45/t-io

5.1. 添加相应依赖

gradle:


implementation 'org.t-io:tio-websocket-spring-boot-starter:3.6.0.v20200315-RELEASE'

maven:

 <dependency>
     <groupId>org.t-io</groupId>
     <artifactId>tio-websocket-spring-boot-starter</artifactId>
     <version>3.6.0.v20200315-RELEASE</version>
</dependency>

5.2. 编写消息处理类


import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;
import org.tio.http.common.HttpRequest;
import org.tio.http.common.HttpResponse;
import org.tio.websocket.common.WsRequest;
import org.tio.websocket.server.handler.IWsMsgHandler;


@Component
public class MyWebSocketMsgHandler implements IWsMsgHandler {

    @Override
    public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext)
            throws Exception {
        return httpResponse;
    }

    @Override

    public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext)
            throws Exception {
        System.out.println("握手成功");
    }

    @Override
    public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        System.out.println("接收到bytes消息");
        return null;
    }

    @Override
    public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
        return null;
    }

    @Override
    public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
        System.out.println("接收到文本消息:" + s);
        return null;
    }
}

5.3. 修改配置文件

application.properties

tio.websocket.server.port=9876
tio.websocket.server.heartbeat-timeout=60000

5.4. 启动tio Websocket Server

启动tio Websocket Server 的方式如下, 执行main方法.


@SpringBootApplication
@EnableTioWebSocketServer
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
            System.out.println("application started");
        };
    }
}

6. STOMP实现websocket

6.1. 添加相应依赖

gradle:

implementation 'org.springframework.boot:spring-boot-starter-websocket'

maven:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

6.2. 实现配置WebSocketMessageBrokerConfigurer接口

WebSocketConfig.java


import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 配置客户端尝试连接地址
        registry.addEndpoint("/ws").setAllowedOrigins("*");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 设置广播节点
        registry.enableSimpleBroker("/topic", "/queue");
        // 客户端向服务端发送消息需有/app 前缀
        registry.setApplicationDestinationPrefixes("/app");
        // 指定用户发送(一对一)的前缀 /user/
        registry.setUserDestinationPrefix("/user/");
    }
}

6.3. 实现消息请求处理的Controller

WSController.java


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;

@Controller
public class WSController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/greeting")
    @SendToUser("/queue/serverReply")
    public String greating(@Payload String data) {
        System.out.println("received greeting: " + data);
        String msg = "server replys: " + data;
        return msg;
    }

    @MessageMapping("/shout")
    public void userShout(Shout shout) {
        //String name = principal.getName();
        String message = shout.getMessage();
        System.out.println("收到的消息是:" + message);
        simpMessagingTemplate.convertAndSend("/queue/notifications", shout);
       
    }

}

domain object

Shout.java


public class Shout {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

6.4. 启动


import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {

            System.out.println("application started");

        };
    }
}

6.5. 实现消息客户端

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <title>Document</title>
    <style>
        .newmessage {
            color: green;
        }
        .errormessage {
            color: red;
        }
    </style>
</head>

<body>
    <div class="row">
        <div class="col-lg-6">
            <div class="input-group">
                <input type="text" id='urlInput' class="form-control" placeholder='输入websocket端点'>
                <span class="input-group-btn">
                    <button class="btn btn-default" type="button" onclick="reconnect();">重新连接</button>
                </span>
            </div>
        </div>
        <div class="col-lg-8" id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin-left:15px">
        </div>
        <div class="col-lg-6">
            <div class="input-group">
                <input type="text" id='messageInput' class="form-control" placeholder='待发信息'>
                <span class="input-group-btn">
                    <button class="btn btn-default" type="button" onclick="doSend();">发送</button>
                </span>
                <span class="input-group-btn">
                    <button class="btn btn-default" type="button" onclick="broadcast();">广播</button>
                </span>
            </div>
        </div>
    </div>

</body>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="http://cdn.static.runoob.com/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">

    var defaultUrl = "ws://localhost:81/ws"
    document.getElementById("urlInput").value = defaultUrl;

    var stomp;

    var appendMessage = function (msg) {
        var div = "<div class='newmessage'>" + msg + "</div>";
        $("#output").append(div);
    }

    var reportError = function (msg) {
        var div = "<div class='errormessage'>" + msg + "</div>";
        $("#output").append(div);
    }

    function connects(url) {
        var promise = new Promise(function (resolve, reject) {
            var client = Stomp.client(url);

            var headers = {
                username: 'admin',
                password: 'admin',
                // additional header
                'client-id': 'stomp-client-id'
            };

            appendMessage('connecting to ' + url)
            client.connect(headers, function (frame) {
                resolve(client);
            }, function (error) {
                reject(error)
            });
        });
        return promise;
    }

    var connect_callback = function (client) {
        stomp = client
        appendMessage('subscribing on ' + '/queue/subscribe')

        client.subscribe('/user/queue/serverReply', function (message) {
            console.log('subscribe topic callback:' + message.body);
            appendMessage('subscribe topic callback:' + message.body)
        });

        appendMessage('subscribing on ' + '/user/queue/notifications')

        client.subscribe("/queue/notifications", function (message) {
            var content = message.body;
            var obj = JSON.parse(content);
            console.log("收到广播消息:" + message.body);
            appendMessage("收到广播消息:" + message.body);
        });

        var payload = JSON.stringify({ 'message': 'greeting to stomp broker!' });
        client.send("/app/greeting", {}, payload);

    };

    var error_callback = function (error) {
        reportError("连接失败!");
    };

    var doSend = function () {
        var msg = document.getElementById("messageInput").value
        var payload = JSON.stringify({ 'message': msg });
        stomp.send("/app/greeting", {}, payload);
    }

    var broadcast = function () {
        var msg = document.getElementById("messageInput").value
        var payload = JSON.stringify({ 'message': msg });
        stomp.send("/app/shout", {}, payload);
    }

    function reconnect() {
        if (stomp) {
            stomp.disconnect(function () {
                console.log("See you next time!");
            })
        }
        connects(document.getElementById("urlInput").value).then(connect_callback).catch(error_callback)

    }
    reconnect();

</script>

</html>

7. 后记

本技术博客原创文章位于鹏叔的IT技术博客空间 - java实现websocket的五种方式, 要获取最近更新请访问原文.

更多技术博客请访问: 鹏叔的IT技术博客空间

8. 参考文档

websocket - spring boot 集成 websocket 的四种方式

websocket在线调试工具

JAVA实现WebSocket服务器

[https://www.cnblogs.com/lr393993507/p/9834889.html](让网络编程更轻松和有趣 t-io)

tio-websocket-spring-boot-starter 的简单使用

STOMP原理与应用开发详解

Spring消息之STOMP

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容