PHP Thrift 简单应用

一、安装篇

博主注:截至2017-10-10,官网上thrift最新版0.10.0一直无法成功编译。所以,请选择0.9.3版本,避免走各种弯路:

wget http://apache.fayea.com/thrift/0.9.3/thrift-0.9.3.tar.gz

1、安装开发平台工具

yum -y groupinstall "Development Tools"

2、安装autoconf

wget http://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz
tar xvf autoconf-2.69.tar.gz
cd autoconf-2.69
./configure --prefix=/usr/local
make
make install

3、安装automake

wget http://ftp.gnu.org/gnu/automake/automake-1.14.tar.gz
tar xvf automake-1.14.tar.gz
cd automake-1.14
./configure --prefix=/usr/local
make
make install

4、安装bison

wget http://ftp.gnu.org/gnu/bison/bison-2.5.1.tar.gz
tar xvf bison-2.5.1.tar.gz
cd bison-2.5.1
./configure --prefix=/usr/local
make
make install

5、安装C++库依赖包

yum -y install libevent-devel zlib-devel openssl-devel

6、安装boost

wget http://sourceforge.net/projects/boost/files/boost/1.53.0/boost_1_53_0.tar.gz
tar xvf boost_1_53_0.tar.gz
cd boost_1_53_0
./bootstrap.sh
./b2

7、安装其它依赖包

yum install gcc gcc-c++ bzip2 bzip2-devel bzip2-libs python-devel -y

8、安装thrift(基本的编译套路)

cd thrift
./bootstrap.sh
./configure --with-lua=no --prefix=/alidata/server/thrift
make
make install

二、Hello World篇

使用套路总结:

1、用IDL语法,定义自己的数据结构与服务接口:

https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/tutorial.thrift
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/shared.thrift

将这两个文件下载到本地,分别保存为tutorial.thrift和shared.thrift,并将这两个文件传到与thrift同目录下。

附:
IDL详细教程:
http://thrift.apache.org/docs/idl
IDL示例(够用了):
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/tutorial.thrift
https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob_plain;f=tutorial/shared.thrift

2、用thrift将上面的thrift的IDL文件,转成对应语言的类:

thrift -gen php:server tutorial.thrift

上面这一句是对应服务端的转换,客户端的转换不用:server;但是服务端方式生成的类文件可以供客户端使用。

3、转换完成后,在本目录下会得到一个gen-php目录,里面有三个文件tutorial.php、shared.php和Types.php。

tutorial.php里面有一个CalculatorIf接口,这个类是我们要实现的服务端业务接口。

shared.php被引用在tutorial.php文件中。

Types.php里面是自定义数据类型。

4、实现server端,即需要创建一个Handler类实再业务接口CalculatorIf

注意:这里的lib文件在thrift源码包lib里,我们移到自己的php需要的路径中。代码中require_once部分、$GEN_DIR部分、Thrift命名空间注册部分的路径需要根据实际情况进行更改

<?php

namespace tutorial\php;

error_reporting(E_ALL);

require_once __DIR__.'/../../lib/php/lib/Thrift/ClassLoader/ThriftClassLoader.php';

use Thrift\ClassLoader\ThriftClassLoader;

$GEN_DIR = realpath(dirname(__FILE__).'/..').'/gen-php';

$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__ . '/../../lib/php/lib');
$loader->registerDefinition('shared', $GEN_DIR);
$loader->registerDefinition('tutorial', $GEN_DIR);
$loader->register();


if (php_sapi_name() == 'cli') {
  ini_set("display_errors", "stderr");
}

use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TPhpStream;
use Thrift\Transport\TBufferedTransport;

class CalculatorHandler implements \tutorial\CalculatorIf {
  protected $log = array();

  public function ping() {
    error_log("ping()");
  }

  public function add($num1, $num2) {
    error_log("add({$num1}, {$num2})");
    return $num1 + $num2;
  }

