前言:
就如前文所講述的, 聊天室每每是最基本的網絡編程的學習案例. 本文以WebSocket爲底層協議, 實現一個簡單的聊天室服務.
服務器採用Netty 4.x來實現, 源於其對websocket的超強支持, 基於卓越的性能和穩定.
本系列的文章連接以下:
1). websocket協議和javascript版的api
2). 基於Netty 4.x的Echo服務器實現 javascript
初步構想:
本文對聊天室服務的定位仍是比較簡單. 只須要有簡單的帳戶體系, 可以實現簡單的羣聊功能便可.
流程設計初稿:
1). 用戶登錄
2). 羣聊界面
這裏沒有聊天室的選擇, 只有惟一的一個. 這邊有沒有表情支持, 內容過濾. 一切皆從簡.html
協議約定:
在websocket協議的基礎之上, 咱們引入應用層的聊天協議.
協議以JSON做爲數據交互格式, 並進行擴展和闡述.
• 請求形態約定java
request: {cmd:"$cmd", params: {}}
• 響應形態約定web
response: {cmd:"$cmd", retcode:$retcode, datas:{}} // retcode => 0: success, 1: fail
1). 用戶登錄請求:編程
request: {cmd:"login", params: {username:"$username"}} response: 成功: {cmd:"login", retcode: 0, datas:{username:$username, userid:$userid}} 失敗: {cmd:"login", retcode:1, datas:{}}
注: 成功後, 返回分配的userid(全局惟一).
2). 消息發送請求:json
request: {cmd:"send_message", params:{message:$message, messageid:$messageid}} response: 成功: {cmd:"send_message", retcode:0, datas:{messageid:$messageid}} 失敗: {cmd:"send_message", retcode:1, datas:{messageid:$messageid}}
注: 這邊的messageid是必須的, 能夠提示那條消息成功仍是失敗
3). 消息接受(OnReceive):api
刷新用戶列表 {cmd:"userlist", retcode:0, datas: {users: [{userid:$userid, username:$username}, {userid:$userid, username:$username}]}} 接收消息 {cmd:"receive_message", retcode:0, datas: {message:"$message", username:"$username", userid:"$userid", timestamp:"$timestamp"}}
服務端實現:
藉助Netty 4.x來構建服務器端, 其Channel的pipeline設定以下:瀏覽器
serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline cp = socketChannel.pipeline(); // *) 支持http協議的解析 cp.addLast(new HttpServerCodec()); cp.addLast(new HttpObjectAggregator(65535)); // *) 對於大文件支持 chunked方式寫 cp.addLast(new ChunkedWriteHandler()); // *) 對websocket協議的處理--握手處理, ping/pong心跳, 關閉 cp.addLast(new WebSocketServerProtocolHandler("/chatserver")); // *) 對TextWebSocketFrame的處理 cp.addLast(new ChatLogicHandler(userGroupManager)); } });
注: HttpServerCodec / HttpObjectAggregator / ChunkedWriteHandler / WebSocketServerProtocolHandler, 依次引入極大簡化了websocket的服務編寫.
ChatLogicHandler的定義, 其對TextWebSocketFrame進行解析處理, 並從中提取聊天協議的請求, 並進行相應的狀態處理.服務器
@Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame twsf) throws Exception { String text = twsf.text(); JSONObject json = JSON.parseObject(text); if ( !json.containsKey("cmd") || !json.containsKey("params") ) { ctx.close(); return; } String cmd = json.getString("cmd"); if ( "login".equalsIgnoreCase(cmd) ) { // *) 處理登錄事件 handleLoginEvent(ctx, json.getJSONObject("params")); } else if ( "send_message".equalsIgnoreCase(cmd) ) { // *) 處理羣聊消息事件 handleMessageEvent(ctx, json.getJSONObject("params")); } }
其實這邊還能夠再加一層, 用於聊天協議的請求/響應的封裝, 這樣結構上更清晰. 但爲了簡單起見, 就省略了. websocket
客戶端的實現:
web客戶端最重要的仍是, 對chatclient的封裝. 其封裝了websocket了, 實現了業務上的login和sendmessage函數.
(function(window) { ChatEvent.LOGIN_ACK = 1001; ChatEvent.SEND_MESSAGE_ACK = 1002; ChatEvent.USERLIST = 2001; ChatEvent.RECEIVE_MESSAGE = 2002; ChatEvent.UNEXPECT_ERROR = 10001; function ChatEvent(type, retcode, msg) { this.type = type; this.retcode = retcode; // retcode : 0 => success, 1 => fail this.msg = msg; } WebChatClient.UNCONNECTED = 0; WebChatClient.CONNECTED = 1; WebChatClient.LOGINED = 1; function WebChatClient() { this.websocket = null; this.state = WebChatClient.UNCONNECTED; this.chatEventListener = null; } WebChatClient.prototype.init = function(wsUrl, chatEventListener) { this.state = WebChatClient.UNCONNECTED; this.chatEventListener = chatEventListener; this.websocket = new WebSocket(wsUrl); //建立WebSocket對象 var self = this; this.websocket.onopen = function(evt) { self.state = WebChatClient.CONNECTED; } this.websocket.onmessage = function(evt) { var res = JSON.parse(evt.data); if ( !res.hasOwnProperty("cmd") || !res.hasOwnProperty("retcode") || !res.hasOwnProperty("datas") ) { self.chatEventListener(new ChatEvent(ChatEvent.UNEXPECT_ERROR, 0)); } else { var cmd = res["cmd"]; var retcode = res["retcode"]; var datas = res["datas"]; switch(cmd) { case "login": self.chatEventListener(new ChatEvent(ChatEvent.LOGIN_ACK, retcode, datas)); break; case "send_message": self.chatEventListener(new ChatEvent(ChatEvent.SEND_MESSAGE_ACK, retcode, datas)); break; case "userlist": self.chatEventListener(new ChatEvent(ChatEvent.USERLIST, retcode, datas)); break; case "receive_message": self.chatEventListener(new ChatEvent(ChatEvent.RECEIVE_MESSAGE, retcode, datas)); break; } } } this.websocket.onerror = function(evt) { } } WebChatClient.prototype.login = function(username) { if ( this.websocket.readyState == WebSocket.OPEN && this.state == WebChatClient.CONNECTED ) { var msgdata = JSON.stringify({cmd:"login", params:{username:username}}); this.websocket.send(msgdata); } } WebChatClient.prototype.sendMessage = function(message) { if ( this.websocket.readyState == WebSocket.OPEN && this.state == WebChatClient.LOGINED ) { var msgdata = JSON.stringify({cmd:"send_message", params:{message:message}}); this.websocket.send(msgdata); } } // export window.ChatEvent = ChatEvent; window.WebChatClient = WebChatClient; })(window);
代碼下載:
因爲採用websocket做爲底層網絡通信協議, 所以須要瀏覽器支持(最好爲Chrome).
服務器和web客戶端的源碼下載地址爲: http://pan.baidu.com/s/1pJDYo3p.
文件的目錄結構以下:
後期展望:
編寫完初步版本後, 一羣有愛的小夥伴們幫我友情測試了一把. 中間反饋了不少體驗上的改進意見. 好比Enter鍵自動發送, 最新留言與頁面底部同步, 本身名稱高亮顯示.
也有反饋添加表情等高大上的功能, ^_^. 總之感受棒棒噠, 以爲再作一件很偉大的事情. oh yeah.
寫在最後:
若是你以爲這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫博客可否給本身帶來一點小小的收益. 不管多少, 都是對樓主一種由衷的確定.