利用Spring_Boot WebSocKet实现一个推送的小Demo

利用Spring_Boot WebSocKet实现一个推送的小Demo

简介:

文章共分为两个模块,包括全局推送功能的实现,和点对点的推送功能实现,源代码在文末给出连接

模块一:全局推送

1. 新建项目,利用引导创建

新建时采用引导的方式,下面给出的是几张重点截图,其他的过程截图省略,

选择项目类型

选择Web 下的 WebSocket

选择Template… 下的 Thymeleaf

以上是根据引导来创建Spring_Boot WebSocket项目过程中的几个主要界面,其余界面较为普遍,不加以介绍,详细的pom.xml文件内容此处不提供,在点对点模块有整体的pom.xml文件。

2. 配置WebSocketConfig

具体内容看代码注释,
注:测试全局推送的时候可将代码中的“/socket”节点相关信息注释掉,也可去一对一模块寻找对应的拦截器代码

//WebSocketConfig配置文件
@Configuration
@EnableWebSocketMessageBroker  //开启使用STOMP协议来传输基于代理的消息
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    //注册STOMP协议的节点,并指定映射的URL
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        /**
         * 注册STOMP协议节点,同时指定使用SockJS协议,在前端界面中会使用该处的节点进行SocKet连接
         * 根据需要注册即可,这里因为只是测试,注册了3个节点
         * 另外,在第三个节点中,可以看到比前两个节点多了一些内容,制定了一个拦截器及其他信息
         * 这个根据需要来扩展,拦截器是定义的一个class文件,该拦截器在demo中主要用在点对点模块,因本人也是初次接触这部分内容,不太了解。
         */
        stompEndpointRegistry.addEndpoint("/endpointSang").withSockJS();
        stompEndpointRegistry.addEndpoint("/endpointChat").withSockJS();
        stompEndpointRegistry.addEndpoint("/socket").withSockJS()
                .setInterceptors( new ChatHandlerShareInterceptor())
                .setStreamBytesLimit(512 * 1024)
                .setHttpMessageCacheSize(1000)
                .setDisconnectDelay(30 * 1000);
    }
    //配置消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /**
         * 配置消息代理,前端通过这个代理来进行消息订阅,
         * 消息代理可以有一个,也可以有多个,用 “,” 号分隔
         * 这里配置了两个,"/topic"用作全局推送,"/queue"用做点对点使用
         */
        registry.enableSimpleBroker("/queue","/topic");
        /**
         * 配置接收前端信息的消息代理,前端通过这个代理来向后台传递消息
         * 简单来说,前端通过这个消息代理访问对应controller中的MessageMapping(value)
         */
        registry.setApplicationDestinationPrefixes("/app");
        //registry.setPathMatcher(new AntPathMatcher("."));
    }

    @Override
    public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(final WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {
                    @Override
                    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
                        // 客户端与服务器端建立连接后,此处记录谁上线了
                        String username = session.getPrincipal().getName();
                        //log.info("online: " + username);
                        System.out.println("online: " + username);
                        super.afterConnectionEstablished(session);
                    }

                    @Override
                    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
                        // 客户端与服务器端断开连接后,此处记录谁下线了
                        String username = session.getPrincipal().getName();
                       // log.info("offline: " + username);
                        System.out.println("offline: " + username);
                        super.afterConnectionClosed(session, closeStatus);
                    }
                };
            }
        });
        super.configureWebSocketTransport(registration);
    }
}
3. 建立实体类

分别建立两个实体类,一个用于封装向浏览器发送的消息,一个用于封装接受来自浏览器的消息

/**
 * 接收来自浏览器的消息
 */
public class  RequestMessage {
    private String name;
    public String getName() {
        return name;
    }
}
/**
 * 保存服务器发给浏览器的信息
 */
public class ResponseMessage {
    private String responseMessage;
    public ResponseMessage(String responseMessage) {
        this.responseMessage = responseMessage;
    }
    public String getResponseMessage() {
        return responseMessage;
    }
}
4. Controller代码

控制器,用来控制页面的跳转和推送服务的使用

/**
 * 控制器
 */
@Controller
public class WsController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;   //实现向浏览器发送信息的功能
    
    //转到ws.html界面,ws是一个测试界面,既可以发布消息也可以接受推送
