先看下效果:

case 1:进入房间初始界面:默认进入房间「Lobby」,用户默认昵称 命名规则为 小可爱 x 号,当有用户进入房间时,系统消息会提醒「『用户昵称』已经进入『房间名』啦!」,当用户更换房间时,系统消息会提醒「房间已更改!」

4227aaad4af7

case 2:当前用户发送消息后,系统会发送给其他用户,发送消息的用户的聊天消息。

注意:当前用户发的消息,是直接插到在当前用户的聊天界面的,此时,会将 当前用户的聊天消息 通过 WebSocket 发送给服务器,服务器再发送给其他用户界面。

4227aaad4af7

4227aaad4af7

case 3:更改昵称,使用命令 「/nick 『用户名』」来更改用户昵称,同时系统广播给其他用户

4227aaad4af7

4227aaad4af7

case 4:更换房间,使用命令「/join 『房间名』」来更换房间,系统只提醒更改房间的用户,同时,进入新房间时,系统会通知当前用户,该房间有哪些用户。

注意:当一个房间没有用户时,该房间会被自动清除掉,除非用户再新建一个。

4227aaad4af7

4227aaad4af7

4227aaad4af7

技术重点:使用 Socket.IO 处理跟聊天相关的消息

项目目录如下:

4227aaad4af7

package.json:

4227aaad4af7

开发步骤:

第一步:创建HTTP服务器——server.js:

//提供HTTP服务端和客户端功能

let http=require("http")

//提供文件系统功能

let fs=require("fs")

//提供文件系统路径功能

let path=require("path")

//根据文件扩展名得出MIME类型

let mime=require("mime")

let cache={}

//创建 HTTP 服务器

let server=http.createServer((request,response)=>{

let filePath=false

if(request.url==='/'){

filePath='public/index.html'

}else{

filePath='public'+request.url

}

let absPath='./'+filePath

//返回静态文件

serveStatic(response,cache,absPath)

})

//启动 http 服务器

//param:端口,callback

server.listen(3000,()=>{

console.log("服务器已启动")

})

//加载一个Socket.IO

let charServer=require("./lib/chat_server")

charServer.listen(server)

//404

function send404(response) {

response.writeHead(404,{"Content-Type":"text/plain"})

response.write("Ooops,找不到页面啦!")

response.end()

}

//发送文件

function sendFile(response,filePath,fileContents) {

// response.writeHead(200,{"Content-Type":mime.lookup(path.basename(filePath))})

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.exists(absPath,(exists)=>{

if(exists){

fs.readFile(absPath,(err,data)=>{

if(err){

send404(response)

}else{

//存进内存中

cache[absPath]=data

sendFile(response,absPath,data)

}

})

}else{

send404(response)

}

})

}

}

第二步:设置 Socket.IO 服务器

let socketio=require("socket.io")

let io

//默认一人

let guestNumber=1

let nickNames={}

//曾用名

let namesUsed=[]

//当前聊天室

let currentRoom={}

//启动Socket.IO服务器=================

exports.listen=(server)=>{

//启动SocketIO服务器,允许它搭载在已有的HTTP服务器上

io=socketio.listen(server)

// console.log(io,"chat_server15")

//限制socketio向控制台输出的日志详细程度

// io.set("log level",1)

io.sockets.on("connection",(socket)=>{

// //初始化

// guestNumber=1

// namesUsed=[]

// currentRoom={}

//assignGuestName: 在用户连接的时候,给他一个名字

//该 guestNumber 是新的未有昵称联系的 昵称number

// console.log(guestNumber)

guestNumber=assignGuestName(socket,guestNumber,nickNames,namesUsed)

//在给名字后,把他默认分配到Lobby聊天室里

joinRoom(socket,"Lobby")

//处理用户消息

handleMessageBroadcasting(socket,nickNames)

//处理用户更换昵称

handleNameChangeAttempts(socket,nickNames,namesUsed)

//处理用户创建/更换聊天室

handleRoomJoining(socket)

//用户发送请求时,向用户提供已经被创建的聊天室的列表

socket.on("rooms",()=>{

// console.log(nickNames)

let realRooms={}

//将用户id给过滤掉

// console.log(io.sockets.adapter.rooms)

for(let room in (io.sockets.adapter.rooms)){

if(!nickNames[room]){

realRooms[room]=1

}

}

// console.log(io.sockets.adapter.rooms)

// console.log(realRooms)

// socket.emit("rooms",io.sockets.adapter.rooms)

socket.emit("rooms",realRooms)

})

//用户断开连接后,清除

handleClientDisconnection(socket,nickNames,namesUsed)

})

}

