Reactor模型

要无障碍阅读本文,需要对NIO有一个大概的了解,起码要可以写一个NIO的Hello World。

说到NIO、Netty,Reactor模型一定是绕不开的,因为这种模式架构太经典了,但是好多人在学习的时候,往往会忽视基础的学习,一上来就是Netty,各种高大上,但是却没有静下心来好好看看Netty的基石——Reactor模型。本文就带着大家看看Reactor模型,让大家对Reactor模型有个浅显而又感性的认识。

说到Reactor,不得不提到一篇文章,文章作者是大名鼎鼎的Doug Lea,Java中的并发包就是出自他之手,下面我试着从文章中挑出一些重要的内容,结合我的理解,来说说Reactor模型,看看Doug Lea大神的脑回路是多么的与众不同。

经典的服务设计

图片.png

这是最为传统的Socket服务设计,有多个客户端连接服务端,服务端会开启很多线程,一个线程为一个客户端服务。

在绝大多数场景下,处理一个网络请求有如下几个步骤:

  1. read:从socket读取数据。
  2. decode:解码,因为网络上的数据都是以byte的形式进行传输的,要想获取真正的请求,必定需要解码。
  3. compute:计算,也就是业务处理,你想干啥就干啥。
  4. encode:编码,同理,因为网络上的数据都是以byte的形式进行传输的,也就是socket只接收byte,所以必定需要编码。

