springboot整合Netty应用

什么是Netty

Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端,Netty是一个NIO客户端服务器框架,可以快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了诸如TCP和UDP套接字服务器之类的网络编程。

“快速简便”并不意味着最终的应用程序将遭受可维护性或性能问题的困扰。Netty经过精心设计,结合了许多协议(例如FTP,SMTP,HTTP以及各种基于二进制和文本的旧式协议)的实施经验。结果,Netty成功地找到了一种无需妥协即可轻松实现开发,性能,稳定性和灵活性的方法。

特性

  • 高性能 事件驱动
  • 异步非堵塞 基于NIO的客户端,服务器端编程框架
  • 稳定性和伸缩性
  • 适用于各种传输类型的统一API-阻塞和非阻塞套接字
  • 基于灵活且可扩展的事件模型,可将关注点明确分离
  • 高度可定制的线程模型-单线程,一个活多个线程池 ,例如SEDA
  • 真正的无连接数据报套接字支持 (从3.1开始)

表现

  • 更高的吞吐量
  • 减少资源消耗
  • 减少不必要的内存复制

使用

在pom.xml中添加依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.42.Final</version>
        </dependency>

一丶Springboot应用启动加载Netty应用

暴露Netty端口,随springboot应用一并启动

/**
 * @dete: 2021/4/21 9:08 上午
 * @author: 徐子木
 */
@SpringBootApplication
public class PmSocketApplication implements CommandLineRunner {
    
// yml中指定netty端口号
    @Value("${netty.port}")
    private int nettyServerPort;

    @Autowired
    private NettyWebSocketServer nettyServer;

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


    @Override
    public void run(String... args) throws Exception {
        //netty 服务端启动的端口不可和Springboot启动类的端口号重复
        nettyServer.start(nettyServerPort);

        //关闭服务器的时候同时关闭Netty服务
         Runtime.getRuntime().addShutdownHook(new Thread(() -> nettyServer.destroy()));
    }
}

二丶Netty整合WebSocket服务端

启动或销毁netty服务端的过程实现

/**
 * Netty整合websocket 服务端
 * 运行流程:
 * 1.创建一个ServerBootstrap的实例引导和绑定服务器
 * 2.创建并分配一个NioEventLoopGroup实例以进行事件的处理,比如接收连接和读写数据
 * 3.指定服务器绑定的本地的InetSocketAddress
 * 4.使用一个NettyServerHandler的实例初始化每一个新的Channel
 * 5.调用ServerBootstrap.bind()方法以绑定服务器
 *
 * @description
 * @author: 徐子木
 * @create: 2020-06-02 14:23
 **/
@Component
@Slf4j
public class NettyWebSocketServer {


    /**
     * EventLoop接口
     * NioEventLoop中维护了一个线程和任务队列,支持异步提交任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务
     * I/O任务即selectionKey中的ready的事件,如accept,connect,read,write等,由processSelectedKeys方法触发
     * 非I/O任务添加到taskQueue中的任务,如register0,bind0等任务,由runAllTasks方法触发
     * 两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的事件与IO任务的执行时间相等
     */
    private final EventLoopGroup boosGroup = new NioEventLoopGroup();

    private final EventLoopGroup workGroup = new NioEventLoopGroup();

    /**
     * Channel
     * Channel类似Socket,它代表一个实体(如一个硬件设备,一个网络套接字) 的开放连接,如读写操作.通俗的讲,Channel字面意思就是通道,每一个客户端与服务端之间进行通信的一个双向通道.
     * Channel主要工作:
     * 1.当前网络连接的通道的状态(例如是否打开?是否已连接?)
     * 2.网络连接的配置参数(例如接收缓冲区的大小)
     * 3.提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时锁清秋的I/O操作已完成.
     * 调用立即放回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功,失败或取消时回调通知调用方.
     * 4.支持关联I/O操作与对应的处理程序.
     * 不同协议,不同的阻塞类型的连接都有不同的Channel类型与之对应,下面是一些常用的Channel类型
     * NioSocketChannel,异步的客户端 TCP Socket连接
     * NioServerSocketChannel,异步的服务端 TCP Socket 连接
     * NioDatagramChannel,异步的UDP连接
     * NioSctpChannel,异步的客户端Sctp连接
     * NioSctoServerChannel,异步的Sctp服务端连接
     * 这些通道涵盖了UDP 和TCP网络IO以及文件IO
     */
    private Channel channel;


