Skip to content

网页版即时聊天工具

背景

客服类型的网页聊天工具,客户点击以后,自动分配一个已在线的客服给对接回答问题。

用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内容如下:

* {
    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;
}
 
 
 

 


版权声明:本文为whylackgoodname原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

聚圣源网店店铺起名净水器公司起名男孩起名的金氏程女宝宝起名绿化公司起名四字周迅老公庆祝大会国旗护卫队都是90后00后网络起名可信吗阿娇门艳高清全集图片包青天之欣的组词起名关于悟空理财果蔬配送公司名字起名大全300英雄官方铁路股票中华起名网免费取名欢迎您姓戴起名字女孩名字蒙娜丽莎卫浴远古战神中国竞彩网比分直播cctv1高清我们假戏真做吧航空公司起名光的传说皮带家族是谁免费好用的起名怎样给英文品牌起名蔬菜公司名起名大全合作社起名大全免费观看全国区号淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男孩疑遭霸凌 家长讨说法被踢出群国产伟哥去年销售近13亿网友建议重庆地铁不准乘客携带菜筐雅江山火三名扑火人员牺牲系谣言代拍被何赛飞拿着魔杖追着打月嫂回应掌掴婴儿是在赶虫子山西高速一大巴发生事故 已致13死高中生被打伤下体休学 邯郸通报李梦为奥运任务婉拒WNBA邀请19岁小伙救下5人后溺亡 多方发声王树国3次鞠躬告别西交大师生单亲妈妈陷入热恋 14岁儿子报警315晚会后胖东来又人满为患了倪萍分享减重40斤方法王楚钦登顶三项第一今日春分两大学生合买彩票中奖一人不认账张家界的山上“长”满了韩国人?周杰伦一审败诉网易房客欠租失踪 房东直发愁男子持台球杆殴打2名女店员被抓男子被猫抓伤后确诊“猫抓病”“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火男孩8年未见母亲被告知被遗忘恒大被罚41.75亿到底怎么缴网友洛杉矶偶遇贾玲杨倩无缘巴黎奥运张立群任西安交通大学校长黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发妈妈回应孩子在校撞护栏坠楼考生莫言也上北大硕士复试名单了韩国首次吊销离岗医生执照奥巴马现身唐宁街 黑色着装引猜测沈阳一轿车冲入人行道致3死2伤阿根廷将发行1万与2万面值的纸币外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万手机成瘾是影响睡眠质量重要因素春分“立蛋”成功率更高?胖东来员工每周单休无小长假“开封王婆”爆火:促成四五十对专家建议不必谈骨泥色变浙江一高校内汽车冲撞行人 多人受伤许家印被限制高消费

聚圣源 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化