@RequestMapping(value = "/ws")
    public String tows(){
        return "/ws";
    }

    /**
     * 进行公告推送,此处只用注解,不使用注解请参考点对点中的@MessageMapping("/chat1")对应具体方法
     * @MessageMapping("/welcome") 对应ws.html中的stompClient.send("/app/welcome")
     * 多出来的“/app"是WebSocKetConfig中定义的,如不定义,则HTML中对应改为stompClient.send("/welcome")
     * @SendTo("/topic/getResponse") 指定订阅路径,对应HTML中的stompClient.subscribe('/topic/getResponse', ……)
     * 意味将信息推送给所有订阅了"/topic/getResponse"的用户
     */
    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse")       //指明发布路径
    public ResponseMessage say(RequestMessage message) {
        System.out.println(message.getName());
        return new ResponseMessage("welcome," + message.getName() + " !");
    }
}
5. 前端ws.html

前提:添加脚本,将stomp.js、sockjs.min.js 以及jqury.js 脚本放在src/main/resources/static下。可在这里下载。
代码:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>广播式WebSocket</title>
    <script th:src="@{js/sockjs.min.js}"></script>
    <script th:src="@{js/stomp.js}"></script>
    <script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">Sorry,浏览器不支持WebSocket</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect();">连接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
    </div>
    <div id="conversationDiv">
        <label>输入你的名字</label><input type="text" id="name"/>
        <button id="sendName" onclick="sendName();">发送</button>
        <p id="response"></p>
    </div>
</div>
<script type="text/javascript">
    var stompClient = null;
    function setConnected(connected) {
        document.getElementById("connect").disabled = connected;
        document.getElementById("disconnect").disabled = !connected;
        document.getElementById("conversationDiv").style.visibility = connected ? 'visible' : 'hidden';
//        $("#connect").disabled = connected;
//        $("#disconnect").disabled = !connected;
        $("#response").html();
    }
    //点击连接按钮后执行
    function connect() {
        //指明连接的节点,节点在WebSocKetConfig中配置
        var socket = new SockJS('/endpointSang');
        //利用Stomp协议创建socket客户端
        stompClient = Stomp.over(socket);
        /**
         * 调用stompClient中的connect方法来连接服务端,
         * 连接成功之后调用setConnected方法,该隐藏的隐藏,该显示的显示
         */
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected:' + frame);
            //调用stompClient中的subscribe方法来订阅/topic/getResponse发送来的消息
            stompClient.subscribe('/topic/getResponse', function (response) {
                showResponse(JSON.parse(response.body).responseMessage);
            })
        });
    }
    function disconnect() {
        if (stompClient != null) {
            //断开连接
            stompClient.disconnect();
        }
        setConnected(false);
        console.log('Disconnected');
    }
    function sendName() {
        var name = $('#name').val();
        console.log('name:' + name);
        /**
         * 发送一条消息到服务端,"/app/welcome"由WebSocketConfig中配置的“/app"
         * 和控制器中对应的MessageMapping("welcome")
         */
        stompClient.send("/app/welcome", {}, JSON.stringify({'name': name}));
    }
    //显示消息
    function showResponse(message) {
        $("#response").html(message);
    }
</script>
</body>
</html>
6. 启动入口DemoApplication.Java

自动生成的入口代码。不做更改

@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
   }
}
7. 结果测试

启动该工程,在浏览器中访问 http://localhost:8080/ws 打开两个或多个,连接后在其中一个界面输入内容,点击“发送”后可在所有的界面看到相关信息,证明全局推送成功。

效果图如下:

全局推送测试

模块二:点对点推送

在进行点对点推送的开发时,考虑到点对点里面的每个“点”都需要有一个唯一的标识符,所以在项目中引入了pring-boot –security,用来对每个“点”进行唯一的标识。

1. pom.xml

本项目采用maven的构建方式,下面是项目的pom.xml文件内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>cn.tourage</groupId>
   <artifactId>demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>demo</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.3.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

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

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

      <!--增加的security-->
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>
2.创建WebSecurityConfig

