STF 系列之二---minitouch 流程源碼分析

這篇文章是之前在testerhome發(fā)表過啊奄,自認(rèn)為寫得很不錯辩蛋,現(xiàn)在搬來博客凑术。

上一篇文章分析STF如何同步設(shè)備截圖的挽霉,這一篇分析用戶在前端頁面touch設(shè)備A時防嗡,STF如何將touch動作同步在設(shè)備A?本文將從前端,服務(wù)端侠坎,手機 三個方向說明蚁趁。

touch流程

touch的整個流程如下所示:

touch-flow.png

其中triproxy/index.js 從pull到pub用虛線標(biāo)識出來原因是,中間省略了一系列過程实胸。這些過程會在服務(wù)端部分詳細說明他嫡。
前端
===
當(dāng)用戶touch設(shè)備時番官,前端如何捕捉用戶動作?捕捉到用戶動作之后钢属,是如何將數(shù)據(jù)傳輸至服務(wù)端徘熔?

捕捉用戶動作-- screen-directive.js

這里說明一下,下文中所有設(shè)備詳情頁指的是http://localhost:7100/#!/control/${serial}頁面,

設(shè)備詳情頁中看到設(shè)備截圖的那個部分淆党,如下圖所示

mscreen.png

是由screen-directive.js 文件管理的酷师。screen-directive.js中除了之前說的渲染圖片二進制文件功能,還可以捕捉用戶點擊染乌,滑動“設(shè)備屏幕”等事件窒升,并調(diào)用ControlService.js 文件中對應(yīng)的函數(shù)。代碼如下所示

 element.on('mousedown', mouseDownListener)  //當(dāng)用戶觸發(fā)mousedown事件后慕匠,調(diào)用mouseDownListener函數(shù)
 function mouseDownListener(){
    ...
    ...
    control.touchDown(nextSeq(), 0, scaled.xP, scaled.yP, pressure)
    ...
    $document.bind('mouseup', mouseUpListener)
 }

當(dāng)用戶在前端點擊設(shè)備上某個位置時饱须,screen-directive.js捕捉了touchDown動作,調(diào)用的是mouseDownListener函數(shù), mouseDownListener會通過$scope.control(即代碼中的control)調(diào)用ControlService.js的中touchDown函數(shù)台谊,向服務(wù)端發(fā)送消息--點擊該臺設(shè)備的(x,y)位置蓉媳。

至于為什么在control-panes-controller.js中定義的$scope.control, 能在screen-directive.js使用?這就屬于angular 1.x知識锅铅,感興趣的同學(xué)可以自己去了解酪呻,這里不贅述。

傳輸數(shù)據(jù)至服務(wù)端 -- control-panes-controller.js

該js文件完成$scope.device,和$scope.control的初始化

...
function getDevice(serial) {
      DeviceService.get(serial, $scope)
        .then(function(device) {
          return GroupService.invite(device)
        })
        .then(function(device) {
          $scope.device = device
          $scope.control = ControlService.create(device, device.channel)

          // TODO: Change title, flickers too much on Chrome
          // $rootScope.pageTitle = device.name

          SettingsService.set('lastUsedDevice', serial)

          return device
        })
        .catch(function() {
          $timeout(function() {
            $location.path('/')
          })
        })
    }
    ...

其中:

$scope.control = ControlService.create(device, device.channel)

我們來分析ControlService.js文件

function sendOneWay(action, data) {
      socket.emit(action, channel, data)
    }
    
    ...
    ...
    this.touchDown = function(seq, contact, x, y, pressure) {
      sendOneWay('input.touchDown', {
        seq: seq
      , contact: contact
      , x: x
      , y: y
      , pressure: pressure
      })
    }
    ...

ControlService.js中封裝了一系列操作設(shè)備的函數(shù):如: this.touchDown盐须。但這些操作設(shè)備函數(shù)都調(diào)用了sendOneWay方法玩荠。sendOneWay方法調(diào)用了socket.emit(),而這個socket來自socket-service.js文件贼邓。這個文件主要內(nèi)容如下所示

