web聊天室

标签: node.js  web  聊天  websocket

最近这两天在看node.js,看大家好多人都拿聊天室这个项目来练习node,我也来凑个热闹。

整个项目是基于node.js ,webSocket的实现实时通信,管理多个房间,统计房间内用户,切换不同房间等功能。

看着nodejs实战这本书,但书中代码老旧,读懂书中代码后,去官网查最新文档,对比着修改代码,最后将功能重新实现。既屡了逻辑,也了解了最新文档,收获蛮多的。

目录结构
这里写图片描述

代码:
server.js:

let http = require('http'); //内置的http模块提供了HTTP服务器和客户端功能
let fs = require('fs');  //内置的fs模块提供了与文件系统相关的功能
let path = require('path');  //内置的path模块提供了与文件系统路径相关的功能
let mime = require('mime');  //附加的mime模块有根据文件扩展名得出MIME类型的能力
let cache = {};  //cache是用来缓存文件内容的对象
let chatServer = require('./lib/chat_server');

function send404(response) {
    response.writeHead(404, {'Content-Type': 'text/plain'});
    response.write('Error 404: resource not found.');
    response.end();
}

function sendFile(response, filePath, fileContents) {
    response.writeHead(
        200,
        {"content-type": mime.getType(path.basename(filePath))} );
    response.end(fileContents);
}

function serveStatic(response, cache, absPath) {
    if (cache[absPath]) {  //检查文件是否缓存在内存中
        sendFile(response, absPath, cache[absPath]);  //从内存中返回文件
    } else {
        fs.access(absPath, (err) => {  //检查文件是否存在
            if (err) {
                send404(response);
            } else {
                fs.readFile(absPath, function(err, data) {  //从硬盘中读取文件
                    if (err) {
                        send404(response);
                    } else {
                        cache[absPath] = data;
                        sendFile(response, absPath, data);  //从硬盘中读取文件并返回
                    }
                });
            }
        });
    }
}

let server = http.createServer(function(request, response) {  //创建HTTP服务器,用匿名函数定义对每个请求的处理行为
    let filePath = false;
    if (request.url === '/') {
        filePath = 'public/index.html';
    } else {
        filePath = 'public' + request.url;  //将URL路径转为文件的相对路径
    }

    let absPath = './' + filePath;
    serveStatic(response, cache, absPath);  //返回静态文件
});

server.listen(3000, function() {
    console.log("Server listening on port 3000.");
});

chatServer.listen(server);

chat-server.js:

let socketio = require('socket.io');
let io;
let guestNumber = 1;
let nickNames = {};
let namesUsed = [];
let currentRoom = {};

exports.listen = function(server) {
    io = socketio.listen(server);   //启动Socket.IO服务器,允许它搭载在已有的HTTP服务器上
    // io.set('log level', 1);
    io.sockets.on('connection', function (socket) {  //定义每个用户连接的处理逻辑
        guestNumber = assignGuestName(socket, guestNumber,nickNames, namesUsed);  //在用户连接上来时赋予其一个访客名
        joinRoom(socket, 'Lobby');  //在用户连接上来时把他放入聊天室Lobby里
        handleMessageBroadcasting(socket, nickNames);  //处理用户的消息,更名,以及聊天室的创建和变更
        handleNameChangeAttempts(socket, nickNames, namesUsed);
        handleRoomJoining(socket);
        socket.on('rooms', function() {  //当用户发出请求时,向其提供已经被占用的聊天室的列表
            socket.emit('rooms', io.sockets.adapter.rooms);
        });
        handleClientDisconnection(socket, nickNames, namesUsed); //定义用户断开连接后的清除逻辑
    });
};

function assignGuestName(socket, guestNumber, nickNames, namesUsed) {
    var name = 'Guest' + guestNumber;  //生成新昵称
    nickNames[socket.id] = name; //把用户昵称跟客户端连接ID关联上
    socket.emit('nameResult', {  //让用户知道他们的昵称
        success: true,
        name: name
    });
    namesUsed.push(name);  //存放已经被占用的昵称
    return guestNumber + 1;  //增加用来生成昵称的计数器
}