//分配用户昵称==================================

function assignGuestName(socket,guestNumber,nickNames,namesUsed) {

let name="小可爱 "+guestNumber+" 号"

// 将用户昵称跟客户端连接ID相关联

nickNames[socket.id]=name

// 发送昵称给客户端,让用户看到他们自己的昵称

socket.emit("nameResult",{

success:true,

name:name

})

// 存放已经被占用的昵称

namesUsed.push(name)

// 增加昵称计数器

return guestNumber+1

}

//进入聊天室====================

function joinRoom(socket,room) {

// console.log(socket,room,"chat_server56")

// 让用户加入房间

// console.log(socket)

// console.log("===============")

socket.join(room)

// console.log(socket)

// 记录当前用户所在的房间

currentRoom[socket.id]=room

// 让用户知道他们进入了新房间

socket.emit("joinResult",{room:room})

// console.log(room)

// (不包括自己)通知该房间的其他用户,有新用户进入了该房间

// socket.broadcast.to(room).emit("message",{

socket.broadcast.in(room).emit("message",{

text:nickNames[socket.id]+" 已经进入「"+room+"」啦!"

})

// (包括自己)通知该房间的其他用户,有新用户进入了该房间

// socket.broadcast.in(room).emit("message",{

// text:nickNames[socket.id]+" 已经进入「"+room+"」啦!"

// })

// 确认有哪些用户在该房间里

// let usersInRoom=io.sockets.clients(room)

// 是一个对象,而不是数组了

let usersInRoom = io.sockets.adapter.rooms[room];

// console.log(io.sockets.adapter.rooms)

// 当用户数量大于1时,收集该房间里所有用户的昵称信息

let roomInfo=""

// console.log(usersInRoom)

if(usersInRoom&&usersInRoom.length>1){

roomInfo="你当前所处的房间「"+room+"」有如下的成员:"

// console.log(usersInRoom,nickNames)

let userArray=Object.keys(usersInRoom.sockets)

// console.log(userArray)

// for(let index in usersInRoom){

for(let index in userArray){

// let userSocketId=usersInRoom[index].id

let userSocketId=userArray[index]

//排除当前用户

if(userSocketId!==socket.id){

//不能在第一个用户如 aa,前面加「,」

if(index>0){

roomInfo+=","

}

roomInfo+=nickNames[userSocketId]

}

}

roomInfo+="。"

// 将房间信息发送给当前用户

socket.emit("message",{text:roomInfo})

}

}

//处理 昵称变更请求

function handleNameChangeAttempts(socket,nickNames,namesUsed){

// 添加 更换昵称的监听器

socket.on("nameChange",(name)=>{

//说明是以Guest开头的

if(name.indexOf("Guest")===0){

socket.emit("nameResult",{

success:false,

message:"名字不能以「Guest」开头,请更换其他昵称重试!"

})

}else{

//说明该昵称未被使用过

if(namesUsed.indexOf(name)===-1){

//将之前的昵称保存

let previousName=nickNames[socket.id]

//当系统自动创建名字的时候,已将该昵称放进namesUsed数组中,现在需要查找该昵称位置

let previousNameIndex=namesUsed.indexOf(previousName)

//将新名字加入namesUsed

namesUsed.push(name)

// 更新名字

nickNames[socket.id]=name

// 删除旧名字,让其他用户使用(除Guest开头)

delete namesUsed[previousNameIndex]

// 通知用户名称更换成功

socket.emit("nameResult",{

success:true,

name:name

})

// 通知当前房间,当前用户更改昵称的消息

// socket.broadcast.to(currentRoom[socket.id]).emit("message",{

socket.broadcast.in(currentRoom[socket.id]).emit("message",{

text:"「"+previousName+"」已更改成「"+name+"」。"

})

}else{

socket.emit("nameResult",{

success:false,

message:"该名称已被他人使用,请更换其他昵称重试!"

})

}

}

})

}

//用户 发送聊天消息

