Xmpp客户端库Smack(4.1.4)文档整理

一、Smack库概述

    Smack是一个开源、易用的XMPP/Jabber客户端库,它使用Java语言开发,由Jive Software开发。
    Smack的优点是编程简单。
    Smack的缺点是其API并非为大量并发用户设计,每个客户都要1个线程,占用资源相对较大,因此用Smack做模拟测试时,1台机器只能模拟有限(数千个)客户。
    截止2015年10月25日,Smack库已经发展到4.1.4版。最新的好消息是Smack在4.1.0版后将直接支持Android系统,而无需再使用以前的Smack移植版aSmack库了。Smack库源码托管于GitHub,主页见: https://github.com/igniterealtime/Smack/

1.1 Smack 4的改变

Smack库从3.4版发展到4.0.x版后,其API有较大的变化,主要有:
  1. 把Connection类重命名为XMPPConnection类,XMPPConnection类是XMPPTCPConnection类和XMPPBOSHConnection类的父类。
  2. 把各种Provider类进行了分包
  3. keep-alive(持久连接)机制从smack-core库移到了smack-extensions库,keep-alive机制现在由PingManager类提供。
  4. PrivacyList类的toString()方法重命名为getName()
  5. 当Chat实例的所有引用都撤掉后,应该调用Chat.close()方法
否则Chat对象会有内存泄露的隐患,直到ChatManager对象被垃圾回收器回收后内存泄露隐患才会消失。
  6. ServerTrustManager类被移除了,如果要使用带SSL认证的XMPP,你只需提供自己的SSLContext对象给ConnectionConfiguration对象即可。
  7. Packet.setProperty()从smack-core库移到了smack-extensions库,其API现在可以在org.jivesoftware.smackx.jiveproperties包中找到。
  8. Connection.getAccountManager()方法现在改成了AccountManager.getInstance(XMPPConnection)方法
  9. 异常API做了改进
  10.ToContains过滤器被移除了

1.2 Smack库的组成

Smack库可以内嵌到任意的Java应用程序中。Smack库有数个JAR文件组成,非常具有灵活性。
  1. smack-core.jar提供了核心XMPP功能。都是XMPP RFC规范定义的XMPP特性。
  2. smack-extensions.jar支持许多由XMPP Standards Foundation定义的扩展(XEP)功能。包括群聊、文件传输、用户搜索等等。以后可查看文档《扩展手册》:https://github.com/igniterealtime/Smack/blob/master/documentation/extensions/index.html(目前还是无效的)
  3. smack-experimental.jar支持许多由XMPP Standards Foundation定义的体验性(XEP)功能。其API和功能特性都被认为是不稳定的。
  4.smack-legacy.jar支持许多由XMPP Standards Foundation定义的遗留(XEP)功能。
  5. smack-bosh.jar支持BOSH通信(XEP-0124规范定义的)。此代码被认为处于Beta阶段。
  6. smack-jingle.jar支持Jingle。此代码很老,目前处于无维护的状态。
  7. smack-resolver-dnsjava.jar支持对DNS SRV记录的解析,主要用于那些不支持javax.naming API的平台。
  8. smack-debug.jar用于协议流量的增强型GUI调试器。当调试模式开启后,如果它在类路径下,它会自动被使用。以后可查看文档《调试模式》:https://github.com/igniterealtime/Smack/blob/master/documentation/debugging.html(目前还是无效的)

二、Smack的配置

Smack的初始化过程涉及到2阶段的调用。
  1.初始化系统属性通过SmackConfiguration类初始化所有的系统可访问属性,这些属性都是通过getXXX方法取回属性值的。
  2. 初始化启动类如果继承了SmackInitializer接口后,都可以在调用initialize()方法后得到初始化,这意味着得到初始化的类在启动后都是活动的。如果没有继承SmackInitializer接口,那么要实现初始化,必须要放置一个静态代码块来实现——他在类装载时会自动执行。初始化是通过配置文件来完成的。默认情况下,Smack会载入Smack JAR文件中内嵌的配置文件(它位于org.jivesoftware.smack/smack-config.xml)。这个指定的配置文件包含了一系列需载入初始化的类列表。所有的管理器类型的类都需要被初始化,这些管理器类就包含在上面所说的初始化列表中。