  public function calculate($logid, \tutorial\Work $w) {
    error_log("calculate({$logid}, {{$w->op}, {$w->num1}, {$w->num2}})");
    switch ($w->op) {
      case \tutorial\Operation::ADD:
        $val = $w->num1 + $w->num2;
        break;
      case \tutorial\Operation::SUBTRACT:
        $val = $w->num1 - $w->num2;
        break;
      case \tutorial\Operation::MULTIPLY:
        $val = $w->num1 * $w->num2;
        break;
      case \tutorial\Operation::DIVIDE:
        if ($w->num2 == 0) {
          $io = new \tutorial\InvalidOperation();
          $io->whatOp = $w->op;
          $io->why = "Cannot divide by 0";
          throw $io;
        }
        $val = $w->num1 / $w->num2;
        break;
      default:
        $io = new \tutorial\InvalidOperation();
        $io->whatOp = $w->op;
        $io->why = "Invalid Operation";
        throw $io;
    }

    $log = new \shared\SharedStruct();
    $log->key = $logid;
    $log->value = (string)$val;
    $this->log[$logid] = $log;

    return $val;
  }

  public function getStruct($key) {
    error_log("getStruct({$key})");
    // This actually doesn't work because the PHP interpreter is
    // restarted for every request.
    //return $this->log[$key];
    return new \shared\SharedStruct(array("key" => $key, "value" => "PHP is stateless!"));
  }

  public function zip() {
    error_log("zip()");
  }

};

header('Content-Type', 'application/x-thrift');
if (php_sapi_name() == 'cli') {
  echo "\r\n";
}

$handler = new CalculatorHandler();
$processor = new \tutorial\CalculatorProcessor($handler);

$transport = new TBufferedTransport(new TPhpStream(TPhpStream::MODE_R | TPhpStream::MODE_W));
$protocol = new TBinaryProtocol($transport, true, true);

$transport->open();
$processor->process($protocol, $protocol);
$transport->close();

5、实现client端:

同样需要注意:这里的lib文件在thrift源码包lib里,我们移到自己的php需要的路径中。代码中require_once部分、$GEN_DIR部分、Thrift命名空间注册部分的路径需要根据实际情况进行更改

<?php

namespace tutorial\php;

error_reporting(E_ALL);

require_once __DIR__.'/../../lib/php/lib/Thrift/ClassLoader/ThriftClassLoader.php';

use Thrift\ClassLoader\ThriftClassLoader;

$GEN_DIR = realpath(dirname(__FILE__).'/..').'/gen-php';

$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__ . '/../../lib/php/lib');
$loader->registerDefinition('shared', $GEN_DIR);
$loader->registerDefinition('tutorial', $GEN_DIR);
$loader->register();


use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\THttpClient;
use Thrift\Transport\TBufferedTransport;
use Thrift\Exception\TException;

try {
  if (array_search('--http', $argv)) {
    $socket = new THttpClient('localhost', 8080, '/php/PhpServer.php');
  } else {
    $socket = new TSocket('localhost', 9090);
  }
  $transport = new TBufferedTransport($socket, 1024, 1024);
  $protocol = new TBinaryProtocol($transport);
  $client = new \tutorial\CalculatorClient($protocol);

  $transport->open();

  $client->ping();
  print "ping()\n";

  $sum = $client->add(1,1);
  print "1+1=$sum\n";

  $work = new \tutorial\Work();

  $work->op = \tutorial\Operation::DIVIDE;
  $work->num1 = 1;
  $work->num2 = 0;

  try {
    $client->calculate(1, $work);
    print "Whoa! We can divide by zero?\n";
  } catch (\tutorial\InvalidOperation $io) {
    print "InvalidOperation: $io->why\n";
  }

  $work->op = \tutorial\Operation::SUBTRACT;
  $work->num1 = 15;
  $work->num2 = 10;
  $diff = $client->calculate(1, $work);
  print "15-10=$diff\n";

  $log = $client->getStruct(1);
  print "Log: $log->value\n";

  $transport->close();

} catch (TException $tx) {
  print 'TException: '.$tx->getMessage()."\n";
}

?>

6、因为php的server端代码没有进行端口侦听的网络服务功能,所以要依靠python来提供这个功能。我们把下面的脚本命名为s.py,需要与server.php放在同一个目录下(/alidata/www/thrift)

#!/bin/py
#coding=utf-8

