手写webserver学习记录

记录一下手写webserver的学习过程

整体的目录结构

一、通过ServerSocket建立服务器与浏览器之间的连接

创建包com.webserver.core

创建类Server,Dispatcher

Server类

/**
 * @Author rainc
 * @create 2019/10/5 17:03
 */
public class Server {
    private ServerSocket serverSocket;
    //判断服务器是否已启动
    private boolean isRunning;
    //线程池
    private ExecutorService threadPool;

    public static void main(String[] args) {
        Server server = new Server();
        //启动服务
        server.start();
    }

    /**
     * 启动服务
     */
    public void start() {
        try {
            //初始化ServerSocket对象并配置端口
            serverSocket = new ServerSocket(8888);
            //初始化线程池
            threadPool = new ThreadPoolExecutor(10, 200, 0, TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
                    new ThreadPoolExecutor.DiscardPolicy());
            isRunning = true;
            //接收连接进行处理
            receive();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服务器启动失败");
            //若启动异常停止服务器
            stop();
        }
    }

    /**
     * 接受连接处理
     */
    public void receive() {
        //循环处理使得服务器能一直运行
        while (isRunning) {
            try {
                //通过accept方法接受建立新的连接并获取到Socket对象
                Socket client = serverSocket.accept();
                System.out.println("一个客户端建立了连接");
                //多线程处理每当有新连接就初始化一个Dispatcher对象并将Socket传入Dispatcher对象中进行详细的处理
                threadPool.execute(new Dispatcher(client));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("客户端错误");
            }
        }
    }