三、XMPPConnection管理

3.1 创建连接

org.jivesoftware.smack.XMPPConnection类可管理到XMPP服务器的连接,它默认的连接实现类是org.jivesoftware.smack.XMPPTCPConnection。它主要使用两个构造方法,
  一个是XMPPTCPConnection(StringserverName)方法,参数为服务器名。连接会使用所有默认的设置,有:
    1)执行DNSSRV查询,找到服务器确切的地址和端口(通常是5222)。
    2)与服务器协商最大数安全,包括TLS加密。但如果有必要,连接会回落到较低的安全设置。
    3)XMPP资源名“Smack”会被用于连接。
  第二个是XMPPTCPConnection(ConnectionConfigurationcc)构造器,它会指定高级的连接设置。其中包括:
    1)手动指定服务器地址和端口,而不是通过DNSSRV查询。
    2)能开启连接压缩。
    3)指定自定义的连接资源名(如Work或Home)。用户到服务器的每一个连接都必须有唯一的资源名。比如对于用户"jsmith@example.com",完整的带资源的地址应该是"jsmith@example.com/Smack"。通过携带唯一的资源名,用户可以同时从不同的位置登录到同一个服务器,这适用于多设备的情况。每一个资源使用的在线优先级值:用于决定由哪一个带资源的指定连接来接收到地址"jsmith@example.com"的消息。

第一种连接方式:

    boolean target = false;  
    AbstractXMPPConnection conn = new XMPPTCPConnection(username, password, serverName);  
    try {  
        conn.connect();  
        target = conn.isConnected();  
        if(target){  
            System.out.println("XMPP 服务器连接成功");  
        }else{  
            System.out.println("XMPP 服务器连接不成功");  
        }  
    } catch (SmackException | IOException | XMPPException e) {  
        e.printStackTrace();  
    }  

第二种连接方式:

   boolean target=false;  
   XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()  
                  .setUsernameAndPassword(username, password)  
                  .setServiceName(serverName)  
                  .setHost(serverName)  
                  .setPort(5222)  
                  .build();  
   AbstractXMPPConnection conn = new XMPPTCPConnection(config);  
        try {  
            conn.connect();  
            target = conn.isConnected();  
            if(target){  
                System.out.println("XMPP 服务器连接成功");  
            }else{  
                System.out.println("XMPP 服务器连接不成功");  
            }  
        } catch (SmackException | IOException | XMPPException e) {  
            e.printStackTrace();  
        }  

3.2 连接和关闭连接

//为新连接创建配置
ConnectionConfigurationconfig = new ConnectionConfiguration(“jabber.org”, 5222);
AbstractXMPPConnectionconn = new XMPPTCPConnection(config);
//连接到服务器
conn.connect();
//登录到服务器
conn.login(“username”,“password”,“SomeResource”);
 
…
//关闭连接
conn.disconnect();

    默认情况下,一旦连接断开,Smack会尝试重建连接。使用ConnectionConfiguration类的setReconnectionAllowed(Boolean)方法可以开启或关闭重连的功能。重连管理器会立即尝试重连到服务器,并且会增加延时设置,以便提高重连的成功率。在重连管理器正在等待下一次重连的期间,如果你想强制重连,可以使用AbstractXMPPConnection类的connect()方法,它会尝试建立一个新连接。如果手动尝试也失败了,那么重连管理器会继续重连的工作。

四、使用Chat消息通信

    来回收发消息是即时通信的核心功能。尽管单条消息是以包的形式发送和接收的,通常还是把他视为聊天的消息字符串,使用org.jivesoftware.smack.Chat类。

4.1 Chat类

    一个聊天Chat会在两个用户之间创建一个消息线程(通过线程ID)。下面的代码片段演示了怎样创建一个新聊天,然后向用户发送一条文本消息:

  //假设已经创建了一个名为"connection"的XMPPConnection
  ChatManager chatmanager = connection.getChatManager();
  Chat newChat = chatmanager.createChat("jsmith@jivesoftware.com", newMessageListener(){

    public void processMessage(Chat chat,Message message){
          System.out.println(“Receivedmessage: “+ message);
    }
  });
  try{
      newChat.sendMessage(“Howdy!”);
  }catch(XMPPExceptione){
      System.out.println(“Error Deliveringblock”);
  }

