基于WebSocket的在線聊天室(一)

效果預(yù)覽

前言


去年在tomcat7自帶的例子中發(fā)現(xiàn)了兩個(gè)有趣的demo,貪食蛇游戲和畫板狠毯。很有意思的是打開的幾個(gè)窗口內(nèi)容都是一樣的策菜,而且還會(huì)同步更新,如果換做以往做web開發(fā)的套路來(lái)實(shí)現(xiàn)這個(gè)效果還是比較費(fèi)勁的国觉。于是心血來(lái)潮就去查了一些關(guān)于websocket的資料并做了這么一個(gè)文字聊天室吧恃。前段時(shí)間應(yīng)別人的需要又把它翻了出來(lái)加上了視頻和語(yǔ)音功能,瞬間高大上了很多麻诀。做完之后當(dāng)然得趁熱打鐵總結(jié)下痕寓,順便作為第一次寫文章的素材。(∩_∩)

簡(jiǎn)介


WebSocket 是 HTML5 一種新的協(xié)議蝇闭。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信呻率,能更好的節(jié)省服務(wù)器資源和帶寬并達(dá)到實(shí)時(shí)通訊,它建立在 TCP 之上呻引,同 HTTP 一樣通過(guò) TCP 來(lái)傳輸數(shù)據(jù)礼仗。

說(shuō)起實(shí)時(shí)通訊就不得不提一些“服務(wù)器推”技術(shù)。

  • 輪詢

    客戶端以一定的時(shí)間間隔發(fā)送Ajax請(qǐng)求,優(yōu)點(diǎn)實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單逻悠、省事,不過(guò)缺點(diǎn)也很明顯元践,請(qǐng)求有很大一部分是無(wú)用的,而且需要頻繁建立和釋放TCP連接童谒,很消耗帶寬和服務(wù)器資源单旁。

  • 長(zhǎng)輪詢

    與普通輪詢不同的地方在于,服務(wù)端接收到請(qǐng)求后會(huì)保持住不立即返回響應(yīng)饥伊,等到有消息更新才返回響應(yīng)并關(guān)閉連接慎恒,客戶端處理完響應(yīng)再重新發(fā)起請(qǐng)求。較之普通輪詢沒有無(wú)用的請(qǐng)求撵渡,但服務(wù)器保持連接也是有消耗的融柬,如果服務(wù)端數(shù)據(jù)變化頻繁的話和普通輪詢并無(wú)兩樣。

  • 長(zhǎng)連接

    在頁(yè)面中嵌入一個(gè)隱藏的iframe,將其src設(shè)為一個(gè)長(zhǎng)連接的請(qǐng)求趋距,這樣服務(wù)端就能不斷向客戶端發(fā)送數(shù)據(jù)粒氧。優(yōu)缺點(diǎn)與長(zhǎng)輪詢相仿。

這些技術(shù)都明顯存在兩個(gè)相同的缺點(diǎn):

  1. 服務(wù)器需要很大的開銷

  2. 都做不到真正意義上的“主動(dòng)推送”节腐,服務(wù)端只能“被動(dòng)”地響應(yīng)外盯,于是就輪到正主出場(chǎng)了。

在websocket中翼雀,只需要做一個(gè)握手動(dòng)作就可以在客戶端和服務(wù)器之間建立連接饱苟,之后通過(guò)數(shù)據(jù)幀的形式在這個(gè)連接上進(jìn)行通訊,并且狼渊,由于連接是雙向的箱熬,在連接建立之后服務(wù)端隨時(shí)可以主動(dòng)向客戶端發(fā)送消息(前提是連接沒有斷開)类垦。

實(shí)現(xiàn)


以前一些websocket的例子都是基于某個(gè)特定的容器(如Tomcat,Jetty),在Oracle發(fā)布了JSR356規(guī)范之后城须,websocket的JavaAPI得到了統(tǒng)一,所以只要Web容器支持JSR356,那么我們寫websocket時(shí),代碼都是一樣的了.Tomcat從7.0.47開始支持JSR356.另外有一點(diǎn)要說(shuō)明的是JDK的要求是7及以上蚤认。
我本地的環(huán)境為 jdk1.7, nginx1.7.8 ( 反向代理 ), tomcat7.0.52( 需要在buildpath中還要添加tomcat7的library ),chrome糕伐。

廢話不多說(shuō)砰琢,先上代碼

// 消息結(jié)構(gòu)Message類
public class Message {
    
        private int type;//消息類型

        private String msg;//消息主題

        private String host;// 發(fā)送者

        private String[] dests;// 接受者