    /**
     * 启动服务
     *
     * @param port
     */
    public void start(int port) {
        log.info("=================Netty 端口启动:{}==================",port);

        /**
         * Future
         * Future提供了另外一种在操作完成时通知应用程序的方式,这个对象可以看做一个异步操作的结果占位符.
         * 通俗的讲,它相当于一位指挥官,发送了一个请求建立完连接,通信完毕了,你通知一声它回来关闭各种IO通道,整个过程,它是不阻塞的,异步的.
         * 在Netty中所有的IO操作都是异步的,不能理科的值消息是否被正确处理,但是可以过一会儿等他执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures.
         * 他们可以注册一个监听,当操作执行成功成功或者失败时监听会自动触发注册的监听事件
         */
        try {
            /**
             * Bootstrap
             * Bootstrap是引导的意思,一个Netty应用通常由一个Bootstrap开始
             * 主要作用是配置整个Netty程序,串联各个组件
             * Netty中Bootstrap类是服务端启动引导类
             */
            ServerBootstrap server = new ServerBootstrap();
            server.group(boosGroup, workGroup)
                    //非阻塞异步服务端TCP Socket 连接
                    .channel(NioServerSocketChannel.class)
                    //设置为前端WebSocket可以连接
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            // HttpServerCodec: 将请求和映带消息节吗为HTTP消息
                            pipeline.addLast("http-codec", new HttpServerCodec());
                            // 讲HTTP消息的多个部分合成一条完整的HTTP消息
                            pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
                            // 向客户端发送HTML5文件
                            socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                            // 进行设置心跳检测
                            socketChannel.pipeline().addLast(new IdleStateHandler(60, 30, 60 * 30, TimeUnit.SECONDS));
                            // 配置通道处理 来进行业务处理
                            pipeline.addLast("handler", new WebSocketServerHandler());
                        }

                    });
            channel = server.bind(port).sync().channel();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @PreDestroy
    public void destroy() {
        log.info("=================Netty服务关闭==================");
        if (channel != null) {
            channel.close();
        }
        boosGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }


}

三丶Socket处理注册连接类

接收客户端连接信息,心跳检测,存储于销毁等的处理

其余部分业务类(messageService),均为自行处理连接存储为业务所用,请忽略

/**
 * @description
 * @author: 徐子木
 * @create: 2020-06-02 14:57
 **/