var io = require('socket.io')
var websocketUrl = AppState.config.websocketUrl || ''

var socket = io(websocketUrl, {
reconnection: false, transports: ['websocket']
})
...

這段代碼創(chuàng)建了websocket client(websocket 在之前的文章已經(jīng)描述過了阶冈,這里不再贅述)。ControlService.js文件使用這個socket向服務(wù)端發(fā)送消息塑径。

總結(jié)寫一下女坑,this.touchDown函數(shù),通過socket.emit向服務(wù)端發(fā)送'input.touchDown'消息统舀,并傳輸該設(shè)備的channel和按壓點的x,y坐標(biāo)匆骗。

需要注意的是,設(shè)備A詳情頁和設(shè)備B詳情頁,var websocketUrl = AppState.config.websocketUrl || ''不同(端口不同)誉简。

服務(wù)端

準(zhǔn)備工作

請先仔細閱讀zeromq官方文檔碉就,zeromq其他參考資料。著重看push/pull,pub/sub部分闷串。

服務(wù)端流程

服務(wù)端是如何接收前端發(fā)送的數(shù)據(jù)瓮钥,并將數(shù)據(jù)傳輸至對應(yīng)手機呢?這里以單個設(shè)備舉例。

server-info.png

服務(wù)端處理信息流轉(zhuǎn)有三個文件,websocket/index.js, triproxy/index.js, processor/index.js 骏庸。最后接收數(shù)據(jù)和minitouch通信是來自device的sub.js,touch/index.js年叮。

在執(zhí)行stf local命令時具被,發(fā)現(xiàn)STF啟動了兩個triproxy的實例app001 和 dev001

INF/util:procutil 40934 [*] Forking "/Users/sheranjun/Code/stf/lib/cli.js triproxy app001 --bind-pub tcp://127.0.0.1:7111 --bind-dealer tcp://127.0.0.1:7112 --bind-pull tcp://127.0.0.1:7113"
INF/util:procutil 40934 [*] Forking "/Users/sheranjun/Code/stf/lib/cli.js triproxy dev001 --bind-pub tcp://127.0.0.1:7114 --bind-dealer tcp://127.0.0.1:7115 --bind-pull tcp://127.0.0.1:7116"

其中app001是處理從前端UI push的消息;dev001 是處理從手機push的消息只损。這兩個實例一起完成了前端UI和手機的雙向流通一姿。當(dāng)然本文只分析從前端UI發(fā)送消息到手機接收消息單方向的流程

websocket/index.js

index.js 做了兩件事,啟動websocket服務(wù)端跃惫,等待接收websocket 客戶端消息叮叹;啟動 app001 push,發(fā)送消息至 app001 pull

  • websocket server 初始化
var socketio = require('socket.io')

var io = socketio.listen(server, {
    serveClient: false
  , transports: ['websocket']
  })
  
  ....
  server.listen(options.port)
  log.info('Listening on port %d', options.port)

設(shè)備A詳情頁和設(shè)備B詳情頁啟動的是不同的websocket server.

  • app001 push 初始化
var push = zmqutil.socket('push')
  log.info("endopoints  is " + options.endpoints)
  log.info("endopoints  is push  " + options.endpoints.push)
  Promise.map(options.endpoints.push, function(endpoint) {
    return srv.resolve(endpoint).then(function(records) {
      return srv.attempt(records, function(record) {
        log.info('Sending output to "%s"', record.url)
        push.connect(record.url)
        return Promise.resolve(true)
      })
    })
  })
  .catch(function(err) {
    log.fatal('Unable to connect to push endpoint', err)
    lifecycle.fatal()
  })

其中push.connect(record.url), record.url是app001 pull 服務(wù)的URL。是唯一的爆存,所有的設(shè)備詳情頁使用的是同一個app001 pull 服務(wù)的URL蛉顽。

  • 什么時候push呢?