function joinRoom(socket, room) {
    socket.join(room);  //让用户进入房间
    currentRoom[socket.id] = room;  //记录用户的当前房间
    console.log(currentRoom)
    socket.emit('joinResult', {room: room});  //让用户知道他们进入了新的房间
    socket.broadcast.to(room).emit('message', {  //让房间里的其他用户知道有新用户进入了房间
        text: nickNames[socket.id] + ' has joined ' + room + '.'
    });
    var usersInRoom = io.sockets.adapter.rooms[room]; //确定有哪些用户在这个房间里
    if (usersInRoom&&Object.keys(usersInRoom).length > 1) {  //如果不止一个用户在这个房间里,汇总下都是谁
        var usersInRoomSummary = 'Users currently in ' + room + ': ';
        for (var index in usersInRoom['sockets']) {
            let userSocketId = index;
            if (userSocketId !== socket.id) {
                if (index > 0) {
                    usersInRoomSummary += ', ';
                }
                usersInRoomSummary += nickNames[userSocketId];
            }
        }
        usersInRoomSummary += '.';
        socket.emit('message', {text: usersInRoomSummary});  //将房间里其他用户的汇总发送给这个用户
    }
}

function handleNameChangeAttempts(socket, nickNames, namesUsed) {
    socket.on('nameAttempt', function(name) {  //添加nameAttempt事件的监听器
        if (name.indexOf('Guest') == 0) {  //昵称不能以Guest开头
            socket.emit('nameResult', {
                success: false,
                message: 'Names cannot begin with "Guest".'
            });
        } else {
            if (namesUsed.indexOf(name) == -1) {  //如果昵称还没注册就注册上
                var previousName = nickNames[socket.id];
                var previousNameIndex = namesUsed.indexOf(previousName);
                namesUsed.push(name);
                nickNames[socket.id] = name;
                delete namesUsed[previousNameIndex];  //删掉之前用的昵称,让其他用户可以使用
                socket.emit('nameResult', {
                    success: true,
                    name: name
                });
                socket.broadcast.to(currentRoom[socket.id]).emit('message', {
                    text: previousName + ' is now known as ' + name + '.'
                });
            } else {
                socket.emit('nameResult', {  //如果昵称已经被占用,给客户端发送错误消息
                    success: false,
                    message: 'That name is already in use.'
                });
            }
        }
    });
}

function handleMessageBroadcasting(socket) {
    socket.on('message', function (message) {
        socket.broadcast.to(message.room).emit('message', {
            text: nickNames[socket.id] + ': ' + message.text
        });
    });
}

function handleRoomJoining(socket) {
    socket.on('join', function(room) {
        socket.leave(currentRoom[socket.id]);
        joinRoom(socket, room.newRoom);
    });
}

function handleClientDisconnection(socket) {
    socket.on('disconnect', function() {
        var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
        delete namesUsed[nameIndex];
        delete nickNames[socket.id];
    });
}

chat.js:

var Chat = function(socket) {
    this.socket = socket;
};

Chat.prototype.sendMessage = function(room, text) {
    var message = {
        room: room,
        text: text
    };
    this.socket.emit('message', message);
};

Chat.prototype.changeRoom = function(room) {
    this.socket.emit('join', {
        newRoom: room
    });
};

Chat.prototype.processCommand = function(command) {
    var words = command.split(' ');
    var command = words[0]
        .substring(1, words[0].length)
        .toLowerCase();  //从第一个单词开始解析命令
    var message = false;
    switch(command) {
        case 'join':
            words.shift();
            var room = words.join(' ');
            var nowRoom = $('#room').text();
            if(room !== nowRoom){
                if(room.length > 10){
                    alert('房间名不能超过10个英文字母')
                }else{
                    this.changeRoom(room);  //处理房间的变换/创建
                }
            }
            break;
        case 'nick':
            words.shift();
            var name = words.join(' ');
            this.socket.emit('nameAttempt', name);  //处理更名尝试
            break;
        default:
            message = 'Unrecognized command.';  //如果命令无法识别,返回错误消息
            break;
    }
    return message;
};

chat-ui.js:

function divEscapedContentElement(message) {
    return $('<div></div>').text(message);
}
function divSystemContentElement(message) {
    return $('<div></div>').html('<i>' + message + '</i>');
}

function processUserInput(chatApp, socket) {
    var message = $('#send-message').val();
    var systemMessage;

    if (message.charAt(0) == '/') {  //如果用户输入的内容以斜杠(/)开头,将其作为聊天命令
        systemMessage = chatApp.processCommand(message);
        if (systemMessage) {
            $('#messages').append(divSystemContentElement(systemMessage));
        }
    } else {
        chatApp.sendMessage($('#room').text(), message);  //将非命令输入广播给其他用户
        $('#messages').append(divEscapedContentElement(message));
        $('#messages').scrollTop($('#messages').prop('scrollHeight'));
    }
    $('#send-message').val('');
}

