vue+node.js手把手教你搭建一个直播平台(一)

上一期,帅气的小羽给老铁们简单介绍了项目的功能以及需要用到的一些环境和工具,现在就让我们荡起双桨,撸起袖子,准备开始敲代码啦!!!

先甩锅,小羽主要是搞前端开发的,所以这期张主要讲后端内容,可能讲的不太好,后端大佬求放过。有错误之处,请大佬们指出哈~

后端项目初始化

1.全局安装express脚手架

额,这个应该是属于准备工作的。给忘记了,那就凑合放在这里吧,别打我,我知道错了,但我就是不改【狗头保命】

cnpm install -g express-generator 

2.新建工程

打开cmd,cd 到大家喜欢的路径下,然后输入下面这条命令

express express_mylive
image-20200905170800271

接着我们使用编辑器打开我们刚刚创建好的项目。像小羽这么帅的男人,当然就是眼疾手快的打开了vs code啦。这就是我们刚刚创建的工程啦。下图就是我们的项目目录。

image-20200905171000959

在vscode的cmd中,通过cnpm install 安装相关的模块。接着通过npm run start 启动后台。express框架默认是启动3000端口,所以老铁们,不用我多说了吧!浏览器输入http://localhost:3000

image-20200905171244447
image-20200905171441888

3.初始化工程

但是这个工程还配不上像咱们老铁那高贵的气质。所以,痛定思痛,老铁们你们还不一起来改造这个气质低下的工程嘛?

3.1 修改package.json

修改package.json,如下。然后在vscode的cmd中输入以下两条命令,全局安装相关的插件模块和pm2。

cnpm install 
cnpm install pm2 -g
{
  "name": "express-mylive",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "node-dev ./bin/www",
    "start": "pm2 start ./bin/www --name=express_living",
    "restart": "pm2 restart ./bin/www --name=express_living",
    "stop": "pm2 stop express_living"
  },
  "dependencies": {
    "base64-img": "^1.0.4",
    "body-parser": "^1.19.0",
    "compression": "^1.7.4",
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "express": "~4.16.1",
    "express-jwt": "^6.0.0",
    "http-errors": "~1.6.3",
    "jade": "~1.11.0",
    "morgan": "~1.9.1",
    "mysql": "^2.18.1",
    "node-media-server": "^2.1.4",
    "socket.io": "^2.3.0"
  },
  "devDependencies": {
    "node-dev": "^5.0.0"
  }
}

3.2 修改启动端口

修改bin目录下的www文件,修改端口号为8512。老铁们喜欢改成啥都可以哈,尽量找一个少用的端口号就行了。保存之后,直接打开http://localhost:8512就可以看到我 们的后端重新运行到了8512端口,而原来的3000端口无法打开了,就好像小三一样被无情的抛弃了。呸,一群渣男!

image-20200905172735700

3.3 配置公共文件

修改public目录下的文件如下

image-20200905180344984

common.js

const base64Img = require('base64-img');
class Common {
    //格式化输出
    outPut(code,data,msg=""){
        return {code:code,data:data,msg:msg}
    }
    //生成len位随机字符串
    getCode(len){
        var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
        var nums = "";
        for (var i = 0; i < len; i++) {
        var id = parseInt(Math.random() * 62);
        nums += chars[id];
        }
        return nums;
    }
    //图片转base64
    async base64Image(url){
        return new Promise((resolve,reject)=>{
            base64Img.requestBase64(url,(err,res,body)=>{
                resolve(body)
            })
        }) 
    }
}
const common = new Common();
module.exports = common;

mysqlModal.js

这里的话。我们使用了db2的配置,所以老铁们改db2的相关配置就阔以啦~

//mysqlModal.js
//初始化数据库配置
//test666
const mysqlConf = {
    db1:{
        host: '127.0.0.1',
        user: 'root',
        password: 'test',
        database: 'test',
        port: 3306
    },
    db2:{
        host: 'localhost',
        user: 'root',
        password: '123456',
        database: 'living',
        port: 3306
    }
};