Chat.sendMessage(String)方法可以方便地创建一个消息Message对象,用字符串参数设置消息正文Body,然后发送消息。在某些情况下你可能希望在发送消息前设置额外的值,使用Chat.createMessage()方法和Chat.sendMessage(Message)方法,如下面的代码片段所示:

   Message newMessage = new Message();
   newMessage.setBody(“Howdy!”);
   message.setProperty(“favoriteColor”,“red”);
   newChat.sendMessage(newMessage);

前面的例子中,我们可以注意到,在创建聊天Chat时指定了一个消息监听器MessageListener,在任意时刻,当来自其它用户的聊天消息到达后,消息监听器会得到通知。下面的代码片段使用了监听器做鹦鹉学舌,它会回显来自其他用户传递的消息。
//假设在聊天Chat中已经设置了消息监听器MessageListener

   publicvoid processMessage(Chat chat, Message message){
       // 把用户发送的消息内容发送给用户
       chat.sendMessage(message.getBody());
   }

4.2 来电聊天

    当提示有另一个用户的聊天消息到了后,设置有轻微的不同,因为你是首次接收到聊天消息。取代明确地创建一个Chat来发送消息,当ChatManager创建了Chat实例后,你需要注册处理新创建的Chat实例。ChatManager会通过线程ID找到匹配的Chat,如果Chat不存在,那么它会创建一个新Chat对象来匹配。要得到这个新Chat,你必须注册来得到通知。可以注册一个消息监听器来接收所有要到来的消息。

  //假定已经创建了名为”connection”的XMPPConnection
  ChatManager  cm = connection.getChatManager().addChatListener(
   new ChatManagerListener(){
      @Override
      public void chatCreated(Chat chat, BooleancreatedLocally){
          if(!createdLocally)
              chat.addMessageListener(newMyNewMessageListener());
    }
});

    除了基于线程的Chat消息,也有一些客户端不发送线程ID作为Chat的一部分。要处理这种情况,Smack会基于JID尝试匹配接收的消息到最匹配现有的Chat。它会尝试用完整的JID来查找Chat,如果搜不到,再尝试用基本的JID来查找Chat。如果找不到现有的Chat来匹配,那么会创建一个新Chat。

五、名单Roster和在线状态Presence

名单可以让你跟踪其他用户是否在线,而且名单可以让你把用户组织到群组,比如朋友群或工作群。而其它的即时通信IM系统则把名单Roster视为好友列表、联系人列表等等。

5.1 名单条目

    Roster用于跟踪其他用户是否在线。用户的联系人可以以分组的方式进行组织,比如“好友”、“同事”。然后就可以查看组中的每个用户是否在线了。要检索Roster,使用XMPPConnection.getRoster()方法。Roster类允许你查找所有的Roster实体,以及他们属于哪个组,每个实体当前的在线状态。名单中的每一个用户都由RosterEntry来表示,它包括:
    1)一个XMPP地址(比如”jsmith@example.com”)
    2)你为用户编写的备注姓名(比如”Joe”)
    3)名单中的群列表。如果名单的条目不属于任何群组,那么它被称为"unfiledentry"。
下面的代码片段会打印名单中所有的条目:

   //假定已经创建了名为”connection”的XMPPConnection
   Rosterroster = connection.getRoster();
   Collection<RosterEntry>entries = roster.getEntries();
   for(RosterEntryentry : entries){
             System.out.println(entry);
   }

还有获取单个条目的方法、获取"unfiledentry"的方法,获取一个群或所有群的方法。

5.2 在线状态

    名单中的每个条目都有一个与之相关的在线状态。Roster.getPresence(Stringuser)方法会返回一个表示用户是否在线的Presence对象。如果为空是你还没有订阅用户是否在线的返回。注意:通常情况下,在线状态的订阅总是绑定到名单中的用户,但这并不适应所有的情况。
    一个用户的在线状态要么是在线,要么是离线。当用户在线时,他们的在线状态还可以包含扩展的信息,比如用户当前正在做什么,用户是否愿意被打扰等等。具体参考Presence类。