var socket=io.connect();

$(document).ready(function() {
    var chatApp = new Chat(socket);

    socket.on('nameResult', function(result) {  //显示更名尝试的结果
        var message;

        if (result.success) {
            message = 'You are now known as ' + result.name + '.';
        } else {
            message = result.message;
        }
        $('#messages').append(divSystemContentElement(message));
    });

    socket.on('joinResult', function(result) {  //显示房间变更结果
        $('#room').text(result.room);
        $('#messages').append(divSystemContentElement('Room changed.'));
    });

    socket.on('message', function (message) {//显示接收到的消息
        var newElement = $('<div></div>').text(message.text);
        $('#messages').append(newElement);
    });

    socket.on('rooms', function(rooms) {  //显示可用房间列表
        $('#room-list').empty();
        for(var room in rooms) {
            if (room.length <= 10) {
                $('#room-list').append(divEscapedContentElement(room));
            }
        }

        $('#room-list div').click(function() {  //点击房间名可以换到那个房间中
            chatApp.processCommand('/join ' + $(this).text());
            $('#send-message').focus();
        });
    });

    setInterval(function() {  //定期请求可用房间列表
        socket.emit('rooms');
    }, 1000);

    $('#send-message').focus();

    $('#send-form').submit(function() {  //提交表单可以发送聊天消息
        processUserInput(chatApp, socket);
        return false;
    });
});

html:

<!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">
    <link rel="stylesheet" href="/css/style.css">
    <title>Chat-Room</title>
</head>
<body>
<div id='content'>
    <div id='room'></div>
    <div id='room-list'></div>
    <div id='messages'></div>
    <form id='send-form'>
        <input id='send-message' />
        <input id='send-button' type='submit' value='Send'/>

        <div id='help'>
            Chat commands:
            <ul>
                <li>Change nickname: <code>/nick [username]</code></li>
                <li>Join/create room: <code>/join [room name]</code></li>
            </ul>
        </div>
    </form>
</div>
<script src='http://code.jquery.com/jquery-3.2.1.min.js'       type='text/javascript'></script>
<script src='/socket.io/socket.io.js' type='text/javascript'></script>
<script src='/js/chat.js' type='text/javascript'></script>
<script src='/js/chat_ui.js' type='text/javascript'></script>
</body>
</html>

css:

body {
    padding: 50px;
    font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
    color: #00B7FF;
}
#content {
    width: 800px;  /*程序界面的宽度是800p,水平居中*/
    margin-left: auto;
    margin-right: auto;
}
#room {
    background-color: #ddd;  /*显示当前聊天室名称那个区域的CSS规则*/
    margin-bottom: 1em;
}
#messages {
    width: 690px;  /*显示消息的区域宽690p,高300p*/
    height: 300px;
    overflow: auto;  /*让显示消息的区域在内容填满后可以向下滚动*/
    background-color: #eee;
    margin-bottom: 1em;
    margin-right: 10px;
}
#room-list {
    float: right;
    width: 100px;
    height: 300px;
    overflow: auto;
}
#room-list div {
    border-bottom: 1px solid #eee;
}
#room-list div:hover {
    background-color: #ddd;
}
#send-message {
    width: 700px;
    margin-bottom: 1em;
    margin-right: 1em;
}
#help {
    font: 10px "Lucida Grande", Helvetica, Arial, sans-serif;
}

代码地址:https://github.com/zhouxinzhouxin/web-chatRoom

版权声明:本文为zhouxin_182原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zhouxin_182/article/details/79134085

智能推荐

线索二叉树-c++实现

基本思想 1.根据用户输入创建二叉树(一般用前序遍历) 2.线索化二叉树(一般用中序遍历线索化)(此时要把head节点加进来) 3.通过输出中序遍历结果来检验线索化是否正确 详细注释在代码中,可以先看main函数中的流程,再看具体函数实现 实验中采用的二叉树如图 完整代码 运行结果如图所示...

Hadoop实战(4)_Hadoop的集群管理和资源分配