配置WebSecurityConfig.java,设置拦截规则,初始化用户及其角色

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //设置拦截规则
                .antMatchers("/","/login")   ////1根路径和/login路径不拦截
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                //开启默认登录页面
                .formLogin()
                //默认登录页面
                .loginPage("/login")
                //默认登录成功跳转页面
                .defaultSuccessUrl("/chat")
                .permitAll()
                .and()
                //设置注销
                .logout()
                .permitAll();
    }
    //设置用户
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("111").password("111").roles("USER")
                .and()
                .withUser("222").password("222").roles("USER")
                .and()
                .withUser("333").password("333").roles("USER")
                .and()
                .withUser("444").password("444").roles("USER");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //设置不拦截规则
        web.ignoring().antMatchers("/resources/static/**");
    }
} 
3. 创建WebSocketConfig

该类与上一模块相同,其中,在设置STOMP节点时引入了一个拦截器功能,具体见“/socket”节点及相关注释信息

@Configuration
@EnableWebSocketMessageBroker  //开启使用STOMP协议来传输基于代理的消息
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    //注册STOMP协议的节点,并指定映射的URL
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        /**
         * 注册STOMP协议节点,同时指定使用SockJS协议,在前端界面中会使用该处的节点进行SocKet连接
         * 根据需要注册即可,这里因为只是测试,注册了3个节点
         * 另外,在第三个节点中,可以看到比前两个节点多了一些内容,制定了一个拦截器及其他信息,该拦截器在demo中主要用在点对点模块
         * 这个根据需要来扩展,拦截器是定义的一个class文件,因本人也是初次接触这部分内容,不太了解。
         */
        stompEndpointRegistry.addEndpoint("/endpointSang").withSockJS();
        stompEndpointRegistry.addEndpoint("/endpointChat").withSockJS();
        stompEndpointRegistry.addEndpoint("/socket").withSockJS()
                .setInterceptors( new ChatHandlerShareInterceptor())
                .setStreamBytesLimit(512 * 1024)
                .setHttpMessageCacheSize(1000)
                .setDisconnectDelay(30 * 1000);
    }

    //配置消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /**
         * 配置消息代理,前端通过这个代理来进行消息订阅,
         * 消息代理可以有一个,也可以有多个,用 “,” 号分隔
         * 这里配置了两个,"/topic"用作全局推送,"/queue"用做点对点使用
         */
        registry.enableSimpleBroker("/queue","/topic");
        /**
         * 配置接收前端信息的消息代理,前端通过这个代理来向后台传递消息
         * 简单来说,前端通过这个消息代理访问对应controller中的MessageMapping(value)
         */
        registry.setApplicationDestinationPrefixes("/app");
        //registry.setPathMatcher(new AntPathMatcher("."));
    }

    @Override
    public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(final WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {
                    @Override
                    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
                        // 客户端与服务器端建立连接后,此处记录谁上线了
                        String username = session.getPrincipal().getName();
                        //log.info("online: " + username);
                        System.out.println("online: " + username);
                        super.afterConnectionEstablished(session);
                    }

                    @Override
                    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
                        // 客户端与服务器端断开连接后,此处记录谁下线了
                        String username = session.getPrincipal().getName();
                       // log.info("offline: " + username);
                        System.out.println("offline: " + username);
                        super.afterConnectionClosed(session, closeStatus);
                    }
                };
            }
        });
        super.configureWebSocketTransport(registration);
    }
}
4.配置拦截器

拦截器功能可通过自己定义来实现具体的功能,此处的拦截器并未在测试项目中有太多价值的用法,增加该功能只是为了在具体使用“点对点推送”功能时进行扩展。

/**
 * websocket拦截器配置,读取session.
 */
public class ChatHandlerShareInterceptor extends HttpSessionHandshakeInterceptor {

    private static Logger logger = LoggerFactory.getLogger(ChatHandlerShareInterceptor.class);

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
                               Exception ex) {
        // TODO Auto-generated method stub
        super.afterHandshake(request, response, wsHandler, ex);
    }

    @Override
    public boolean beforeHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2,
                                   Map<String, Object> arg3) throws Exception {

        if(arg0 instanceof ServletServerHttpRequest){
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) arg0;
            HttpSession session = servletRequest.getServletRequest().getSession(false);
            if (session != null) {
                //使用userName区分WebSocketHandler,以便定向发送消息
                String httpSessionId = session.getId();
                logger.info(httpSessionId);
                arg3.put("HTTP_SESSION_ID",httpSessionId);
            }else{
            }
        }
        return true;
    }
}
5. Controller控制器
/**
 * 控制器
 */