//封装数据库sql操作
async function DB1(sql){
    var mysql = require('mysql');
    var pool = mysql.createPool(mysqlConf.db1);
    return new Promise((resolve,reject)=>{
        pool.getConnection(function(err,conn){
            if(err){
                console.log(err)
            }else{
                conn.query(sql,function(err,results,fields){
                    //事件驱动回调
                    if(err){
                        resolve(err)
                    }else{
                        resolve(results)
                    }
                });
                //释放连接,需要注意的是连接释放需要在此处释放,而不是在查询回调里面释放
                conn.release();
            }
        });
    })
}
async function DB2(sql){
    var mysql = require('mysql');
    var pool = mysql.createPool(mysqlConf.db2);
    return new Promise((resolve,reject)=>{
        pool.getConnection(function(err,conn){
            if(err){
                console.log(err)
            }else{
                conn.query(sql,function(err,results,fields){
                    //事件驱动回调
                    if(err){
                        resolve(err)
                    }else{
                        resolve(results)
                    }
                });
                //释放连接,需要注意的是连接释放需要在此处释放,而不是在查询回调里面释放
                conn.release();
            }
        });
    })
}



module.exports = {DB1,DB2};

4.导入数据库

像小羽这么聪明的男人,早就知道你们这群人懒得要死,所以早早就给你们准备好了sql文件。你们说说看嘛,要是没了我这么帅气的男人,你们怎么活呀?

使用navicate连接上自己的数据库,创建一个叫living的库,然后导入上面说到的sql文件。最后可以看到帮我们新建了两个表。

image-20200905174628878
image-20200905174702624
image-20200905174800752

5.修改api接口

image-20200905175847616

5.1 index

routes文件夹下的index.js

这里是我们的默认请求的接口,也就是传说中的牌面,所以老铁们,不要客气有多高大上就给它整到多高大上。

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  //res.render('index', { title: 'Express' });
  res.send("小羽直播间-后台")
});

module.exports = router;

5.2 users

router文件夹下的users.js

这里是我们的用户接口,都是用sql语句写的,不会orm,后端大佬不要埋汰小弟,小弟夸您帅好不好?

var express = require('express');
var router = express.Router();
var commonJS = require('../public/js/common');
var sqlHandle = require('../public/config/mysqlModal')
const jwt = require('jsonwebtoken')

router.get("/test",async(req,res,next)=>{
  let img = await commonJS.base64Image("https://c-ssl.duitang.com/uploads/item/201807/08/20180708222217_wayur.jpeg")
  res.send(img)
})


/**
 * @description: 用户登录
 * @Date: 2020-09-01 10:46:49
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.post("/login",async(req,res,next)=>{
  let data = req.body
  if(data.account&&data.password){
    let sql = `select * from user where email='${data.account}' and password='${data.password}' limit 1`
    let result = await sqlHandle.DB2(sql)
    if (result.length == 1) {
      const token = 'Bearer ' + jwt.sign(
        {
          id:result[0].id,
          name:result[0].name,
          age:result[0].age,
          email:result[0].email
        },
        'living_xiaoyu',
        {
          expiresIn: 3600 * 24 * 7
        }
      )
      
      res.send(commonJS.outPut(200, token, 'success'))
    }else{
      res.send(commonJS.outPut(500, "账号/密码错误", 'fail'))
    }
  }else{
    res.send(commonJS.outPut(500, "账号/密码不能为空", 'fail'))
  }
})


/**
 * @description: 获取用户信息
 * @Date: 2020-08-31 11:13:14
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.get('/getUserInfo',async(req,res,next)=>{
  let data = req.query;
  let sql = `select * from user where id = '${data.id}' limit 1`
  let result = await sqlHandle.DB2(sql)
  if (result.length == 1) {
    res.send(commonJS.outPut(200, result[0], 'success'))
  }else{
    res.send(commonJS.outPut(500, result, 'fail'))
  }
})


/**
 * @description: 新增用户
 * @Date: 2020-08-31 11:13:05
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.post('/addUser', async (req, res, next)=>{
  let data = req.body;
  if(!data.email||!data.password){
    res.send(commonJS.outPut(500, "邮箱/密码不能为空", 'fail'))
    return
  }
  let sql = `insert into user (id,name,age,email,password) values ('${commonJS.getCode(32)}','${data.name}','${data.age}','${data.email}','${data.password}')`
  let result = await sqlHandle.DB2(sql)
  if (result.affectedRows == 1) {
    res.send(commonJS.outPut(200, data, 'success'))
  }else{
    res.send(commonJS.outPut(500, result, 'fail'))
  }
});


/**
 * @description: 编辑用户信息
 * @Date: 2020-08-31 13:38:44
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.post("/editUser",async(req,res,next)=>{
  let data = req.data
  if(!data.id){
    let sql = `update user set name=${data.name},age=${data.age},password=${data.password} where id=${data.id}`
    let result = await sqlHandle.DB2(sql)
    if (result.affectedRows == 1) {
      res.send(commonJS.outPut(200, data, 'success'))
    }else{
      res.send(commonJS.outPut(500, result, 'fail'))
    }
  }
})

module.exports = router;

5.3 livingRoom

router文件夹下的livingRoom.js

直播间接口,也是用sql语句写的。

/*
 * @description: 
 * @author: 小羽
 * @github: https://github.com/lyff1006
 * @lastEditors: 小羽
 * @Date: 2020-08-31 22:27:40
 * @LastEditTime: 2020-09-05 17:58:10
 * @Copyright: 1.0.0
 */
