Node实战之聊天室

Node实战之聊天室

Node如何同时处理Http和WebSocket

  1.只出现在用户访问聊天程序网站时:Web浏览器->Http请求->Node服务器->Http响应->Web浏览器

  2.在用户聊天时持续发生:Web浏览器->WebSocket数据发送->Node服务器->WebSocket数据接收->Web浏览器

 

开始搭建

  1.创建程序文档结构(如下图所示)

  

  2.指明依赖项

    程序的依赖项是在package.json文件中指明的。这个文件总是被放在程序的根目录下。package。json文件用于描述你的应用程序,它包含一些JSON表达式,并遵循CommonJS包描述标准。在package.json文件中可以定义很多事情,但最重要的是程序的名称、版本号、对程序的描述,以及程序的依赖项。

  包描述文件

  

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

 

  3.安装依赖项

    定义好package.json文件之后,安装程序的依赖项就是小菜一碟了。Node包管理器(npm)是Node自带的工具,他有很多功能,可以轻松安装第三方Node模块,可以把你自己创建的任何Node模块向全球发布。它用一行命令就能从package.json文件中读出依赖项,把他们都装好。

  在根目录下(E:\nodeTest\chatrooms)输入如下命令

    npm install

  在看这个目录,你应该能看到node_modules目录,这个目录中存放的就是程序的依赖项。

 

  4.创建静态文件服务器 server.js

    (1).发送文件数据及错误响应

//(1)请求的文件不存在时发送404错误
function send404(response){
    response.writeHead(404,{'Content-Type':'text/plain'});
    response.write('Error 404 :resource not found.');
    response.end();
}

//(2)辅助函数提供文件数据服务。这个函数先写出正确的HTTP头,然后发送文件的内容。
function sendFile(response,filePath,fileContents){
    
    response.writeHead(
        200,
        {"Content-Type":mime.lookup(path.basename(filePath))}
    );
    response.end(fileContents);
}
//(3)访问内存(RAM)要比访谈我呢文件系统快得多,所以Node程序通常会把常用的数据缓存到内存里。
//我们的聊天程序就要把静态文件缓存到内存中,只有第一次访问的时候才会从文件系统中读取。
//下一个辅助函数会确定文件是否缓存了,如果是,就返回它。如果文件还没被缓存,它会从硬盘中
//读取并返回它。如果文件不存在,则返回一个HTTP 404错误作为响应。
function serveStatic(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);
            }
        });
    }
}

     (2).创建HTTP服务器

      在创建HTTP服务期时,需要给createServer传入一个匿名函数作为回调函数,由它来处理每个HTTP请求。这个回调函数接受两个参数:request和response。在这个    回调函数执行时,HTTP服务器会分别组装这两个参数对象,以便你可以对请求的细节进行处理,并返回一个响应。

//创建HTTP服务器,用匿名函数定义对每个请求的处理行为
var server = http.createServer(function(request,response){
    var filePath = false;
    if(request.url == '/'){
        filePath = 'public/index.html'; //确定返回的默认HTML文件
    }else{
        filePath = 'public' + request.url; //将url路径转换成文件的相对路径
    }
    
    var absPath = './' + filePath;
    serveStatic(response,cache,absPath); //返回静态文件
})

    (3).启动HTTP服务器

  

server.listen(3000,function(){
    console.log("服务器已启动  端口号3000");
});

   5.创建html和css

  

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>聊天室</title>
        <link rel='stylesheet' href='/css/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='发送' />
                
                <div id='help'>
                    聊天室操作
                    <ul>
                        <li>昵称:<code>/nick[username]</code></li>
                        <li>进入/创建房间:<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.min.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>
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;
}

  6.用Socket,IO处理与聊天相关的消息

  Socket.IO为Node及客户端JavaScript提供了基于WebSocket以及其他传输方式的封装,它提供了一个抽象层。如果浏览器没有实现WebSocket,Socket.IO会自动启用一个备选方案,而对外提供的API还是一样的。

  Socket.IO提供了开箱即用的虚拟通道,所以程序不用把每条消息都向已经连接的用户广播,而只向那些预订了某个通道的用户广播。用这个功能实现程序里的聊天室非常简单,很快你就能看到。

  Socket.IO还是事件发射器(Event Emitter)的好例子。事件发射器本质上是组织异步逻辑的一种很方便的设计模式。

  事件发射器是跟某种资源相关联的,它能向这个资源发送消息,也能从这个资源接收消息。资源可以链接远程服务器,或者更抽象的东西。

    (1)设置Socket.IO服务器

