背景
客服类型的网页聊天工具,客户点击以后,自动分配一个已在线的客服给对接回答问题。
用netty当作服务端。用简单的html语言搭建网页,消息记录存储在sessionStorage中,勉强实现了消息记录的功能
效果如图
后续:目前实现的功能,应该是可以满足小范围的使用。毕竟每个客户都会建立一条ws链接。没测试过具体能够抗住多少并发
服务端
构建服务器的server
public class NettyWebSocketServer implements Runnable {
// 创建两个线程组 boosGroup、workerGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
@PreDestroy
public void close() {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
@Override
public void run() {
try {
// 创建服务端的启动对象,设置参数
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
// 设置服务端通道实现类型
.channel(NioServerSocketChannel.class)
// 设置线程队列得到连接个数
.option(ChannelOption.SO_BACKLOG, 128)
// 设置保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
// 给pipeline管道设置处理器
channel.pipeline()
// 对http的支持
.addLast(new HttpServerCodec())
// 对大数据块的支持
.addLast(new ChunkedWriteHandler())
// post请求分三部分. request line / request header / message body
// HttpObjectAggregator将多个信息转化成单一的request或者response对象
.addLast(new HttpObjectAggregator(8000))
// 将http协议升级为ws协议. websocket的支持
.addLast(new WebSocketServerProtocolHandler("/chat")).addLast(new WebSocketHandler());
}
});
// 绑定端口号,启动服务端
ChannelFuture channelFuture = bootstrap.bind(8888).sync();
System.out.println("java技术爱好者的服务端已经准备就绪...");
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
消息处理:这边设定的是客服都以“客服”命名开头, 一个客服可以和多个人聊天。其他客户访问网页时就会和某个客户建立1对1聊天
其次是消息处理格式需要是TextWebSocketFrame。这个是封装后的和网页ws交互的消息格式。
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static Map<String, Channel> users = new HashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
System.out.println("有新链接");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
System.out.println("端口链接");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception {
String msgStr = textWebSocketFrame.text();
System.out.println("msg:" + msgStr);
获取客户端发送过来的消息
Message msg = JSON.parseObject(msgStr, Message.class);
System.out.println("接收到消息:" + JSON.toJSONString(msg));
switch (msg.getType()) {
case "register":
users.put(msg.getSrcUserId(), ctx.channel());
if (!msg.getSrcUserId().startsWith("客服")) {
String onlineWorker = getOnlineWorker();
if (onlineWorker == null) {
break;
}
Message res = new Message();
res.setType("init_user");
res.setSrcUserName(onlineWorker);
res.setSrcUserId(onlineWorker);
res.setContent(String.format("你好,%s很高兴为你服务", onlineWorker));
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(res)));
res.setSrcUserId(msg.getSrcUserId());
res.setSrcUserName(msg.getSrcUserName());
res.setContent("...");
users.get(onlineWorker).writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(res)));
}
break;
case "group":
for (Entry<String, Channel> entry : users.entrySet()) {
if (entry.getKey().equals(msg.getSrcUserId())) {
continue;
}
entry.getValue().writeAndFlush(textWebSocketFrame);
}
break;
default:
Channel dstChannel = users.get(msg.getDestUserId());
if (dstChannel == null) {
msg.setContent("sorry, I'am not online, please contact me later");
msg.setSrcUserName(msg.getDestUserName());
msg.setSrcIconUrl(msg.getDestIconUrl());
ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(msg)));
} else {
dstChannel.writeAndFlush(new TextWebSocketFrame(msgStr));
}
}
}
private String getOnlineWorker() {
Map<String, Channel> users = WebSocketHandler.users;
for (String user : users.keySet()) {
if (user != null && user.startsWith("客服")) {
return user;
}
}
return null;
}
}
服务端的逻辑不算多。主要是构建一个消息交换的通道。
网页端
网页是基于一个网上的聊天模板改的。html内容如下:
- 左侧:根据聊天信息渲染左侧人名
- 左侧:根据聊天内容,显示最后一条内容
- 右侧:根据当前选中的人名,渲染聊天内容
- 右侧:他人聊天内容在左边,自己的在右边
- html的访问链接在query位置带上参数user=你的昵称
各类图片头像什么的随便网上抄一抄完事
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>PC聊天</title>
<link rel="stylesheet" href="../static/css/session-eg.css">
</head>
<body>
<div class="main">
<div class="top">
<div class="top-left">
<div class="header"></div>
<div class="search">
<input type="text">
<i class="icon-sear"></i>
</div>
</div>
<div class="top-type">
<a href="#" class="news icon-site"></a>
<a href="#" class="friend icon-site"></a>
<a href="#" class="file icon-site"></a>
</div>
<div class="top-right">
<i class="ic-menu ic-same"></i>
<i class="ic-shrink ic-same"></i>
<i class="ic-boost ic-same"></i>
<i class="ic-close ic-same"></i>
</div>
</div>
<div class="box">
<div class="chat-list" id="chat-list">
</div>
<div class="box-right">
<div class="recvfrom">
<div class="nav-top">
<p id="self-chat_userName">公众号</p>
</div>
<div class="news-top">
<ul id="self-chat">
</ul>
</div>
</div>
<div class="sendto">
<div class="but-nav">
<ul>
<li class="font"></li>
<li class="face"></li>
<li class="cut"></li>
<li class="page"></li>
<li class="old"></li>
</ul>
</div>
<div class="but-text">
<textarea name="" id="chatMessage" cols="110" rows="6"></textarea>
<button class="button" onclick="sendMessage()" type="submit">发送</button>
<!-- <a href="#" class="button" onchange="sendMessage()">发送</a> -->
</div>
</div>
</div>
</div>
</div>
</body>
<script>
function showChatUserName(name) {
document.getElementById('self-chat_userName').innerHTML = name
}
function buildLeft(user) {
old = document.getElementById(user.id)
if (old != null) {
return;
}
usersBox = document.getElementById("chat-list")
userDiv = document.createElement('div')
userDiv.innerHTML = `<div class="list-box" onclick="chooseOne(this)" id="${user.id}">
<img class="chat-head" src="../static/img/img-header2.jpg" alt="">
<div class="chat-rig">
<p class="title" id='name'>${user.name}</p>
<p class="text" id='${user.id}_last'>${user.lastMsg}</p>
</div>
</div>`
selectUser = document.getElementsByClassName('list-box select')
if (selectUser.length == 0) {
userDiv.className = 'list-box select'
sessionStorage.setItem("dst", user.id)
showChatUserName(user.name)
}
usersBox.appendChild(userDiv)
}
function buildNews(type, msg) {
msgLi = document.createElement('li')
msgLi.innerHTML = `<li class="${type}">
<div class="avatar"><img src="../static/img/img-header2.jpg" alt=""></div>
<div class="msg">
<p class="msg-name">${msg.userName}</p>
<p class="msg-text">${msg.message}</p><time>${msg.time}</time>
</div>
</li>`
document.getElementById('self-chat').appendChild(msgLi)
}
function getUserName() {
var url = new URL(window.location.href)
return url.searchParams.get('user')
}
function getDst() {
result = sessionStorage.getItem("dst");
src = getUserName()
if (src == result) {
return 'unknown'
} else {
return result
}
}
function record(userId, record) {
msgsJSON = sessionStorage.getItem(userId)
msgsObj = JSON.parse(msgsJSON)
if (msgsObj == 'undefined' || msgsObj == null) {
msgsObj = []
}
console.log(msgsObj)
msgsObj.push(record)
sessionStorage.setItem(userId, JSON.stringify(msgsObj))
}
function empty(e) {
while (e.firstChild) {
e.removeChild(e.firstChild);
}
}
function showHistory(userId) {
newsDiv = document.getElementById('self-chat')
empty(newsDiv)
msgsJSON = sessionStorage.getItem(userId)
msgsObj = JSON.parse(msgsJSON)
curUsr = getUserName()
for (i in msgsObj) {
var record = msgsObj[i]
buildNews(record.type, record.value)
}
}
var ws = new WebSocket("ws://localhost:8888/chat");
ws.onopen = function () {
console.log("连接成功.")
var serverMsg = {
"type": "register",
"srcUserId": getUserName(),
"srcUserName": getUserName()
}
ws.send(JSON.stringify(serverMsg))
}
ws.onmessage = function (evt) {
console.log(evt)
data = JSON.parse(evt.data)
if (data.type == 'init_user') {
buildLeft({ 'name': data.srcUserName, 'lastMsg': data.content, 'id': data.srcUserId })
showMessage(data)
} else {
showMessage(data);
}
}
function showMessage(data) {
var msg = {
"userName": data.srcUserName,
"message": data.content,
"iconUrl": "../static/img/img-header2.jpg",
"time": "20:19"
}
if (getDst() == data.srcUserId) {
buildNews('other', msg)
}
showLast(data.srcUserId, data.content)
record(data.srcUserId, { "type": 'other', "value": msg })
}
function recordLeft(userId, last) {
leftMap = JSON.parse(sessionStorage.getItem('chat-left'))
if (leftMap == null || leftMap == 'undefined') {
leftMap = new Map()
}
leftMap[userId] = last
sessionStorage.setItem('chat-left', JSON.stringify(leftMap))
}
function showLeft() {
leftMap = JSON.parse(sessionStorage.getItem('chat-left'))
if (leftMap == null || leftMap == 'undefined') {
return;
}
keys = Object.keys(leftMap)
for (i in keys) {
buildLeft({ 'name': keys[i], 'lastMsg': leftMap[keys[i]], 'id': keys[i] })
}
}
function showLast(userId, content) {
lastP = document.getElementById(userId + "_last")
lastP.innerHTML = content
recordLeft(userId, content)
}
function sendMessage() {
msg = {
"userName": getUserName(),
"message": document.getElementById('chatMessage').value,
"iconUrl": "../static/img/img-header2.jpg",
"time": "20:21"
}
buildNews('self', msg)
messageObj = {
"srcUserName": getUserName(),
"srcUserId": getUserName(),
"srcIconUrl": msg.iconUrl,
"type": "self-chat",
"content": msg.message,
"time": msg.time,
"destUserId": getDst(),
"destUserName": getDst(),
"destIconUrl": msg.iconUrl
}
ws.send(JSON.stringify(messageObj))
record(messageObj.destUserId, { "type": "self", "value": msg })
document.getElementById('chatMessage').val('')
}
function chooseOne(ele) {
old = document.getElementsByClassName('list-box select')
for (i in old) {
old[i].className = 'list-box'
}
ele.className = 'list-box select'
var user = {
"id": ele.id,
"name": ele.children[1].children[0].innerHTML
}
console.log(user)
sessionStorage.setItem("dst", user.id)
showChatUserName(user.name)
showHistory(user.id)
}
function init() {
showLeft()
dst = sessionStorage.getItem('dst')
if (dst != null && dst != 'undefined') {
showHistory(dst)
showChatUserName(dst)
}
}
init()
</script>
</html>
css内容如下:文章来源:https://www.toymoban.com/news/detail-441731.html
* {
list-style: none;
padding: 0;
margin: 0;
font-size: 14px;
text-decoration: none;
color: black;
outline: none;
}
html, body {
width: 100%;
height: 100%;
}
.main {
height: 800px;
width: 1005px;
margin: auto;
box-shadow: 0 0 3px 5px #e1e1e1;
}
.main .top {
width: 1005px;
height: 60px;
background-color: #3bb4f2;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.main .top .top-left {
height: 100%;
width: 200px;
float: left;
position: relative;
}
.main .top .top-left .header {
height: 48px;
width: 48px;
border-radius: 50%;
background-image: url("../img/header.jpg");
line-height: 60px;
display: inline-block;
margin: 6px;
border: 1px solid transparent;
}
.main .top .top-left .header:hover {
border: 1px solid white;
}
.main .top .top-left .search {
display: inline-block;
height: 30px;
position: absolute;
margin: 17px 14px;
}
.main .top .top-left .search input {
display: inline-block;
width: 110px;
height: 30px;
border-radius: 40px;
border: 1px solid ghostwhite;
text-indent: 40px;
background-color: #3bb4f2;
}
.main .top .top-left .search input:hover {
border: 1px solid white;
}
.main .top .top-left .search .icon-sear {
background-image: url("../img/sou.png");
background-size: 100% 100%;
height: 30px;
width: 30px;
position: absolute;
margin-top: -31px;
margin-left: 7px;
}
.main .top .top-type {
height: 100%;
width: 200px;
float: left;
margin-left: 200px;
}
.main .top .top-type a.icon-site {
display: inline-block;
height: 40px;
width: 40px;
background-size: 100% 100%;
margin: 10px 11px;
}
.main .top .top-type .news {
background-image: url("../img/news.png");
}
.main .top .top-type .friend {
background-image: url("../img/friend.png");
}
.main .top .top-type .file {
background-image: url("../img/file.png");
}
.main .top .top-right {
height: 100%;
width: 200px;
float: right;
}
.main .top .top-right i.ic-same {
display: inline-block;
height: 20px;
width: 20px;
background-size: 100% 100%;
margin: 19px 7px;
}
.main .top .top-right i.ic-same.ic-menu {
margin-left: 48px;
}
.main .top .top-right .ic-menu {
background-image: url("../img/menu.png");
}
.main .top .top-right .ic-menu:hover {
background-image: url("../img/menu (1).png");
}
.main .top .top-right .ic-shrink {
background-image: url("../img/shrink.png");
}
.main .top .top-right .ic-shrink:hover {
background-image: url("../img/shrink (1).png");
}
.main .top .top-right .ic-boost {
background-image: url("../img/boost.png");
}
.main .top .top-right .ic-boost:hover {
background-image: url("../img/boost (1).png")
}
.main .top .top-right .ic-close {
background-image: url("../img/close.png");
}
.main .top .top-right .ic-close:hover {
background-image: url("../img/close (1).png");
}
.main .box {
width: 100%;
height: 740px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.main .box .chat-list {
float: left;
width: 250px;
height: 100%;
background-color: #f4f4f4;
}
.main .box .chat-list .list-box {
height: 80px;
width: 250px;
}
.main .box .chat-list .list-box.select {
background-color: #dbdbdb;
}
.main .box .chat-list .list-box:hover {
background-color: #dbdbdb;
}
.main .box .chat-list .list-box img.chat-head {
height: 50px;
width: 50px;
border-radius: 50%;
border: 1px solid #f4f4f4;
margin: 15px 10px;
}
.main .box .chat-list .list-box .chat-rig {
float: right;
height: 50px;
width: 178px;
margin: 15px 0;
}
.main .box .chat-list .list-box .chat-rig .title {
font-weight: 600;
font-size: 17px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.main .box .chat-list .list-box .chat-rig .text {
font-size: 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #6C6C6C;
}
.main .box .box-right {
float: left;
width: 750px;
height: 100%;
}
.main .box .box-right .recvfrom {
width: 752px;
height: 560px;
}
.main .box .box-right .recvfrom .nav-top {
height: 45px;
width: 100%;
}
.main .box .box-right .recvfrom .nav-top p {
line-height: 45px;
font-size: 18px;
font-weight: 600;
margin-left: 25px;
}
.main .box .box-right .recvfrom .news-top {
height: 510px;
border-top: 1px solid #6C6C6C;
border-bottom: 1px solid #6C6C6C;
overflow-y: scroll;
}
.main .box .box-right .recvfrom .news-top ul {
height: 100%;
width: 100%;
}
.main .box .box-right .recvfrom .news-top ul li {
margin: 10px;
min-height: 80px;
position: relative;
overflow: hidden;
}
.main .box .box-right .recvfrom .news-top ul li .avatar img {
height: 30px;
width: 30px;
border-radius: 50%;
}
.main .box .box-right .recvfrom .news-top ul li .msg {
top: -10px;
margin: 8px;
min-height: 80px;
}
.main .box .box-right .recvfrom .news-top ul li::after {
clear: both;
content: "";
display: inline-block;
}
.main .box .box-right .recvfrom .news-top ul li .msg .msg-text {
background-color: #6C6C6C;
border-radius: 5px;
padding: 8px;
}
.main .box .box-right .recvfrom .news-top ul li .msg time {
float: right;
color: #ccc;
}
.main .box .box-right .recvfrom .news-top ul li.other .avatar {
position: absolute;
left: 0;
top: 0;
}
.main .box .box-right .recvfrom .news-top ul li.other .msg {
position: absolute;
left: 40px;
}
.main .box .box-right .recvfrom .news-top ul li.self .avatar {
position: absolute;
right: 0;
top: 0;
}
.main .box .box-right .recvfrom .news-top ul li.self .msg {
position: absolute;
right: 38px;
}
.main .box .box-right .sendto {
width: 752px;
height: 180px;
}
.main .box .box-right .sendto .but-nav {
height: 40px;
}
.main .box .box-right .sendto .but-nav ul li {
float: left;
height: 22px;
width: 22px;
margin: 7px 15px;
background-size: 100% 100%;
}
.main .box .box-right .sendto .but-nav ul li:hover {
background-color: #dbdbdb;
}
.main .box .box-right .sendto .but-nav ul li.font {
background-image: url("../img/font.png");
}
.main .box .box-right .sendto .but-nav ul li.face {
background-image: url("../img/face.png");
}
.main .box .box-right .sendto .but-nav ul li.cut {
background-image: url("../img/cut.png");
}
.main .box .box-right .sendto .but-nav ul li.page {
background-image: url("../img/page.png");
}
.main .box .box-right .sendto .but-nav ul li.old {
background-image: url("../img/old.png");
}
.main .box .box-right .sendto .but-text textarea {
border: none;
font-size: 22px;
margin-left: 20px;
width: 732px;
height: 100px;
}
.main .box .box-right .sendto .but-text .button {
float: right;
padding: 5px 25px;
background-color: #3bb4f2;
margin-right: 20px;
}
文章来源地址https://www.toymoban.com/news/detail-441731.html
到了这里,关于网页版即时聊天工具的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!