1. 程序概览
- 用户可以在一个简单的表单中输入消息,相互聊天。消息输入后会发送给同一个聊天室内的其他所有用户。
- 进入聊天室后,程序会自动给用户分配一个昵称,但他们可以用聊天命令修改自己的昵称,如图2-2所示。聊天命令以斜杠(/)开头。
- 同样,用户也可以输入命令创建新的聊天室(或加入已有的聊天室) ,如图2-3所示。在加入或创建聊天室时,新聊天室的名称会出现在聊天程序顶端的水平条上,也会出现在聊天消息区域右侧的可用房间列表中。
- 在用户换到新房间后,系统会确认这一变化,如图2-4所示。
2. 程序需要及初始设置
- 提供静态文件(比如HTML、 CSS和客户端JavaScript);
- 在服务器上处理与聊天相关的消息;
- 在用户的浏览器中处理与聊天相关的消息。
- 第三方的模块mime
- WebSocket 这是一个为支持实时通讯而设计的轻量的双向通信协议。
- Socket.IO库 它给不能使用WebSocket的浏览器提供了一些后备措施,包括使用Flash。
- Express
2.1 提供HTTP和WebSocket服务
2.2 创建程序的文件结构
- 项目目录:主程序文件会直接放在这个目录下
- lib子目录:用来放一些服务端逻辑
- public子目录: 用来放客户端文件
- 在public子目录下,创建一个javascripts子目录和一个stylesheets目录。
2.2.3 指明依赖项
程序的依赖项是在package.json文件中指明的。这个文件总是被放在程序的根目录下。在package.json文件中可以定义很多事情,但最重要的是程序的名称、版本号、对程序的描述,以及程序的依赖项。
{
"name": "chatrooms",
"version": "0.0.1",
"description": "Minimalist multiroom chat server",
"dependencies": {
"socket.io": "~0.9.6",
"mime": "~1.2.7"
}
}
2.2.4 安装依赖项
在跟目录输入下面这条命令:
npm install
2.3 提供 HTML、 CSS 和客户端 JavaScript 的服务
2.3.1 创建静态文件服务器
创建静态文件服务器既要用到Node内置的功能,也要用第三方的mime附加模块来确定文件
的的MIME类型。
- 发送文件数据及错误响应
接下来要添加三个辅助函数以提供静态HTTP文件服务。第一个是在所请求的文件不存在时
发送404错误的。把下面的辅助函数加到server.js中:
function send404(response) {
response.writeHead( 404, { ‘Content-Type‘: ‘text/plain‘ } ) ;
response.write( ‘Error 404: resource not found.‘ ) ;
response.end() ;
}
第二个辅助函数提供文件数据服务。这个函数先写出正确的HTTP头,然后发送文件的内容。
把下面的代码添加到server.js中:
function sendFile(response, filePath, fileContents) {
response.writeHead(
200,
{ ‘Content-type‘: mime.lookup( path.basename( filePath ) ) }
) ;
response.end( fileContents ) ;
}
访问内存(RAM)要比访问文件系统快得多,所以Node程序通常会把常用的数据缓存到内
存里。我们的聊天程序就要把静态文件缓存到内存中,只有第一次访问的时候才会从文件系统中
读取。下一个辅助函数会确定文件是否缓存了,如果是,就返回它。如果文件还没被缓存,它会
从硬盘中读取并返回它。如果文件不存在,则返回一个HTTP 404错误作为响应。把这个辅助函
数加到server.js中:
- 创建HTTP服务器
- 启动HTTP服务器
server.listen( 3000, function () {
console.log( ‘Server listening on port 3000.‘ ) ;
} ) ;
启动服务器:
node server.js
2.3.2 添加HTML和CSS文件
2.4 用 Socket.IO 处理与聊天相关的消息
处理浏览器和服务器之间的通信。现代浏览器能用WebSocket处理浏览器跟服务器两者之间的通
信(参见Socket.IO浏览器支持页以了解详情: http://socket.io/#browser-support)。
- 简要介绍下Socket.IO,并确定要在服务器端使用的Socket.IO功能;
- 添加代码设置Socket.IO服务器;
- 添加代码处理各种聊天程序的事件。
Socket.IO提供了开箱即用的虚拟通道,所以程序不用把每条消息都向已连接的用户广播,而
是只向那些预订了某个通道的用户广播。
Socket.IO还是事件发射器(Event Emitter)的好例子。事件发射器本质上是组织异步逻辑的
一种很方便的设计模式。
事件发射器:事件发射器是跟某种资源相关联的,它能向这个资源发送消息,也能从这个资源接收消息。
资源可以连接远程服务器,或者更抽象的东西,比如游戏中的角色。 Johnny-Five项目
(https://github.com/rwldrn/johnny-five)是一个用Node做的机器人程序,实际上就是用事件发射
器控制Arduino微控制器。
2.4.1 设置Socket.IO服务器
server.js
var chatServer = require( ‘./lib/chat_server‘ ) ;
chatServer.listen( server ) ;
现在你要在lib目录中创建一个新文件, chat_server.js。先把下面的变量声明添加到这个文件
中。这些声明让我们可以使用Socket.IO,并初始化了一些定义聊天状态的变量:
var socketio = require( ‘socket.io‘ ) ;
var io ;
var guestNumber = 1 ;
var nickNames = {} ;
var namesUsed = [] ;
var currentRoom = {} ;
- 确立建立连接
接下来添加代码清单2-7中的逻辑,定义聊天服务器函数listen。server.js中会调用这个函数。
它启动Socket.IO服务器,限定Socket.IO向控制台输出的日志的详细程度,并确定该如何处理每个
接进来的连接。
2.4.2 处理程序场景及事件
- 分配昵称
- 房间更换请求
- 昵称更换请求
- 发送聊天服务
- 房间创建
- 用户断开连接
- 分配昵称
要添加的第一个辅助函数是assignGuestName,用来处理新用户的昵称。当用户第一次连到聊天服务器上时,用户会被放到一个叫做Lobby的聊天室中,并调用assignGuestName给他们分配一个昵称,以便可以相互区分开。
程序分配的所有昵称基本上都是在Guest后面加上一个数字,有新用户连进来时这个数字就会往上增长。用户昵称存在变量nickNames中以便于引用,并且会跟一个内部socket ID关联。昵称还会被添加到namesUsed中,这个变量中保存的是已经被占用的昵称。把下面清单中的代码添加到lib/chat_server.js中实现这个功能。
- 进入聊天室
- 处理昵称变更请求
- 发送聊天消息
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 ] ;
} ) ;
}
2.5 在程序的用户界面上使用客户端 JavaScript
- 向服务器发送用户的消息和昵称/房间变更请求
- 显示其他用户的消息,以及可用房间的列表
2.5.2 将消息和昵称/房间变更请求传给服务器
要添加的第一段客户端JavaScript代码是一个JavaScript原型对象,用来处理聊天命令、发送
消息、请求变更房间或昵称。
在public/javascripts目录下创建一个chat.js文件,把下面的代码放进去。
/**
* Created by 23782 on 2016/4/19.
*/
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程序中有两种文本数据。一种是受信的文本数据,由程序提供的文本组成,另一种是可疑的文本数据,是由程序的用户创建的文本,或从用户创建的文本中提取出来的。我们之所以认为来自用户的文本数据是可疑的,是因为恶意用户可能会蓄意在提交的文本数据中包含\
function divEscapedContentElement(message) {
return $( ‘<div></div>‘ ).text( message ) ;
}
function divSystemContentElement(message) {
return $( ‘<div></div>‘ ).html( ‘<i>‘ + message + ‘</i>‘ ) ;
}
下一个要加到chat_ui.js中的函数是用来处理用户输入的,具体内容见代码清单2-12。如果用
户输入的内容以斜杠(/)开头,它会将其作为聊天命令处理。如果不是,就作为聊天消息发送
给服务器并广播给其他用户,并添加到用户所在聊天室的聊天文本中。
// 处理原始的用户输入
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 known as ‘ + result.name + ‘. ‘ ;
} else {
messge = result.messge ;
}
$( ‘#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;
width: 100px;
height: 300px;
overflow: auto;
}
#room-list div {
border-bottom: 1px solid #eeeeee;
}
#room-list div:focus {
background-color: #dddddd;
}
#send-message {
width: 700px;
margin-bottom: 1em;
margin-right: 1em;
}
#help {
font: 10px "Lucida Grande", Helvetica, Arial, sans-serif;
}
加好最后的代码,让我们把程序跑起来试试(用node server.js) 。
- 个人效果图: