基于Node的Web聊天室

1 项目名称

    Web聊天室(《这是NodeJs实战》第二章的一个案例,把整个开发过程记录下来)javascript

 

2 项目描述

    该项目是一个简单的在线聊天程序。打开聊天页面,程序自动给用户分配一个昵称,进入默认的Lobby聊天室。用户能够发送消息,也可使用聊天命令(聊天命令以/开头)修改本身的昵称或者加入已有的聊天室(聊天室不存在时,建立新的聊天室)。在加入或建立聊天室时,新聊天室的名称会出如今聊天程序顶端的水平条上,也会出如今聊天消息区域右侧的可用房间列表中。在用户换到新房间后,系统会显示信息以确认这一变化。css

3 系统设计

    该项目使用Node实现,由于Node用一个端口就能够轻松地提供HTTP和WebSocket两种服务。使用HTTP处理静态文件的同时使用WebSocket实现实时数据(聊天消息)。程序的实现能够划分如下几个功能模块:html

  1. 提供静态文件(好比HTML、CSS和客户端JavaScript)
  2. 在服务器上处理与聊天相关的消息
  3. 在用户的浏览器中处理与聊天相关的消息

    为了提供静态文件,须要使用Node内置的http模块。但经过HTTP提供文件时,一般不能只是发送文件中的内容,还应该有所发送文件的类型。也就是说要用正确的MIME类型设置HTTP 头的Content-Type。为了查找这些MIME类型,会用到第三方的模块mime。java

    为了处理与聊天相关的消息,须要用Ajax轮询服务器。为了让这个程序能尽量快的做出响应,咱们不会用传统的Ajax发送消息。采用WebSocket,这是一个为支持实时通信而设计的轻量的双向通讯协议。由于在大多数状况下,只有兼容HTML5的浏览器才支持WebSocket,因此这个程序会使用流行的Socket.IO库,他给不能使用WebSocket的浏览器提供了一些后备措施。node

4 系统实现

    使用WebStorm开发该项目。WebStorm被称为“最强大的HTML5编辑器”、“最智能的JavaScript IDE”。jquery

4.1 建立程序的文件结构

   使用WebStorm,选择一个目录,建立一个新的空项目。设计项目结构以下所示:npm

 

4.2 指明依赖项

    程序的依赖项是在package.json文件中指明的。这个文件老是被放在程序的根目录下。 package.json文件用于描述你的应用程序,它包含一些JSON表达式。在package.json文件中能够定义不少事情,但最重要的是程序的名称、版本号、对程序的描述,以及程序的依赖项。 代码清单1中是一个包描述文件,描述了项目的功能和依赖项。将这个文件保存到项目的根目录中,命名为package.json。json

{
  "name": "chatrooms",
  "version": "0.0.1",
  "description":"Minimalist multiroom chat server",
  "dependencies":{
    "socket.io":"~0.9.6",
    "mime":"~1.2.11"
 
}
}数组

 

4.3 安装依赖项

切换到DOS窗口,在项目的根目录下输入如下这条命令浏览器

npm install

若是按照失败,切换到国内的npm镜像,而后再安装。镜像使用方法(三种办法任意一种都能解决问题,建议使用第三种,将配置写死,下次用的时候配置还在):

1.经过config命令

npm config set registry https://registry.npm.taobao.org

npm info underscore (若是上面配置正确,这个命令会有字符串response)

2.命令行指定

npm --registry https://registry.npm.taobao.org info underscore

3.编辑 ~/.npmrc 加入下面内容

registry = https://registry.npm.taobao.org

 

    安装成功后,在根目录下建立的node_modules目录,这个目录中放的就是程序的依赖项。 

 

4.4提供HTML、CSS和客户端 JavaScript的服务

    程序的逻辑是由一些文件实现的,有些运行在服务器上,有些运行在客户端。 在客户端运行的JavaScript须要做为静态资源发给浏览器,而不是在Node上执行。

服务器端的文件:

server.js

lib/chat_server.js

发送给客户端的文件:

public/index.html

public/stylesheets/style.css

public/javascripts/chat.js

public/javascripts/chat_ui.js

4.4.1 在server.js中提供静态文件服务器

/**
 * Created by Administrator on 2016-05-05.
 */
var http = require('http');
var fs = require('fs');
var path = require('path');
var mime = require('mime');
var cache={};//缓存文件内容的对象

