共计 10191 个字符,预计需要花费 26 分钟才能阅读完成。
上节课讲了群聊,这次来说说单聊,单聊要比群聊复杂点,但是代码也不是很多,主要是前端显示比较麻烦点。
效果:
登陆
首先一个新的用户,需要先登陆,输入自己的昵称,然后点击登陆。后端服务会把你的用户名和当前的线程进行邦定,这样就可以通过你的用户名找到你的线程。登陆成功,后端返回定义好的消息 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 啥都想搞
求关注啊。