import os
import BaseHTTPServer
import CGIHTTPServer
# chdir(2) into the tutorial directory.
os.chdir('/alidata/www/thrift')
# 指定目录 ,如果目录错误 请求会失败
class Handler(CGIHTTPServer.CGIHTTPRequestHandler):
    cgi_directories  = ['/']

BaseHTTPServer.HTTPServer(('', 8080), Handler).serve_forever()

7、到现在为止,hello world级别的工程已经完成。

运行服务端s.py

运行客户端php client.php --http

便可以看到响应的实际效果

三、生产使用篇

hello world级别的工程,只有一个进程进行侦听和服务,在多核CPU的时代,只有一个进程不但无法充分利用CPU的性能,也无法为用户提供最高效的服务。

方案一:用php自己实现网络服务方案(这是一个牛人写的多进程服务demo,完全可以参照) http://blog.csdn.net/flynetcn/article/details/47837975

<?php  
/** 
 * 多进程形式的server. 
 * @package thrift.server 
 * @author flynetcn 
 */  
namespace Thrift\Server;  
  
use Thrift\Server\TServer;  
use Thrift\Transport\TTransport;  
use Thrift\Exception\TException;  
use Thrift\Exception\TTransportException;  
  
class TMultiProcessServer extends TServer  
{  
    /** 
     * 捕获的信号编号 
     */  
    static $catchQuitSignal = 0;  
  
    /** 
     * worker进程数量 
     */  
    private $workProcessNum = 4;  
  
    /** 
     * 每个worker进程处理的最大请求数 
     */  
    private $maxWorkRequestNum = 2000;  
  
    /** 
     * 当前worker进程已处理的请求数 
     */  
    private $currentWorkRequestNum = 0;  
      
    /** 
     * 当前连接调用次数 
     */  
    private $currentConnectCallNum = 0;  
  
    /** 
     * 发送超时 
     */  
    private $sendTimeoutSec = 1;  
  
    /** 
     * 接收超时 
     */  
    private $recvTimeoutSec = 1;  
  
    /** 
     * 当前进程pid 
     */  
    private $pid = 0;  
  
    /** 
     * Flag for the main serving loop 
     */  
    private $stop_ = false;  
  
    /** 
     * List of children. 
     */  
    protected $childrens = array();  
  
    /** 
     * 服务器日志文件 
     */  
    protected static $logFiles;  
    protected static $pidFile;  
  
    /** 
     * run 
     */   
    public function serve($daemon=false, array $config=array())  
    {  
        if (isset($config['workProcessNum'])) {  
            $this->workProcessNum = intval($config['workProcessNum']);  
        }  
        if ($this->workProcessNum < 1) {  
            self::log(1, "child workProcessNum can not be less than 1");  
            throw new TException('child workProcessNum can not be less than 1');  
        }  
        if (isset($config['maxWorkRequestNum'])) {  
            $this->maxWorkRequestNum = intval($config['maxWorkRequestNum']);  
        }  
        if ($this->maxWorkRequestNum < 1) {  
            self::log(1, "child maxWorkRequestNum can not be less than 1");  
            throw new TException('child maxWorkRequestNum can not be less than 1');  
        }  
        if (isset($config['sendTimeoutSec'])) {  
            $this->sendTimeoutSec = intval($config['sendTimeoutSec']);  
        }  
        if (isset($config['recvTimeoutSec'])) {  
            $this->recvTimeoutSec = intval($config['recvTimeoutSec']);  
        }  
        if ($daemon) {  
            $this->daemon();  
            $this->registerSignalHandler();  
            self::$logFiles = isset($config['logFiles']) && is_array($config['logFiles']) ? $config['logFiles'] : array();  
            self::$pidFile = isset($config['pidFile']) ? $config['pidFile'] : '';  
            declare(ticks=3);  
        }  
        $this->pid = posix_getpid();  
        self::createPidFile($this->pid);  
        self::log(0, "manage process({$this->pid}) has started");  
        $this->transport_->listen();  
        while (!$this->stop_) {  
            while ($this->workProcessNum > 0) {  
                try {  
                    $pid = pcntl_fork();  
                    if ($pid > 0) {  
                        $this->handleParent($pid, $this->workProcessNum);  
                    } else if ($pid === 0) {  
                        $this->pid = posix_getpid();  
                        $this->handleChild($this->workProcessNum);  
                    } else {  
                        self::log(1, "Failed to fork");  
                        throw new TException('Failed to fork');  
                    }  
                    $this->workProcessNum--;  
                } catch (Exception $e) {  
                }  
            }  
            $this->collectChildren();  
            sleep(2);  
            if (\Thrift\Server\TMultiProcessServer::$catchQuitSignal) {  
                $this->stop();  
            }  
        }  
    }  
      
