最近这两天在看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;
}