5.3 监听名单Roster和在线状态Presence的改变

    Roster类的典型应用场景是以树状结构显示用户群和列表,并且用户列表中包含用户是否在线的状态。比如,参考下图所示的一个ExodusXMPP客户端的Roster。



    在线状态的信息可能会经常变化,Roster条目也可能经常修改或删除。要监听Roster和Presence数据的变化,你应该使用RosterListener。要得到Roster改变的所有提醒,那么必须在登录XMPP服务器之前注册RosterListener。下面的代码片段注册了一个Roster的RosterListener,它能够在标准输出中打印任何Presence的改变。一个标准的客户端可以使用类似的代码用变化的信息来更新Roster界面。

   //假定已经创建了名为”connection”的XMPPConnection
   Roster roster = connection.getRoster();
   roster.addRosterListener(new RosterListener(){
       // 忽略事件
       public void entriesAdded(Collection<String> addresses){}

       public void entriesDeleted(Collection<String>addresses){}

       public void entriesUpdated(Collection<String> addresses){}

       public void presenceChanged(Presencepresence){
           System.out.println(“Presencechanged: “+ presence.getFrom() + “ “ + presence);
    }
});

5.4 添加Entries到Roster

    Roster和Presence使用一种基于权限的模型,用户必须得到其他人的许可才能把这些人添加到Roster。这样可以保护用户的隐私,确保了只有获得同意的用户才能查看到他们的Presence信息。因此,当你想添加某个用户到你的Roster中,必须得到该用户接受你的请求才可以。
如果有用户请求订阅你的在线状态Presence,这个用户必须先把你添加到他的Roster,因此他会发起请求,你必须选择接受或拒绝该请求。Smack通过以下三种方式来处理Presence的预订请求:
    1)自动接受所有Presence的预订请求
    2)自动拒绝所有Presence的预订请求
    3)手动处理每一个Presence预订请求
    这三种方式可以通过Roster.setSubscriptionMode(intsubscriptionMode)方法来设置请求的处理方式。简单的客户端通常使用第一种自动方式处理预订请求,而功能比较全的客户端应该选择第三种手动处理请求的方式,让终端用户自行决定是接受请求或是拒绝请求。如果使用手动方式,应该注册一个PacketListener来监听Presence.Type.SUBSCRIBE类型的Presence包。

六、读写Packet(数据包)

    从客户端发送到XMPP服务器的每一条消息都称为一个Packet(数据包)。org.jivesoftware.smack.packet库中包含了XMPP支持的消息Message、在线状态Presence、IQ)三种不同的基本数据包类型的封装类。而像Chat或GroupChat这样的类则提供了更高层的结构来管理数据包的自动创建和发送。但是,开发者还是可以直接创建和发送数据包的。下面的代码就是修改自己的在线状态,让其他人知道你不在线。

   //假设已经创建了一个名为"connection"的XMPPConnection
   Presence presence = new Presence(Presence.Type.unavailable);  
   presence.setStatus("Gone fishing");  

   connection.sendStanza(presence);  

6.1 PacketListener与PacketCollector

Smack提供了两种读取到来的数据包的方式:
    org.jivesoftware.smack.PacketCollector(包收集器):该类提供synchronously(同步)方法,等待接受数据包(Packets).
    org.jivesoftware.smack.PacketListener(包监听器): 该接口提供asynchronously(异步)方法,等待结束数据(Packets)。
    两者都使用PacketFilter实例来判断应该处理哪一个数据包。PacketListener(包监听器)用于事件风格的编程,而PacketCollector(包收集器)有一个数据包的结果队列,你可以做轮询或阻塞等操作。也就是说,如果你想在数据包到来时执行一些动作,那么包监听器很适合。如果你想等待指定的数据包的到来,那么包收集器很适合。包收集器和包监听器都使用Connection连接实例创建。
    数据包集合(PacketCollector)和数据包接口(PacketListener)是通过XMPPConnection实例对象创建。

6.2 拦截器

    org.jivesoftware.smack.filter.StanzaFilter接口可以决定,那些特定的数据包会被传递到PacketCollector 或者PacketListener。
许多预先定义的拦截器都实现了org.jivesoftware.smack.filter接口。