io.on('connection', function(socket) {
    socket.on('input.touchDown', function(channel, data){
    log.info("touch down from websocket")
          push.send([
            channel
          , wireutil.envelope(new wire.TouchDownMessage(
              data.seq
            , data.contact
            , data.x
            , data.y
            , data.pressure
            ))
          ])
    })
}

這段語句先较,將websocket和push聯(lián)系起來携冤,先來看看前端發(fā)送的input.touchDown消息,傳輸?shù)臄?shù)據(jù)

function sendOneWay(action, data) {
      socket.emit(action, channel, data)
    }
    
    ...
    ...
    this.touchDown = function(seq, contact, x, y, pressure) {
      sendOneWay('input.touchDown', {
        seq: seq
      , contact: contact
      , x: x
      , y: y
      , pressure: pressure
      })
    }
    ...

兩端代碼聯(lián)系起來闲勺,前端發(fā)送input.touchDown消息曾棕,后端成功接收input.touchDown消息之后,調(diào)用 app001 的push端菜循,將channel和封裝了data(封裝方式這里不重要翘地,不討論),發(fā)送至app 001 的pull端。
同時可以知道癌幕,盡管設(shè)備A和設(shè)備B詳情頁使用的不同的websocket衙耕,但它們使用的push端相同。

triproxy/index.js

triproxy中的index.js勺远,主要是新建pull端臭杰,dealer端,以及pub端谚中。代碼如下所示

  function proxy(to) {
    return function() {
      to.send([].slice.call(arguments))
    }
  }
  
  // App/device output
  var pub = zmqutil.socket('pub')
  pub.bindSync(options.endpoints.pub)
  log.info('PUB socket bound on', options.endpoints.pub)

  // Coordinator input/output
  var dealer = zmqutil.socket('dealer')
  dealer.bindSync(options.endpoints.dealer)
  dealer.on('message', proxy(pub))
  log.info('DEALER socket bound on', options.endpoints.dealer)

  // App/device input
  var pull = zmqutil.socket('pull')
  pull.bindSync(options.endpoints.pull)
  pull.on('message', proxy(dealer))
  log.info('PULL socket bound on', options.endpoints.pull)

在啟動STF時渴杆,新建了兩個triproxy:app001,dev001。這里拿app001 triproxy來舉例宪塔,

  • pull.on('message', proxy(dealer)),意味著pull端接受來自websocket/index.js push端的channel/data后磁奖,將channel/data通過 dealer 服務(wù)端發(fā)送出去。
  • dealer.on('message', proxy(pub)),意思是 dealer服務(wù)端接收到channel/data后某筐,將channel/data通過pub 端發(fā)送出去比搭。

前面提到過,STF新建了兩個triproxy:app001 和dev 001南誊。所以身诺,websocket/index.js 中的channel/data通過 app001 push到了app001的pull端蜜托。app001pull端,將channel/data從app001 dealer 服務(wù)端發(fā)送出去霉赡,那channel/data又在哪里被接收了呢橄务?

processor/index.js

processor中的index.js,主要是為了app001 dealer client 和dev001 dealear client之間的消息轉(zhuǎn)發(fā)穴亏。代碼如下

 // App side
  var appDealer = zmqutil.socket('dealer')
  Promise.map(options.endpoints.appDealer, function(endpoint) {
    return srv.resolve(endpoint).then(function(records) {
      return srv.attempt(records, function(record) {
        log.info('App dealer connected to "%s"', record.url)
        appDealer.connect(record.url)
        return Promise.resolve(true)
      })
    })
  })
  .catch(function(err) {
    log.fatal('Unable to connect to app dealer endpoint', err)
    lifecycle.fatal()
  })

  // Device side
  var devDealer = zmqutil.socket('dealer')

  appDealer.on('message', function(channel, data) {
    devDealer.send([channel, data])
  })

  Promise.map(options.endpoints.devDealer, function(endpoint) {
    return srv.resolve(endpoint).then(function(records) {
      return srv.attempt(records, function(record) {
        log.info('Device dealer connected to "%s"', record.url)
        devDealer.connect(record.url)
        return Promise.resolve(true)
      })
    })
  })
  .catch(function(err) {
    log.fatal('Unable to connect to dev dealer endpoint', err)
    lifecycle.fatal()
  })