        private RoomInfo roomInfo;//聊天室信息

        public class MsgConstant {

            public final static int Open = 1;// 新連接

            public final static int Close = 2;// 連接斷開

            public final static int MsgToAll = 3;// 發(fā)送給所有人

            public final static int MsgToPoints = 4;// 發(fā)送給指定用戶

            public final static int RequireLogin = 5;// 需要登錄

            public final static int setName = 6;// 設(shè)置用戶名
        }

        public static class RoomInfo {
    
            private String name;// 聊天室名稱

            private String creater;//創(chuàng)建人

            private String createTime;// 創(chuàng)建時(shí)間

            public RoomInfo(String creater, String createTime) {
                this.creater = creater;
                this.createTime = createTime;
            }
    
            public RoomInfo(String name) {
                this.name = name;
            }
            // 省略set get
        }

        public Message() {
            setType(MsgConstant.MsgToAll);
        }
    
        public Message(String host, int type) {
            setHost(host);
            setType(type);
        }
    
        public Message(String host, int type, String msg) {
            this(host, type);
            setMsg(msg);
        }

        public Message(String host, int type, String[] dests) {
            this(host, type);
            setDests(dests);
        }

        @Override
        public String toString() {
            // 序列化成json串
            return JSONObject.toJSONString(this);
        }
    }

public class wsConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        //通過(guò)配置來(lái)獲取httpsession
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}

@ServerEndpoint(value = "/websocket/chat/{uid}", configurator = wsConfigurator.class)
public class textController {

    private Session session;

    private LoginUser loginUser;

    private static RoomInfo roomInfo;

    //連接集合
    private static final Set<textController> connections = new CopyOnWriteArraySet<textController>();