@Slf4j
@Component
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {

    public static final byte PING_MSG = 1;
    public static final byte PONG_MSG = 2;
    public static final byte CUSTOM_MSG = 3;
    private int heartbeatCount = 0;

  // 配置客户端是否为https的控制
    @Value("${netty.ssl-enabled:false}")
    private Boolean useSsl;

    /**
     * 这里可以引入自己业务类来处理进行的客户端连接
     */
    @Autowired
    private MessageService messageService;
    
    public static WebSocketServerHandler webSocketServerHandler;

    /**
     * 解决启动加载不到自己业务类
     */
    @PostConstruct
    public void init() {
        webSocketServerHandler = this;
    }


    private WebSocketServerHandshaker handshaker;


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        //http请求和tcp请求分开处理
        if (msg instanceof HttpRequest) {
            handlerHttpRequest(ctx, (HttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            //踩坑: simpleChannelInboundHandler 他会进行一次释放(引用计数器减一),参考源码,而我们释放的时候就变为了0,所以必须手动进行引用计数器加1
            WebSocketFrame frame = (WebSocketFrame) msg;
            frame.retain();
            handlerWebSocketFrame(ctx, frame);
        }

    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    /**
     * WebSocket 消息处理
     *
     * @param ctx
     * @param frame
     */
    private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        //判断是否是关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            log.info("【" + ctx.channel().remoteAddress() + "】已关闭(服务器端)");
            //移除channel
            NioSocketChannel channel = (NioSocketChannel) ctx.channel();
            webSocketServerHandler.messageService.removeConnection(channel);
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame);
            return;
        }
        //判断是否是ping消息
        if (frame instanceof PingWebSocketFrame) {
            log.info("【ping】");
            return;
        }
        //判断实时是pong消息
        if (frame instanceof PongWebSocketFrame) {
            log.info("【pong】");
            return;
        }
        //本例子只支持文本,不支持二进制
        if (!(frame instanceof TextWebSocketFrame)) {
            log.info("【不支持二进制】");
            throw new UnsupportedOperationException("不支持二进制");
        }

        // 传送的消息 ,接收客户端指定格式(自己与客户端约定json格式)的消息,并进行处理
        MessageObject messageObject = JSONObject.parseObject(((TextWebSocketFrame) frame).text().toString(), MessageObject.class);
        webSocketServerHandler.messageService.sendMessage(messageObject, ctx);
    }

    /**
     * websocket第一次连接握手
     *
     * @param ctx
     */
    @SuppressWarnings("deprecation")
    private void handlerHttpRequest(ChannelHandlerContext ctx, HttpRequest req) {
// 这里接收客户端附加连接参数,根据自己业务与客户端指定需要哪些参数来辨别连接唯一性
        String userUid = null;
        String sectionId = null;
        if ("GET".equalsIgnoreCase(req.getMethod().toString())) {
            String uri = req.getUri();
            userUid = req.getUri().substring(uri.indexOf("/", 2) + 1, uri.lastIndexOf("/"));
            sectionId = req.getUri().substring(uri.lastIndexOf("/") + 1);
            //对用户信息进行存储
            NioSocketChannel channel = (NioSocketChannel) ctx.channel();

            webSocketServerHandler.messageService.putConnection(userUid, sectionId, channel);
        }

        // http 解码失败
        if (!req.getDecoderResult().isSuccess() || (!"websocket".equalsIgnoreCase(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, (FullHttpRequest) req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
        }
        //可以通过url获取其他参数
        WebSocketServerHandshakerFactory factory;
// 这里主要用于 客户端为wss连接的处理
        if (useSsl != null && useSsl) {
            factory = new WebSocketServerHandshakerFactory(
                    "wss://" + req.headers().get("Host") + "/" + req.getUri() + "", null, false
            );
        } else {
            factory = new WebSocketServerHandshakerFactory(
                    "ws://" + req.headers().get("Host") + "/" + req.getUri() + "", null, false
            );
        }
        handshaker = factory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
        } else {
            //进行连接
            handshaker.handshake(ctx.channel(), (FullHttpRequest) req);
        }
    }

    @SuppressWarnings("deprecation")
    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
        // 返回应答给客户端
        if (res.getStatus().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);

            // buf.release();
        }
        // 如果是非Keep-Alive,关闭连接
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }


    /**
     * 这里是保持服务器与客户端长连接  进行心跳检测 避免连接断开
     *
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent stateEvent = (IdleStateEvent) evt;
            PingWebSocketFrame ping = new PingWebSocketFrame();
            switch (stateEvent.state()) {
                //读空闲(服务器端)
                case READER_IDLE:
                    //log.info("【" + ctx.channel().remoteAddress() + "】读空闲(服务器端)");
                    ctx.writeAndFlush(ping);
                    break;
                //写空闲(客户端)
                case WRITER_IDLE:
                    //log.info("【" + ctx.channel().remoteAddress() + "】写空闲(客户端)");
                    ctx.writeAndFlush(ping);
                    break;
                case ALL_IDLE:
                    //log.info("【" + ctx.channel().remoteAddress() + "】读写空闲");
                    break;
                default:
                    break;
            }
        }
    }


    /**
     * 出现异常时
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        //移除channel
        webSocketServerHandler.messageService.removeConnection((NioSocketChannel) ctx.channel());
        ctx.close();
        log.info("【" + ctx.channel().remoteAddress() + "】已关闭(服务器端)");
    }


}

四丶与业务整合并存储客户连接

以下均为自己业务与socket连接对象的存储,发送,踢出等处理,可根据自己业务自行参考

当时公司业务需要将连接存储两级维度,可根据自己的业务给客户端分组,或不分组都可以

/**
 * @description
 * @author: 徐子木
 * @create: 2020-09-30 15:06
 **/
@Slf4j
@Service(value = "messageService")
public class MessageServiceImpl implements MessageService {

// 是消息通信dubbo类,请忽略
    @Autowired
    private ChatMsgService chatMsgService;
// 是db存储类 ,请忽略
    @Autowired
    private BidPresentDao bidPresentDao;

// 这里是netty可为指定的唯一key去与连接进行分组处理并存储
    private HashedWheelTimer hashedWheelTimer = new HashedWheelTimer();

    private final AttributeKey<String> userKey = AttributeKey.valueOf("user");

    private final AttributeKey<String> sectionKey = AttributeKey.valueOf("section");

    /**
     * 装载标段与对应在线的用户
     */
    private static final Map<String, ChannelGroup> SECTION_GROUPS = new ConcurrentHashMap<>();


    /**
     * 维护某标段中的socket连接
     *
     * @param sectionId
     * @param channel
     */
    @Override
    public void putConnection(String userId, String sectionId, NioSocketChannel channel) {

        channel.attr(userKey).set(userId);
        channel.attr(sectionKey).set(sectionId);

        bidPresentDao.comeOnlineByUserId(userId, sectionId);

        //存储用户标段对应连接
        ChannelGroup channelGroup = SECTION_GROUPS.get(sectionId);
        if (null == channelGroup) {
            //保存全局的,连接上的服务器的客户
            channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
            channelGroup.add(channel);
            SECTION_GROUPS.put(sectionId, channelGroup);
        } else {
            channelGroup.add(channel);
        }
    }

    /**
     * 判断一个通道是否有用户在使用
     *
     * @param channel
     * @return
     */
    private boolean hasUser(Channel channel) {
        return ((channel.hasAttr(userKey) || channel.hasAttr(sectionKey)));
    }

    /**
     * 获取连接对应用户
     *
     * @param channel
     * @return
     */
    @Override
    public String getBindUserId(NioSocketChannel channel) {
        if (hasUser(channel)) {
            return channel.attr(userKey).get();
        }
        return null;
    }

    /**
     * 获取连接对应标段Id
     *
     * @param channel
     * @return
     */
    @Override
    public String getBindSectionId(NioSocketChannel channel) {
        if (hasUser(channel)) {
            return channel.attr(sectionKey).get();
        }
        return null;
    }


    /**
     * 用户退出标段在线连接
     *
     * @param channel
     */
    @Override
    public void removeConnection(NioSocketChannel channel) {
        String userId = getBindUserId(channel);
        String sectionId = getBindSectionId(channel);

        Iterator<Map.Entry<String, ChannelGroup>> iterator = SECTION_GROUPS.entrySet().iterator();
        while (iterator.hasNext()) {
            ChannelGroup channelGroup = iterator.next().getValue();
            if (channelGroup.contains(channel)) {
                channelGroup.remove(channel);
            }
            if (null == channelGroup || channelGroup.size() == 0) {
                iterator.remove();
            }
        }
        if (StringUtils.isNotEmpty(userId) && StringUtils.isNotEmpty(sectionId)) {
            bidPresentDao.exitOnlineByUserId(userId, sectionId);
        }
    }

    /**
     * 根据用户Id获取连接
     *
     * @param userId
     * @return
     */
    private NioSocketChannel getChannel(String userId) {
        Iterator<Map.Entry<String, ChannelGroup>> iterator = SECTION_GROUPS.entrySet().iterator();
        while (iterator.hasNext()) {
            ChannelGroup channelGroup = iterator.next().getValue();
            for (Channel channel : channelGroup) {
                if (userId.equalsIgnoreCase(channel.attr(userKey).get())) {
                    return (NioSocketChannel) channel;
                }
            }
        }
        return null;
    }

