即时通讯基础

即时通讯系列阅读

  1. 即时通讯基础
  2. 即时通讯:XMPP基础
  3. 即时通讯:XMPP项目实践-微聊
  4. Smack类库最好的学习资料

1. 即时通讯简介

即时通讯(Instant Messaging)是目前Internet 上最为流行的通讯方式,各种各样的即时通讯软件也层出不穷;服务提供商也提供了越来越丰富的通讯服务功能。不容置疑,Internet 已经成为真正的信息高速公路。从实际工程应用角度出发,以计算机网络原理为指导,结合当前网络中的一些常用技术,编程实现基于C/S 架构的网络聊天工具是切实可行的。

目前,中国市场上的企业级即时通信工具主要包括:信鸽、视高科技的视高可视协同办公平台、263EM、群英CC2010、通软联合的GoCom、腾讯公司的RTX、IBM 的Lotus Sametime、点击科技的GKE、中国互联网办公
室的imo、中国移动的企业飞信、华夏易联的e-Link、擎旗的UcStar 等。相对于个人即时通信工具而言,企业级即时通信工具更加强调安全性、实用性、稳定性和扩展性。

1.1 即时聊天的解决方案

  • socket:套接字,连接需要ip端口,分为tcp和udp两种形式
  • xmpp:xmpp + openfire + asmack

1.2 常见协议

im_protocol.png

1.3 常见的术语

  • xmpp:基于xml的可拓展协议.
  • jabber:xmpp的前身.
  • openfire:支持xmpp的开源服务器
  • smack.jar:对xmpp协议封装.方便开发的jar包.
  • spark.exe:基于xmpp的pc客户端;
  • asmack.jar:smack.jar的精简版.专门针对android端开发

2. 基本概念和原理

2.1 常用的网络通信协议

TCP/IP:Transmission Control Protocol/Internet Protocol 的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet 最基本的协议、Internet 国际互联网络的基础,由网络层的IP 协议和传输层的TCP协议组成。TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。协议采用了4 层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言:TCP 负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP 是给因特网的每一台联网设备规定一个地址。

UDP:UDP 协议全称是用户数据报协议,在网络中它与TCP 协议一样用于处理数据包,是一种无连接的协议。在OSI 模型中,在第四层——传输层,处于IP 协议的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP 用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用UDP协议。UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今天UDP 仍然不失为一项非常实用和可行的网络传输层协议。

TCP/IP 协议栈主要分为四层:应用层、传输层、网络层、数据链路层,每层都有相应的协议,如下图:

所谓的协议就是双方进行数据传输的一种格式。

2.2 TCP、UDP 特点对比

TCP 协议是面向连接、保证高可靠性(数据无丢失、数据无失序、数据无错误、数据无重复到达)传输层协议。UDP 协议也是传输层协议,它是无连接,不保证可靠的传输层协议。

2.3 TCP 三次握手过程

1、请求端(通常称为客户)发送一个SYN 段指明客户打算连接的服务器的端口,以及初始序号(ISN)
2、服务器发回包含服务器的初始序号的SYN 报文段(报文段2)作为应答。同时,将确认序号设置为客户的ISN加1 以对客户的SYN 报文段进行确认。

TCP UDP
面向连接 面向非连接
可靠的连接 不可靠的连接
速度慢 速度快
大文件、重要的数据等 适合小数据、不重要

3、客户必须将确认序号设置为服务器的ISN 加1 以对服务器的SYN 报文段进行确认(报文段3)这三个报文段完成连接的建立。这个过程也称为三次握手(three-way handshake)。

上面的过程如下图所示:

2.4 即时通讯形式

直接通讯

两个不同客户端之间不经过服务器,直接通过网络进行数据的交互。常用的p2p 技术就是直接通讯的形式。

在线代理通讯

一个客户端发送的消息先发送到服务器,服务器接收到消息后再发送给指定的另外一个客户端。QQ 的消息
尤其是离线消息就是同在线代理的方式实现的。

离线代理通讯

