• 第二章(构建有多个房间的聊天室程序)


    本章内容

    1:认识各种Node组件

    2:一个用Node做的实时程序

    3:服务器跟客户端交互

      完成本章你将:

      游览这个程序,了解他是如何工作的;

      审查技术需求,并完成程序的初始设置;

      提供程序所需的HTML、CSS和客户端JavaScript;

      用Socket.IO处理跟聊天相关的消息;

      用客户端JavaScript做程序的UI;

      

      2.1程序概览

      本章会构建一个在线聊天程序,用户可以在一个简单的表达中输入消息,相互聊天,消息输入后会发送给同一个聊天室内的其他所有用户。

      进入聊天室以后,程序会自动给用户分配一个昵称,但他们可以用聊天命令修改自己的昵称,聊天命令以斜杆/开头。

      同样,用户也可以输入命令创建新的聊天室(或已加入的已有聊天室),在加入或创建聊天室时,新聊天室的名称会出现在聊天程序顶端的水平条上,也会出现在聊天消息区域右侧的可用房间列表中。

      在用户换到新房间以后,系统会确认这一变化。

      

      2.2程序需求及初始设置

      将要创建的聊天程序需要完成如下任务:

      1:提供静态文件(比如HTML、CSS和客户端JavaScript);

      2:在服务器上处理与聊天相关的消息;

      3:在用户的浏览器中处理与聊天相关的消息;

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

      MIME类型 MIME类型在维基百科上的文章HTTP://en.wikipedia.org/wiki/MIME中有详细论述。

      为了处理与聊天相关的消息,需要用Ajax轮询服务器。单位了让这个程序能竟可能快地做出响应,我们不会用传统的Ajax发送消息。Ajax用HTTP作为传输机制,并且HTTP本来就不是做实时通信的。在用HTTP发送消息时,必须用一个新的TCP/IP连接。打开和关闭连接所需要的时间。此外,因为每次请求都要发送HTTP头,所以传输的数据流也比较大。这个程序没用依赖于HTTP的方案,而是采用了WebSocket,这是一个为支持实时通信而设计的轻量的双向通信协议。

      在开始做程序的文件结构和依赖项设置这些症状的初期工作之前,我们先聊聊Node如何同时处理HTTP和websocket,这是选它做实时程序最好的理由之一。

      2.2.1 提供HTTP和WebSocket服务

      尽管这个程序不会用Ajax发送和接收聊天消息,但它仍要用HTTP发送用在用户浏览器中的HTML、CSS和客户端JavaScript。

      HTTP连接是短连接,用在不需要实时请求相应的场合,WebSocket连接,用在需要实时请求响应的场合。

      2.2.2创建程序的文件结构

      开始前,我们先创建一个项目目录。主程序文件会直接放在这个目录下。你需要添加一个lib子目录,用来放一些服务器逻辑。还需要创建一个public的子目录,用来放客户端的文件。在public子目录下,创建一个JavaScript子目录和一个Stylesheets目录。

      A.2在Windows上的安装方法

      下载地址:https://nodejs.org/en/#download

      在项目目录下:

      输入下面这条命令创建express包:

      局部安装:npm install express

      全局安装:npm install -g express

      写一个server.js文件

    //内置的http模块提供了HTTP服务器和客户端功能
    var http = require('http');
    //内置的fs模块提供了与文件系统相关的功能
    var fs = require('fs');
    //内置的path模块提供了与文件系统路径相关的功能
    var path = require('path');
    //附加的mime模块有根据文件扩展名得出mime类型的能力
    var mime = require('mime');
    //用来缓存文件内容的对象
    var cache = {};
    //发送文件数据及错误响应
    function send404(response){
        response.writeHead(404,{'Content-Type':'text/plain'});
        response.write('Error 404 : resource not fount.Hi!');
        response.end();
    }
    //提供文件数据服务
    function sendFile(response, filePath, fileContents){
        response.writeHead(
            200,
            {"content-type":mime.lookup(basename(filePath))}
        );
        response.end(fileContents);
    }
    //提供静态文件服务
    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);
                }
            });
        }   
    }
    //创建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;
        serveStatic(response, cache, absPath);
    })
    //启动http服务器
    server.listen(3000, function(){
        console.log("Server listening on port 3000.")
    })

    启动服务器命令:node server.js

      服务器运行起来后再浏览器中访问127.0.0.1:3000会激发404错误的辅助函数,页面上会显示 Error 404 : resource not fount.Hi! 消息,因为没添加静态文件。

     Ctrl+c关闭正在运行的服务器.

      接下来添加静态文件。

      

      2.3.2添加HTML和CSS文件

      第一个加默认的HTML文件。在public目录下创建index.html文件,并引入一个CSS文件,设置显示程序内容的div元素,价值一些客户端JavaScript文件。这些JavaScript文件提供了客户端Socket.IO功能、jQuery,以及两个该程序特有的文件,用来提供聊天的功能。

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>
                Chat
            </title>
            <link href="./stylesheets/style.css" rel="stylesheet" type="text/css"/>
        </head>
        <body>
            <div id="content">
                <!--显示当前聊天室的名称-->
                <div id="room"></div>
                <!--显示当前可用聊天室列表的div-->
                <div id="room-list"></div>
                <!--显示聊天信息的div-->
                <div id="messages"></div>
                <form id="send-from">
                    <!--用来输入聊天命令和消息的表单输入元素-->
                    <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>
    body {
        padding: 50;
        font: 14px "Lucida Grande",Helvetica, Arial, sans-serif;
    }
    a {
        color: #00B7ff;
    }
    #content {
         800px;
        margin-left: auto;
        margin-right: auto;
    }
    #room {
        background-color: #ddd;
        margin-bottom: 1em;
    }
    #messages {
         690px;
        height: 300px;
        overflow: auto;
        background-color: #eee;
        margin-bottom: 1em;
        margin-right: 10px;
    }

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

      我们前面说过程序必须要做三件事,其中第一个提供静态文件已经做了,现在来解决第二个,处理浏览器和服务器之间的通信。现代浏览器能用WebSocket处理浏览器跟服务器两者之间的通信。

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

      1:简要介绍下Socket.IO,并确定要在服务器端使用的Socket.IO功能;

      2:添加代码设置Socket.IO服务器;

      3:添加代码处理各种聊天程序的事件;

      Socket.IO提供了开箱即用的虚拟通道,所有程序不用吧每条消息都向已连接的用户广播,而是只向那些预定了某个通道的用户广播。用这个功能实现程序里的聊天室功能非常简单。

      Socket.IO还是事件发射器的好例子。时间发射器本质上是组织异步逻辑的一种很方便的设计模式。本章中会有一些事件发射器的代码,但下一章才会做更深入的讨论。

      事件发射器是跟某种资源相关联的,它能向这个资源发送消息,也能从这个资源接收消息。

      我们先开始做服务器上的功能,并确立处理连接的逻辑。然后会定义服务器所需要的功能。

      2.4.1设置Socket.IO服务器

      在lib下创建chat._server.js:

    //声明被提供使用的Socket.IO,并初始化部分定义聊天状态的变量
    var socketio = require('socket.io');
    var io;
    var guestNumber = 1;
    var nickNames = {};
    var namesUsed = {};
    var currentRoom = {};
    
    //加载一个定制的Node模块,提供处理基于Socket.IO的服务端聊天功能的,暂未定义。
    var chatServer = require('./lib/chat_server');
    //启动Socket.IO服务器,给她提供一个已经定义好的HTTP服务器,跟HTTP服务器共享同一个TCP/IP端口
    chatServer.listen(server);
    
    exports.listen = function(server){
        //启动Socket.IO服务器允许它搭载在已有的HTTP服务器上
        io = socketio.listen(server);
        io.set('log level',1);
        //定义每个用户连接处理的逻辑
        io.sockets.on('connection',function(socket){
            //在用户连接上来时赋予一个访问名
            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);
        });
    };

      已经确定了连接的处理逻辑,现在该添加用来处理程序需求的所有辅助函数了。

      

      2.4.2 处理程序场景及事件

      聊天程序需要处理下面这些场景和事件

      1:分配昵称

      2:房间更换请求

      3:昵称更换请求

      4:发送聊天消息

      5:房间创建

      6:用户断开连接

      要实现这些功能得添加几个辅助函数,如下文所述。

      1.分配昵称

      要添加的第一个辅助函数是assignGuestName,用来处理新用户的昵称。当用户第一次连接到聊天服务器上时,用户会被放到一个叫做Lobby的聊天室中,并调用assignGuestName给他们分配一个昵称,以便可以相互区分开来。

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

      程序分配的所有昵称基本上都是在Guest后面加上一个数字,,有新用户连接进来时这个数字就会增长。用户昵称存在变量nickNames中以便于引用,并且会跟一个内部socketID关联。昵称还会被添加到namesUsed中,这个变量中保存的是已经被占用的昵称。把下面清单中的代码添加到lib/chat_server.js中实现这个功能。

      2.进入聊天室相关的逻辑

      要添加到chat_server.js中的第二个辅助行数是joinRoom。处理逻辑跟用户加入聊天室有关。

      

    //进入聊天室相关的逻辑
    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);
        //如果不止一个用户在这个房间里,汇总下有哪些用户
        if(usersInRoom.length > 1){
            var usersInRoomSummary = 'Userd currently in ' + room + ':';
            for(var index in userdInRoom){
                var userSocketId = usersInRoom[index].id;
                if(userSocketId != socket.id){
                    if(index > 0){
                        usersInRoomSummary += ', ';
                    }
                    usersInRoomSummary +=nickNames[userSocketId];
                }
            }
            usersInRoomSummary += '.';
            //将房间里其他用户的汇总发送给这个用户
            socket.emit('message', {text:usersInRoomSummary});
        }
    }

      调用socket对象上的join方法就可以将用户加入Socket.IO房间。然后程序会吧相关的细节向这个用户及同一房间中的其他用户发送。程序会让用户知道有哪些用户在这个房间里,还会让其他用户知道这个用户进来了。

      3.处理昵称变更请求

      如果用户都用程序分配的昵称,很难记住谁是谁。因此聊天程序允许用户发起更名请求。更名需要用户的浏览器通过Socket.IO发送一个请求,并接收表示成功或失败的响应。

      以下定义了一个出来用户更名请求的函数,加入到lib/chat_server.js中,用户不能将你从改成以Guest开头,或改成其他已经被占用的昵称。

    //更名请求的处理逻辑
    function handleNameChangeAttempts(socket, nickNames, namesUsed){
        //添加nameAttempt事件的监听器
        socket.on('nameAttempt', function(name){
            //昵称不能以Guest开头
            if(name.indexOf('Guest') == 0){
                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.'
                    })
                }
            }
        });
    }

      4.发送聊天消息

      用户昵称没问题了,现在需要加个函数处理用户发过了的聊天消息。基本流程是:用户发射一个事件,表明消息是从哪个房间发出来的,已经消息的内容是什么;然后服务器将这条消息,转发给同一房间的所有用户。

      将下面的代码加入到lib/chat_server.js中。Socket.IO的broadcast函数是用来转发消息的。

      

    //发送聊天消息
    function handleMessageBroadcasting(socket){
        socket.on('message',function(message){
            socket.broadcast.to(message.room).emit('message', {
                text: nickNames[socket.id] + ': ' + message.text
            });
        });
    }

      5.创建房间

      如果还没有房间的话则创建一个房间,以下代码加入到lib/chat_server.js中,实现更好房间的功能。注意leave方法的使用。

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

      6.用户断开连接

      当用户离开聊天程序时,从NickNames和namesUsed中移除用户的昵称,将下面的代码加入到lib/chat_server.js中。

    //用户断开连接
    function handleClientDisconnection(socket){
        socket.on('disconnect',function(){
            var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
            delete namesUsed(nameIndex);
            delete nickNames[socket.id];
        });
    }

      

      

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

      在服务端分发浏览器发来的消息的Socket.IO逻辑已经加上了,现在该添加跟服务器通信所需要的客户端JavaScript了。客户端JavaScript需要实现以下功能:

      1:向服务端发送用户的消息和昵称/房间变更请求;

      2:显示其他用户消息,以及可用房间的列表;

      2.5.1将消息和昵称/房间变更去请求传送给服务器

      要添加的第一段客户端JavaScript代码是一个JavaScript原型对象,用来处理聊天命令,发送消息,请求变更房间或昵称

      在public/javascripts目录下创建一个chat.js文件,把下面的代码放进去。这段代码相当于定义了一个JavaScript“类”,在初始化时可用传入一个Socket.IO的 参数socket:

       

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

      2.5.2 在用户界面中显示消息及可用房间

      现在该添加使用jQuery跟用户界面(基于浏览器)直接交互的逻辑了。要添加的第一个功能是显示文本数据。

      从安全角度来看,Web程序中有两种文本数据。一种是受信的文本数据,由程序提供的文本组成,另一种是可疑的文本数据,是由程序的用户创建的文本,或从用户创建的文本中提取出来的。我们之所以认为来自用户的文本数据时可疑的,是因为恶意用户可能会蓄意在提交的文本数据中包含<script>标签,放入JavaScript逻辑。如果不经修改就把这些数据展示给其他用户,可能会发生令人厌恶的事情,比如将用户转到其他页面。这种劫持Web程序的方法称作跨域脚本(XSS)攻击。

      这个聊天程序会用聊个负责函数显示文本数据。一个函数用来显示可疑的文本数据,另一个函数显示受信的文本数据。

      函数divEscapedContentElement用来显示可疑的文本。他会净化文本,将特殊字符转换成HTML实体,这样浏览器就会按输入的样子显示它们,而不会视图按HTML标签解释它们。

      函数divSystemContentElement用来显示系统创建的受信内容,而不是其他用户创建的。在public/javascripts目录下创建chat_ui.js文件,并把下面两个辅助函数放进去:

     

    //用来显示可疑的文本
    function divEscapedContentElement(message){
        return $('<div></div>').text(message);
    };
    
    //用来显示系统创建的受信内容
    function divSystemContentElement(message){
        return $('<div></div>').html('<i>' + message + '</i>');
    }

      下面要加到chat_ui.js中的函数是用来处理用户输入的,如果用户输入的内容以斜杆/开头,它会将其作为聊天命令处理。如果不是,就作为聊天消息发送给服务器并广播给其他用户,并添加到用户所在的聊天室的聊天文本中。

    //处理原始的用户输入
    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 chatApp(socket);
        //显示更名尝试的结果
        socket.on('nameResult', function(result){
            var message;
            if(result.success){
                message = 'Youare 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();
            });
        });
        //定期请求可用房间列表
        setInterval(function(){
            socket.emit('rooms');
        },1000);
        $('#send-message').focus();
        //提交表单可用发送聊天消息
        $('#send-form').submit(function(){
            processUserInput(chatApp, socket);
            return false;
        });
    });

      接下来让我们把程序做完,将下面代码清单中的CSS样式代码添加到public/stylesheets/style.css文件中。

    #room-list{
        float: right;
         100px;
        height: 300px;
        overflow: auto;
    }
    
    #room-list div{
        border-bottom: 1px solid #eee;
    }
    
    #room-list div:hover{
        background-color: #ddd;
    }
    
    #send-message{
         700px;
        margin-bottom: 1em;
        margin-right: 1em;
    }
    
    #help {
        font: 10px "Lucida Grande", Helvetica, Arial sans-serif;
    }

    结束语:最笨的办法也不错哦。

     
  • 相关阅读:
    2017沈阳站 Tree
    P2146 [NOI2015]软件包管理器
    hdu3307 欧拉函数
    poj 3126 Prime Path bfs
    CSL的字符串
    P1045 麦森数
    洛谷 P1338 末日的传说
    树链剖分
    SQL[Err] ORA-00933: SQL command not properly ended
    Postman 快速入门之脚本
  • 原文地址:https://www.cnblogs.com/erfsfj-dbc/p/10224101.html
Copyright © 2020-2023  润新知