第一行代码读书笔记 9 -- 网络技术

本篇文章主要介绍以几下个知识点:

  • 使用 HTTP 协议访问网络:
    使用 HttpURLConnection 和 OKHttp;
  • 解析 XML 格式数据:
    Pull 和 SAX 解析;
  • 解析 JSON 数据:
    JSONObject 和 GSON 解析。
图片来源于网络

9.1 使用 HTTP 协议访问网络

HTTP 协议,其工作原理很简单:客户端向服务器发出一条 HTTP 请求,服务器收到请求后会返回一些数据给客户端,然后客户端再对这些数据进行解析和处理。

9.1.1 使用 HttpURLConnection

下面学习 HttpURLConnection 的用法,其请求步骤代码如下:

   /**
     *  HttpURLConnection 发送请求
     */
    private void sendRequestWithHttpURLConnection() {
        // 开启线程来发送网络请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                BufferedReader reader = null;
                try{
                    URL url = new URL("http://www.baidu.com");
                    // 1. 获取 HttpURLConnection 实例
                    connection = (HttpURLConnection) url.openConnection();
                    // 2. 设置请求方法
                    connection.setRequestMethod("GET");
                    // 3. 自由定制,如设置连接超时、读取超时等
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    // 4. 获取服务器返回的输入流
                    InputStream in = connection.getInputStream();
                    // 下面对获取到的输入流进行读取
                    reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine())!= null){
                        response.append(line);
                    }
                    showResponse(response.toString());// 显示请求结果
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    if (reader != null){
                        try{
                            reader.close();
                        }catch (IOException e){
                            e.printStackTrace();
                        }
                    }
                    if (connection != null){
                        // 5.把 HTTP 连接关掉
                        connection.disconnect();
                    }
                }
            }
        }).start();
    }

别忘了声明网络权限:

<uses-permission android:name="android.permission.INTERNET" />

若是想要提交数据给服务器只需把请求方法改为 POST,并在获取输入流之前把要提交的数据写出即可。注意每条数据要以键值对的形式存在,数据与数据之间用 “&” 隔开,比如向服务器提交用户名和密码可写成:

connection.setRequestMethod("POST");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes("username=admin&password=123456");

9.1.2 使用 OKHttp

接下来学习下网络请求开源项目 OKHttp,其项目主页地址是:https://github.com/square/okhttp

在使用 OKHttp 前,需要在项目中添加 OKHttp 库的依赖,如下:

compile 'com.squareup.okhttp3:okhttp:3.5.0'

下面学习 OKHttp 请求步骤,如下:

   /**
     *  OKHttp 发送请求
     */
    private void sendRequestWithOKHttp() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // 1. 创建 OkHttpClient 实例
                    OkHttpClient client = new OkHttpClient();
                    // 2. 创建 Request 对象
                    Request request = new Request.Builder().url("http://www.baidu.com").build();
                    // 3. 调用 OkHttpClient 的 newCall() 方法来创建 Call 对象
                    Response response = client.newCall(request).execute();
                    // 4. 获取返回的内容
                    String responseData = response.body().string();
                    showResponse(responseData);// 显示请求结果
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
    }

相比 HttpURLConnection,OKHttp 简单易用,若是发起一条 POST 请求,会比 GET 请求稍微复杂点,需要构建一个 RequestBody 对象来存放待提交的参数:

RequestBody requestBody = new FormBody.Builder()
        .add("username","admin")
        .add("password","123456")
        .build();

然后在 Request.Builder 中调用一下 post() 方法,并将 RequestBody 对象传入:

Request request = new Request.Builder()
        .url("http://www.baidu.com")
        .post(RequestBody)
        .build();

9.1.3 网络编程的最佳实践

在实际开发中,我们通常将这些通用的网络操作提取到一个公共类里,接下来就简单封装下网络操作。

首先针对 HttpURLConnection 定义一个回调接口:

public interface HttpCallbackListener {
    void onFinish(String response);// 请求成功时调用
    void onError(Exception e);// 请求失败时调用
}

接着编写工具类 HttpUtil:

public class HttpUtil {