//一.设置Socket.IO服务器
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){
        console.log("a new user connection!");
        //在用户链接上来时,赋予其一个访客名
        guestNumber = assignGuestName(socket,guestNumber,nickNames,namesUsed);
        //在用户连接上来时把它放入聊天室Lobby里
        joinRoom(socket,'Lobby');
        
        handleMessageBroadcasting(socket,nickNames);
        
        handleNameChangeAttempts(socket,nickNames,namesUsed);
        
        handleRoomJoining(socket);
        //用户发出请求时,向其提供已经被占用的聊天室的列表
        socket.on('rooms',function(){
            socket.emit('rooms',io.sockets.manager.rooms);
        });
        //定义用户断开连接后的清除逻辑
        handleClientDisconnection(socket,nickNames,namesUsed);
    });
}

//二.处理程序场景及事件
//1.分配用户昵称
function assignGuestName(socket,guestNumber,nickNames,namesUsed){
    //生成新昵称
    var  name = 'Guest' +guestNumber;
    nickNames[socket.id] = name;
    //让用户知道他们的昵称
    socket.emit('nameResult',{
        success : true,
        name : name
    });
    //存放已经被占用的昵称
    namesUsed.push(name);
    //增加用来生成昵称的计数器
    return guestNumber + 1;
}

//2.进入聊天室
function joinRoom(socket,room){
    console.log(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);
    //如果不止一个用户在这个房间里,汇总一下都是谁
    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});
    }
}
//3.处理昵称变更请求
function handleNameChangeAttempts(socket,nickNames,namesUsed){
    socket.on('nameAttempt',function(name){
        //昵称不能以Guest开头
        if(name.indexOf('Guest') ==0){
            socket.emit('nameResult',{
                success: false,
                message: 'Name 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 konwn as ' + name + '.'
                });
            }else{
                //如果用户名已经被占用,给客户端发送错误消息
                socket.emit('nameResult',{
                    success:false,
                    message:'That name is aleady in use.'
                });
            }
        }
    });
}

//4.发送聊天消息 Socket.IO的broadcase函数是用来转发消息的.

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

//5.创建房间
function handleRoomJoining(socket){
    socket.on('join',function(room){
        //console.log(room.newRoom);
        //console.log(currentRoom[socket.id]);
        socket.leave(currentRoom[socket.id]);
        joinRoom(socket,room.newRoom);
    });
}
//6.用户断开连接
function handleClientDisconnection(socket){
    socket.on('disconnect',function(){
        var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
        delete namesUsed[nameIndex];
        delete nickNames[socket.id];
    });
}

    7.在程序的用户界面上使用客户端JavaScript

      (1).向服务器发送用户的消息和昵称/房间变更请求;(在js目录中新建chat.js)

      (2).显示其他用户的消息,以及可用房间的列表(在js目录中新建chat_ui.js)

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;
    console.log(command);
    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 = 'Unrecongnized 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('');
}



//用户的浏览器加载完页面后执行  对客户端Socket.IO事件处理进行初始化
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 konw as ' + result.name + '.';
        }else{
            message = result.message;
        }
        $('#messages').append(divSystemContentElement);
    });
    
    //显示房间变更结果
    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();
        console.log(rooms);
        for(var room in rooms){
            //console.log(rooms.size);
            room = room.substring(1,room.length);
            console.log(room);
            if(room !=''){
                $('#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;
    });
});

 

最后效果图:

posted @ 2016-08-12 13:26  DemonGao  阅读(3404)  评论(2编辑  收藏  举报