var express = require('express');
var router = express.Router();
var commonJS = require('../public/js/common');
var sqlHandle = require('../public/config/mysqlModal')

/**
 * @description: 获取直播间信息
 * @Date: 2020-08-31 11:32:31
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.get("/roomList",async (req,res,next)=>{
    let data = req.query
    let sql
    if(!data.keyword){
        sql = `select living_room.id,living_room.user_id,living_room.title,user.name,living_room.image,user.avatar,living_room.type from living_room left join user on living_room.user_id = user.id  where living_room.status != 0`
    }else{
        sql = `select living_room.id,living_room.user_id,living_room.title,user.name,living_room.image,user.avatar,living_room.type from living_room left join user on living_room.user_id = user.id where title like '%${data.keyword}%' or user.name like '%${data.keyword}%' and living_room.status !=0 limit 20`
    }
    let result = await sqlHandle.DB2(sql)
    if (result.length >= 0) {
        res.send(commonJS.outPut(200, result, 'success'))
    }else{
        res.send(commonJS.outPut(500, result, 'fail'))
    }
})


/**
 * @description: 根据类型获取直播间信息
 * @Date: 2020-08-31 11:32:31
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.get("/roomListByType",async (req,res,next)=>{
    let data = req.query
    let sql
    if(!data.type){
        sql = `select living_room.id,living_room.user_id,living_room.title,user.name,living_room.image,user.avatar,living_room.type from living_room left join user on living_room.user_id = user.id  where living_room.status != 0`
    }else{
        sql = `select living_room.id,living_room.user_id,living_room.title,user.name,living_room.image,user.avatar,living_room.type from living_room left join user on living_room.user_id = user.id where type = '${data.type}' and living_room.status !=0 limit 20`
    }
    let result = await sqlHandle.DB2(sql)
    if (result.length >= 0) {
        res.send(commonJS.outPut(200, result, 'success'))
    }else{
        res.send(commonJS.outPut(500, result, 'fail'))
    }
})


/**
 * @description: 新建房间
 * @Date: 2020-08-31 11:38:49
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.post("/addRoom",async (req,res,next)=>{
    let data = req.body
    let sql = `insert into living_room (id,title,user_id,type) value ('${commonJS.getCode(32)}','${data.title}','${data.user_id}','${data.type}')`
    let result = await sqlHandle.DB2(sql)
    if (result.affectedRows == 1) {
        res.send(commonJS.outPut(200, data, 'success'))
    }else{
        res.send(commonJS.outPut(500, result, 'fail'))
    }
})


/**
 * @description: 编辑房间
 * @Date: 2020-08-31 11:40:27
 * @author: 小羽
 * @param {type} 
 * @return {type} 
 */
router.post("/editRoom",async (req,res,next)=>{
    let data = req.body
    let sql = `update living_room set title='${data.title}',status='${data.status}',user_id='${data.user_id}' where id ='${data.id}' `
    let result = await sqlHandle.DB2(sql)
    if (result.affectedRows == 1) {
        res.send(commonJS.outPut(200, data, 'success'))
    }else{
        res.send(commonJS.outPut(500, result, 'fail'))
    }
})

module.exports = router;

5.4 修改app.js

5.4.1 添加路由
//路由相关
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/livingRoom", roomRouter);
5.4.2 开启gzip压缩
//开启gzip
var compression = require('compression');
app.use(compression());
5.4.3 设置跨域头
//设置跨域头,这里设置全部允许跨域了
app.all('*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization,Origin,Accept,X-Requested-With');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.header('Access-Control-Allow-Credentials', true);
  res.header('X-Powered-By', ' 3.2.1');
  res.header('Content-Type', 'application/json;charset=utf-8');
  next();
});
5.4.4 设置jwt
app.use(expressJwt({
  secret:"living_xiaoyu",
  algorithms:['HS256'],
  credentialsRequired:true, //是否校验
}).unless({
  path:['/users/login','/livingRoom/roomList','/livingRoom/roomListByType']
}))
5.4.5 设置完的app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var expressJwt = require("express-jwt")
var compression = require('compression');


var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var roomRouter = require('./routes/livingRoom');