    /**
     * 用 HttpURLConnection 发送请求
     * @param address
     * @param listener
     */
    public static void sendHttpRequest(final String address,final HttpCallbackListener listener){
        new Thread(new Runnable() {
            @Override
            public void run() {
                HttpURLConnection connection = null;
                try{
                    URL url = new URL(address);
                    // 1. 获取 HttpURLConnection 实例
                    connection = (HttpURLConnection) url.openConnection();
                    // 2. 设置请求方法
                    connection.setRequestMethod("GET");
                    // 3. 自由定制,如设置连接超时、读取超时等
                    connection.setConnectTimeout(8000);
                    connection.setReadTimeout(8000);
                    connection.setDoInput(true);
                    connection.setDoOutput(true);
                    // 4. 获取服务器返回的输入流
                    InputStream in = connection.getInputStream();
                    // 下面对获取到的输入流进行读取
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine())!= null){
                        response.append(line);
                    }
                    if (listener != null){
                        // 回调 onFinish() 方法
                        listener.onFinish(response.toString());
                    }
                }catch (Exception e){
                    if (listener != null){
                        // 回调 onError() 方法
                        listener.onError(e);
                    }
                }finally {
                    if (connection != null){
                        // 5.把 HTTP 连接关掉
                        connection.disconnect();
                    }
                }
                
            }
        }).start();
    }

    /**
     * 用 OKHttp 发送请求
     * @param address
     * @param callback
     */
    public static void sendOKHttpRequest(String address, Callback callback){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(address).build();
        client.newCall(request).enqueue(callback);
    }
}

这时候用 HttpURLConnection 发送请求就可以写成:

        HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
            @Override
            public void onFinish(String response) {
                // 在这里根据返回内容执行具体的逻辑
            }

            @Override
            public void onError(Exception e) {
                // 在这里对异常情况进行处理
            }
        });

用 OKHttp 发送请求就可以写成:

       HttpUtil.sendOKHttpRequest("http://www.baidu.com", new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                 // 在这里对异常情况进行处理
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // 得到服务器返回的具体内容
                String responseData = response.body().string();
               }
        });

另外需要注意的是,不管是使用 HttpURLConnection 还是 OKHttp,最终回调接口都还是在子线程中运行的。

下面举个例子巩固下,在布局中放置 Button 用于发送 HTTP 请求,放置一个 TextView 用于显示服务器返回的数据,主要代码如下:

public class HttpActivity extends AppCompatActivity implements View.OnClickListener {

    private Button send_url_request,send_okHttp_request,clear_content;
    private TextView response_text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_http);

        response_text = (TextView) findViewById(R.id.response_text);

        send_url_request = (Button) findViewById(R.id.send_url_request);
        send_okHttp_request = (Button) findViewById(R.id.send_okHttp_request);
        clear_content = (Button) findViewById(R.id.clear_content);
        send_url_request.setOnClickListener(this);
        send_okHttp_request.setOnClickListener(this);
        clear_content.setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.send_url_request:
                sendRequestWithHttpURLConnection();

            case R.id.send_okHttp_request:
                sendRequestWithOKHttp();

            case R.id.clear_content:
                showResponse(""); //清空数据
        }
    }

    /**
     *  OKHttp 发送请求
     */
    private void sendRequestWithOKHttp() {
        HttpUtil.sendOKHttpRequest("http://www.baidu.com", new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                 // 在这里对异常情况进行处理
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // 得到服务器返回的具体内容
                String responseData = response.body().string();
                showResponse(responseData);
            }
        });
    }

    /**
     *  HttpURLConnection 发送请求
     */
    private void sendRequestWithHttpURLConnection() {
        HttpUtil.sendHttpRequest("http://www.baidu.com", new HttpCallbackListener() {
            @Override
            public void onFinish(String response) {
                // 在这里根据返回内容执行具体的逻辑
                showResponse(response);
            }

            @Override
            public void onError(Exception e) {
                // 在这里对异常情况进行处理
            }
        });
    }

    /**
     * 显示请求结果
     * @param response
     */
    private void showResponse(final String response) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 在这里进行UI 操作,将结果显示到界面上
                response_text.setText(response);
            }
        });
    }
}

运行效果如下:

两种网络请求效果

9.2 解析 XML 格式数据

在网络上传输数据时最常用的格式有两种:XML 和 JSON。本节来学习下如何解析 XML 格式的数据。

解析 XML 格式的数据有多种方式,这里主要介绍 Pull 解析和 SAX 解析。解析前先来看看等下要解析的 XML 文本:

xml 格式的内容

9.2.1 Pull 解析方式