    /**
     * 发送纯状态码的消息
     *
     * @param toUserId
     * @param message
     */
    @Override
    public void sendMessage(String toUserId, String message) {
        NioSocketChannel channel = getChannel(toUserId);
        if (channel != null) {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("message", message);
            channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(jsonObject)));
        }
    }

    /**
     * 向指定投标人发送状态
     *
     * @param toUserId
     */
    @Override
    public void sendMessage(String toUserId, MessageEnum messageEnum) {
        NioSocketChannel channel = getChannel(toUserId);
        if (channel != null) {
            MessageObject messageObject = MessageObject.builder().
                    code(messageEnum.getCode())
                    .build();
            channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageObject)));
        }
    }

    /**
     * 向当前标段所有投标人发送消息
     *
     * @param sectionId
     */
    @Override
    public void sendMessageAll(String sectionId, MessageEnum messageEnum) {
        log.debug("标段Id: {},发送状态码: {}", sectionId, messageEnum);
        MessageObject messageObject = MessageObject.builder()
                .code(messageEnum.getCode())
                .build();
        sendMessageAll(sectionId, messageObject);
    }

    @Override
    public void sendMessageAll(String sectionId, MessageObject messageObject) {
        ChannelGroup channelGroup = SECTION_GROUPS.get(sectionId);
        if (channelGroup == null || channelGroup.size() == 0) {
            log.warn("暂时无客户端在线 sectionId:{}", sectionId);
            return;
        }
        channelGroup.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageObject)));
    }


    /**
     * 根据状态码处理消息
     *
     * @param message
     */
    @Override
    public void sendMessage(MessageObject message, ChannelHandlerContext ctx) {
        MessageEnum messageEnum = MessageEnum.valuesOf(message.getCode());
        switch (messageEnum) {
            case CHAT_SEND_MESSAGE:
                ChatMsg msg = JSONObject.parseObject(message.getData().toString(), ChatMsg.class);
                flushSectionChat(msg, ctx);
                break;
            case HEART_CONNECT:
                //心跳连接
                break;
            case REDIRECT:
                RedirectEntity redirectEntity = JSONObject.parseObject(message.getData().toString(), RedirectEntity.class);
                flushSectionToObject(redirectEntity.getSectionId(), redirectEntity);
                break;
            case DECODE:
                String sectionId = JSONObject.parseObject(message.getData().toString(), String.class);
                sendMessageAll(sectionId, MessageEnum.DECODE);
                break;
            default:

                break;
        }

    }

    /**
     * 向该标段中发送系统消息
     *
     * @param sectionId
     * @param msg
     */
    @Override
    public void flushSectionSystem(String sectionId, String msg) {
        ChatMsg chatMsg = ChatMsg.builder()
                .sectionId(sectionId)
                .msgType(ChatMsgType.NOTICE.getCode())
                .content(msg)
                .build();
        flushSectionChat(chatMsg, null);
    }

    /**
     * 向当前标段的在线人员刷新一条消息
     *
     * @param chatMsg
     */
    private void flushSectionChat(ChatMsg chatMsg, ChannelHandlerContext ctx) {
        if (ObjectUtil.isNotEmpty(ctx)) {
            InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
            String ip = inetSocketAddress.getAddress().getHostAddress();
            chatMsg.setIp(ip);
        }

        String sectionId = chatMsg.getSectionId();
        MessageObject messageObject = MessageObject.builder()
                .code(MessageEnum.CHAT_SEND_MESSAGE.getCode())
                .data(chatMsg)
                .build();
        sendMessageAll(sectionId, messageObject);
        chatMsgService.create(chatMsg);
    }


    /**
     * 当前标段在线人员发送自定义数据
     *
     * @param sectionId
     * @param object
     */
    private void flushSectionToObject(String sectionId, Object object) {
        MessageObject messageObject = MessageObject.builder()
                .code(MessageEnum.REDIRECT.getCode())
                .data(object)
                .build();

        sendMessageAll(sectionId, messageObject);
    }


    /**
     * 向指定用户发送自定义数据
     *
     * @param toUserId
     */
    @Override
    public void sendMessage(String toUserId, Object object, MessageEnum messageEnum) {
        NioSocketChannel channel = getChannel(toUserId);
        if (channel != null) {
            MessageObject messageObject = MessageObject.builder().
                    code(messageEnum.getCode())
                    .data(object)
                    .build();
            if(MessageEnum.EXTRACT_PARAM_RESULT.equals(messageEnum)){
                log.debug("参数抽取发送内容:{}",JSONObject.toJSONString(messageObject));
            }
            channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageObject)));
        }
    }


}

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

推荐阅读更多精彩内容