下面我们来看看传统的BIO代码:

      public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(9696);
            Socket socket = serverSocket.accept();
            new Thread(() -> {
                try {
                    byte[] byteRead = new byte[1024];
                    socket.getInputStream().read(byteRead);

                    String req = new String(byteRead, StandardCharsets.UTF_8);//encode
                    // do something

                    byte[] byteWrite = "Hello".getBytes(StandardCharsets.UTF_8);//decode
                    socket.getOutputStream().write(byteWrite);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

这段代码应该不需要解释了,应该都看得懂,不然是什么支撑着你看到这里的。。。

这种处理方式有什么弊端呢,一眼就可以知道答案:需要开启大量的线程。

所以我们需要改进它,改进一个东西,肯定需要有目标,我们的目标是什么?没有蛀牙。小伙计,你走错片场了。

我们的目标是:

  1. 随着负载的增加可以优雅降级;
  2. 能够随着资源的改进,性能可以持续提升;
  3. 同时还要满足可用性和性能指标:
    3.1 低延迟
    3.2 满足高峰需求
    3.3 可调节的服务质量

让我们想想为什么传统的Socket会有如此的弊端:

  1. 阻塞
    不管是等待客户端的连接,还是等待客户的数据,都是阻塞的,一夫当关,万夫莫开,不管你什么时候连接我,不管你什么时候给我数据,我都依然等着你。
    让我们试想下:如果accept()、read()这两个方法都是不阻塞的,是不是传统的Socket问题就解决一半了?
  2. 同步
    服务端是死死的盯着客户端,看客户端有没有连接我,有没有给我发数据。
    如果我可以喝着茶,打着农药,而你发了数据,连接了我,系统通知我一下,我再去处理,那该多好,这样传统的Socket问题又解决了一半。

所以神说要有NIO,便有了NIO。

NIO

NIO是什么意思?是什么的简写?Non-blocking,非阻塞的IO模型,这是主流的说法,但是我觉得理解成New IO——新一代的IO模型或许会更好,起码在Java领域会更好。到底如何理解,就看各位看官的了。

NIO就很好的解决了传统Socket问题:

  1. 一个线程可以监听多个Socket,不再是一夫当关,万夫莫开;
  2. 基于事件驱动:等发生了各种事件,系统可以通知我,我再去处理。

关于NIO的更多概念就不在这里阐述了,上面写的只是为了引入今天的主角:Reactor。

Reactor

在讲Rector模型之前,我先把客户端代码放出来,后面实现Reactor模型会用到:

public class Client {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress("localhost", 9090));
            new Thread(() -> {
                while (true) {
                    try {
                        InputStream inputStream = socket.getInputStream();
                        byte[] bytes = new byte[1024];
                        inputStream.read(bytes);
                        System.out.println(new String(bytes, StandardCharsets.UTF_8));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            while (true) {
                Scanner scanner = new Scanner(System.in);
                while (scanner.hasNextLine()) {
                    String s = scanner.nextLine();
                    socket.getOutputStream().write(s.getBytes());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

单Reactor单线程模型

图片.png

这是最简单的Reactor模型,可以看到有多个客户端连接到Reactor,Reactor内部有一个dispatch(分发器)。

有连接请求后,Reactor会通过dispatch把请求交给Acceptor进行处理,有IO读写事件之后,又会通过dispatch交给具体的Handler进行处理。

此时一个Reactor既然负责处理连接请求,又要负责处理读写请求,一般来说处理连接请求是很快的,但是处理具体的读写请求就要涉及到业务逻辑处理了,相对慢太多了。Reactor正在处理读写请求的时候,其他请求只能等着,只有等处理完了,才可以处理下一个请求。

画外音:弱弱的说下,菜的抠脚的我在学习NIO和Reactor的时候,有一个问题是百思不得其解:不是说NIO很强大吗,在不开启的线程的时候,一个服务端可以同时处理多个客户端吗?为什么这里又说只有处理完一个请求,才能处理下一个请求。不知道是否有人和我一个想法,希望我不是唯一一个。。。NIO在不开启线程的时候,一个服务端可以同时处理多个客户端,是指的一个服务端可以监听多个客户端的连接、读写事件,真正做业务处理还是“一夫当关,万夫莫开”的效果。

单线程Reactor模型编程简单,比较适用于每个请求都可以快速完成的场景,但是不能发挥出多核CPU的优势,在一般情况下,不会使用单Reactor单线程模型。

万年不变的道理,有很多东西只有真正实践过了,才能记住,就像Reactor模型,如果仅仅看看图,哪怕当时自认为理解的非常透彻了,相信用不了半个月也会全部忘记,所以还是要自己敲敲键盘,实现一个单Reactor单线程模型。

public class Reactor implements Runnable {
    ServerSocketChannel serverSocketChannel;
    Selector selector;

    public Reactor(int port) {
        try {
            serverSocketChannel = ServerSocketChannel.open();
            selector = Selector.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            serverSocketChannel.configureBlocking(false);
            SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            selectionKey.attach(new Acceptor(selector, serverSocketChannel));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    dispatcher(selectionKey);
                    iterator.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void dispatcher(SelectionKey selectionKey) {
        Runnable runnable = (Runnable) selectionKey.attachment();
        runnable.run();
    }
}

定义了一个Reactor类。

在构造方法中,注册了连接事件,并且在selectionKey对象附加了一个Acceptor对象,这是用来处理连接请求的类。

Reactor类实现了Runnable接口,并且实现了run方法,在run方法中,
监听各种事件,有了事件后,调用dispatcher方法,在dispatcher方法中,拿到了selectionKey附加的对象,随后调用run方法,注意此时是调用run方法,并没有开启线程,只是一个普通的调用而已。

public class Acceptor implements Runnable {
    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    public Acceptor(Selector selector, ServerSocketChannel serverSocketChannel) {
        this.selector = selector;
        this.serverSocketChannel = serverSocketChannel;
    }

    @Override
    public void run() {
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("有客户端连接上来了," + socketChannel.getRemoteAddress());
            socketChannel.configureBlocking(false);
            SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
            selectionKey.attach(new WorkHandler(socketChannel));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

目前如果有事件发生,那一定是连接事件,因为在Reactor类的构造方法中只注册了连接事件,还没有注册读写事件。

发生了连接事件后,Reactor类的dispatcher方法拿到了Acceptor附加对象,调用了Acceptor的run方法,在run方法中又注册了读事件,然后在selectionKey附加了一个WorkHandler对象。

Acceptor的run方法执行完毕后,就会继续回到Reactor类中的run方法,负责监听事件。

此时,Reactor监听了两个事件,一个是连接事件,一个是读事件。

当客户端写事件发生后,Reactor又会调用dispatcher方法,此时拿到的附加对象是WorkHandler,所以又跑到了WorkHandler中的run方法。

public class WorkHandler implements Runnable {
    private SocketChannel socketChannel;

    public WorkHandler(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        try {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            socketChannel.read(byteBuffer);
            String message = new String(byteBuffer.array(), StandardCharsets.UTF_8);
            System.out.println(socketChannel.getRemoteAddress() + "发来的消息是:" + message);
            socketChannel.write(ByteBuffer.wrap("你的消息我收到了".getBytes(StandardCharsets.UTF_8)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

WorkHandler就是真正负责处理客户端写事件的了。

public class Main {
    public static void main(String[] args) {
        Reactor reactor = new Reactor(9090);
        reactor.run();
    }
}

下面我们可以进行测试了:

有客户端连接上来了,/127.0.0.1:63912
/127.0.0.1:63912发来的消息是:你好
有客户端连接上来了,/127.0.0.1:49290
有客户端连接上来了,/127.0.0.1:49428
/127.0.0.1:49290发来的消息是:我不好
/127.0.0.1:49428发来的消息是:嘻嘻嘻嘻 

画外音:本文的目的只是为了让大家更方便、更轻松的了解Reactor模型,所以去除了很多东西,比如注册写事件、读写切换、唤醒等等,如果加上这些琐碎的东西,很可能让大家误入歧途,纠结为什么要注册写事件,不注册不是照样可以写吗,为什么要唤醒,不唤醒不是照样可以监听到新加的事件吗,而这些和Reactor模型关系不是很大。

单Reactor多线程模型

我们知道了单Reactor单线程模型有那么多缺点,就可以有针对性的去解决了。让我们再回顾下单Reactor单线程模型有什么缺点:在处理一个客户端的请求的时候,其他请求只能等着。

那么我们只要+上多线程的概念不就可以了吗?没错,这就是单Reactor多线程模型。


图片.png

可以看到,Reactor还是既要负责处理连接事件,又要负责处理客户端的写事件,不同的是,多了一个线程池的概念。

当客户端发起连接请求后,Reactor会把任务交给acceptor处理,如果客户端发起了写请求,Reactor会把任务交给线程池进行处理,这样一个服务端就可以同时为N个客户端服务了。

让我们继续敲敲键盘,实现一个单Reactor多线程模型把:

public class Reactor implements Runnable {

    ServerSocketChannel serverSocketChannel;

    Selector selector;

    public Reactor(int port) {
        try {
            serverSocketChannel = ServerSocketChannel.open();
            selector = Selector.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(9090));
            serverSocketChannel.configureBlocking(false);
            SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            selectionKey.attach(new Acceptor(serverSocketChannel, selector));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    dispatcher(selectionKey);
                    iterator.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void dispatcher(SelectionKey selectionKey) {
        Runnable runnable = (Runnable) selectionKey.attachment();
        runnable.run();
    }
}
public class Acceptor implements Runnable {
    ServerSocketChannel serverSocketChannel;

    Selector selector;

    public Acceptor(ServerSocketChannel serverSocketChannel, Selector selector) {
        this.serverSocketChannel = serverSocketChannel;
        this.selector = selector;
    }


    @Override
    public void run() {
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("有客户端连接上来了," + socketChannel.getRemoteAddress());
            socketChannel.configureBlocking(false);
            SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("acceptor thread:" + Thread.currentThread().getName());
            selectionKey.attach(new WorkHandler(socketChannel));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class WorkHandler implements Runnable {

    static ExecutorService pool = Executors.newFixedThreadPool(2);

    private SocketChannel socketChannel;

    public WorkHandler(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        try {
            System.out.println("workHandler thread:" + Thread.currentThread().getName());
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            socketChannel.read(buffer);
            pool.execute(new Process(socketChannel, buffer));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class Process implements Runnable {

    private SocketChannel socketChannel;

    private ByteBuffer byteBuffer;

    public Process(SocketChannel socketChannel, ByteBuffer byteBuffer) {
        this.byteBuffer = byteBuffer;
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        try {
            System.out.println("process thread:" + Thread.currentThread().getName());
            String message = new String(byteBuffer.array(), StandardCharsets.UTF_8);
            System.out.println(socketChannel.getRemoteAddress() + "发来的消息是:" + message);
            socketChannel.write(ByteBuffer.wrap("你的消息我收到了".getBytes(StandardCharsets.UTF_8)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Reactor reactor = new Reactor(9100);
        reactor.run();
    }
}

单Reactor单线程和单Reactor多线程代码区别不大,只是有了一个多线程的概念而已。

让我们再测试一下:

有客户端连接上来了,/127.0.0.1:55789
acceptor thread:main
有客户端连接上来了,/127.0.0.1:56681
acceptor thread:main
有客户端连接上来了,/127.0.0.1:56850
acceptor thread:main
workHandler thread:main
process thread:pool-1-thread-1
/127.0.0.1:55789发来的消息是:我是客户端1
workHandler thread:main
process thread:pool-1-thread-2
/127.0.0.1:56681发来的消息是:我是客户端2
workHandler thread:main
process thread:pool-1-thread-1
/127.0.0.1:56850发来的消息是:我是客户端3

可以很清楚的看到acceptor、workHandler还是主线程,但是到了process就开启多线程了。

单Reactor多线程模型看起来是很不错了,但是还是有缺点:一个Reactor还是既然负责连接请求,又要负责读写请求,连接请求是很快的,而且一个客户端一般只要连接一次就可以了,但是会发生很多次写请求,如果可以有多个Reactor,其中一个Reactor负责处理连接事件,多个Reactor负责处理客户端的写事件就好了,这样更符合单一职责,所以主从Reactor模型诞生了。

主从Reactor模型

图片.png

这就是主从Reactor模型了,可以看到mainReactor只负责连接请求,而subReactor
只负责处理客户端的写事件。

下面来实现一个主从Reactor模型,需要注意的是,我实现的主从Reactor模型和图片上有区别。图片上是一主一从,而我实现的是一主八从,图片上一个subReactor下面开了一个线程池,而我实现的subReactor之下没有线程池,虽然有所不同,但是核心思路是一样的。

public class Reactor implements Runnable {
    private ServerSocketChannel serverSocketChannel;

    private Selector selector;

    public Reactor(int port) {
        try {
            serverSocketChannel = ServerSocketChannel.open();
            selector = Selector.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            selectionKey.attach(new Acceptor(serverSocketChannel));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    dispatcher(selectionKey);
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void dispatcher(SelectionKey selectionKey) {
        Runnable runnable = (Runnable) selectionKey.attachment();
        runnable.run();
    }
}
public class Acceptor implements Runnable {
    private ServerSocketChannel serverSocketChannel;
    private final int CORE = 8;

    private int index;

    private SubReactor[] subReactors = new SubReactor[CORE];
    private Thread[] threads = new Thread[CORE];
    private final Selector[] selectors = new Selector[CORE];

    public Acceptor(ServerSocketChannel serverSocketChannel) {
        this.serverSocketChannel = serverSocketChannel;
        for (int i = 0; i < CORE; i++) {
            try {
                selectors[i] = Selector.open();
            } catch (IOException e) {
                e.printStackTrace();
            }
            subReactors[i] = new SubReactor(selectors[i]);
            threads[i] = new Thread(subReactors[i]);
            threads[i].start();
        }
    }

    @Override
    public void run() {
        try {
            System.out.println("acceptor thread:" + Thread.currentThread().getName());
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("有客户端连接上来了," + socketChannel.getRemoteAddress());
            socketChannel.configureBlocking(false);
            selectors[index].wakeup();
            SelectionKey selectionKey = socketChannel.register(selectors[index], SelectionKey.OP_READ);
            selectionKey.attach(new WorkHandler(socketChannel));
            if (++index == threads.length) {
                index = 0;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class SubReactor implements Runnable {
    private Selector selector;

    public SubReactor(Selector selector) {
        this.selector = selector;
    }


    @Override
    public void run() {
        while (true) {
            try {
                selector.select();
                System.out.println("selector:" + selector.toString() + "thread:" + Thread.currentThread().getName());
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    dispatcher(selectionKey);
                    iterator.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void dispatcher(SelectionKey selectionKey) {
        Runnable runnable = (Runnable) selectionKey.attachment();
        runnable.run();
    }
}
public class WorkHandler implements Runnable {
    private SocketChannel socketChannel;

    public WorkHandler(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        try {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            socketChannel.read(byteBuffer);
            String message = new String(byteBuffer.array(), StandardCharsets.UTF_8);
            System.out.println(socketChannel.getRemoteAddress() + "发来的消息是:" + message);
            socketChannel.write(ByteBuffer.wrap("你的消息我收到了".getBytes(StandardCharsets.UTF_8)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Reactor reactor = new Reactor(9090);
        reactor.run();
    }
}

最大的不同在于Acceptor类的构造方法,我开了8个线程,8个subReactor,8个selector,程序一启动,8个线程就会执行,执行的就是subReactor中定义的run方法,监听事件。在Acceptor中的run方法中,又注册了读事件,所以ubReactor中定义的run方法监听的就是读事件了。

下面我们来测试下:

acceptor thread:main
有客户端连接上来了,/127.0.0.1:57986
selector:sun.nio.ch.WindowsSelectorImpl@94f1d6thread:Thread-0
acceptor thread:main
有客户端连接上来了,/127.0.0.1:58142
selector:sun.nio.ch.WindowsSelectorImpl@1819b93thread:Thread-1
acceptor thread:main
有客户端连接上来了,/127.0.0.1:58183
selector:sun.nio.ch.WindowsSelectorImpl@1d04799thread:Thread-2
selector:sun.nio.ch.WindowsSelectorImpl@94f1d6thread:Thread-0
/127.0.0.1:57986发来的消息是:1
selector:sun.nio.ch.WindowsSelectorImpl@1819b93thread:Thread-1
/127.0.0.1:58142发来的消息是:2
selector:sun.nio.ch.WindowsSelectorImpl@1d04799thread:Thread-2
/127.0.0.1:58183发来的消息是:3
acceptor thread:main
有客户端连接上来了,/127.0.0.1:59462
selector:sun.nio.ch.WindowsSelectorImpl@11d3ebfthread:Thread-3
selector:sun.nio.ch.WindowsSelectorImpl@11d3ebfthread:Thread-3
/127.0.0.1:59462发来的消息是:1111

可以很清楚的看到,从始至终,acceptor都只有一个main线程,而负责处理客户端写请求的是不同的线程,而且还是不同的reactor、selector。

Reactor模型结构图

看完了三种Reactor模型,我们还要看下Reactor模型的结构图,图片来自在业内的公认讲Reactor模型最好的论文,没有之一。

图片.png

看起来有点复杂,我们一个个来看。

  • Synchronous Event Demultiplexer:同步事件分离器,用于监听各种事件,调用方调用监听方法的时候会被阻塞,直到有事件发生,才会返回。对于Linux来说,同步事件分离器指的就是IO多路复用模型,比如epoll,poll 等, 对于Java NIO来说, 同步事件分离器对应的组件就是selector,对应的阻塞方法就是select。
  • Handler:本质上是文件描述符,是一个抽象的概念,可以简单的理解为一个一个事件,该事件可以来自于外部,比如客户端连接事件,客户端的写事件等等,也可以是内部的事件,比如操作系统产生的定时器事件等等。
  • Event Handler:事件处理器,本质上是回调方法,当有事件发生后,框架会根据Handler调用对应的回调方法,在大多数情况下,是虚函数,需要用户自己实现接口,实现具体的方法。
  • Concrete Event Handler: 具体的事件处理器,是Event Handler的具体实现。
  • Initiation Dispatcher:初始分发器,实际上就是Reactor角色,提供了一系列方法,对Event Handler进行注册和移除;还会调用Synchronous Event Demultiplexer监听各种事件;当有事件发生后,还要调用对应的Event Handler。

本文到这里就结束了。

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

推荐阅读更多精彩内容

  • 无论是C++还是Java编写的网络框架,大多数都是基于Reactor模式进行设计和开发,Reactor模式基于事件...
    壹点零阅读 7,100评论 2 8
  • 无处不在的C/S架构 在这个充斥着云的时代,我们使用的软件可以说99%都是C/S架构的! 你发邮件用的Outloo...
    IvanEye阅读 13,814评论 7 59
  • 传统的BIO模式 上面的代码中,我们在主线程中处理客户端的连接请求,然后为每个建立的连接分配一个线程去执行。soc...
    德彪阅读 915评论 0 0
  • 本周复盘:目标:每天5点之前起床,演讲练习和技能学习,每天一篇日记记录当天笔记和自我总结,晚上减肥,早晚可控时间为...
    Anne靳阅读 169评论 0 0
  • 莱姨往事 自从收到了WL先生的第二封情书,我觉得这个故事也许真的可以作为我的小说题材,但是怎么样才能挖得到他们这桩...
    卓橙爱读书阅读 507评论 0 0