下面的代码片段,演示了注册的数据包监听器和数据包集合

// Create a packet filter to listen for new messages from a particular  
// user. We use an AndFilter to combine two other filters._  
StanzaFilter filter = new AndFilter(new StanzaTypeFilter(Message.class),  
        new FromContainsFilter("mary@jivesoftware.com"));  

// Assume we've created an XMPPConnection name "connection".  
// First, register a packet collector using the filter we created.  
PacketCollector myCollector = connection.createPacketCollector(filter);  

// Normally, you'd do something with the collector, like wait for new packets.   
// Next, create a packet listener. We use an anonymous inner class for brevity.
PacketListener myListener = new PacketListener() {  
        **public** **void** processPacket(Packet packet) {  
            // Do something with the incoming packet here._  
        }  
    };  
// Register the listener._  
connection.addPacketListener(myListener, filter);  

标准段落(字符)拦截器
    smack 类库中已经包含很多包拦截器,你也可以创建属于自己的拦截器,只需要代码实现StanzaFilter接口,默认的拦截器包含如下:

  • StanzaTypeFilter --特定类型的数据包过滤器
  • StanzaIdFilter -- 特定数据包标识筛选器
  • ThreadFilter -- 特定线程标识数据包过滤器
  • ToContainsFilter -- 特定发送地址数据包过滤器
  • FromContainsFilter -- 特定接受地址数据包过滤器.
  • StanzaExtensionFilter -- 特定包扩展数据包过滤器
  • AndFilter -- implements the logical AND operation over two filters.
  • OrFilter -- implements the logical OR operation over two filters.
  • NotFilter -- implements the logical NOT operation on a filter.

七、信息包插件提供者

    Smack提供的体系是堵塞自定义的XML信息包扩展和IQ包分析器的系统(The Smack provider architecture is a system for plugging in custom XML parsing of packet extensions and IQ packets)。标准的Smack扩展(Smack Extensions)是使用提供者的体系结构搭建的。存在以下两种类型的提供者:

  • IQProvider –将IQ请求( IQ requests)解析成Java对象(Java objects)
  • PacketExtension – 将附属在信息包上的XML子文档解析成信息包扩展实例(PacketExtension instances)

7.1 IQProvider

     IQProvider 默认情况下,Smack只知道如何处理类似以下几个名字空间的子信息包的IQ信息包(IQ packets):
     jabber:iq:auth
     jabber:iq:roster
     jabber:iq:register
因为许多IQ类型是XMPP及其扩展部分的一部分,所以提供一个可插入的IQ分析机制。IQ Providers被程序自动的注册或通过创建在你的JAR 文件的META-INF目录下创建一个mack.providers文件。该文件是一个包含一个或多个iqProvider条目(iqProvider entries)的XML文档,如下例所示:

<?xml version="1.0"?>  
<smackProviders>
       <iqProvider>          
                <elementName>query</elementName>          
                <namespace>jabber:iq:time</namespace>          
                <className>org.jivesoftware.smack.packet.Time</className>      
       </iqProvider> 
</smackProviders>  

    每一个IQ provider都和一个元素名(element name)和名字空间( namespace)相联系。在上面的例子中,元素名是query,名字空间是abber:iq:time。如果有多重提供者条目(multiple provider entries)尝试注册并控制相同的名字空间,那么从类路径(classpath)载入的第一个条目将有优先权。
    IQ provider类可以实现IQProvide接口,或者继承IQ类。在前面的例子中,每一个IQProvider负责解析原始的XML流从而创建一个IQ实例。在下面的例子中,bean introspection将被用于尝试自动使用在IQ packet XML中发现的值设置IQ实例的属性。一个XMPP时间信息包如下所示:

<iq type=’result’ to=’joe@example.com’ from=’mary@example.com’ id=’time_1’>    
     <query xmlns=’jabber:iq:time’>         
          <utc>20020910T17:58:35</utc>         
          <tz>MDT</tz>         
          <display>Tue Sep 10 12:58:35 2002</display>     
    </query> 
</iq>  

    为了让这个信息包自动的映射成上面的providers file中所列的时间对象(Time object),它必须有以下几个方法:setUtc(String), setTz(String), 和 setDisplay(String)。自动检查(introspection)的服务将试着自动的将字符串值转化成a boolean, int, long, float, double,或 Class 类型。转化成何种类型由IQ实例的需要来决定。

