上節課講了羣聊,這次來說說單聊,單聊要比羣聊複雜點,但是代碼也不是很多,主要是前端顯示比較麻煩點。
效果:
登陸
首先一個新的用戶,需要先登陸,輸入自己的暱稱,然後點擊登陸。後端服務會把你的用戶名和當前的線程進行邦定,這樣就可以通過你的用戶名找到你的線程。登陸成功,後端返回定義好的消息 success
,前端判斷記錄CHAT.me
,這樣給別人發消息時就可以攜帶自己的信息。
查找用戶
在輸入框輸入用戶名,就可以返回對應的用戶的線程,這樣你就可以把消息發送給你要聊天的對象。如果不存在,後端回返回消息給前端,該用戶不存在。如果存在,就記錄此用戶名到CHAT.to
中,這樣你發送消息的時候就可以發送給對應用戶了。
開始聊天
發送聊天信息時me:to:消息
,這樣後端就知道是誰要發給誰,根據用戶名去找到具體的線程去單獨推送消息,實現單聊。
前端待完善
左側聊天列表沒有實現,每搜索一個在線用戶,應該動態顯示在左側,點擊該用戶,動態顯示右側聊天窗口進行消息發送。現在是你和所有人的單聊消息都會顯示在右側,沒有完成拆分,因爲這是一個頁面,處理起來比較麻煩,我一個後端就不花時間搞了,感興趣的可以自己去實現。
前端代碼
因爲注視比較詳細,就直接複製整個代碼到這裏,大家自己看。
<!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和Channel
package 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()));
}
}
}
註釋很詳細,自己看
總結
消息模型應該定義一個單獨的類來管理,我目前是用的String
字符串來判斷,提前規定了一些模型,通過判斷來響應前端的請求,比較簡單。還有就是沒有使用數據庫,前端不能顯示聊天記錄,不能實現消息的已讀未讀。實際場景中應該對消息進行加密存儲,且不能窺探用戶隱私。
前端可以使用localstorage
來存儲聊天記錄,自己可以擴展。
前端的顯示可能有點問題,自己可以調。其實主要是學習netty後端的搭建
別忘了關注我 mike啥都想搞
求關注啊。