基于 Smack 的 xmpp 学习笔记

XMPP 开发学习

由于 aSmack 已经弃用,目前使用的是 smack 原版 4.2.0

aSmack is deprecated and obsolete. Starting with Version 4.1 Smack is able to run without modifications on Android.

More information on how to use Smack 4.1 in your Android Project can be found in the Smack 4.1 Readme and Upgrade Guide.

smack 的 github repo

Instructions how to use Smack in your Java or Android project are provided in the Smack 4.2 Readme and Upgrade Guide.

使用Android 的同学可以进入上面的链接。

Android 需要依赖的

dependencies {
  compile "org.igniterealtime.smack:smack-android-extensions:4.2.0"
  compile "org.igniterealtime.smack:smack-tcp:4.2.0"
}

A typical Smack setup may also want to additional declare dependencies on smack-tcp, smack-extensions and smack-experimental

如果是一个完整的 xmpp 还需要额外依赖 这几个库,我没有使用

但是后面在做注册用户这个需求的时候发现 AccountManager这个类在 smack-extensions里面,如果需要一些完整的功能,还是全加上好了,或者按照自己需求。

完整的 JID
Username@Domain/Resource
基本组成部分

Node/Username - 用户名/节点 用户的基本标识
Domain - 登陆的XMPP服务器域名
Resource - 资源/来源,用于区别客户端来源, XMPP协议设计为可多客户端同时登陆, Resource就是用于区分同一用户不同端登陆
Bare - 除去Resource部分, 包含Username@Domain

接口使用

连接 (Connection)

