上節課講了羣聊,此次來講說單聊,單聊要比羣聊複雜點,可是代碼也不是不少,主要是前端顯示比較麻煩點。
效果:
javascript
首先一個新的用戶,須要先登錄,輸入本身的暱稱,而後點擊登錄。後端服務會把你的用戶名和當前的線程進行邦定,這樣就能夠經過你的用戶名找到你的線程。登錄成功,後端返回定義好的消息 success
,前端判斷記錄CHAT.me
,這樣給別人發消息時就能夠攜帶本身的信息。
css
在輸入框輸入用戶名,就能夠返回對應的用戶的線程,這樣你就能夠把消息發送給你要聊天的對象。若是不存在,後端回返回消息給前端,該用戶不存在。若是存在,就記錄此用戶名到CHAT.to
中,這樣你發送消息的時候就能夠發送給對應用戶了。
html
發送聊天信息時me:to:消息
,這樣後端就知道是誰要發給誰,根據用戶名去找到具體的線程去單獨推送消息,實現單聊。前端
左側聊天列表沒有實現,每搜索一個在線用戶,應該動態顯示在左側,點擊該用戶,動態顯示右側聊天窗口進行消息發送。如今是你和全部人的單聊消息都會顯示在右側,沒有完成拆分,由於這是一個頁面,處理起來比較麻煩,我一個後端就不花時間搞了,感興趣的能夠本身去實現。java
由於注視比較詳細,就直接複製整個代碼到這裏,你們本身看。jquery
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>單人聊天</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/css/zui.min.css"> <link rel="stylesheet" href="zui-theme.css"> </head> <body> <div class="container"> <div class="row"> <h1>mike單人聊天室,等你來聊</h1></div> <div class="row"> <div class="input-control has-icon-left has-icon-right" style="width:50%;"> <input id="userName" type="text" class="form-control" placeholder="聊天暱稱"> <label for="inputEmailExample1" class="input-control-icon-left"><i class="icon icon-user"></i></label> <label for="inputEmailExample1" class="input-control-icon-right"><a onclick="login()">登錄</a></label> </div> </div> <br> <div class="row"> <div class="input-control search-box search-box-circle has-icon-left has-icon-right" id="searchUser"> <input id="inputSearch" type="search" class="form-control search-input" placeholder="輸入在線好友暱稱聊天...enter開始查找"> <label for="inputSearchExample1" class="input-control-icon-left search-icon"><i class="icon icon-search"></i></label> <a href="#" class="input-control-icon-right search-clear-btn"><i class="icon icon-remove"></i></a> </div> </div> <hr> <div class="row"> <div class="col-lg-3"> <p class="with-padding bg-success">聊天列表</p> <div class="list-group"> <a href="#" class="list-group-item"> <h4 class="list-group-item-heading"><i class="icon-user icon-2x"></i> may</h4> </a> <a href="#" class="list-group-item active"> <h4 class="list-group-item-heading"><i class="icon-user icon-2x"></i> steve</h4> </a> </div> </div> <div class="col-lg-1"></div> <div class="col-lg-8"> <div class="comments"> <section class="comments-list" id="chatlist"> </section> <footer> <div class="reply-form" id="commentReplyForm1"> <a href="###" class="avatar"><i class="icon-user icon-2x"></i></a> <form class="form"> <div class="form-group"> <textarea id="inputMsg" class="form-control new-comment-text" rows="2" value="" placeholder="開始聊天... 輸入enter 發送消息"></textarea> </div> </form> </div> </footer> </div> </div> </div> </div> <!-- ZUI Javascript 依賴 jQuery --> <script src="https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/lib/jquery/jquery.js"></script> <!-- ZUI 標準版壓縮後的 JavaScript 文件 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/js/zui.min.js"></script> <script type="text/javascript"> window.CHAT = { isLogin: false, to: "", me: "", WS:{}, init: function () { if (window.WebSocket) { this.WS = new WebSocket("ws://A156B7L58CCNY4B:8090/ws"); this.WS.onmessage = function(event) { var data = event.data; console.log("收到數據:" + data); //返回搜索消息 if(data.indexOf("search") != -1){ new $.zui.Messager('提示消息:'+data, { type: 'info' // 定義顏色主題 }).show(); if(data.indexOf("已找到")){ //能夠進行會話 CHAT.to = data.split(":")[1]; } } //返回登錄消息 if(data == "success"){ CHAT.isLogin = true; new $.zui.Messager('提示消息:登錄成功', { type: 'success' // 定義顏色主題 }).show(); //鏈接成功再也不修改暱稱 $("#userName").attr("disabled","disabled"); CHAT.me = $("#userName").val(); } //返回聊天信息 if (data.split(":").length==3 && CHAT.me == data.split(":")[1]) { CHAT.to = data.split(":")[0]; //設置對話 appendOtherchat(data); } }, this.WS.onclose = function(event) { console.log("鏈接關閉"); CHAT.isLogin = false; $("#userName").removeAttr("disabled"); new $.zui.Messager('提示消息:聊天中斷', { type: 'danger' // 定義顏色主題 }).show(); }, this.WS.onopen = function(evt) { console.log("Connection open ..."); }, this.WS.onerror = function(event) { console.log("鏈接失敗...."); CHAT.isLogin = false; $("#userName").removeAttr("disabled"); new $.zui.Messager('提示消息:聊天中斷', { type: 'danger' // 定義顏色主題 }).show(); } } else { alert("您的瀏覽器不支持聊天,請更換瀏覽器"); } }, chat:function (msg) { this.WS.send(msg); } } CHAT.init(); function login() { var userName = $("#userName").val(); if (userName != null && userName !='') { //初始化聊天 CHAT.chat("init:"+userName); } else { alert("請輸入用戶名登陸"); } } function Trim(str) { return str.replace(/(^\s*)|(\s*$)/g, ""); } function appendMy (msg) { //拼接本身的聊天內容 document.getElementById('chatlist').innerHTML+="<div class='comment'><a class='avatar pull-right'><i class='icon-user icon-2x'></i></a><div class='content pull-right'><div><strong>我</strong></div><div class='text'>"+msg+"</div></div></div>"; } function appendOtherchat(msg) { //拼接別人的聊天信息到聊天室 var msgs = msg.split(":"); document.getElementById('chatlist').innerHTML+="<div class='comment'><a class='avatar'><i class='icon-user icon-2x'></i></a><div class='content'><div><strong>"+msgs[0]+"</strong></div><div class='text'>"+msgs[2]+"</div></div></div>"; } //搜索在線人員發送消息 document.getElementById("inputSearch").addEventListener('keyup', function(event) { if (event.keyCode == "13") { //回車執行查詢 CHAT.chat("search:"+$('#inputSearch').val()); } }); //發送聊天消息 document.getElementById('inputMsg').addEventListener('keyup', function(event) { if (event.keyCode == "13") { //回車執行查詢 var inputMsg = $('#inputMsg').val(); if (inputMsg == null || Trim(inputMsg) == "" ) { alert("請輸入聊天消息"); } else { var userName = $('#userName').val(); if (userName == null || userName == '') { alert("請輸入聊天暱稱"); } else { //發送消息 定義消息格式 me:to:[消息] CHAT.chat(userName+":"+CHAT.to+":"+inputMsg); appendMy(inputMsg); //發送完清空輸入 document.getElementById('inputMsg').focus(); document.getElementById('inputMsg').value=""; } } } }); </script> </body> </html>
UserMap
,邦定user和Channelpackage netty; import java.util.HashMap; import java.util.Map; import io.netty.channel.Channel; /** * The class UserMap */ public class UserMap { private HashMap<String, Channel> users = new HashMap(); private static UserMap instance; public static UserMap getInstance () { if (instance == null) { instance = new UserMap(); } return instance; } private UserMap () { } public void addUser(String userId, Channel ch) { this.users.put(userId, ch); } public Channel getUser (String userId) { return this.users.get(userId); } public void deleteUser (Channel ch) { for (Map.Entry<String, Channel> map: users.entrySet()) { if (map.getValue() == ch) { users.remove(map.getKey()); break; } } } }
ChatHandler
改造package netty; import java.time.LocalDateTime; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.util.concurrent.GlobalEventExecutor; /** * */ public class ChatHandler extends SimpleChannelInboundHandler{ public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); public static UserMap usermap = UserMap.getInstance(); /** * 每當從服務端收到新的客戶端鏈接時,客戶端的 Channel 存入ChannelGroup列表中,並通知列表中的其餘客戶端 Channel */ @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Channel incoming = ctx.channel(); for (Channel channel : channels) { channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 加入\n"); } channels.add(ctx.channel()); } /** * 每當從服務端收到客戶端斷開時,客戶端的 Channel 移除 ChannelGroup 列表中,並通知列表中的其餘客戶端 Channel */ @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { Channel incoming = ctx.channel(); for (Channel channel : channels) { channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 離開\n"); } channels.remove(ctx.channel()); } /** * 會話創建時 */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5) Channel incoming = ctx.channel(); System.out.println("ChatClient:"+incoming.remoteAddress()+"在線"); } /** * 會話結束時 */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6) Channel incoming = ctx.channel(); System.out.println("ChatClient:"+incoming.remoteAddress()+"掉線"); //清除離線用戶 this.usermap.deleteUser(incoming); } /** * 出現異常 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (7) Channel incoming = ctx.channel(); System.out.println("ChatClient:"+incoming.remoteAddress()+"異常"); // 當出現異常就關閉鏈接 cause.printStackTrace(); ctx.close(); } /** * 讀取客戶端發送的消息,並將信息轉發給其餘客戶端的 Channel。 */ @Override protected void channelRead0(ChannelHandlerContext ctx, Object request) throws Exception { if (request instanceof FullHttpRequest) { //是http請求 FullHttpResponse response = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1,HttpResponseStatus.OK , Unpooled.wrappedBuffer("Hello netty" .getBytes())); response.headers().set("Content-Type", "text/plain"); response.headers().set("Content-Length", response.content().readableBytes()); response.headers().set("connection", HttpHeaderValues.KEEP_ALIVE); ctx.channel().writeAndFlush(response); } else if (request instanceof TextWebSocketFrame) { // websocket請求 //此處id爲neety自動分配給每一個對話線程的id,有兩種,一個長id一個短id,長id惟一,短id可能會重複 String userId = ctx.channel().id().asLongText(); //客戶端發送過來的消息 String msg = ((TextWebSocketFrame)request).text(); System.out.println("收到客戶端"+userId+":"+msg); //發送消息給全部客戶端 羣聊 //channels.writeAndFlush(new TextWebSocketFrame(msg)); // 邦定user和channel // 定義每一個上線用戶主動發送初始化信息過來,攜帶本身的name,而後完成綁定 模型 init:[usrname] // 實際場景中應該使用user惟一id if (msg.indexOf("init") != -1) { String userNames[] = msg.split(":"); if ("init".equals(userNames[0])) { // 記錄新的用戶 this.usermap.addUser(userNames[1].trim(), ctx.channel()); ctx.channel().writeAndFlush(new TextWebSocketFrame("success")); } } //搜索在線用戶 消息模型 search:[username] if (msg.indexOf("search") != -1) { Channel ch = this.usermap.getUser(msg.split(":")[1].trim()); if (ch != null) { //此用戶存在 ctx.channel().writeAndFlush(new TextWebSocketFrame("search:"+msg.split(":")[1].trim()+":已找到")); } else { // 此用戶不存在 ctx.channel().writeAndFlush(new TextWebSocketFrame("search:"+msg.split(":")[1].trim()+":未找到")); } } //發送消息給指定的用戶 消息模型 me:to:[msg] if (msg.split(":").length == 3) { //判斷是單聊消息 this.usermap.getUser(msg.split(":")[1].trim()).writeAndFlush(new TextWebSocketFrame(msg)); } //ctx.channel().writeAndFlush(new TextWebSocketFrame(((TextWebSocketFrame)request).text())); } } }
註釋很詳細,本身看web
消息模型應該定義一個單獨的類來管理,我目前是用的String
字符串來判斷,提早規定了一些模型,經過判斷來響應前端的請求,比較簡單。還有就是沒有使用數據庫,前端不能顯示聊天記錄,不能實現消息的已讀未讀。實際場景中應該對消息進行加密存儲,且不能窺探用戶隱私。
前端可使用localstorage
來存儲聊天記錄,本身能夠擴展。
前端的顯示可能有點問題,本身能夠調。其實主要是學習netty後端的搭建ajax
別忘了關注我 mike啥都想搞
數據庫
求關注啊。後端