@Controller
public class WsController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;   //实现向浏览器发送信息的功能

    //  转到相关的HTML界面
    @RequestMapping(value = "/login")
    public String toLogin() {
        return "/login";
    }

@RequestMapping(value = "/chat")
public String toChat() {
    return "/chat";
}

    /**
     * 转到消息推送,对应前端chat.html中的stomp.send("/chat1",{},text)
     * 此处未使用注解,而是使用代码编程来完成推送,目的是为了方便对批量(可理解为分组)推送进行扩展
     * 编程实现消息推送,点对点使用convertAndSendToUser,广播使用convertAndSendTo
     * 使用注解可参考全局推送里面的具体实现,注:点对点用SendToUser()来代替SendTo()
     */
    @MessageMapping("/chat1")
//    可以直接在参数中获取Principal,Principal中包含有当前用户的用户名。
    public void handleChat(Principal principal, String msg) {
        List<String> list = new ArrayList<String>();
        list.add("222");
        list.add("333");
        list.add("444");
        if (principal.getName().equals("111")) {
            for (String s : list)
            /**
             * 第一个参数:用户名;第二个参数:订阅路径;第三个参数:要推送的消息文本
             * 注:全局推送对应方法有两个参数,第一个参数:订阅路径;第二个参数:要推送的消息文本
             */
                messagingTemplate.convertAndSendToUser(s, "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
        } else {
            messagingTemplate.convertAndSendToUser("111", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
        }
    }
}
6. 前端界面

这部分使用两个前端界面,一个简单的登录界面login.html,还有一个聊天界面chat.html,在chat.html中同时可以使用发布信息和接受推送的功能,可按照实际需求进行自我定制
注:在前端编写的时候一定要记得添加相关的js文件,下载地址:(http://pan.baidu.com/s/1gfKJCAJ)

Login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
    <title>登陆页面</title>
</head>
<body>
<div th:if="${param.error}">
    无效的账号和密码
</div>
<div th:if="${param.logout}">
    你已注销
</div>
<form th:action="@{/login}" method="post">
    <div><label> 账号 : <input type="text" name="username"/> </label></div>
    <div><label> 密码: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登陆"/></div>
</form>
</body>
</html>

Chat.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>聊天室</title>
    <script th:src="@{js/sockjs.min.js}"></script>
    <script th:src="@{js/stomp.js}"></script>
    <script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body>
<p>聊天室</p>
<form id="sangForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit" value="发送"/>
</form>
<script th:inline="javascript">
    $("#sangForm").submit(function (e) {
        e.preventDefault();
        var textArea = $("#sangForm").find('textarea[name="text"]');
        var text = textArea.val();
        sendSpittle(text);
        textArea.val('');
    });
    var sock = new SockJS("/endpointChat");
    var stomp = Stomp.over(sock);
    /**
     * 连接服务端,成功后注册监听,注册地址为/user/queue/notifications
     * 监听地址比WebSocket配置文件中的多了一个/user,这个/user是必不可少的,使用了它消息才会点对点传送。
     */
    stomp.connect('guest','guest',function (frame) {
        stomp.subscribe("/user/queue/notifications", handleNotification);
    });
    function handleNotification(message) {
        $("#output").append("<b>Received: "+message.body+"</b><br/>")
    }
    /**
     * 消息转发,对应相关controller中的MessageMapping("/chat1")
     * 增加的“/app”源于WebSocketConfig中配置的结果
     * @param text
     */
    function sendSpittle(text) {
        stomp.send("/app/chat1", {}, text);
    }
//  关闭连接
    $("#stop").click(function () {
        sock.close();
    });
</script>
<div id="output"></div>
</body>
</html>
7. 启动入口:

略,项目默认的启动界面

8. 结果测试

采用两个浏览器分别登入两个用户,输入测试可看到如下结果
注意:一定要使用两个浏览器登录,不然会默认为同一个Session

Paste_Image.png
项目地址:

https://github.com/AnRic/WebSocket_demo
GitHub新人,求关注

写在最后

本文是我在参考网上资源并亲身实践后编写的一篇小文章,做一个简单的学习记录,也希望能帮助到大家。在其中可能也会有一些的不足之处,请包容。如有疑问,请发送私信联系我,谢谢。

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

推荐阅读更多精彩内容