public void connect() {

        try {
            XMPPTCPConnectionConfiguration.Builder configBuilder = XMPPTCPConnectionConfiguration.builder();
            configBuilder.setHostAddress(InetAddress.getByName(SERVER_IP));
            configBuilder.setHost(SERVER_IP);
            configBuilder.setPort(SERVER_PORT);
            configBuilder.setXmppDomain(SERVER_IP);
            configBuilder.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled);
            configBuilder.setDebuggerEnabled(true);
            configBuilder.setCompressionEnabled(true);
            configBuilder.setSendPresence(false);

            mConnection = new XMPPTCPConnection(configBuilder.build());

            mConnection.connect();

            Log.d(TAG, "connection status is -> " + String.valueOf(mConnection.isConnected()));
        } catch (UnknownHostException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (XmppStringprepException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (IOException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (SmackException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (XMPPException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        }
    }

端口记得要去openfire后台去看,默认是 5222, 我一开始写成了9090,后来发现9090 的openfire 管理后台的端口。切记切记

登录(Login)

/**
     * 登录
     * @param user_name 用户名
     * @param passwd 密码
     * @return 是否成功
     */
    fun login(user_name: String, passwd: String): Boolean {
        log_d(TAG, "login")
        if (mConnection != null && mConnection!!.isConnected) {
            try {
                mConnection?.login(user_name, passwd)
                setStatus(XMPP_STATUS_ONLINE)

//                mConnection?.addConnectionListener(this)
                ChatManager.getInstanceFor(mConnection).addIncomingListener(this)
                ChatManager.getInstanceFor(mConnection).addOutgoingListener(this)

                log_d(TAG, "login successful")
                return true

            } catch (e: XMPPException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            } catch (e: SmackException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
                if (e is SmackException.AlreadyLoggedInException) {
                    return true
                }
            } catch (e: IOException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            } catch (e: InterruptedException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            }

        }
        return false
    }

setStatus是修改状态

/**
     * 更改用户状态
     * @param code 状态常量
     */
    fun setStatus(code: Int) {
        log_d(TAG, "setStatus")
        if (mConnection != null && mConnection!!.isConnected) {

            try {
                var presence: Presence? = null

                when (code) {
                    XMPP_STATUS_ONLINE -> {
                        log_d(TAG, "设置在线")
                        presence = Presence(Presence.Type.available)
                    }

                    XMPP_STATUS_CHAT_ME -> {
                        log_d(TAG, "设置Q我吧")
                        presence = Presence(Presence.Type.available)
                        presence.mode = Presence.Mode.chat
                    }

                    XMPP_STATUS_BUSY -> {
                        log_d(TAG, "设置忙碌")
                        presence = Presence(Presence.Type.available)
                        presence.mode = Presence.Mode.dnd
                    }

                    XMPP_STATUS_LEAVE -> {
                        log_d(TAG, "设置离开")
                        presence = Presence(Presence.Type.available)
                        presence.mode = Presence.Mode.away
                    }

                    XMPP_STATUS_OFFLINE -> {

                        log_d(TAG, "设置离线")
                        presence = Presence(Presence.Type.unavailable)
                    }
                }

                mConnection?.sendStanza(presence)
                log_d(TAG, "set status successful")
            } catch (e: SmackException.NotConnectedException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
                connect()
            } catch (e: InterruptedException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            }


        }
    }

注册(Register)

java.lang.IllegalStateException: Creating account over insecure connection

不安全的连接创建 account

accountManager.sensitiveOperationOverInsecureConnection(true);

现在不安全了

public boolean registerUser(String user_name, String passwd) {
        if (mConnection != null && mConnection.isConnected()) {
            // 已经connect 上了,才可以进行注册操作
            try {
                AccountManager accountManager = AccountManager.getInstance(mConnection);
                if (accountManager.supportsAccountCreation()) {
                    accountManager.sensitiveOperationOverInsecureConnection(true);
                    accountManager.createAccount(Localpart.from(user_name), passwd);

                    return true;
                }
            } catch (SmackException.NoResponseException e) {
                e.printStackTrace();
            } catch (XMPPException.XMPPErrorException e) {
                e.printStackTrace();
                if (e.getXMPPError().getCondition() == XMPPError.Condition.conflict) {
                    // 用户名已存在
                }
            } catch (SmackException.NotConnectedException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (XmppStringprepException e) {
                e.printStackTrace();
            }
        }

        return false;

    }

创建成功。openfile 可以看到,虽然没有 name [doge]

如果已经存在用户的,则会有异常。

发送消息(Send Message)

/**
     * 发送单人聊天 消息
     * @param chat 单人聊天室
     * @param message 发送的消息
     */
    fun sendSingleMessage(chat: Chat, message: String) {
        log_d(TAG, "sendSingleMessage->$message")
        if (mConnection != null) {
            try {
                chat.send(message)
            } catch (e: SmackException.NotConnectedException) {
                log_e(TAG, e.toString())
                e.printStackTrace()
                connect()
            } catch (e: InterruptedException) {
                log_e(TAG, e.toString())
                e.printStackTrace()
            }
        }
    }

之前一直使用 chat.send(message: String) 这个接口,在用spark 聊天的时候,一直显示我发送的是“广播”,但我确实是只发给了一个人,也就是“发给一个人的广播“。这本身就是一个很怪的事情。而且这个“广播”的丢失率还很高。后面看了接口,发现 chat.send(message: String) 默认发送的是 normal类型的(normal为什么是广播?),

后来改动了一下,好像改善了消息丢失的问题。

val stanza = Message()
stanza.body = message
stanza.type = Message.Type.chat
chat.send(stanza)

也只是修改了message 的 类型为 chat 而已。起码 spark 里面不会显示 广播了。

发送消息必须要先关注(订阅)对方,不然的话发送不能成功。

添加好友

    /**
     * 添加好友 无分组
     * @param user_name jid
     * @param nick_name 用户昵称
     * @return 是否添加成功
     */
    fun addFriend(user_name: String, nick_name: String): Boolean {
        log_d(TAG, "addFriend")
        if (mConnection != null) {
            try {
                Roster.getInstanceFor(mConnection).createEntry(JidCreate.bareFrom(generateJID(user_name)),
                        nick_name, null)
                return true
            } catch (e: Exception) {
                log_e(TAG, e.toString())
                e.printStackTrace()
            }
        }
        return false
    }

添加好友到指定分组

    /**
     * 添加好友 加入到指定分组
     * @param user_name jid
     * @param nick_name 用户昵称
     * @param group_name 用户组
     * @return 是否添加成功
     */
    fun addFriendToGroup(user_name: String, nick_name: String, group_name: String): Boolean {
        log_d(TAG, "addFriendToGroup")
        if (mConnection != null) {
            try {
                val subscription = Presence(Presence.Type.subscribe)
                subscription.to = JidCreate.entityBareFrom(generateJID(user_name))
                mConnection?.sendStanza(subscription)
                Roster.getInstanceFor(mConnection).createEntry(JidCreate.entityBareFrom(generateJID(user_name)),
                        nick_name,
                        arrayOf(group_name))

                return true
            } catch (e: Exception) {
                log_e(TAG, e.toString())
            }
        }
        return false
    }

获取好友(Get All Friends)

    /**
     * 获取所有好友信息
     * @return 所有好友列表
     */
    fun getAllFriends(): List<RosterEntry>? {
        log_d(TAG, "getAllFriends")
        if (mConnection != null) {
            val entryList = ArrayList<RosterEntry>()
            val rotryEntry = Roster.getInstanceFor(mConnection).entries
            entryList += rotryEntry
            return entryList
        }

        return null
    }

获取好友列表

还封装了好几个接口

  • isAuthenticated() 判断是否已经登录
  • isConnect() 判断是否连接
  • disconnect() 断开连接
  • getGroups() 获取所有分组
  • getFriendsInGroup 获取指定分组内的所有好友
  • ... ... 等等

使用的时候,我用了 RxJava 的 链式调用,用起来还不错。

登录
fun login(user_name: String, passwd: String) {
            log_d(TAG, "login name->$user_name")

            curUserName = user_name
            curPasswd = passwd
            if (mXmppApiManager.isAuthenticated()) {
                // 已经登录过
                val userName = mXmppApiManager.getAuthenticatedUser()
                if (!TextUtils.isEmpty(userName)) {
                    log_d(TAG, "account logined as -> " + userName!!)
                } else {
                    log_d(TAG, "login successful as -> " + user_name)
                }
            } else {
                if (!mXmppApiManager.isConnected()) {
                    // 先进行连接

                    Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                        emitter.onNext(mXmppApiManager.connect())
                    })
                            .subscribeOn(Schedulers.io())
                            .flatMap { isConnectSuccessful ->
                                if (isConnectSuccessful) {
                                    // 连接成功后
                                    // 进行注册
                                    log_d(TAG, "xmpp connect successful")
                                    Observable.just(mXmppApiManager.registerUser(user_name, passwd))
                                } else {
                                    // 连接失败
                                    log_d(TAG, "xmpp connect failed")
                                    // 几秒后进行重连
                                    handler.postDelayed(reconnectRunnable, RECONNECT_TIME_MILLSECOND)
                                    Observable.just(false)
                                }
                            }
                            .flatMap { isRegisterSuccessful ->
                                if (isRegisterSuccessful) {
                                    // 注册成功
                                    // 进行登录
                                    log_d(TAG, "xmpp register successful")
                                    Observable.just(mXmppApiManager.login(user_name, passwd))
                                } else {
                                    // 注册失败
                                    log_d(TAG, "xmpp register failed")
                                    Observable.just(false)
                                }
                            }.observeOn(AndroidSchedulers.mainThread())
                            .subscribe({ isLoginSuccessful ->
                                if (isLoginSuccessful!!) {
                                    log_d(TAG, "login successful as -> " + mXmppApiManager.getAuthenticatedUser()!!)
                                } else {
                                    log_d(TAG, "login failed")
                                }
                            })
                } else {
                    // 直接进行登录操作
                    Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                        emitter.onNext(mXmppApiManager.registerUser(user_name, passwd))
                    })
                            .subscribeOn(Schedulers.io())
                            .flatMap { isRegisterSuccessful ->
                                if (isRegisterSuccessful) {
                                    // 注册成功
                                    // 进行登录
                                    log_d(TAG, "xmpp register successful")
                                    Observable.just(mXmppApiManager.login(user_name, passwd))
                                } else {
                                    // 注册失败
                                    log_d(TAG, "xmpp register failed")
                                    Observable.just(false)
                                }
                            }.observeOn(AndroidSchedulers.mainThread())
                            .subscribe { isLoginSuccessful ->
                                if (isLoginSuccessful!!) {
                                    val userName = mXmppApiManager.getAuthenticatedUser()
                                    if (!TextUtils.isEmpty(userName)) {
                                        log_d(TAG, "account logined as -> " + userName!!)
                                    } else {
                                        log_d(TAG, "login successful as -> " + userName)
                                    }
                                } else {
                                    log_d(TAG, "login failed")
                                }
                            }
                }

            }
        }