Pull 解析整个过程比较简单,具体看代码注释:

   /**
     * pull 解析
     * @param xmlData 要解析的xml数据
     */
    private void parseXMLWithPull(String xmlData) {
        try {
            // 1. 获取 XmlPullParserFactory 实例
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            // 2. 借助 XmlPullParserFactory 实例得到 XmlPullParser 对象
            XmlPullParser xmlPullParser = factory.newPullParser();
            // 3. 调用 setInput() 方法设置xml数据
            xmlPullParser.setInput(new StringReader(xmlData));
            // 4. 获取当前的解析事件
            int eventType = xmlPullParser.getEventType();
            String id = "";
            String name = "";
            String sex = "";
            // 5. 通过 while 循环不断地进行解析
            while (eventType != XmlPullParser.END_DOCUMENT){
                String nodeName = xmlPullParser.getName();
                switch (eventType){
                    // 开始解析某个节点
                    case XmlPullParser.START_TAG:
                        if ("id".equals(nodeName)){
                            id = xmlPullParser.nextText();
                        }else if ("name".equals(nodeName)){
                            name = xmlPullParser.nextText();
                        }else if ("sex".equals(nodeName)){
                            sex = xmlPullParser.nextText();
                        }
                        break;

                    // 完成解析某个节点
                    case  XmlPullParser.END_TAG:
                        if ("student".equals(nodeName)){
                            Log.d("pull解析:", "id is" + id);
                            Log.d("pull解析:", "name is" + name);
                            Log.d("pull解析:", "sex is" + sex);
                        }
                        break;

                    default:
                        break;
                }
                // 获取下一个解析事件
                eventType = xmlPullParser.next();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

9.2.2 SAX 解析方式

SAX 解析的用法比 Pull 解析要复杂些,但在语义方面会更加清楚。

用 SAX 解析需要建一个类继承 DefaultHandler,并重写父类的5个方法。为实现上面同样的功能,新建一个 ContentHandler 类,如下所示:

public class ContentHandler extends DefaultHandler {

    private String nodeName;
    private StringBuilder id;
    private StringBuilder name;
    private StringBuilder sex;

    /**
     * 开始 XML 解析时调用
     * @throws SAXException
     */
    @Override
    public void startDocument() throws SAXException {
        id = new StringBuilder();
        name = new StringBuilder();
        sex = new StringBuilder();
    }

    /**
     * 开始解析某个节点时调用
     * @param uri
     * @param localName
     * @param qName
     * @param attributes
     * @throws SAXException
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        // 记录当前节点名
        nodeName = localName;
    }

    /**
     * 获取节点中的内容时调用
     * @param ch
     * @param start
     * @param length
     * @throws SAXException
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        // 根据当前节点名判断将内容添加到哪一个 StringBuilder 对象中
        if ("id".equals(nodeName)){
            id.append(ch,start,length);
        }else if ("name".equals(nodeName)){
            name.append(ch,start,length);
        }else if ("sex".equals(nodeName)){
            sex.append(ch,start,length);
        }
    }

    /**
     * 完成解析某个节点时调用
     * @param uri
     * @param localName
     * @param qName
     * @throws SAXException
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("student".equals(localName)){
            Log.d("sax解析:", "id is" + id.toString().trim());
            Log.d("sax解析:", "name is" + name.toString().trim());
            Log.d("sax解析:", "sex is" + sex.toString().trim());
            // 最后要将 StringBuilder 清空掉
            id.setLength(0);
            name.setLength(0);
            sex.setLength(0);
        }
    }


    /**
     * 完成整个 XML 解析时调用
     * @throws SAXException
     */
    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
    }
}

接下来就非常简单了,代码如下:

   /**
     * sax 解析
     * @param xmlData
     */
    private void parseXMLWithSAX(String xmlData){
        try {
            // 创建 SAXParserFactory 对象
            SAXParserFactory factory = SAXParserFactory.newInstance();
            // 获取 XMLReader 对象
            XMLReader xmlReader = factory.newSAXParser().getXMLReader();
            ContentHandler handler = new ContentHandler();
            // 将 ContentHandler 的实例设置到 XMLReader 中
            xmlReader.setContentHandler(handler);
            // 开始执行解析
            xmlReader.parse(new InputSource(new StringReader(xmlData)));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

9.2.3 举个例子实在点

下面在布局中放置两个按钮,分别进行pull解析和sax解析:

public class ParseXMLActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btn_pull,btn_sax;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_parse_xml);

        btn_pull = (Button) findViewById(R.id.btn_pull);
        btn_sax = (Button) findViewById(R.id.btn_sax);

        btn_pull.setOnClickListener(this);
        btn_sax.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_pull:
                HttpUtil.sendOKHttpRequest("http://10.0.2.2/get_data.xml", new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        //ToastUtils.showShort("请求失败");
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        String responseData = response.body().string();
                        parseXMLWithPull(responseData);  // pull 解析
                    }
                });
                break;

            case R.id.btn_sax:
                HttpUtil.sendOKHttpRequest("http://10.0.2.2/get_data.xml", new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        //ToastUtils.showShort("请求失败");
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        String responseData = response.body().string();
                        parseXMLWithSAX(responseData);  // sax 解析
                    }
                });
                break;
        }
    }

    /**
     * pull 解析
     * @param xmlData 要解析的xml数据
     */
    private void parseXMLWithPull(String xmlData) {  . . .  }

    /**
     * sax 解析
     * @param xmlData
     */
    private void parseXMLWithSAX(String xmlData){  . . .  }
}