一个客户端发送消息给服务器,服务器存储在数据库中个,当另外一个客户端上线后在发送过去。

离线扩展通讯

一个客户端发送消息给服务器,服务器通过邮件、短信等其他形式将消息发送给接收者。

3. ServerSocket 和Socket

3.1 使用Java 完成简单的Socket 通信

在Java 中Socket 可以理解为客户端或者服务器端的一个特殊的对象,这个对象有两个关键的方法,一个是getInputStream 方法,另一个是getOutputStream 方法。getInputStream 方法可以得到一个输入流,客户端的Socket对象上的getInputStream 方法得到的输入流其实就是从服务器端发回的数据流。GetOutputStream 方法得到一个输出流,客户端Socket 对象上的getOutputStream 方法返回的输出流就是将要发送到服务器端的数据流,(其实是一个缓冲区,暂时存储将要发送过去的数据)。

下面就让我们写一个简单的Demo 来演示Socket 是如何使用的。

建立服务器类

服务类使用到的核心类的是ServerSocket。这里我们只需要建立一个Java Project 即可。

public class IMServer {
     private static ServerSocket   serverSocket;
     private static BufferedReader reader;

     public static void main(String[] args) {
         try {
             serverSocket = new ServerSocket(7788);
             /**
              * 等待接收客户端连接进来,该方法是线程阻塞的
              */
             Socket accept = serverSocket.accept();
             /**

              * 获取输入流,用于接收客户端发来的数据
              */
             InputStream inputStream = accept.getInputStream();
             /**

              * 将字节输入流转化为字符输出流
              */
             reader = new BufferedReader(new InputStreamReader(inputStream));
             /**

              * 打印数据
              */
             String tmp = null;
             while ((tmp = reader.readLine()) != null) {
                 System.out.println(tmp);
             }
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
             try {
                 if (serverSocket != null) {
                     serverSocket.close();
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             }
             if (reader != null) {
                 try {
                     reader.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
 }

建立客户端类

public class IMClient {
     private static Socket socket;

     private static BufferedWriter writer;

     /**
      * @param args
      */
     public static void main(String[] args) {
         try {
             socket = new Socket("127.0.0.1", 7788);
             /**
              * 获取输出流
              */
             OutputStream outputStream = socket.getOutputStream();
             writer = new BufferedWriter(new OutputStreamWriter(outputStream));
             writer.write("hello wo shi wzy!" + new Date().getTime());
             writer.close();
         } catch (IOException e) {
             e.printStackTrace();
         } finally {
             if (socket != null) {
                 try {
                     socket.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
             if (writer != null) {
                 try {
                     writer.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
 }

在上面的代码中我们仅仅实现了一个最简单的服务器和客户端,服务器启动起来后只能接受到一次消息,然后就关闭了。如果想让服务器一直运行,应该通过死循环来处理不同的发送进来的消息。

Socket调试工具

TCP/UDP Socket调试工具提供了TCP Server,TCP Client,UDP Server,UDP Client,UDP Group 五种Socket调试方案

Socket调试工具

TCP

手机作为Client,PC作为Server

app和pc socket通信成功
public class MyClientActivity extends Activity {
    private EditText mEditText = null;
    private Button connectButton = null;
    private Button sendButton = null;
    private TextView mTextView = null;

    private Socket clientSocket = null;
    private OutputStream outStream = null;

    private Handler mHandler = null;

    private ReceiveThread mReceiveThread = null;
    private boolean stop = true;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mEditText = (EditText) this.findViewById(R.id.edittext);
        mTextView = (TextView) this.findViewById(R.id.retextview);
        connectButton = (Button) this.findViewById(R.id.connectbutton);
        sendButton = (Button) this.findViewById(R.id.sendbutton);
        sendButton.setEnabled(false);

        // 连接按钮监听
        connectButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                try {
                    // 实例化对象并连接到服务器
                    clientSocket = new Socket("172.27.35.1", 60000);
                } catch (UnknownHostException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                displayToast("连接成功!");
                // 连接按钮使能
                connectButton.setEnabled(false);
                // 发送按钮使能
                sendButton.setEnabled(true);

                mReceiveThread = new ReceiveThread(clientSocket);
                stop = false;
                // 开启线程
                mReceiveThread.start();
            }
        });

        // 发送数据按钮监听
        sendButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                byte[] msgBuffer = null;
                // 获得EditTex的内容
                String text = mEditText.getText().toString();
                try {
                    // 字符编码转换
                    msgBuffer = text.getBytes("GB2312");
                } catch (UnsupportedEncodingException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }

                try {
                    // 获得Socket的输出流
                    outStream = clientSocket.getOutputStream();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                try {
                    // 发送数据
                    outStream.write(msgBuffer);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                // 清空内容
                mEditText.setText("");
                displayToast("发送成功!");
            }
        });

        // 消息处理
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                // 显示接收到的内容
                mTextView.setText((msg.obj).toString());
            }
        };

    }

    // 显示Toast函数
    private void displayToast(String s) {
        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
    }

    private class ReceiveThread extends Thread {
        private InputStream inStream = null;

        private byte[] buf;
        private String str = null;

        ReceiveThread(Socket s) {
            try {
                // 获得输入流
                this.inStream = s.getInputStream();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            while (!stop) {
                this.buf = new byte[512];

                try {
                    // 读取输入数据(阻塞)
                    this.inStream.read(this.buf);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                // 字符编码转换
                try {
                    this.str = new String(this.buf, "GB2312").trim();
                } catch (UnsupportedEncodingException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                Message msg = new Message();
                msg.obj = this.str;
                // 发送消息
                mHandler.sendMessage(msg);

            }
        }

    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (mReceiveThread != null) {
            stop = true;
            mReceiveThread.interrupt();
        }
    }

}

手机作为Server,PC作为Client

TCP
socket tcp
public class MyServerActivity extends Activity {
    private TextView ipTextView = null;
    private EditText mEditText = null;
    private Button sendButton = null;
    private TextView mTextView = null;

    private OutputStream outStream = null;
    private Socket clientSocket = null;
    private ServerSocket mServerSocket = null;

    private Handler mHandler = null;

    private AcceptThread mAcceptThread = null;
    private ReceiveThread mReceiveThread = null;
    private boolean stop = true;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ipTextView = (TextView) this.findViewById(R.id.iptextview);
        mEditText = (EditText) this.findViewById(R.id.sedittext);
        sendButton = (Button) this.findViewById(R.id.sendbutton);
        sendButton.setEnabled(false);
        mTextView = (TextView) this.findViewById(R.id.textview);

        // 发送数据按钮监听
        sendButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                byte[] msgBuffer = null;
                // 获得EditTex的内容
                String text = mEditText.getText().toString();
                try {
                    // 字符编码转换
                    msgBuffer = text.getBytes("GB2312");
                } catch (UnsupportedEncodingException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }

                try {
                    // 获得Socket的输出流
                    outStream = clientSocket.getOutputStream();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                try {
                    // 发送数据
                    outStream.write(msgBuffer);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                // 清空内容
                mEditText.setText("");
                displayToast("发送成功!");

            }
        });
        // 消息处理
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                case 0: {
                    // 显示客户端IP
                    ipTextView.setText((msg.obj).toString());
                    // 使能发送按钮
                    sendButton.setEnabled(true);
                    break;
                }
                case 1: {
                    // 显示接收到的数据
                    mTextView.setText((msg.obj).toString());
                    break;
                }
                }

            }
        };

        mAcceptThread = new AcceptThread();
        // 开启监听线程
        mAcceptThread.start();

    }

    // 显示Toast函数
    private void displayToast(String s) {
        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
    }

    private class AcceptThread extends Thread {
        @Override
        public void run() {
            try {
                // 实例化ServerSocket对象并设置端口号为7100
                mServerSocket = new ServerSocket(60000);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            try {
                // 等待客户端的连接(阻塞)
                clientSocket = mServerSocket.accept();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            mReceiveThread = new ReceiveThread(clientSocket);
            stop = false;
            // 开启接收线程
            mReceiveThread.start();

            Message msg = new Message();
            msg.what = 0;
            // 获取客户端IP
            msg.obj = clientSocket.getInetAddress().getHostAddress();
            // 发送消息
            mHandler.sendMessage(msg);

        }

    }

    private class ReceiveThread extends Thread {
        private InputStream mInputStream = null;
        private byte[] buf;
        private String str = null;

        ReceiveThread(Socket s) {
            try {
                // 获得输入流
                this.mInputStream = s.getInputStream();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            while (!stop) {
                this.buf = new byte[512];

                // 读取输入的数据(阻塞读)
                try {
                    this.mInputStream.read(buf);
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }

                // 字符编码转换
                try {
                    this.str = new String(this.buf, "GB2312").trim();
                } catch (UnsupportedEncodingException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

                Message msg = new Message();
                msg.what = 1;
                msg.obj = this.str;
                // 发送消息
                mHandler.sendMessage(msg);

            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (mReceiveThread != null) {
            stop = true;
            mReceiveThread.interrupt();
        }
    }

}

UDP

udp
socket udp
public class MainActivity extends Activity {
    private static String TAG = "CallActivity";
    private Handler handler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            // 播放声音
            initBeepSound();
            playBeepSoundAndVibrate();
            Log.i(TAG, "reciever_msg");
            String result = (String) msg.obj;
            System.out.println(result);
            tv_result.setText(result);
        };
    };
    private String ip;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ip = IpUtil.getIp(this);
        Toast.makeText(this, ip, Toast.LENGTH_LONG).show();
        System.out.println("ip:" + ip);
        tv_result = (TextView) findViewById(R.id.tv_result);
    }

    /**
     * 发送者
     */
    public void send(View v) {
        new Thread() {
            @Override
            public void run() {
                try {
                    //1.  创建一个DatagramSocket对象
                    DatagramSocket socket = new DatagramSocket(5678);
                    //2.  创建一个 InetAddress , 相当于是地址,就是想要发送的ip地址
                    InetAddress serverAddress = InetAddress.getByName("172.27.35.1");
                    //3.  这是随意发送一个数据
                    String str = "来自android手机的问候";
                    //4.  转为byte类型
                    byte data[] = str.getBytes("GBK");
                    //5.  创建一个DatagramPacket 对象,并指定要讲这个数据包发送到网络当中的哪个地址,以及端口号
                    DatagramPacket pack = new DatagramPacket(data, data.length, serverAddress, 5678);
                    //6.  调用DatagramSocket对象的send方法 发送数据
                    socket.send(pack);
                } catch (SocketException e) {
                    Log.i(TAG, "error");
                    e.printStackTrace();
                } catch (UnknownHostException e) {
                    Log.i(TAG, "error");
                    e.printStackTrace();
                } catch (IOException e) {
                    Log.i(TAG, "error");
                    e.printStackTrace();
                }
            }
        }.start();

    }

    /**
     * 接收者
     * @param v
     */
    public void receive(View v) {
        new Thread() {
            @Override
            public void run() {
                // 执行完毕后给handler发送一个空消息
                try {
                    // 1. 创建一个DatagramSocket对象,并指定监听的端口号/\
                    DatagramSocket socket = new DatagramSocket(5678);
                    // 2. 创建一个byte数组用于接收
                    byte data[] = new byte[1024];
                    // 3. 创建一个空的DatagramPackage对象
                    DatagramPacket pack = new DatagramPacket(data, data.length);
                    // 4. 使用receive方法接收发送方所发送的数据,同时这也是一个阻塞的方法
                    while (true) {
                        Log.i(TAG, "reciever_1");
                        socket.receive(pack);
                        Log.i(TAG, "reciever_2");
                        // 5. 得到发送过来的数据
                        //                      String result = new String(pack.getData(), pack.getOffset(), pack.getLength(),"GBK");
                        String result = new String(pack.getData(), pack.getOffset(), pack.getLength());
                        Message msg = new Message();
                        msg.obj = result;
                        handler.sendMessage(msg);
                        Log.i(TAG, "sendmsg_1");
                    }
                } catch (UnknownHostException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                    Log.i(TAG, "error");
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                    Log.i(TAG, "error");
                }
            }
        }.start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    // 播放声音
    private static final float BEEP_VOLUME = 0.10f;
    private MediaPlayer mediaPlayer;

    private void initBeepSound() {
        if (mediaPlayer == null) {
            setVolumeControlStream(AudioManager.STREAM_MUSIC);
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mediaPlayer.setOnCompletionListener(beepListener);
            AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.beep);
            try {
                mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
                file.close();
                mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
                mediaPlayer.prepare();
            } catch (IOException e) {
                mediaPlayer = null;
            }
        }
    }

    private void playBeepSoundAndVibrate() {
        if (mediaPlayer != null) {
            mediaPlayer.start();
        }
        // 震动
        Vibrator mVibrator = (Vibrator) getApplication().getSystemService(Service.VIBRATOR_SERVICE);
        mVibrator.vibrate(2000);
        long[] pattern = { 0, 100, 200, 100, 200 };
        mVibrator.vibrate(pattern, -1);
    }

    private final OnCompletionListener beepListener = new OnCompletionListener() {
        public void onCompletion(MediaPlayer mediaPlayer) {
            mediaPlayer.seekTo(0);
        }
    };
    // 退出提醒
    private long exitTime;
    private TextView tv_result;

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            if ((System.currentTimeMillis() - exitTime) > 2000) {
                Toast.makeText(getApplicationContext(), "再按一次退出" + getResources().getString(R.string.app_name),
                        Toast.LENGTH_SHORT).show();
                exitTime = System.currentTimeMillis();
            } else {
                finish();
            }
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}

IpUtil.java

public class IpUtil {
    /**
     * 获取手机ip
     * 
     * @return
     */
    public static String getLocalIpAddress() {
        try {
            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
                NetworkInterface intf = en.nextElement();
                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isLoopbackAddress()) {
                        String ip = inetAddress.getHostAddress().toString();
                        System.out.println("getLocalIpAddressIP:"+ip);
                        return ip;
                    }
                }
            }
        } catch (SocketException ex) {
            Log.e("ifo", ex.toString());
        }
        return "";
    }

    public static String getIp(Activity activity) {
        WifiManager wifiManager = (WifiManager) activity.getSystemService(Context.WIFI_SERVICE);
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipAddress = wifiInfo.getIpAddress();

        // 格式化IP address,例如:格式化前:1828825280,格式化后:192.168.1.109
        String ip = String.format("%d.%d.%d.%d", (ipAddress & 0xff), (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff));
        System.out.println("getIpIP:"+ip);
        return ip;

    }

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

推荐阅读更多精彩内容

  • 前面关于即时通讯基础Socket,大家学习使用XMPP之前可以先看看即时通讯系列之Socket简介 前言 前段时间...
    音符上的码字员阅读 4,197评论 3 16
  • 前言 本文会用实例的方式,将iOS各种IM的方案都简单的实现一遍。并且提供一些选型、实现细节以及优化的建议。 注:...
    涂耀辉阅读 94,116评论 232 1,747
  • 大学学习网络基础的时候老师讲过,网络由下往上分为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。通过初...
    西门丨不吹雪阅读 1,779评论 0 19
  • 现在很多社交软件都很火,FaceBook、易信、陌陌等,QQ、微信当然依然是中文社交最大霸主。除此之外伙星也很火。...
    软工官博阅读 1,303评论 0 5
  • 想了半天不知道应该用什么样的开头,以后尤其注意下别人的开头。 来到1z恰好一周,从上周六到店,这周六上完,已然过去...
    绵花不白阅读 539评论 0 1