在登录的时候先进行一些连接的判断。

这样使用的时候就不需要去处理是否连接的问题。

还有注册也是一样

        fun register(user_name: String, passwd: String) {
            // 如果已经验证过的,需要退出登录?
            log_d(TAG, "register name->$user_name")

            if (!mXmppApiManager.isConnected()) {
                // 先进行连接
                Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                    emitter.onNext(mXmppApiManager.connect())
                })
                        .subscribeOn(Schedulers.io())
                        .observeOn(Schedulers.io())
                        .flatMap { isConnectSuccessful ->
                            if (isConnectSuccessful) {
                                // 连接成功后
                                // 进行注册
                                log_d(TAG, "xmpp connect successful")
                                Observable.just(mXmppApiManager.registerUser(user_name, passwd))
                            } else {
                                // 连接失败
                                log_d(TAG, "xmpp connect failed")
                                // 几秒后进行重连
                                handler.postDelayed(reconnectRunnable, RECONNECT_TIME_MILLSECOND)
                                Observable.just(false)
                            }
                        }
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe { isRegisterSuccessful ->
                            if (isRegisterSuccessful) {
                                // 注册成功
                                // 进行登录
                                log_d(TAG, "xmpp register successful")
                            } else {
                                // 注册失败
                                log_d(TAG, "xmpp register failed")
                            }
                        }
            } else {
                // 直接进行注册操作
                Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                    emitter.onNext(mXmppApiManager.registerUser(user_name, passwd))
                })
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe { isRegisterSuccessful ->
                            if (isRegisterSuccessful) {
                                // 注册成功
                                log_d(TAG, "xmpp register successful")
                            } else {
                                // 注册失败
                                log_d(TAG, "xmpp register failed")
                            }
                        }
            }
        }