运行程序,打印的日志分别如下:

pull 解析
sax 解析

  可以看到,已经将 XML 数据成功解析出来了。

9.3 解析 JSON 数据

  类似的,解析 JSON 格式的数据有多种方式,这里主要介绍官方提供的 JSONObject 和谷歌的开源库 GSON 来解析。解析前先来看看等下要解析的 JSON 文本:

json 格式的内容

9.3.1 使用 JSONObject

  使用 JSONObject 解析上面内容比较简单,具体看代码:

   /**
     * 用 JSONObject 解析
     * @param jsonData 需要解析的数据
     */
    private void parseJSONWithJSONObject(String jsonData) {
        try {
            // 把需要解析的数据传入到 JSONArray 对象中
            JSONArray jsonArray = new JSONArray(jsonData);
            for (int i = 0;i < jsonArray.length();i++){
                JSONObject jsonObject = jsonArray.getJSONObject(i);
                String id = jsonObject.getString("id");
                String name = jsonObject.getString("name");
                String sex = jsonObject.getString("sex");
                Log.d("JSONObject解析", "id is "+id);
                Log.d("JSONObject解析", "name is "+name);
                Log.d("JSONObject解析", "sex is "+sex);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

9.3.2 使用 GSON

  接下来学习下开源库 GSON,其项目主页地址是:https://github.com/google/gson

  在使用 GSON 前,需要在项目中添加 GSON 库的依赖,如下:

compile 'com.google.code.gson:gson:2.8.0'

  GSON 可以将一段 JSON 格式的字符串自动映射成一个对象,从而不需要手动去编写代码进行解析了。

  比如解析一段 JSON 格式数据:

{"name":"Tom","age":20}

  就可以定义一个 Person 类,并加入 name 和 age 两字段,然后只需调用如下代码就可以将 JSON 数据自动解析成一个 Person 对象:

Gson gson = new Gson();
Person person = gson.fromJson(jsonData,Person.class);

  若解析一段 JSON 数组会麻烦些,需要借助 TypeToken 把期望解析成的数据类型传入到 fromJson() 方法中:

List<Person> people = gson.fromJson(jsonData,new TypeToken<List<Person>>(){}.getType());

  GSON 的基本用法就是这样。下面来解析上面的 JSON 文本,首先新增一个 Student 类:

public class Student {
    
    private String id;
    private String name;
    private String sex;

    // Getter and Setter
    . . .
}

  接下来就非常简单了,代码如下:

   /**
     *  用 GSON 解析
     * @param jsonData
     */
    private void parseJSONWithGSON(String jsonData){
        Gson gson = new Gson();
        List<Student>studentList = gson.fromJson(jsonData,new TypeToken<List<Student>>(){}.getType());
        for (Student student:studentList){
            Log.d("GSON解析", "id is "+student.getId());
            Log.d("GSON解析", "name is "+student.getName());
            Log.d("GSON解析", "sex is "+student.getSex());
        }
    }

9.3.3 举个例子实在点

  下面在布局中放置两个按钮,分别用 JSONObject 和 GSON 进行 json 解析:

public class ParseJSONActivity extends AppCompatActivity implements View.OnClickListener {

    private Button btn_object,btn_gson;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_parse_json);

        btn_object = (Button) findViewById(R.id.btn_object);
        btn_gson = (Button) findViewById(R.id.btn_gson);

        btn_object.setOnClickListener(this);
        btn_gson.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_object:
                HttpUtil.sendOKHttpRequest("http://10.0.2.2/get_data.json", new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {}

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                          String responseData = response.body().string();
                        parseJSONWithJSONObject(responseData);   // 用 JSONObject 解析
                    }
                });
                break;

            case R.id.btn_gson:
                HttpUtil.sendOKHttpRequest("http://10.0.2.2/get_data.json", new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {}

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        String responseData = response.body().string();
                        parseJSONWithGSON(responseData);  // 用 GSON 解析
                    }
                });
                break;
        }

    }

    /**
     * 用 JSONObject 解析
     * @param jsonData 需要解析的数据
     */
    private void parseJSONWithJSONObject(String jsonData) { . . . }

    /**
     *  用 GSON 解析
     * @param jsonData
     */
    private void parseJSONWithGSON(String jsonData){ . . . }
}

  运行程序,打印的日志分别如下:

JSONObject 解析
GSON 解析

  关于网络编程先学习到这,下篇文章将进入安卓四大组件之服务的学习。

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

推荐阅读更多精彩内容