/* 请求的文件不存在时,发送404错误*, /
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.lookup(path.basename(filePath))});
    response.end(fileContents);
}
/*提供静态文件服务*/
function serverStatic(response,cache,absPath){
    if(cache[absPath]){
        sendFile(response, absPath,cache[absPath]);//从内存中返回数据
    }else{
        fs.exists(absPath,function(exists){//检查文件是否存在
            if(exists){
                fs.readFile(absPath,function(err,data){
                    if(err){
                        send404(response);
                    }else{
                        cache[absPath] = data;
                        sendFile(response,absPath,data);
                    }
                });
            }else{
                send404(response);
            }
        });
    }
}
/* 1 建立HTTP服务器*,从该句代码开始阅读*/
var server= http.createServer(function(request,response){
    var filePath = false;
    if(request.url == '/'){
        filePath = 'public/index.html';
    }else{
        filePath = 'public' + request.url;
    }
    var absPath='./' + filePath;
    serverStatic(response,cache,absPath);
});
server.listen(3000,function(){
    console.log("server listening on port 3000.");
});
/* 2 加载chat_server,建立聊天服务器,chat_server 模块随后实现*/
var chatServer = require('./lib/chat_server');
chatServer.listen(server);

 

4.4.2添加HTML和CSS文件

    Index.html文件内容:

<!doctype html>
<html lang='en'>

<head>
    <title>Chat</title>
    <link rel='stylesheet' href='/stylesheets/style.css'></link>
</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='/socket.io/socket.io.js' type='text/javascript'></script>
<script src='http://code.jquery.com/jquery-1.8.0.min.js' type='text/javascript'></script>
<script src='/javascripts/chat.js' type='text/javascript'></script>
<script src='/javascripts/chat_ui.js' type='text/javascript'></script>
</body>
</html>

 

    style.css文件内容

body {
    padding: 50px;
    font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
    color: #00B7FF;
}

#content {
    width: 800px;
    margin-left: auto;
    margin-right: auto;
}
#room {
    background-color: #ddd;
    margin-bottom: 1em;
}
#messages {
    width: 690px;
    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;
}

 

4.5用Socket.IO处理与聊天相关的功能

4.5.1 chat_server.js实现服务器端功能

var socketio = require('socket.io');
var io;
var guestNumber = 1;
var nickNames = {};
var namesUsed = [];
var currentRoom = {};
exports.listen = function(server){
//启动socket.io服务器,容许它搭载在已有的http服务器上
    io = socketio.listen(server);
    io.set('log level',1);
//定义每一个用户链接的处理逻辑
    io.sockets.on('connection',function(socket){

        // 1 客户端链接后,分配用户昵称
        guestNumber=assignGuestName(socket,guestNumber,nickNames,namesUsed);
        // 2 加入Lobby聊天室
        joinRoom(socket,'Lobby');
        // 3 处理广播消息
        handleMessageBroadcasting(socket,nickNames);
        // 4处理修改昵称命令
        handleNameChangeAttempts(socket,nickNames,namesUsed);
        // 5 处理切换/建立聊天室命令
        handleRoomJoining(socket);
        // 6 当收到客户端请求后,发送给客户端房间列表
        socket.on('rooms',function(){
            socket.emit('rooms',io.sockets.manager.rooms);
        });
        // 7 处理客户端断开链接
        handleClientDisconnection(socket,nickNames,namesUsed);
    });
};
/* 分配用户昵称*/
function assignGuestName(socket,guestNumber,nickNames,namesUsed){
    var name='Guest'+guestNumber;
//将昵称保存在昵称集合nickNames中
    nickNames[socket.id] = name;
//发送给客户端知悉其昵称
    socket.emit('nameResult',{
        success:true,
        name:name
    });
//将昵称保存在另外一个已使用的昵称数组中
    namesUsed.push(name);
    return guestNumber + 1;
}


/*加入房间*/
function joinRoom(socket, room) {
//用户进入房间
    socket.join(room);
//记录用户的当前房间
    currentRoom[socket.id] = room;
//让用户知悉他进入了新的房间
    socket.emit('joinResult', {room: room});
    //让该房间里的其余用户知悉有新用户进入了房间
socket.broadcast.to(room).emit('message', {
        text: nickNames[socket.id] + ' has joined ' + room + '.'
    });
    //获取该房间里全部的用户
    var usersInRoom = io.sockets.clients(room);
//若是用户数量大于1
    if (usersInRoom.length > 1) {
        var usersInRoomSummary = 'Users currently in ' + room + ': ';
        for (var index in usersInRoom) {
            var userSocketId = usersInRoom[index].id;
//判断非当前用户
            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) {
        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.'
                });
            }
        }
    });
}
/*发送聊天消息,即Node服务器收到客户端消息,转发给该房间的其它用户*/
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];
    });
}

 