这样用起来就比较方便。

需要注意的问题

如何保持XMPP连接稳定

遇到问题

应用程序处于活动状态。但是迟早,XMPP连接都没有任何提示。服务器表示客户端仍处于联机状态,但未发送或者接收数据包。

XMPPConnection connection.isConnected()返回 True。

实际上 客户端 无法知道 实际连接已经丢失。

解决方案
  1. 首先在 openfire 服务器后台发现了这个

    服务器可以在断开闲置连接前发送XMPP Ping请求给该客户端。客户端必须回复 Ping请求,这样服务器能判断客户端连接确实是闲置状态。 XMPP规范要求所有客户端必须响应 Ping请求。如果客户端不支持该Ping请求,必须返回错误(这本身就是一个响应)。

    所以,我们的客户端必须对 服务器的 ping 请求进行回复。但是 smack 4.2 的 incomingMessage 仅仅会返回 用户消息,所以在 incomingmessage 回调里面没有办法完成这件事情。

    搜索一番之后发现一个:

    connection = new XMPPTCPConnection(config);  
    PingManager pingManager = PingManager.getInstanceFor(connection); pingManager.setPingInterval(300);//seconds
    

    在 connect 完成后 加了这个设置,测试了一下,仍然后断线的问题,但是频率少了。应该是有一点作用的。

  2. 添加 XMPPConnectionListener 连接监听

    如果需要保持长期的连接,需要对很多异常进行进行处理,也就是重连机制的实现。

    比如说

    • 监听到 connectitonCloseError 的时候
    • 监听到 connectionClose 的时候
    • 或者是连接异常的时候

    都可以加入 自动重连的逻辑,从而保证 连接的稳定性。

  3. 添加 网络变化 监听

    移动应用的网络情况千变万化,有时候并不稳定,所以需要加入网络情况的判断

    class NetworkChangeReceiver: BroadcastReceiver() {
    
        private val TAG = NetworkChangeReceiver::class.java.simpleName
    
        override fun onReceive(context: Context?, intent: Intent?) {
            log_i(TAG, "onReceive 网络状态发生变化")
    
            // 如果api小于21,getNetworkinfo(int networType) 已弃用
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                log_i(TAG, "API 小于 21")
    
                val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    
                // Wi-Fi 连接
                val wifiNetworkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
                // 移动数据连接
                val dataNetworkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
    
                if (wifiNetworkInfo.isConnected && dataNetworkInfo.isConnected) {
                    log_i(TAG, "Wi-Fi 已连接,移动数据已连接")
                    RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_WIFI_CONNECTED) as Object)
                } else if (wifiNetworkInfo.isConnected && !dataNetworkInfo.isConnected) {
                    log_i(TAG, "Wi-Fi 已连接,移动数据已断开")
                    RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_WIFI_CONNECTED) as Object)
                } else if (!wifiNetworkInfo.isConnected && dataNetworkInfo.isConnected) {
                    log_i(TAG, "Wi-Fi 已断开,移动数据已连接")
                    RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_MOBILE_CONNECTED) as Object)
                } else {
                    RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_DISCONNETED) as Object)
                    log_i(TAG, "Wi-Fi 已断开,移动数据已断开")
                }
    
            } else {
                log_i(TAG, "API 大于 21")
    
                val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    
                val networks = connectivityManager.allNetworks
    
                var result = 0 // mobile false = 1, mobile true = 2 wifi = 4
    
                for (network in networks) {
                    val networkInfo = connectivityManager.getNetworkInfo(network)
    
                    networkInfo?.let {
                        //检测到有数据连接,但是并连接状态未生效,此种状态为wifi和数据同时已连接,以wifi连接优先
                        if (networkInfo.type == ConnectivityManager.TYPE_MOBILE && !networkInfo.isConnected) {
                            result += 1
                        }
    
                        //检测到有数据连接,并连接状态已生效,此种状态为只有数据连接,wifi并未连接上
                        if (networkInfo.type == ConnectivityManager.TYPE_MOBILE && networkInfo.isConnected) {
                            result += 2
                        }
    
                        //检测到有wifi连接,连接状态必为true
                        if (networkInfo.type == ConnectivityManager.TYPE_WIFI) {
                            result += 4
                         }
                   }
             }
              
              // 存在组合情况,以组合相加的唯一值作为最终状态的判断
               when (result) {
                   0   ->  {
                       log_i(TAG, "Wi-Fi 已断开,移动数据已断开")
                   }
                   2   ->  {
                       log_i(TAG, "Wi-Fi 已断开,移动数据已连接")
                   }
                   4   ->  {
                       log_i(TAG, "Wi-Fi 已连接,移动数据已断开")
                   }
                   5   ->  {
                       log_i(TAG, "Wi-Fi 已连接,移动数据已连接")
                   }
               }
           }
    
       }
    }           
    

在网络变化的时候,进行连接判断,或者重连操作,也是一种保持稳定性的方法。

使用的第三方库

compile "org.igniterealtime.smack:smack-android-extensions:4.2.0"
compile "org.igniterealtime.smack:smack-tcp:4.2.0"
compile "org.igniterealtime.smack:smack-extensions:4.2.0"

// 只是在使用的时候 方便一些
compile 'io.reactivex.rxjava2:rxjava:2.1.8'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
  • 可以根据网络状况自动重连
  • 可以在中断后自动进行重连

代码在这里
已发布至Jimbray

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

推荐阅读更多精彩内容