    public function getCurrentWorkRequestNum()  
    {  
        return $this->currentWorkRequestNum;  
    }  
      
    public function getCurrentConnectCallNum()  
    {  
        return $this->currentConnectCallNum;  
    }  
  
    /** 
     * Code run by the parent 
     * 
     * @param int $pid 
     * @param int $num 进程编号 
     * @return void 
     */  
    private function handleParent($pid, $num)  
    {  
        $this->childrens[$pid] = $num;  
    }  
  
    /** 
     * Code run by the child. 
     * 
     * @param int $num 进程编号 
     * @return void 
     */  
    private function handleChild($num)  
    {  
        self::log(0, "child process($this->pid) has started");  
        $this->childrens = array();  
        while (!$this->stop_) {  
            try {  
                $transport = $this->transport_->accept();  
                if ($transport != null) {  
                    $transport->setSendTimeout($this->sendTimeoutSec * 1000);  
                    $transport->setRecvTimeout($this->recvTimeoutSec * 1000);  
                    $this->currentWorkRequestNum++;  
                    $this->currentConnectCallNum = 0;  
                    $inputTransport = $this->inputTransportFactory_->getTransport($transport);  
                    $outputTransport = $this->outputTransportFactory_->getTransport($transport);  
                    $inputProtocol = $this->inputProtocolFactory_->getProtocol($inputTransport);  
                    $outputProtocol = $this->outputProtocolFactory_->getProtocol($outputTransport);  
                    while ($this->processor_->process($inputProtocol, $outputProtocol)) {  
                        $this->currentConnectCallNum++;  
                    }  
                    @$transport->close();  
                }  
            } catch (TTransportException $e) {  
            } catch (Exception $e) {  
                self::log(1, $e->getMessage().'('.$e->getCode().')');  
            }  
            if (\Thrift\Server\TMultiProcessServer::$catchQuitSignal) {  
                $this->stop();  
            }  
            if ($this->currentWorkRequestNum >= $this->maxWorkRequestNum) {  
                self::log(0, "child process($this->pid) has processe {$this->currentWorkRequestNum} requests will be exit");  
                $this->stop();  
                break;  
            }  
        }  
        exit(0);  
    }  
  
    /** 
     * Collects any children we may have 
     * 
     * @return void 
     */  
    private function collectChildren()  
    {  
        foreach ($this->childrens as $pid => $num) {  
            if (pcntl_waitpid($pid, $status, WNOHANG) > 0) {  
                unset($this->childrens[$pid]);  
                $this->workProcessNum++;  
            }  
        }  
    }  
  
    /** 
     * @return void 
     */  
    public function stop()  
    {  
        $this->transport_->close();  
        $this->stop_ = true;  
        foreach ($this->childrens as $pid => $num) {  
            if (!posix_kill($pid, SIGTERM)) {  
            }  
        }  
    }  
  
    /** 
     * 附加信号处理 
     */  
    public static function sig_handler($signo)  
    {  
        switch ($signo) {  
            case SIGTERM:  
            case SIGHUP:  
            case SIGQUIT:  
            case SIGTSTP:  
                $pid = posix_getpid();  
                self::log(0, "process($pid) catch signo: $signo");  
                \Thrift\Server\TMultiProcessServer::$catchQuitSignal = $signo;  
                break;  
            default:  
        }  
    }  
  
    /** 
     * 附加信号处理 
     */  
    private function registerSignalHandler()  
    {  
        pcntl_signal(SIGTERM, '\Thrift\Server\TMultiProcessServer::sig_handler');  
        pcntl_signal(SIGHUP, '\Thrift\Server\TMultiProcessServer::sig_handler');  
        pcntl_signal(SIGQUIT, '\Thrift\Server\TMultiProcessServer::sig_handler');  
        pcntl_signal(SIGTSTP, '\Thrift\Server\TMultiProcessServer::sig_handler');  
        declare(ticks=3);  
    }  
  