function handleMessageBroadcasting(socket,nickNames) {

//接收前端发过来的message

socket.on("message",(message)=> {

// console.log(message.room,nickNames[socket.id]+":"+message.text)

//然后向该房间转发该用户的信息

//注意:socket.broadcast.to是除了当前用户,向其他用户发送消息

//所以当前用户自己是看不到「xxx:yyy」这样的,只能看到「yyy」这样的

// socket.broadcast.to(message.room).emit("message",{

//如果自己想看到自己昵称「xxx:yyy」的话,把 to 改成 in

socket.broadcast.in(message.room).emit("message",{

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

})

})

}

//让用户加入已有的房间,如果没有房间,则创建一个

function handleRoomJoining(socket) {

socket.on("join",(room)=>{

//离开当前房间

socket.leave(currentRoom[socket.id])

console.log(room)

//加入新房间,没有房间则创建新的

joinRoom(socket,room.newRoom)

})

}

//用户断开连接后,清空用户数据

function handleClientDisconnection(socket) {

socket.on("disconnect",()=>{

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

delete namesUsed[nameIndex]

delete nickNames[socket.id]

})

}

第三步:向服务器发送用户的消息和昵称,以及变更昵称和房间的请求——chat.js

//采用es6 写法

class Chat{

constructor(socket){

this.socket=socket

}

//发送消息

sendMessage(room,text){

let message={

room:room,

text:text

}

//发送给服务端

// console.log(this.socket)

this.socket.emit("message",message)

}

//变更房间

changeRoom(room){

this.socket.emit("join",{

newRoom:room

})

}

// 处理聊天命令

// 注意使用命令一般是 /xxx ttttt,xxx后务必要加上空格!否则无效

processCommand(command){

//按照command中的空格,给分成数组

let wordsArray=command.split(" ")

// 去掉 /

command=wordsArray[0].substring(1,wordsArray[0].length)

let message=false

switch (command){

case "join":

wordsArray.shift()

//连接成字符串

let room=wordsArray.join(" ")

this.changeRoom(room)

break

case "nick":

wordsArray.shift()

//连接成字符串

let name=wordsArray.join(" ")

this.socket.emit("nameChange",name)

break

default:

message="无法识别该命令"

break

}

return message

}

}

第四步:显示其他用户的消息,以及可使用房间的列表——chat_ui.js

//客户端初始化

$(document).ready(()=>{

// let socket=io.connect()

let socket=io()

// console.log(socket)

let chatApp=new Chat(socket)

// console.log(chatApp)

// 显示改变名字的结果

socket.on("nameResult",(result)=>{

let message

if(result.success){

message="你已更名为「"+result.name+"」。"

}else{

message=result.message

}

$("#messages").append(credible(message))

})

// 显示房间改变的结果

socket.on("joinResult",(result)=>{

// console.log(result,"joinResult")

$("#room").text(result.room)

$("#messages").append(credible("房间已更改!"))

})

// 显示接收到的消息

socket.on("message",(message)=>{

// console.log(message)

let newElement=$("

$("#messages").append(newElement)

})

// 显示可用房间列表

socket.on("rooms",(rooms)=>{

$("#room-list").empty()

console.log(rooms)

for(let room in rooms){

// room=room.substring(1,room.length)

if(room!==""){

$("#room-list").append(doubtful(room))

}

}

$("#room-list div").click(()=>{

chatApp.processCommand("/join"+$(this).text())

$("#send-message").focus()

})

})

// 定期请求可用房间列表

setInterval(()=>{

socket.emit("rooms")

// console.log("rooms1111")

},6000)

$("#send-message").focus()

$("#send-form").submit(()=>{

processUserInput(chatApp,socket)

return false

})

})

//显示可疑的文本

function doubtful(message) {

//text返回其文本内容

return $("

}

//显示守信的文本(斜体显示)

function credible(message) {

return $("

"+message+"")

}

//处理原始输入

function processUserInput(chatApp,socket) {

let message=$("#send-message").val()

let systemMessage

// /开头视为命令

if(message.charAt(0)==="/"){

systemMessage=chatApp.processCommand(message)

if(systemMessage){

$("#messages").append(credible(systemMessage))

}

}else{

chatApp.sendMessage($("#room").text(),message)

// console.log(message)

$("#messages").append(doubtful(message))

$("#messages").scrollTop($("#messages").prop("scrollHeight"))

}

$("#send-message").val("")

}

第五步:npm install 安装依赖包,然后在终端输入 node server.js ,打开浏览器,输入 url:localhost:3000 即可运行本程序。

欢迎关注本公众号:

4227aaad4af7

image

(完)

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