processor中的index.js 中新建了兩個dealer client:appDealer client 和 devDealer client蜂挪。分別連接triproxy中,app001 dealer 服務(wù)端和dev001 dealer 服務(wù)端嗓化。其中

appDealer.on('message', function(channel, data) {
    devDealer.send([channel, data])
  })

appDealer client 接收到來自 app001 dealer 服務(wù)端的channel/data之后棠涮,將channel/data傳遞給devDealer client,devDealer client發(fā)送至 dev001 dealer服務(wù)端刺覆。再聯(lián)系triproxy/index.js一小節(jié)的流程严肪,dev001 的dealer服務(wù)端接收到channel/data后,將channel/data通過dev001的pub端發(fā)送出去谦屑。

至此服務(wù)端channel/data流轉(zhuǎn)前半截就將完了诬垂。接下來將dev001的pub端的channel/data流向哪里了?

lib/units/device 下sub.js,solo.js,以及touch/index.js

每一個手機都會創(chuàng)造一個device對象伦仍。device對象中包含很多功能结窘。和本篇文章相關(guān)的是兩個功能,一是dev001 sub端并訂閱固定channel充蓝。這個功能由sub.js和solo.js兩個文件完成隧枫;二是創(chuàng)造一個touch對象,STF用它來向手機發(fā)送指令谓苟,這個功能由touch/index.js文件完成官脓。如下圖所示:

device-serial.png