4.5.2 chat.js以及chat_ui.js实现客户端功能

    客户端JavaScript须要实现如下功能:向服务器发送用户的消息和昵称/房间变动请求; 显示其余用户的消息,以及可用房间的列表。Chat.js中定义一个原型对象,用于处理聊天消息和命令,该原型对象中的函数在chat_ui.js中调用。

chat.js文件内容:

/**
 * Created by Administrator on 2016-05-05.
 */
/*JavaScript原型对象,处理发送聊天消息、变动房间、处理聊天命令*/
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(' ');
            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文件内容:

/**
 * Created by Administrator on 2016-05-05.
 */
/*用来显示可疑的文本。它会净化文本,将特殊字符转换 成HTML实体*/
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) {
            room = room.substring(1, room.length);
            if (room != '') {
                $('#room-list').append(divEscapedContentElement(room));
            }
        }

        $('#room-list div').click(function() {
            chatApp.processCommand('/join ' + $(this).text());
            $('#send-message').focus();
        });
    });
    //每间隔1秒,向服务器从新请求房间列表
    setInterval(function() {
        socket.emit('rooms');
    }, 1000);

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

 

4.6 服务器与客户端的事件分析

服务器与客户端的交互主要是经过相互发送事件-处理事件完成的,如下是在整个流程中发生的事件:

4.6.1服务器事件流程

'connection'事件//接收客户端链接

{

    // 1 assignGuestName()函数中的事件

    socket.emit('nameResult',{ //分配默认昵称

        success:true,

        name:name

    });

 

   // 2 joinRoom()函数中的事件

   socket.emit('joinResult', {room: room});//加入房间

   socket.broadcast.to(room).emit('message', {//广播消息

        text: nickNames[socket.id] + ' has joined ' + room + '.'

    });

   socket.emit('message', {text: usersInRoomSummary});//发送给每一个用户包含该房间的其余用户的列表

 

   // 3 handleMessageBroadcasting()函数中的事件

   socket.on('message', function (message) {//收到客户端消息后,发射给同房间的其余用户

        socket.broadcast.to(message.room).emit('message', {

            text: nickNames[socket.id] + ': ' + message.text

        });

    });

 

   // 4 handleNameChangeAttempts()函数中的事件

   socket.emit('nameResult', {//改名

                    success: true,

                    name: name

                });

   socket.broadcast.to(currentRoom[socket.id]).emit('message', {//房间中其余用户知悉当前用户已改名

                    text: previousName + ' is now known as ' + name + '.'

                });

    // 5 handleRoomJoining()函数中的事件

    socket.on('join', function(room) {//切换聊天室

        socket.leave(currentRoom[socket.id]);

        joinRoom(socket, room.newRoom);

    });

    // 6 当收到客户端请求后,发送给客户端房间列表

        socket.on('rooms',function(){

            socket.emit('rooms',io.sockets.manager.rooms);

        });

    // 7 handleClientDisconnection()函数中的事件

    socket.on('disconnect', function() {

        var nameIndex = namesUsed.indexOf(nickNames[socket.id]);

        delete namesUsed[nameIndex];

        delete nickNames[socket.id];

    });

 

}

4.6.2客户端事件流程

// 1 链接服务器

var socket = io.connect();

// 2 链接服务器后,处理服务器发送过来的事件

socket.on('nameResult', function(result) {...});//显示昵称

socket.on('joinResult', function(result) {...});//显示加入房间

socket.on('message', function (message) {...});//显示该房间的其余用户

socket.on('rooms', function(rooms) {...});//显示房间列表

setInterval(function() {

        socket.emit('rooms');//按期向服务器请求房间列表

    }, 1000);

 

// 3 点击房间列表

$('#room-list div').click(function() {

       this.socket.emit('join', { newRoom: room});//切换聊天室

     });

 

// 4 点击提交按钮调用processUserInput()函数中触发的客户端事件

this.socket.emit('message', message);//发送消息

this.socket.emit('nameAttempt', name);//改名

this.socket.emit('join', { newRoom: room});//建立聊天室

相关文章
相关标签/搜索