    /**
     * websocket連接建立后觸發(fā)
     * 
     * @param session
     * @param config
     */
    @OnOpen
    public void OnOpen(Session session, EndpointConfig config, @PathParam(value = "uid") String uid) {
        //設(shè)置websocket連接的session
        setSession(session);
        // 獲取HttpSession
        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        // 從HttpSession中取得當(dāng)前登錄的用戶作為當(dāng)前連接的用戶
        setLoginUser((LoginUser) httpSession.getAttribute("LoginUser"));
        if (getLoginUser() == null) {
            requireLogin();// 未登錄需要進(jìn)行登錄
            return;
        }
        // 設(shè)置聊天室信息
        if (getConnections().size() == 0) {// 如果當(dāng)前聊天室為空,建立新的信息
            setRoomInfo(new RoomInfo(getUserName(), (new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")).format(new Date())));
        }
        //加入連接集合
        getConnections().add(this);
        //廣播通知所有連接有新用戶加入
        broadcastToAll(new Message(getUserName(), MsgConstant.Open, getUsers()));
    }

    /**
     * websocket連接斷開后觸發(fā)
     */
    @OnClose
    public void OnClose() {
        //從連接集合中移除
        getConnections().remove(this);
        //廣播通知所有連接有用戶退出
        broadcastToAll(new Message(getUserName(), MsgConstant.Close, getUsers()));
    }

    /**
     * 接受到客戶端發(fā)送的字符串時(shí)觸發(fā)
     * 
     * @param message
     */
    @OnMessage(maxMessageSize = 1000)
    public void OnMessage(String message) {
        //消息內(nèi)容反序列化
        Message msg = JSONObject.parseObject(message, Message.class);
        msg.setHost(getUserName());
        //對(duì)html代碼進(jìn)行轉(zhuǎn)義
        msg.setMsg(txt2htm(msg.getMsg()));
        if (msg.getDests() == null)
            broadcastToAll(msg);
        else
            broadcastToSpecia(msg);
    }

    @OnError
    public void onError(Throwable t) throws Throwable {
        System.err.println("Chat Error: " + t.toString());
    }

    /**
     * 廣播給所有用戶
     * 
     * @param msg
     */
    private static void broadcastToAll(Message msg) {
        for (textController client : getConnections())
            client.call(msg);
    }

    /**
     * 發(fā)送給指定的用戶
     * 
     * @param msg
     */
    private static void broadcastToSpecia(Message msg) {
        for (textController client : getConnections())
            // 感覺用map進(jìn)行映射會(huì)更好點(diǎn)
            if (Contains(msg.getDests(), client.getUserName()))
                client.call(msg);
    }

    private void call(Message msg) {
        try {
            synchronized (this) {
                if (getUserName().equals(msg.getHost()) && msg.getType() == MsgConstant.Open)
                    msg.setRoomInfo(getRoomInfo());
                this.getSession().getBasicRemote().sendText(msg.toString());
            }
        } catch (IOException e) {
            try {
                //斷開連接
                this.getSession().close();
            } catch (IOException e1) {
            }
            OnClose();
        }
    }

    private void requireLogin() {
        Message msg = new Message();
        msg.setType(MsgConstant.RequireLogin);
        call(msg);
    }

    public void setSession(Session session) {
        this.session = session;
    }

    public Session getSession() {
        return this.session;
    }

    public LoginUser getLoginUser() {
        return loginUser;
    }

    public void setLoginUser(LoginUser loginUser) {
        this.loginUser = loginUser;
    }

    /**
     * 設(shè)置聊天室信息
     */
    public static void setRoomInfo(RoomInfo info) {
        roomInfo = info;
    }

    public static RoomInfo getRoomInfo() {
        return roomInfo;
    }

    private String getUserName() {
        if (getLoginUser() == null)
            return "";
        return getLoginUser().getUserName();
    }

    public static Set<textController> getConnections() {
        return connections;
    }

    private String[] getUsers() {
        int i = 0;
        String[] destArrary = new String[getConnections().size()];
        for (textController client : getConnections())
            destArrary[i++] = client.getUserName();
        return destArrary;
    }

    /**
     * html代碼轉(zhuǎn)義
     * 
     * @param txt
     * @return
     */
    public static String txt2htm(String txt) {
        if (StringUtils.isBlank(txt)) {
            return txt;
        }
        return txt.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll(" ", " ").replaceAll("\n", "<br/>").replaceAll("\'", "'");
    }

    /**
     * 字符串?dāng)?shù)組是否包含指定字符串
     * 
     * @param strs
     * @param str
     * @return
     */
    public static boolean Contains(String[] strs, String str) {
        if (StringUtils.isBlank(str) || strs.length == 0)
            return false;
        for (String s : strs)
            if (s.equals(str))
                return true;
        return false;
    }
}

服務(wù)端代碼就這么三個(gè)類良瞧,還是比較簡(jiǎn)單的(>_<|||還是比別人的例子復(fù)雜好多)陪汽。

  • Message類是與客戶端統(tǒng)一的消息結(jié)構(gòu),消息序列化成json串進(jìn)行傳輸褥蚯,客戶端再反序列化為對(duì)象進(jìn)行操作挚冤,感覺還是比較方便的。
  • wsConfigurator類繼承ServerEndpointConfig.Configurator并實(shí)現(xiàn)了modifyHandshake方法遵岩,將其作為ServerEndpoint的configurator參數(shù)值你辣,這里的用途是拿到HttpSession巡通,之后就可以取得HttpSession中的內(nèi)容(比如登錄用戶信息)尘执。
  • textController類,上面兩個(gè)類都是可有可無(wú)的東西宴凉,這個(gè)就是websocket的關(guān)鍵誊锭,這里以注解的方式實(shí)現(xiàn),很方便弥锄。除此之外丧靡,另一種方式是繼承javax.websocket.Endpoint類,不過(guò)我沒試過(guò)就不廢話了籽暇。

@ServerEndpoint

用來(lái)標(biāo)記一個(gè)websocket服務(wù)器終端

  • value : websocket連接的url温治,類似spring mvc 的 @RequestMapping,區(qū)別的話就是不用再補(bǔ)后綴了
  • configurator : 這個(gè)參數(shù)沒有深究過(guò)戒悠,只用到過(guò)之前說(shuō)的提取HttpSession的作用

@ClientEndpoint

用來(lái)標(biāo)記一個(gè)websocket客戶器

@OnOpen

websocket連接建立后執(zhí)行熬荆,主要進(jìn)行一些初始化操作

  • session : 此session非HttpSession,而是websocket通訊所使用的session,因此需要上述方法另外得到HttpSession
  • EndpointConfig : 應(yīng)該包含一些這個(gè)Endpoint的配置信息之類的(瞎猜的)
  • @PathParam(value = "uid") : 這個(gè)是自定義的參數(shù)绸狐,主要對(duì)應(yīng)ServerEndpoint注解的value值中的{uid}參數(shù)占位符卤恳,除此之外還可以通過(guò)session.getRequestParameterMap()來(lái)獲取url參數(shù)(如"/websocket/chat?uid=123")

@OnClose

websocket連接斷開后執(zhí)行,沒什么好說(shuō)的

@OnMessage

接收到客戶端發(fā)送端的消息后執(zhí)行寒矿,值得注意的是突琳,OnMessage注解的方法可以有多個(gè)重載,方法參數(shù)可以為String,ByteBuffer等類型符相,相應(yīng)的,session有這么幾個(gè)方法可以向客戶端發(fā)送消息:sendText拆融,sendBinary,sendObject等。另外冠息,上面代碼里向客戶端發(fā)消息用的是session.getBasicRemote().sendText方法挪凑,這是阻塞的方式,還有一種異步方式session.getAsyncRemote().sendText逛艰,雖說(shuō)是異步躏碳,不過(guò)高頻率發(fā)送并沒有出現(xiàn)錯(cuò)亂的情況,還有待研究散怖。

  • maxMessageSize : 用來(lái)指定消息字節(jié)最大限制菇绵,超過(guò)限制就會(huì)關(guān)閉連接,文字聊天不設(shè)置基本沒什么問(wèn)題镇眷,默認(rèn)的大小夠用了咬最,傳圖片或者文件可能就會(huì)因?yàn)槌鱿拗贫鴮?dǎo)致連接“莫名其妙”被關(guān)閉,這個(gè)坑還是比較難發(fā)現(xiàn)的欠动。

@OnError

報(bào)錯(cuò)的時(shí)候會(huì)執(zhí)行永乌,不過(guò)試過(guò)各種異常下這個(gè)方法都沒有執(zhí)行,很奇怪

服務(wù)端功能比較簡(jiǎn)單具伍,主要實(shí)現(xiàn)了幾個(gè)注解的方法翅雏,對(duì)客戶端傳來(lái)的消息進(jìn)行廣播,并無(wú)其他額外操作人芽。再來(lái)看下前端的代碼:

(function(window) {
    Blob.prototype.appendAtFirst = function(blob) {
        return new Blob([blob, this]);
    };
    var WS_Open = 1,
        WS_Close = 2,
        WS_MsgToAll = 3,
        WS_MsgToPoints = 4,
        WS_RequireLogin = 5,
        WS_setName = 6,
        types = ["文本", "視頻", "語(yǔ)音"],
        getWebSocket = function(host) {
            var socket;
            if ('WebSocket' in window) {
                socket = new WebSocket(host);
            } else if ('MozWebSocket' in window) {
                socket = new MozWebSocket(host);
            }
            return socket;
        },
        WSClient = function(option) {
            var isReady = false,
                init = function(client, option) {
                    client.socket = null;
                    client.online = false;
                    client.isUserClose = false;
                    client.option = option || {};
                };

            this.connect = function(host) {
                var client = this,
                    socket = getWebSocket(host);

                if (socket == null) {
                    console.log('錯(cuò)誤: 當(dāng)前瀏覽器不支持WebSocket望几,請(qǐng)更換其他瀏覽器', true);
                    alert('錯(cuò)誤: 當(dāng)前瀏覽器不支持WebSocket,請(qǐng)更換其他瀏覽器');
                    return;
                }

                socket.onopen = function() {
                    var onopen = client.option.onopen,
                        type = types[client.option.type];
                    console.log('WebSocket已連接.');
                    console.log("%c類型:" + type, "color:rgb(228, 186, 20)");
                    onopen && onopen();
                };

                socket.onclose = function() {
                    var onclose = client.option.onclose,
                        type = types[client.option.type];
                    client.online = false;
                    console.error('WebSocket已斷開.');
                    console.error("%c類型:" + type, "color:rgb(228, 186, 20)");
                    onclose && onclose();
                    if (!client.isUserClose) {
                        client.initialize();
                    }
                };

                socket.onmessage = function(message) {
                    var option = client.option;
                    if (typeof(message.data) == "string") {
                        var msg = JSON.parse(message.data);
                        switch (msg.type) {
                        case WS_Open:
                            option.wsonopen && option.wsonopen(msg);
                            break;
                        case WS_Close:
                            option.wsonclose && option.wsonclose(msg);
                            break;
                        case WS_MsgToAll:
                        case WS_MsgToPoints:
                            option.wsonmessage && option.wsonmessage(msg);
                            break;
                        case WS_RequireLogin:
                            option.wsrequirelogin && option.wsrequirelogin();
                            break;
                        case WS_setName:
                            option.userName = msg.host;
                            option.wssetname && option.wssetname(msg);
                            break;
                        }
                    } else if (message.data instanceof Blob) {
                        option.wsonblob && option.wsonblob(message);
                    }

                };

                isReady = true;
                this.socket = socket;
                return this;
            };

            this.initialize = function(param) {
                return this.connect(this.option.host + (param ? "?" + param : ""));
            };

            this.sendString = function(message) {// 向服務(wù)端發(fā)送給字符串
                return isReady && this.socket.send(message);
            };

            this.sendBlob = function(blob) {// 向服務(wù)端發(fā)送二進(jìn)制數(shù)據(jù)
                return isReady && this.socket.send(blob.appendAtFirst(this.option.userName));
            };

            this.close = function() {
                this.isReady = false;
                this.online = false;
                this.isUserClose = true;
                this.socket.close();
                return true;
            };

            this.isMe = function(name) {
                return this.option.userName == name;
            }

            init(this, option);
        };

    window.WSClient = WSClient;

})(window);

這里的代碼我做了下粗劣的封裝萤厅,就不給出具體實(shí)現(xiàn)了橄抹,調(diào)用的時(shí)候?qū)崿F(xiàn)具體的邏輯即可。如下形式:

var textClient = new WSClient({
    host: "ws://" + window.location.host + "/websocket/chat/123",// 注意這里不是http協(xié)議
    type: MODE_TEXT,
    onopen: function() {
        console.log('WebSocket已連接.');
    },
    onclose: function() {
        console.log('Info: WebSocket已斷開.');
    },
    wsonopen: function(msg) {
        console.log("***加入聊天室");
    },
    wsonclose: function(msg) {
        console.log("***退出了聊天室");
    },
    wsonmessage: function(msg) {
        console.log(“收到消息:” + msg.msg);
    },
    wsrequirelogin: function(msg) {
        document.location.href = "http://" + window.location.host + "/login.htm?to_url=" + document.location.href;
    },
    wssetname: function(msg) {
    }
});

和服務(wù)端要實(shí)現(xiàn)的幾個(gè)方法類似惕味,就不多說(shuō)了楼誓。其中,socket.onmessage中message.data有兩種類型:string和Blob,Blob表示二進(jìn)制數(shù)據(jù)名挥,比如圖片和聲音疟羹,文件就可以通過(guò)Blob對(duì)象來(lái)傳輸。另外躺同,服務(wù)端發(fā)送消息的send方法是有好幾種的阁猜,而這里WebSocket對(duì)象的send方法只有一個(gè),參數(shù)可以是Blob或string蹋艺。