    /**
     * 停止服务
     */
    public void stop() {
        isRunning = false;
        try {
            this.serverSocket.close();
            System.out.println("服务器已停止");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

​ 分发器

/**
 * @Author rainc
 * @create 2019/10/5 17:11
 * 分发器,用于详细处理请求并响应
 */
public class Dispatcher implements Runnable {
    private Socket client;

    public Dispatcher(Socket client) {
        this.client = client;
    }

    public void run() {
        try {
            //获取输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
            String temp;
            //判断当下一行长度为零时停止读取
            while ((temp = br.readLine()).length() != 0) {
                System.out.println(temp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //结束时释放Socket
        relesase();
    }

    /**
     * 释放Socket
     */
    private void relesase() {
        try {
            client.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

启动Server的main方法,打开浏览器输入http://localhost:8888/login?uname=rainc,可以看到捕捉到的请求

二、对得到的http协议请求进行解析

创建request类进行请求处理

/**
 * @Author rainc
 * @create 2019/10/6 11:55
 */
public class Request {
    //请求方式
    private String method;
    //请求url
    private String url;
    //存储参数可能存在一键多值因此值使用了list
    private Map<String, List<String>> parameterMap;
    private BufferedReader br;

    public Request(Socket client) {
        try {
            //初始化输入流
            this.br = new BufferedReader(new InputStreamReader(client.getInputStream()));
            //初始化map
            parameterMap = new HashMap<>();
            //解析请求
            parseRequest();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析请求行
     */
    private void parseRequest() {
        try {
            //读取第一行内容 GET /login?uname=a HTTP/1.1
            String line = br.readLine();
            //如果为空则停止接下来的步骤
            if (line==null)
                return;
            //通过空格拆分为三部分取得请求方式和url
            String[] temp = line.split(" ");
            method = temp[0].trim();
            //通过问号分析是否有传递参数
            if (temp[1].contains("?")) {
                //通过分割问号得到url和参数
                temp = temp[1].split("\\?");
                url = temp[0].trim();
                convertMap(temp[1].trim());
            } else {
                url = temp[1].trim();
            }
            System.out.println("method:" + method);
            System.out.println("url:" + url);
            //如果请求方式为post则读取post请求内容
            if (method.equals("POST"))
                parsePostParams();
            System.out.println(parameterMap);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析post参数
     */
    private void parsePostParams() {
        String line;
        int len = 0;
        try {
            //请求体与post参数中间有一个空行,长度会被解析成0,这个循环会正好在读取参数前结束
            while ((line = br.readLine()).length() != 0) {
                //若为post方式则请求中会有Content-Length行,通过Content-Length可以得到参数长度并读取
                if (line.contains("Content-Length")) {
                    //对Content-Length行进行拆分得到长度
                    String[] temp = line.split(":");
                    len = Integer.parseInt(temp[1].trim());
                }
            }
            char[] buffers = new char[len];
            br.read(buffers);
            convertMap(new String(buffers));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将参数分割存入map
     */
    private void convertMap(String params) {
        //1、分割字符串&
        String[] keyValues = params.split("&");
        for (String temp : keyValues) {
            //2、再次分割字符串 =
            String[] kv = temp.split("=");
            kv = Arrays.copyOf(kv, 2);
            //获取key和value
            String key = kv[0];
            //存入时对内容进行中文处理防止中文乱码
            String value = kv[1] == null ? null : decode(kv[1], "utf-8");
            //存储到map中如果没有该键则创建该键对应的list
            if (!parameterMap.containsKey(key)) {
                parameterMap.put(key, new ArrayList<String>());
            }
            //如果有则直接存入
            parameterMap.get(key).add(value);
        }
    }

    /**
     * 处理中文乱码
     */
    private String decode(String value, String enc) {
        try {
            return java.net.URLDecoder.decode(value, enc);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 对外提供get方法获取
     *
     * @return
     */
    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    /**
     * 通过key获取对应的多个值
     *
     * @param key
     * @return
     */
    public String[] getParameterValues(String key) {
        List<String> values = this.parameterMap.get(key);
        if (null == values || values.size() < 1) {
            return null;
        }
        return values.toArray(new String[0]);
    }

    /**
     * 通过key获取对应的一个值
     *
     * @param key
     * @return
     */
    public String getParameter(String key) {
        //调用getParameterValues,并获取第一个值
        String[] values = getParameterValues(key);
        return values == null ? "" : values[0];
    }

修改Dispatcher类的构造器,和run方法

    public Dispatcher(Socket client) {
        this.client = client;
        //初始化请求处理
        request=new Request(client);
    }
    public void run() {
        //结束时释放Socket
        relesase();
    }

运行服务器,通过http测试插件(我这里用的是RESTer)模拟post请求

可以看到内容成功获取到了

三、对请求进行响应

创建Response类进行响应处理

/**
 * 响应处理
 *
 * @Author rainc
 * @create 2019/10/6 15:15
 */
public class Response {
    BufferedWriter bw;
    //正文
    private StringBuilder content;
    //协议头(状态行与请求头 回车)信息
    private StringBuilder responseInfo;
    //协议头返回的
    private int len = 0;
    //通过BLACK来表示一个空格
    private final String BLACK = " ";
    //通过CRLF来表示回车加换行
    private final String CRLF = "\r\n";

    private Response() {
        content = new StringBuilder();
        responseInfo = new StringBuilder();
    }

    public Response(Socket client) {
        //回调无参构造器进行初始化
        this();
        try {
            bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
        } catch (IOException e) {
            responseInfo = null;
        }
    }


    //动态添加内容
    public Response print(String info) {
        content.append(info);
        len += info.getBytes().length;
        return this;
    }

    //动态添加内容并且换行
    public Response println(String info) {
        content.append(info).append(CRLF);
        len += (info + CRLF).getBytes().length;
        return this;
    }

    //推送响应信息
    public void pushToBrower(int code) throws IOException {
        //如果错误则返回505错误
        if (responseInfo == null) {
            code = 505;
        }
        //构建头信息
        createResponseInfo(code);
        //放入头信息
        bw.append(responseInfo);
        System.out.println(responseInfo);
        //放入正文信息
        bw.append(content);
        //刷新输出流
        bw.flush();
    }

    //构建头信息
    private void createResponseInfo(int code) {
        //1.响应行:HTTP/1.1 200 OK 响应的状态码 200表示正常应答
        responseInfo.append("HTTP/1.1").append(BLACK);
        responseInfo.append(code).append(BLACK);
        switch (code) {
            case 200:
                responseInfo.append("OK").append(CRLF);
                break;
            case 404:
                responseInfo.append("NOT FOUND").append(CRLF);
                break;
            case 505:
                responseInfo.append("SERVER ERROR").append(CRLF);
        }

        //2、响应头(最后一行存在空行)
        //Date:   生成消息的具体时间和日期
        responseInfo.append("Date:").append(new Date()).append(CRLF);
        //Server:apache  生成服务器的名称
        responseInfo.append("Server:").append("apache").append(CRLF);
        //Content-Type: text/html;charset=utf-8   http服务器告诉浏览器自己响应的对象类型和字符集(并且告诉客户端实际返回的内容的内容类型)
        responseInfo.append("Content-Type:").append("text/html;charset=utf-8").append(CRLF);
        //Content-Length:   http服务器的响应实体正文的长度
        responseInfo.append("Content-length:").append(len).append(CRLF);//必须过去字节长度
        //以回车换行标记结束
        responseInfo.append(CRLF);
    }
}

修改Dispatcher类的构造器,和run方法

    public Dispatcher(Socket client) {
        this.client = client;
        //初始化请求处理
        request = new Request(client);
        //初始化响应处理
        response = new Response(client);
    }
    public void run() {
        try {
            //写入内容
            response.print("你好"+request.getParameter("uname")/*通过键获取参数值*/);
            //推送
            response.pushToBrower(200);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //结束时释放Socket
        relesase();
    }

运行服务器,在网页上输入http://localhost:8888/?uname=rainc,可以看到网页上显示了我们想要输出的内容

四、建立Servlet接口标准,创建Servlet的实例进行业务处理

创建接口Servlet

/**
 * servlet接口,统一servlet类标准
 * @Author rainc
 * @create 2019/10/6 17:02
 */
public interface Servlet {
    void service(Request request, Response response);
}

创建IndexServlet类并引入Servlet接口

/**
 * @Author rainc
 * @create 2019/10/6 17:07
 */
public class IndexServlet implements Servlet {

    @Override
    public void service(Request request, Response response) {

        //将之前Dispatcher中的业务代码转移到了这里
        response.print("你好" + request.getParameter("uname")/*通过键获取参数值*/);
    }
}

修改Dispatcher的run方法

    public void run() {
        //推送
        try {
            //通过IndexServlet实现Servlet接口
            Servlet servlet = new IndexServlet();
            //调用service方法进行业务处理
            servlet.service(request, response);
            response.pushToBrower(200);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //结束时释放Socket
        relesase();
    }

运行服务器,再次在网页上输入http://localhost:8888/?uname=rainc,成功的话结果和上面那次相同。

五、创建web.xml,和WebApp通过反射来动态生成servlet

由于web服务是动态的,因此通过new的方法来静态的新建对象是不可取的,因此需要通过反射来进行动态生成servlet

大致过程:

  • 通过request解析得到url
  • 解析web.xml
  • 通过得到的url在servlet-mapping中找到对应的servlet-name
  • 通过servlet-mapping中的servlet-name找到servlet中对应的servlet-class
  • 使用servlet-class通过反射动态生成对应的servlet对象

创建web.xml通过web.xml来管理servlet

<?xml version="1.0" encoding="UTF-8"?>

<web-app>
<!--真实路径-->
<servlet>
    <servlet-name>index</servlet-name>
    <servlet-class>com.webserver.servlet.IndexServlet</servlet-class>
</servlet>
<!--路径映射-->
<servlet-mapping>
    <servlet-name>index</servlet-name>
    <url-pattern>/index</url-pattern>
</servlet-mapping>
</web-app>

创建Entity类对应真实路径

package com.webserver.core;

/**
 * <servlet>
 *     <servlet-name>index</servlet-name>
 *     <servlet-class>com.webserver.servlet.IndexServlet</servlet-class>
 * </servlet>
 * @Author rainc
 * @create 2019/10/6 17:53
 */
public class Entity {
    private String name;
    private String clz;

    public Entity() {
    }

    public Entity(String name, String clz) {
        this.name = name;
        this.clz = clz;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClz() {
        return clz;
    }

    public void setClz(String clz) {
        this.clz = clz;
    }

    @Override
    public String toString() {
        return "Entity{" +
                "name='" + name + '\'' +
                ", clz='" + clz + '\'' +
                '}';
    }
}

创建Mapping对应映射路径

package com.webserver.core;

import java.util.HashSet;
import java.util.Set;

/**
 * <servlet-mapping>
 *     <servlet-name>index</servlet-name>
 *     <url-pattern>/index</url-pattern>
 * </servlet-mapping>
 * @Author rainc
 * @create 2019/10/6 17:56
 */
public class Mapping {
    private String name;
    private Set<String> patterns;

    public Mapping() {
        patterns = new HashSet<>();
    }

    public Mapping(String name, Set<String> pattens) {
        this.name = name;
        this.patterns = pattens;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<String> getPattens() {
        return patterns;
    }

    public void setPattens(Set<String> pattens) {
        this.patterns = pattens;
    }

    public void addPattern(String pattern) {
        this.patterns.add(pattern);
    }

}

创建Webapp对象

package com.webserver.core;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;

/**
 * @Author rainc
 * @create 2019/10/6 17:51
 */
public class WebApp {
    private static WebContext context;

    static {
        try {
            //1、获取解析工厂
            SAXParserFactory factory = SAXParserFactory.newInstance();
            //2、从解析工厂获取解析器
            SAXParser parse = factory.newSAXParser();
            //3、加载文档 Document 注册处理器
            //4、编写处理器
            WebHandler handler = new WebHandler();
            //5、解析数据
            parse.parse(Thread.currentThread().getContextClassLoader().getResourceAsStream("web.xml"), handler);
            //将数据存入context中进行统一管理
            context = new WebContext(handler.getEntities(), handler.getMappings());
        } catch (Exception e) {
            System.out.println("解析配置文件错误");
        }
    }

    //通过url获取对应的servlet
    public static Servlet getServletFromUrl(String url) {
        //通过url获取class对应的路径名
        String className = context.getClz(url);

        if (className == null)
            return null;
        try {
            //通过路径名反射生成相应的Servlet
            Class clz = Class.forName(className);
            Servlet servlet = (Servlet) clz.getConstructor().newInstance();
            return servlet;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }


    }
}

创建 WebHandler类编写处理器对web.xml进行处理

package com.webserver.core;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.util.ArrayList;
import java.util.List;

/**
 * web.xml处理器
 * @Author rainc
 * @create 2019/10/6 18:01
 */
class WebHandler extends DefaultHandler {
    //创建容器保存全部entity和mapping
    private List<Entity> entities;
    private List<Mapping> mappings;
    private Entity entity;
    private Mapping mapping;
    //保存xml的标记
    private String tag;
    //用来判断是映射还是实体
    private boolean isMapping = false;
    //对外提供get方法
    public List<Entity> getEntities() {
        return entities;
    }

    public List<Mapping> getMappings() {
        return mappings;
    }

    //开始解析
    @Override
    public void startDocument() {
        entities = new ArrayList<>();
        mappings = new ArrayList<>();

    }

    //开始接收某个元素
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        tag = qName;
        //通过标记来判断是映射还是实体并生成相应的对象
        switch (tag) {
            case "servlet":
                entity = new Entity();
                isMapping = false;
                break;
            case "servlet-mapping":
                mapping = new Mapping();
                isMapping = true;
            default:
                break;
        }
    }

    //解析元素中内容
    @Override
    public void characters(char[] ch, int start, int length) {
        String contents = new String(ch, start, length).trim();
        //判断是映射还是实体并对相应的对象赋值
        if (!isMapping) {
            switch (tag) {
                case "servlet-name":
                    entity.setName(contents);
                    break;
                case "servlet-class":
                    entity.setClz(contents);
            }
        } else {
            switch (tag) {
                case "servlet-name":
                    mapping.setName(contents);
                    break;
                case "url-pattern":
                    mapping.addPattern(contents);
            }
        }
    }

    //结束某个元素接收
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        //判断实体还是映射保存至容器中
        switch (qName) {
            case "servlet":
                entities.add(entity);
                break;
            case "servlet-mapping":
                mappings.add(mapping);
            default:
                break;
        }
        //重置tag
        tag = "";
    }

    //结束解析
    @Override
    public void endDocument() throws SAXException {
    }
}

创建WebContext对解析获得的内容进行统一管理

package com.webserver.core;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Web上下文统一管理
 *
 * @Author rainc
 * @create 2019/10/6 18:00
 */
public class WebContext {
    List<Entity> entities;
    List<Mapping> mappings;
    //key-->servlet-name value-->serblet-class
    private Map<String, String> entityMap = new HashMap<>();
    //key-->url-pattern value-->servlet-name
    private Map<String, String> mappingMap = new HashMap<>();

    public WebContext(List<Entity> entities, List<Mapping> mappings) {
        this.entities = entities;
        this.mappings = mappings;
        //将entityList转成了对应的map
        for (Entity temp : entities) {
            entityMap.put(temp.getName(), temp.getClz());
        }
        //将mappingsList转成了对应的map
        for (Mapping temp : mappings) {
            for (String st : temp.getPattens()) {
                mappingMap.put(st, temp.getName());
            }
        }
    }

    /**
     * 通过传入的路径得到对应的名字,再通过名字找到对应的class路径
     *
     * @param pattern
     * @return
     */
    public String getClz(String pattern) {
        String name = mappingMap.get(pattern);
        return entityMap.get(name);
    }
}

修改Dispatcher的run方法

    public void run() {
        //推送
        try {
            //通过反射动态生成Servlet
            Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
            //判断servlet是否存在
            if (null != servlet) {
                //调用service方法进行业务处理
                servlet.service(request, response);
                response.pushToBrower(200);
            } else {
                response.print("网页没有");
                response.pushToBrower(404);
            }
        } catch (IOException e) {
            try {
                response.print("网页炸了");
                response.pushToBrower(500);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        }
        //结束时释放Socket
        relesase();
    }

到这里一个简单的服务器就基本完成了,接下来尝试着使用一下

六、模拟登录

创建login.html

<!DOCTYPE html>
<html lang="ch">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form method="post" action="http://localhost:8888/index">
    用户名:<input type="text" name="uname" id="uname"/>
    <br/>
    密码:<input type="password" name="pwd" id="pwd"/>
    <br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

创建LoginServlet

package com.webserver.servlet;

import com.webserver.core.Request;
import com.webserver.core.Response;
import com.webserver.core.Servlet;

import java.io.IOException;
import java.io.InputStream;

/**
 * @Author rainc
 * @create 2019/10/6 19:13
 */
public class LoginServlet implements Servlet {
    @Override
    public void service(Request request, Response response) {
        try {
            //读取login.html的内容并使用response输出到浏览器端
            InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("login.html");
            response.print(new String(is.readAllBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

修改IndexServlet的service方法

    public void service(Request request, Response response) {
        String uname = request.getParameter("uname");
        String pwd = request.getParameter("pwd");
        if (uname.equals("rainc") && pwd.equals("123456")) {
            response.print("你好" + request.getParameter("uname")/*通过键获取参数值*/);
        } else {
            response.print("账号或密码错误");
        }
    }

在web.xml中加入login的配置内容

    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>com.webserver.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>

在浏览器上输入http://localhost:8888/login,进入了下图的登录界面

输入正确的账号密码便会跳转到下图

如果错误则会显示账号或密码错误

参考内容:https://www.bilibili.com/video/av30023103/?p=251

个人博客:https://www.rainc.top/2019/10/05/java-study/webserver-study/
最后附上我的项目做参考
github:https://github.com/amerainc/Server_Study
码云:https://gitee.com/amerainc/Server_Study

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

推荐阅读更多精彩内容