至于device對象何時創(chuàng)造?如何被創(chuàng)建?這個流程和lib/cli.js涝焙,/lib/provider/index.js文件有關(guān)卑笨。關(guān)鍵詞是fork 以及.command('device <serial>'),有興趣自己研究。

  • sub.js

    為每個手機創(chuàng)建連接到dev001 pub端的sub端仑撞。代碼:

    var sub = zmqutil.socket('sub')
    
    return Promise.map(options.endpoints.sub, function(endpoint) {
        return srv.resolve(endpoint).then(function(records) {
          return srv.attempt(records, function(record) {
            log.info('Receiving input from "%s"', record.url)
            sub.connect(record.url)
            return Promise.resolve(true)
          })
        })
      })
      .then(function() {
        // Establish always-on channels
        [wireutil.global].forEach(function(channel) {
          log.info('Subscribing to permanent channel "%s"', channel)
          sub.subscribe(channel)
        })
      })
      .return(sub)
    

    這里的sub.connect(record.url),record.url是dev001 pub端赤兴,這一點可以通過stf啟動日志驗證。

    STF日志中dev001 pub端口地址如下所示:

    INF/util:procutil 47526 [*] Forking "/Users/****/Code/stf/lib/cli.js triproxy dev001 --bind-pub tcp://127.0.0.1:7114 --bind-dealer tcp://127.0.0.1:7115 --bind-pull tcp://127.0.0.1:7116"
    

    而STF日志中sub.js打印出的日志:

    INF/device:support:sub 48054 [63a5b447] Receiving input from "tcp://127.0.0.1:7114"
    
  • solo.js

    solo.js隧哮,為sub.js中創(chuàng)建的sub端桶良,訂閱該手機的固定頻道。代碼如下

        function makeChannelId() {
          var hash = crypto.createHash('sha1')
          hash.update(options.serial)
          return hash.digest('base64')
        }
    
        var channel = makeChannelId()
    
        log.info('Subscribing to permanent channel "%s"', channel)
        sub.subscribe(channel)
    

    solo.js通過手機的serial創(chuàng)建channel沮翔≡煞可能會有人問,怎么確定dev001 pub端發(fā)送的channel(本質(zhì)上來自前端手機A詳情頁傳輸?shù)腸hannel),和該手機在solo.js中訂閱的channel是同一個呢疲牵?這里簡單解釋一下承二。前端傳遞過來的channel,也來自于后端纲爸。前端獲取channel的代碼在device-service.js文件中亥鸠,代碼如下所示:

    deviceService.load = function(serial) {
    return $http.get('/api/v1/devices/' + serial)
      .then(function(response) {
        return response.data.device
      })
    }
    

    請求的api,最終執(zhí)行的后端函數(shù)如下所示:

        function getDeviceBySerial(req, res) {
      var serial = req.swagger.params.serial.value
      var fields = req.swagger.params.fields.value
    
      dbapi.loadDevice(serial)
        .then(function(device) {
            ...
        }
    

    其中dbapi.loadDevice是操作數(shù)據(jù)庫函數(shù)缩焦,而對device對象的一系列數(shù)據(jù)庫操作中,只有setDeviceReady函數(shù)是更新了device的channel责静,所以dbapi.loadDevice中channel數(shù)據(jù)來自于setDeviceReady函數(shù)袁滥。

        dbapi.setDeviceReady = function(serial, channel) {
      return db.run(r.table('devices').get(serial).update({
        channel: channel
      , ready: true
      , owner: null
      , reverseForwards: []
      }))
        }
    

    dbapi.setDeviceReady函數(shù)又在哪里被調(diào)用呢?是的灾螃,在processor/index.js题翻。

    devDealer.on('message', wirerouter()
    .on(wire.DeviceReadyMessage, function(channel, message, data) {
      dbapi.setDeviceReady(message.serial, message.channel)
        .then(function() {
          devDealer.send([
            message.channel
          , wireutil.envelope(new wire.ProbeMessage())
          ])
    
          appDealer.send([channel, data])
        })
    })
    

    而processor/index.js接收的channel/message/data,來自solo.js腰鬼。至于solo.js的push嵌赠,如何將channel/message/data傳輸至processor/index.js的devDealer client中,按照上一小節(jié)的triproxy.js的流程圖熄赡,對照STF日志姜挺,自己分析。這里不再贅述彼硫。

    return {
      channel: channel
    , poke: function() {
        push.send([
          wireutil.global
        , wireutil.envelope(new wire.DeviceReadyMessage(
            options.serial
          , channel
          ))
        ])
      }
    }
    

    所以炊豪,前端的channel,也是來自于solo.js,而且channel和serial有一一對應(yīng)的關(guān)系。這就解決了來自前端手機A詳情頁的數(shù)據(jù)一定會發(fā)送到后端手機A的對應(yīng)的channel上拧篮。

  • touch/index.js

    touch/index.js中做了兩件事:一是啟動手機的minitouch服務(wù)词渤,而是訂閱dev001 pub端的數(shù)據(jù),并將固定頻道channel數(shù)據(jù)傳輸至手機串绩。每一個手機都會有一個touch/index.js

    • 啟動手機minitouch服務(wù)

      • 啟動手機上的minitouch服務(wù)
      TouchConsumer.prototype._startService = function() {
            log.info('Launching screen service')
            return minitouch.run()
              .timeout(10000)
          }
      
      • 將手機端啟動的minitouch tcp 服務(wù)端端口映射到pc端缺虐。連接到該pc端口,返回socket礁凡。該功能由adb.openLocal函數(shù)完成高氮,代碼如下所示
      TouchConsumer.prototype._connectService = function() {
            function tryConnect(times, delay) {
              return adb.openLocal(options.serial, 'localabstract:minitouch')
                .timeout(10000)
                .then(function(out) {
                  return out
                })
                .catch(function(err) {
                  if (/closed/.test(err.message) && times > 1) {
                    return Promise.delay(delay)
                      .then(function() {
                        return tryConnect(times - 1, delay * 2)
                      })
                  }
                  return Promise.reject(err)
                })
            }
            log.info('Connecting to minitouch service')
            // SH-03G can be very slow to start sometimes. Make sure we try long
            // enough.
            return tryConnect(7, 100)
          }
      

    有興趣的,可以研究一下adb.openLocal函數(shù)顷牌,這里不再詳細討論該函數(shù)流程

    • 訂閱dev001 pub端固定channel

      touch/index.js通過引用sub.js和solo.js,使用訂閱固定頻道的dev001 sub纫溃,等待來自dev001 pub端數(shù)據(jù),代碼如下所示:

      router
          .on(wire.TouchDownMessage, function(channel, message) {
          log.info("touch down from toucb")
          queue.push(message.seq, function() {
            touchConsumer.touchDown(message)
          })
        })
      

      而router來自router.js中韧掩,router.js引用了sub.js

      module.exports = syrup.serial()
          .dependency(require('./sub'))
          .define(function(options, sub, channels) {
              var log = logger.createLogger('device:support:router')
              var router = wirerouter()
           
              sub.on('message', router.handler())
                      
              return router
            })
      

      其中dev001 push端在channelA上發(fā)送數(shù)據(jù)后紊浩,訂閱了channelA的dev001 sub端接收該數(shù)據(jù)。并調(diào)用touchConsumer.touchDown(message)。而touchDown函數(shù)的相關(guān)代碼如下所示:

          TouchConsumer.prototype.touchDown = function(point) {
        log.info("touch down from touch index")
        this._queueWrite(function() {
          return this._write(util.format(
            'd %s %s %s %s\n'
          , point.contact
          , Math.floor(this.touchConfig.origin.x(point) * this.banner.maxX)
          , Math.floor(this.touchConfig.origin.y(point) * this.banner.maxY)
          , Math.floor((point.pressure || 0.5) * this.banner.maxPressure)
          ))
        })
      }
      
      
      TouchConsumer.prototype._write = function(chunk) {
        this.socket.stream.write(chunk)
      }
      
      

      touchDown函數(shù)中的this._write調(diào)用的this.socket來自this._connectService()返回值坊谁。this.socket代表著連接手機minitouch tcp服務(wù)端的tcp 客戶端费彼。touchDown函數(shù)接收touchDown消息后,向手機發(fā)送以'd'開頭的字符串命令口芍。

    到此箍铲,服務(wù)端消息的流程已經(jīng)全部解析完成。再看看最開始畫的流程圖鬓椭,是不是清晰很多呢颠猴?

手機

手機上主要是啟動了minitouch的tcp服務(wù),接收STF服務(wù)端操作手機指令小染。并根據(jù)指令翘瓮,操作手機它的代碼如下所示

  • start_server函數(shù),啟動tcp服務(wù)

    static int start_server(char* sockname)
    {
      int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    
      if (fd < 0)
      {
        perror("creating socket");
        return fd;
      }
    
      struct sockaddr_un addr;
      memset(&addr, 0, sizeof(addr));
      addr.sun_family = AF_UNIX;
      strncpy(&addr.sun_path[1], sockname, strlen(sockname));
    
      if (bind(fd, (struct sockaddr*) &addr,
        sizeof(sa_family_t) + strlen(sockname) + 1) < 0)
      {
        perror("binding socket");
        close(fd);
        return -1;
      }
    
      listen(fd, 1);
    
      return fd;
    }
    

    fd既是創(chuàng)造TCP 服務(wù)端

  • 等到STF 服務(wù)端發(fā)送的指令裤翩,根據(jù)指令操作設(shè)備资盅,這里以touchDown指令,舉例說明

     int client_fd = accept(server_fd, (struct sockaddr *) &client_addr,
      &client_addr_length);
      
      .....
      while (io_length < sizeof(io_buffer) &&
        read(client_fd, &io_buffer[io_length], 1) == 1)
      {
        if (io_buffer[io_length++] == '\n')
        {
          break;
        }
      }
      
      ...
      switch (io_buffer[0])
      {
        case 'c': // COMMIT
          commit(&state);
          break;
        case 'r': // RESET
          touch_panic_reset_all(&state);
          break;
        case 'd': // TOUCH DOWN
          contact = strtol(cursor, &cursor, 10);
          x = strtol(cursor, &cursor, 10);
          y = strtol(cursor, &cursor, 10);
          pressure = strtol(cursor, &cursor, 10);
          touch_down(&state, contact, x, y, pressure);
          break;
          .....
       }
    

    其中client_fd接收來自STF服務(wù)端的指令踊赠,io_buffer存儲STF服務(wù)端消息呵扛,并根據(jù)第一字符判斷操作類型。若第一個字符為d筐带,調(diào)用touch_down函數(shù)今穿。

后記

touch動作源碼分析,整理出來算是一個大的工程了伦籍。涉及的核心知識點是zeromq荣赶,這里只分析了前端操作是如何同步到設(shè)備,逆向流程并沒有涉及鸽斟,有興趣的可以自己了解拔创。

由于項目需要改造STF,閱讀了STF的源碼富蓄,但項目改造STF重點在STF的前端和STF認(rèn)證剩燥。這兩塊沒有涉及多少STF整體架構(gòu)設(shè)計,更多的是angular 1.x框架的知識立倍。對這兩塊有興趣的灭红,歡迎討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末口注,一起剝皮案震驚了整個濱河市变擒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寝志,老刑警劉巖娇斑,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件策添,死亡現(xiàn)場離奇詭異,居然都是意外死亡毫缆,警方通過查閱死者的電腦和手機唯竹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苦丁,“玉大人浸颓,你說我怎么就攤上這事⊥” “怎么了产上?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蛾狗。 經(jīng)常有香客問我晋涣,道長,這世上最難降的妖魔是什么淘太? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任姻僧,我火速辦了婚禮规丽,結(jié)果婚禮上蒲牧,老公的妹妹穿的比我還像新娘。我一直安慰自己赌莺,他們只是感情好冰抢,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著艘狭,像睡著了一般挎扰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巢音,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天遵倦,我揣著相機與錄音,去河邊找鬼官撼。 笑死梧躺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傲绣。 我是一名探鬼主播掠哥,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秃诵!你這毒婦竟也來了续搀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤菠净,失蹤者是張志新(化名)和其女友劉穎禁舷,沒想到半個月后彪杉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡榛了,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年在讶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霜大。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡构哺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出战坤,到底是詐尸還是另有隱情曙强,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布途茫,位于F島的核電站碟嘴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏囊卜。R本人自食惡果不足惜娜扇,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栅组。 院中可真熱鬧雀瓢,春花似錦、人聲如沸玉掸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽司浪。三九已至泊业,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間啊易,已是汗流浹背吁伺。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留租谈,地道東北人篮奄。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像垦垂,于是被迫代替她去往敵國和親宦搬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容

  • "use strict";function _classCallCheck(e,t){if(!(e instanc...
    久些閱讀 2,029評論 0 2
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理劫拗,服務(wù)發(fā)現(xiàn)间校,斷路器,智...
    卡卡羅2017閱讀 134,651評論 18 139
  • 從三月份找實習(xí)到現(xiàn)在页慷,面了一些公司憔足,掛了不少胁附,但最終還是拿到小米、百度滓彰、阿里控妻、京東、新浪揭绑、CVTE弓候、樂視家的研發(fā)崗...
    時芥藍閱讀 42,239評論 11 349
  • 人世間的所有事情菇存,都可以用一份耕耘,一份收獲的定律邦蜜。付出多少依鸥,得到多少,唯獨愛不是悼沈,若是遇到了脾氣贱迟,品行不同的人,...
    楠木姑娘閱讀 385評論 0 1
  • 看韓劇《她很漂亮》的時候絮供,室友說“臉是改變不了了衣吠,咱努力做個好人吧!”杯缺。沒想到蒸播,有一天睡榆,我也被打著“善良人好”的大...
    L勤勞閱讀 337評論 2 2