var app = express();

//开启gzip
app.use(compression());

//设置跨域头
app.all('*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization,Origin,Accept,X-Requested-With');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.header('Access-Control-Allow-Credentials', true);
  res.header('X-Powered-By', ' 3.2.1');
  res.header('Content-Type', 'application/json;charset=utf-8');
  next();
});

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

//使用中间件
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(expressJwt({
  secret:"living_xiaoyu",
  algorithms:['HS256'],
  credentialsRequired:true, //是否校验
}).unless({
  path:['/users/login','/livingRoom/roomList','/livingRoom/roomListByType']
}))

//路由相关
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/livingRoom", roomRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

6.使用postman测试api接口

6.1 设置全局变量

image-20200905181501856
image-20200905181519636

6.2 将api接口导入postman

嗯,不用你们提醒,我也给你们准备好了,拿走吧https://www.getpostman.com/collections/f52ee7c44169bd915f4a。小羽欧巴超级贴心的,有木有?【手动乖巧】

image-20200905181825224

6.3 设置token

这里可以设置token哦,token可以从登录接口中获取,啥?账号密码?自己进数据看看就知道了嘛。敲黑板,敲黑板,敲黑板,token 中不需要带Bearer

image-20200905182117457
image-20200905182352678

7.添加websocket和rtmp服务

修改app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var expressJwt = require("express-jwt")
var compression = require('compression');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var roomRouter = require('./routes/livingRoom');
var http = require('http').Server(app);
var io = require('socket.io')(http);
var port = process.env.PORT || 8511;

var app = express();

//开启gzip
app.use(compression());

//设置跨域头
app.all('*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization,Origin,Accept,X-Requested-With');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.header('Access-Control-Allow-Credentials', true);
  res.header('X-Powered-By', ' 3.2.1');
  res.header('Content-Type', 'application/json;charset=utf-8');
  next();
});

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

//使用中间件
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(expressJwt({
  secret:"living_xiaoyu",
  algorithms:['HS256'],
  credentialsRequired:true, //是否校验
}).unless({
  path:['/users/login','/livingRoom/roomList','/livingRoom/roomListByType']
}))

//路由相关
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/livingRoom", roomRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});


//rtmp服务
const NodeMediaServer = require('node-media-server');
const config = {
  rtmp: {
    port: 1935,
    chunk_size: 60000,
    gop_cache: true,
    ping: 30,
    ping_timeout: 60
  },
  http: {
    port: 8000,
    allow_origin: '*'
  }
};
var nms = new NodeMediaServer(config)
nms.run();


//socket服务
require('events').EventEmitter.prototype._maxListeners = 1000;
let namespaceList = [{name:"barrage",description:"弹幕模块"}]
for(let i in namespaceList){
  if(namespaceList[i].name){
    newNamespace(namespaceList[i].name)
  }
}
//监听新的命名空间
function newNamespace(space){
  io.of(space).on('connection',(socket)=>{
    socket.on('chat message',(data)=>{
      socket.emit('chat message',data)
    })
    //加入房间
    socket.on('JOINROOM',(name)=>{
      socket.join(name)
    })
    //离开房间
    socket.on('LEAVEROOM',(name)=>{
      socket.leave(name)
    })
    //发送房间聊天
    socket.on('ROOMCHAT',(data)=>{
      io.of(space).to(data.room).emit(data.func,data.data)
    })
    //加入自己的房间
    socket.on('JOINUSER',(id)=>{
      socket.join(id)
    })
    //发送用户私聊
    socket.on('USETCHAT',(data)=>{
      io.of(space).to(data.id).emit(data.func,data.data)
    })
    //发送公聊
    socket.on('COMMONCHAT',(data)=>{
      socket.emit(data.func,data.data)
    })
  })
}
http.listen(port, function(){
  console.log('listening on *:' + port);
});

module.exports = app;

websocket服务就暂时不测试了,到时候接入前端再测,现在测一下rtmp服务。

obs中设置推流如下,其中密钥为直播间的id可以在数据库中找到,然后添加窗口捕获后,点击开始推流,在http:localhost:8000/admin中即可看到自己的直播啦。

image-20200905183335132
image-20200905183451053
image-20200905183650534

小结

帅到睡不着的小羽在这期为大家讲解了如何搭建一个直播平台的后端,主要功能有以下三点:

1.数据库相关的接口

2.websocket实时通讯接口

3.视频直播接口

最后附上github地址:https://github.com/lyff1006/mylive

觉得小羽教得还阔以得话就点波关注呗~

ps:纯原创,转载请标明出处

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