    /** 
     * 附加守护进程方式 
     */  
    private function daemon()  
    {  
        if (!function_exists('posix_setsid')) {  
            return;  
        }  
        if (($pid1 = pcntl_fork()) != 0) {  
            exit;  
        }  
        posix_setsid();  
        if (($pid2 = pcntl_fork()) != 0) {  
            exit;  
        }  
    }  
  
    public static function log($type, $msg)  
    {  
        static $fds;  
        $msg = date('Y-m-d H:i:s')." $type {$msg}\n";  
        if (isset(self::$logFiles[$type]) && self::$logFiles[$type]) {  
            if (file_exists(self::$logFiles[$type])) {  
                if (empty($fds[$type])) {  
                    $fds[$type] = fopen(self::$logFiles[$type], 'a');  
                }  
                if (!$fds[$type]) {  
                    $fds[$type] = fopen('php://stdout', 'w');  
                    fwrite($fds[$type], date('Y-m-d H:i:s')." WARNING fopen(".self::$logFiles[$type].") failed\n");  
                }  
            } else {  
                if (!is_dir(dirname(self::$logFiles[$type])) && !mkdir(dirname(self::$logFiles[$type]), 0755, true)) {  
                    $fds[$type] = fopen('php://stdout', 'w');  
                    fwrite($fds[$type], date('Y-m-d H:i:s')." WARNING mkdir(".self::$logFiles[$type].") failed\n");  
                } elseif (!($fds[$type] = fopen(self::$logFiles[$type], 'a'))) {  
                    $fds[$type] = fopen('php://stdout', 'w');  
                    fwrite($fds[$type], date('Y-m-d H:i:s')." WARNING fopen(".self::$logFiles[$type].") failed\n");  
                }  
            }  
        } else {  
            $fds[$type] = fopen('php://stdout', 'w');  
        }  
        $ret = fwrite($fds[$type], $msg);  
        if (!$ret && self::$logFiles[$type]) {  
            fclose($fds[$type]);  
            $fds[$type] = fopen(self::$logFiles[$type], 'a');  
            $ret = fwrite($fds[$type], $msg);  
        }  
        return true;  
    }  
  
    public static function createPidFile($pid=0)  
    {  
        if (!$pid) {  
            $pid = posix_getpid();  
        }  
        if (file_exists(self::$pidFile)) {  
            $fd = fopen(self::$pidFile, 'w');  
            if (!$fd) {  
                self::log(1, "fopen(".self::$pidFile.") failed");  
                return false;  
            }  
        } else {  
            if (!is_dir(dirname(self::$pidFile)) && !mkdir(dirname(self::$pidFile), 0755, true)) {  
                self::log(1, "mkdir(".self::$pidFile.") failed");  
                return false;  
            } elseif (!($fd = fopen(self::$pidFile, 'w'))) {  
                self::log(1, "fopen(".self::$pidFile.") failed");  
                return false;  
            }  
        }  
        if (!fwrite($fd, "$pid")) {  
            self::log(1, "fwrite(".self::$pidFile.",$pid) failed");  
            return false;  
        }  
        fclose($fd);  
        return  true;  
    }  
}

方案二:方案一固然不错,但是因为没有实践的检验,所以用起来还是有一点担心的。所以,个人建议采用workman的thrift方案,请参考:http://www.workerman.net/workerman-thrift

1、因为要使用多进程,所以需要先安装pcntl扩展。(安装过程很简单,不是重点,不详细描述过程)

2、workman-thrift下载解压后,目录中Applications/ThriftRpc/Services存放的自己的服务端服务,即业务接口If、自定义数据类型Type和业务接口的实现。

3、根目录下的start.php加载并运行了Applications下的各个服务的start.php文件

4、解压后的目录中Applications/ThriftRpc是一个HelloWorld服务的demo。

5、现在,看Applications/ThriftRpc/start.php中的关键代码:

$worker = new ThriftWorker('tcp://0.0.0.0:9090');
$worker->count = 16;
$worker->class = 'HelloWorld';

这里,设置了侦听的IP与端口、设置了服务进程数量与服务的类

6、查看Applications/ThriftPrc/ThriftWorker.php中的关键代码:

// 载入该服务下的所有文件
foreach(glob(THRIFT_ROOT . '/Services/'.$this->class.'/*.php') as $php_file)
{
    require_once $php_file;
}
// 检查类是否存在
$processor_class_name = "\\Services\\".$this->class."\\".$this->class.'Processor';
if(!class_exists($processor_class_name))
{
    ThriftWorker::log("Class $processor_class_name not found" );
    return;
}
        
// 检查类是否存在
$handler_class_name ="\\Services\\".$this->class."\\".$this->class.'Handler';
if(!class_exists($handler_class_name))
{
    ThriftWorker::log("Class $handler_class_name not found" );
    return;
}

根据第5步指定的class,在ThriftWorker中会载入对应路径下的所有php文件进行检查,并检查对应的Processor和Handler类是否存在。最终创建Handler和Processor

7、运行根目录下的php start.php start,可以看到

恭喜,服务端运行成功。

8、关于客户端

复制 http://www.workerman.net/workerman-thrift 上的客户端代码,即可正常运行。

<?php  // 引入客户端文件
require_once __DIR__.'/Applications/ThriftRpc/Clients/ThriftClient.php';
use ThriftClient\ThriftClient;

// 传入配置,一般在某统一入口文件中调用一次该配置接口即可
thriftClient::config(
    array(
        'HelloWorld' => array(
            'addresses' => array(
                '127.0.0.1:9090',
                '127.0.0.2:9191',
            ),
            'thrift_protocol' => 'TBinaryProtocol',//不配置默认是TBinaryProtocol,对应服务端HelloWorld.conf配置中的thrift_protocol
            'thrift_transport' => 'TBufferedTransport',//不配置默认是TBufferedTransport,对应服务端HelloWorld.conf配置中的thrift_transport
        ),
        'DInfoCenter' => array(
            'addresses' => array(
                '127.0.0.1:9090',
            ),
            'thrift_protocol' => 'TBinaryProtocol',//不配置默认是TBinaryProtocol,对应服务端HelloWorld.conf配置中的thrift_protocol
            'thrift_transport' => 'TBufferedTransport',//不配置默认是TBufferedTransport,对应服务端HelloWorld.conf配置中的thrift_transport
        ),
    )
);
// =========  以上在WEB入口文件中调用一次即可  ===========


// =========  以下是开发过程中的调用示例  ==========

// 初始化一个HelloWorld的实例
$client = ThriftClient::instance('HelloWorld');

// --------同步调用实例----------
var_export($client->sayHello('JIM');
//var_export($client->recv_request("req",strval(time())));

// --------异步调用示例-----------
/*
// 异步调用 之 发送请求给服务端(注意:异步发送请求格式统一为 asend_XXX($arg),既在原有方法名前面增加'asend_'前缀)
$client->asend_sayHello("JERRY");
$client->asend_sayHello("KID");

// 这里是其它业务逻辑
sleep(1);

// 异步调用 之 接收服务端的回应(注意:异步接收请求格式统一为 arecv_XXX($arg),既在原有方法名前面增加'arecv_'前缀)
var_export($client->arecv_sayHello("KID"));
var_export($client->arecv_sayHello("JERRY"));
*/

9、截止这里,已经可以成功运行workman-thrift的hello world工程

10、关于定义自己的服务:

到这里根据了解,我们可以很容易自己的的服务。比如,我们想定义自己的服务,DInfoCenter,这个过程中有三个一定需要注意的地方:

第一点:目录名称需要改成DInfoCenter

第二点:三个文件的namespace声明(第一行),一定要改为:namespace Services\DInfoCenter;

第三点:默认生成的DInfoCenter.php中,下面很多类以及函数的调用,都带有命名空间(\DInfoCenter),需要把这一段删掉

到这里,就大致完成了,最后将start.php中的class赋值成DInfoCenter,就全部完成了。

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

推荐阅读更多精彩内容