最后附上websocket的nginx配置:

location /websocket/chat { 
    proxy_pass http://localhost:8080/websocket/chat; 
    include websocket.conf;
}

websocket.conf:

#避免nginx超時(shí)
proxy_read_timeout 86400;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

參考文章:


唉剃袍,第一次寫文章,加上基礎(chǔ)不扎實(shí)捎谨,磨磨蹭蹭寫了一晚上才結(jié)束戰(zhàn)斗民效,真是不容易憔维。水平有限,有講錯(cuò)的地方歡迎指出畏邢,之后會(huì)繼續(xù)關(guān)于視頻和音頻通訊方式的總結(jié)业扒。


轉(zhuǎn)載請(qǐng)注明出處:http://www.reibang.com/p/62790429acef

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舒萎,隨后出現(xiàn)的幾起案子程储,更是在濱河造成了極大的恐慌,老刑警劉巖臂寝,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件章鲤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡咆贬,警方通過(guò)查閱死者的電腦和手機(jī)败徊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)掏缎,“玉大人皱蹦,你說(shuō)我怎么就攤上這事【祢冢” “怎么了沪哺?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)端蛆。 經(jīng)常有香客問(wèn)我凤粗,道長(zhǎng)酥泛,這世上最難降的妖魔是什么今豆? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮柔袁,結(jié)果婚禮上呆躲,老公的妹妹穿的比我還像新娘。我一直安慰自己捶索,他們只是感情好插掂,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腥例,像睡著了一般辅甥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上燎竖,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天璃弄,我揣著相機(jī)與錄音,去河邊找鬼构回。 笑死夏块,一個(gè)胖子當(dāng)著我的面吹牛疏咐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脐供,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼浑塞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了政己?” 一聲冷哼從身側(cè)響起酌壕,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎歇由,沒想到半個(gè)月后仅孩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡印蓖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年辽慕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赦肃。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溅蛉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出他宛,到底是詐尸還是另有隱情船侧,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布厅各,位于F島的核電站镜撩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏队塘。R本人自食惡果不足惜袁梗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望憔古。 院中可真熱鬧遮怜,春花似錦、人聲如沸鸿市。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)焰情。三九已至陌凳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間内舟,已是汗流浹背合敦。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谒获,地道東北人蛤肌。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓壁却,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親裸准。 傳聞我的和親對(duì)象是個(gè)殘疾皇子展东,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容