7.2 PacketExtensionProvider

    PacketExtensionProvider 信息包插件提供者(Packet extension providers)为信息包提供一个可插入的系统,这些信息包是一个IQ, message和presence packets的自定义名字空间的子元素。每一个插件提供者(extension provider)使用一个元素名(element name)和名字空间(namespace)在smack.providers文件中注册,如下例所示:

 <?xml version="1.0"?> 
      <smackProviders>    
             <extensionProvider>        
                     <elementName>x</elementName>         
                     <namespace>jabber:iq:event</namespace>
 <className>org.jivesoftware.smack.packet.MessageEvent</className>
               </extensionProvider> 
</smackProviders>  

    如果有多重提供者条目(multiple provider entries)尝试注册并控制相同的名字空间,那么从类路径(classpath)载入的第一个条目将有优先权。 一旦在一个信息包中发现信息包插件,解析器将传递到正确的提供者。每一个提供者可以实现PacketExtensionProvider接口或者是一个标准的Java Bean。在前面的例子中,每一个插件提供者(extension provider)负责解析原始的XML流去构造一个实例。在下面的例子中,bean introspection将被用于尝试自动使用在信息包插件子元素(packet extension sub-element)中的值设置类的属性。 当一个插件提供者(extension provider)没有用元素名(element name)和名字空间(namespace)对注册是,Smack将存储所有在缺省信息包插件(DefaultPacketExtension)对象中的最高级别元素(top-level elements),并匹配到信息包上。

七、smack debug模式讲解

    smakc 内置两套debuging 控制台,让你追踪XMPP服务端和客户端之间所有XML 流动,在smack-debug.jar包含Lite调试器和增强调试器,在smack-core.jar包含debug输出控制台。
调试模式可以用不同的方式来启用:
1、在创建新连接之前,先添加以下代码行:

SmackConfiguration.DEBUG = true;  

1、设置Java系统属性smack.debugenabled真实。可以在命令行上设置系统属性,例如:

java -Dsmack.debugEnabled=true SomeApp  

如果你希望在你的应用程序中显式禁用调试模式,可以通过使用命令行参数 或者是在在打开新连接之前,请在应用程序中添加以下行:

SmackConfiguration.DEBUG = false;  

smack 使用以下逻辑决定调试控制台使用;
1、它首先会尝试使用在Java系统属性指定类smack.debuggerclass调试器。如果你需要开发属于自己的Debug,需要smackdebugger接口,然后设置系统属性的命令行如:

java -Dsmack.debuggerClass=my.company.com.MyDebugger SomeApp  

2、如果第一步失败,Smack会尝试使用增强型Debug,smackx-debug.jar文件中包含了增强型Debug,因此,你需要将jar文件的路径添加到ClassPath路径中。smack-core.jar Lite调试器和增强调试器不可用的情况下,只有控制台调试器。

增强调试器



    当smack Debug模式是启动时,Debug窗口会显示每一个创建连接信息,同时也包含以下信息:

  • XMPPConnection tabs -- 每个选项卡显示有关连接的调试信息.
  • Smack info tab -- shows information about Smack (e.g. Smack version, installed components, etc.). The connection tab will contain the following information:
  • All Stanzas --通过smack 分析发送和接收的数据包的信息分析.
  • Raw Sent Stanzas -- 通过Smack产生的原始XML流量发送到服务器.
  • Raw Received Stanzas -- 服务器向客户端发送的原始XML.
  • Ad-hoc message -- 允许发送特定类型的数据包.
  • Information -- 显示连接状态和统计数.

Lite Debugger



    当smack Debug模式是启动时,Debug窗口会显示每一个创建连接信息,同时也包含以下信息:

  • Client Traffic (red text) -- 通过Smack产生的原始XML流量发送到服务器.
  • Server Traffic (blue text) --服务器向客户端发送的原始XML.
  • Interpreted Stanzas (green text) -- shows XML packets from the server as parsed by Smack. Right click on any of the panes to bring up a menu with the choices to copy of the contents to the system clipboard or to clear the contents of the pane.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容