系列目录: Hadoop实战(1)_阿里云搭建Hadoop2.x的伪分布式环境 Hadoop实战(2)_虚拟机搭建Hadoop的全分布模式 Hadoop实战(3)_虚拟机搭建CDH的全分布模式 DataNode数据目录 如果有多个挂载点,可以有多个DataNode数据目录。 目前服务器硬件,标准小型机配置:32核、64G(128G)、64T(4T*16盘SAS盘)。通常为了提升磁盘吞吐量,每个盘单...

Tornado day02

一,项目模板: Tornado的项目也可以像Django和flask一样,将功能细分为几个模块 1.1 _ _ init _ _.py 1.2 setting .py 1.3 urls .py 1.4 views .py 1.5 manage .py 将这个模板拷贝下来,以后创建新项目的时候可以直接拷贝一份,在此模板上修改使用 文件链接 链接:https://pan.baidu.com/s/11E...

PAT乙级 | 1095 解码PAT准考证 (25分)(做题过程+注意事项+运行超时解决方法)

PAT 准考证号由 4 部分组成: 第 1 位是级别,即 T 代表顶级;A 代表甲级;B 代表乙级; 第 2~4 位是考场编号,范围从 101 到 999; 第 5~10 位是考试日期,格式为年、月、日顺次各占 2 位; 最后 11~13 位是考生编号,范围从 000 到 999。 现给定一系列考生的准考证号和他们的成绩,请你按照要求输出各种统计信息。 输入格式: 输入首先在一行中给出两个正整数 ...

谈谈Java异常

0 概述 对于java工程师来说,是经常和异常打交道的,本文主要来谈一谈java中的异常。 1 异常类的继承关系 从下图(说明:图中只是列出部分异常类)可以看出: 异常的基类为Throwable,主要分为两个分支,即Error体系和Exception体系。 Exception下面分为RuntimeException和非RuntimeException(如IOException) 2 几种异常的区别...

猜你喜欢

通过设立FatFS隐藏分区,实现系统文件和用户文件的隔离

嘛。。这是一个关于个人使用FatFS文件系统的 一点小的经验。 我知道大家都会百度和谷歌,关于文件系统有什么用,文件系统怎么移植上自己的平台,看看资料也就懂了,在这里不再详述( 打字太慢一分钟50-60字懒得写)。本系列默认已经可以将设备模拟成u盘,并且已经通过修改diskio.c,可以实现ff.c中的各项功能( 不能实现的自行面壁)。FatFS项目官网 http://elm-chan.org/f...

Mysql之锁与事务知识要点小结

Mysql之锁与事务 平时的业务中,顶多也就是写写简单的sql,连事务都用的少,对锁这一块的了解就更加欠缺了,之前一个大神分享了下mysql的事务隔离级别,感觉挺有意思的,正好发现一个很棒的博文,然后也收集了一些相关知识,正好来学习下,mysql中锁与事务的神秘面纱,主要内容包括 共享锁和排它锁的区别以及适合范围 mysql的表锁和行锁的区别 怎么判断一个sql是否执行了锁,执行的是表锁还是行锁 ...

响应式图片二 通过srcset实现

具体方法如下: srcset=”图片地址+空格+尺寸描述符,图片地址+空格+尺寸描述符,图片地址+空格+尺寸描述符….” 浏览器会当前浏览的环境进行感知,这个感知包括网速、界面分辨率、DPR(屏幕像素比)等等,然后在图片中选择一个进行加载。 实际上,在相同DPR下,浏览器会根据屏幕的分辨率加载图片,但是加载了大的图片后再缩小还是会使用大的图片。综合考虑的算法非...

Training_model(2)

已经清洗处理了两个数据文件: application_{train|test}.csv :客户详细信息 bureau.csv : 客户历史信用报告 下面对这两个数据中的特征进行合并,然后Light Gradient Boosting Machine训练模型,之前只用客户数据的预测评分结果是0.734,这次加入了客户信用报告信息 load data 新增加了客户历史信用记录 Build Model ...

微信小程序 页面跳转(传参跟不传参)

跳转页面传参 1.首先我的目录结构是这样的,并在 cinema.wxml 定义了一个点击事件 bindtap=‘indetai’ 2.然后在 cinema.js 的data里面定义了一个 score,并实现了 indetai 方法 3.在 detai.js 的 data 里面也定义一个 score ,再在 onLoad 函数里面接收传递过来的值 4.在页面上显示得到的值 这...