实现一个简单的tcp聊天室

通过数十行代码实现一个支持私聊、修改昵称、群聊及查看在线用户的TCP聊天室,深入理解net API与流处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里写一个简单的聊天室,用来理解和使用net的api,方便自己理解,

聊天室设计

可以展示当前在线人数,在线用户,使用指定命令进行操作

  • 默认情况下用户应该是匿名状态
  • 通过关机命令改名:r: newName
  • 支持显示在线用户列表 l
  • 广播功能: b: xxx
  • 私聊的功能: p:nickName: msg

创建服务器

const net = require('net')
const clients = {} // 保存用户
// 保存所有的用户名
let nickNames = []

const server = net.createServer(function (scoket) {
  server.maxConnections = 20
  // 创建用户ID


  // 用户名
  scoket.setEncoding('utf8')
  scoket.on('data', chunk => {
  })
})
let port = 3000
server.listen(port, 'localhost', () => {
  console.log(`server start ${port}`)
})

// 当服务端发生错误时,会调用监听函数
server.on('error', err => {
  if (err.code == 'EADDRINUSE') server.listen(++port)
  if (err) console.log(err)
})

// 当服务器错误时
server.on('close', () => {
  console.log('服务端关闭')
})

设置默认参数

// 默认情况下用户应该是匿名状态
// 通过关机命令改名:r: newName
// 支持显示在线用户列表 l
// 广播功能: b: xxx
// 私聊的功能: p:nickName: msg

const net = require('net')
const clients = {} // 保存用户
// 保存所有的用户名
const server = net.createServer(function (scoket) {
  server.maxConnections = 20
  // 创建用户ID
  let key = scoket.remoteAddress + scoket.remotePort
  clients[key] = {nickName: '匿名', scoket}
  nickNames.push(key)

  server.getConnections((err, count) => {
    scoket.write(`欢迎来到聊天室 当前用户数量:${count}个, 请输入用户名\r\n`);
  })

  // 用户名
  scoket.setEncoding('utf8')
  scoket.on('data', chunk => {
    // 处理换行回车的问题
    chunk = chunk.replace(/(\r\n)|(\r)/, '')
    let chars = chunk.split(':')

    switch (chars[0]) {
      case 'r':
        //改名
        rename(key, chars[1])
        break;
      case 'l':
        //展示用户列表
        showUsers(key, scoket)
        break;
      case 'b':
        //广播
        broadcast(key, chars[1])
        break;
      case 'p':
        //私聊
        private(chars[1], chars[2], key)
        break;
      default:
        scoket.write('您输入的问题无法解毒,请重新输入\r\n')
        break;
    }
  })
})
let port = 3000
server.listen(port, 'localhost', () => {
  console.log(`server start ${port}`)
})

// 当服务端发生错误时,会调用监听函数
server.on('error', err => {
  if (err.code == 'EADDRINUSE') server.listen(++port)
  if (err) console.log(err)
})

// 当服务器错误时
server.on('close', () => {
  console.log('服务端关闭')
})

完成设置(修改)昵称

/**
 * @description 重命名
 * @param {string} id
 * @param {string} name
 */
function rename (id, name) {
  // 如果存在这个用户,就赋值
  let res
  // 判断是否又当前用户名
  nickNames.forEach(key => {
    if (clients[key].nickName === name) res = true
  })
  // 如果当前用户名已经存在,提示用户重新填写用户名
  // 如果目前没有存在该用户名,则让用户修改
  if (res) return clients[id].scoket.write('当前用户名已经存在,请重新输入\r\n')
  else clients[id].nickName = name
  clients[id].scoket.write(`您修改昵称成功,当前昵称${name}\r\n`)
}
// 链接服务器:输入 r:new name 
// 服务器会返回给你结果

实现广播逻辑

/**
 * @description 广播消息
 * @param {*} id 用户唯一标识
 * @param {*} msg 发送的消息
 */
function broadcast(id, msg) {
  // 广播消息的用户昵称
  let name = clients[id].nickName
  nickNames.forEach(key => {
    if (key !== id) {
      clients[key].scoket.write(`${name}: ${msg} \r\n`)
    }
  })
}

// 链接服务器以后输入: b:testMsg 
// 其他用户就能收到消息

实现展示用户列表

/**
 * 展示用户列表
 * @param {string} id 用户位移表示
 */
function  showUsers(id, scoket) {
  let str = '当前在线用户:\r\n'
  let nameList = ''
  // 遍历 拿到所有的
  nickNames.forEach(key => {
    if (key !== id) {
      nameList += `${clients[key].nickName}\r\n`
    }
  })
  // 返回用户列表
  scoket.write(str + nameList)
}
// 链接服务器,输入: l 回车
// 展示当前用户列表

实现私聊

/**
 * @description 私聊
 * @param {string} nickname 私聊的目标用户
 * @param {string} msg 发送的消息
 */
function private(nickname, msg, id) {
  let users
  // 从连接列表拿到当前用的昵称
  let form = clients[id].nickName
  // 找到当前用户要@的人,这里默认用户名不能重复
  nickNames.forEach(name => {
    if(clients[name].nickName === nickname) {
      users = clients[name]
    }
  })
  // 发送消息给用户
  users.scoket.write(`${form}: ${msg}\r\n`)
}
// 测试
// 链接当前服务,输入: p:xxx:我们走私吧

这里是完整的代码

// 默认情况下用户应该是匿名状态
// 通过关机命令改名:r: newName
// 支持显示在线用户列表 l
// 广播功能: b: xxx
// 私聊的功能: p:nickName: msg

const net = require('net')
const clients = {} // 保存用户
// 保存所有的用户名
let nickNames = []
// 当客户端连接服务室 辉触发回调函数,默认提示输入用户名,就可以通信了
// 自己的说的话,不应该通知自己,应该通知别人呢
/**
 * @description 重命名
 * @param {string} id
 * @param {string} name
 */
function rename (id, name) {
  // 如果存在这个用户,就赋值
  let res
  // 判断是否又当前用户名
  nickNames.forEach(key => {
    if (clients[key].nickName === name) res = true
  })
  // 如果当前用户名已经存在,提示用户重新填写用户名
  // 如果目前没有存在该用户名,则让用户修改
  if (res) return clients[id].scoket.write('当前用户名已经存在,请重新输入\r\n')
  else clients[id].nickName = name
  clients[id].scoket.write(`您修改昵称成功,当前昵称${name}\r\n`)
}
/**
 * @description 私聊
 * @param {string} nickname 私聊的目标用户
 * @param {string} msg 发送的消息
 */
function private(nickname, msg, id) {
  let users
  // 从连接列表拿到当前用的昵称
  let form = clients[id].nickName
  // 找到当前用户要@的人,这里默认用户名不能重复
  nickNames.forEach(name => {
    if(clients[name].nickName === nickname) {
      users = clients[name]
    }
  })
  // 发送消息给用户
  users.scoket.write(`${form}: ${msg}\r\n`)
}

/**
 * 展示用户列表
 * @param {string} id 用户位移表示
 */
function  showUsers(id, scoket) {
  let str = '当前在线用户:\r\n'
  let nameList = ''
  // 遍历 拿到所有的
  nickNames.forEach(key => {
    if (key !== id) {
      nameList += `${clients[key].nickName}\r\n`
    }
  })
  // 返回用户列表
  scoket.write(str + nameList)
}

/**
 * @description 广播消息
 * @param {*} id 用户唯一标识
 * @param {*} msg 发送的消息
 */
function broadcast(id, msg) {
  // 广播消息的用户昵称
  let name = clients[id].nickName
  nickNames.forEach(key => {
    if (key !== id) {
      clients[key].scoket.write(`${name}: ${msg} \r\n`)
    }
  })
}

const server = net.createServer(function (scoket) {
  server.maxConnections = 20
  // 创建用户ID
  let key = scoket.remoteAddress + scoket.remotePort
  clients[key] = {nickName: '匿名', scoket}
  nickNames.push(key)

  server.getConnections((err, count) => {
    scoket.write(`欢迎来到聊天室 当前用户数量:${count}个, 请输入用户名\r\n`);
  })

  // 用户名
  scoket.setEncoding('utf8')
  scoket.on('data', chunk => {
    // 处理换行回车的问题
    chunk = chunk.replace(/(\r\n)|(\r)/, '')
    let chars = chunk.split(':')

    switch (chars[0]) {
      case 'r':
        //改名
        rename(key, chars[1])
        break;
      case 'l':
        //展示用户列表
        showUsers(key, scoket)
        break;
      case 'b':
        //广播
        broadcast(key, chars[1])
        break;
      case 'p':
        //私聊
        private(chars[1], chars[2], key)
        break;
      default:
        scoket.write('您输入的问题无法解毒,请重新输入\r\n')
        break;
    }
  })
})
let port = 3000
server.listen(port, 'localhost', () => {
  console.log(`server start ${port}`)
})

// 当服务端发生错误时,会调用监听函数
server.on('error', err => {
  if (err.code == 'EADDRINUSE') server.listen(++port)
  if (err) console.log(err)
})

// 当服务器错误时
server.on('close', () => {
  console.log('服务端关闭')
})

结束

通过上边几十行代码,实现了一个简单的tcp聊天室,聊天室支持私聊,修改昵称,群聊,查看在线用户,

通过上述的练习,对net的基本功能,流有了一定的认识

tcp是没有状态的,所以需要我们自己定义规则,然后按照自己定义的规则做一定的处理,http作为应用层,就会有很多可以操作的地方,如cookie,缓存,分片等等

里面包含聊天室客户端和服务器端的源文件和一份完整的设计报告。 一、 系统概要 本系统能实现基于VC++的网络聊天室系统。有单独的客户端、服务器端。 服务器应用程序能够接受来自客户端的广播,然后向客户端发送本机的IP与服务端口,让客户端接入到服务器进行聊天,检测用户名是否合法(重复),服务器责接收来自客户端的聊天信息,并根据用户的需求发送给指定的人或所有人,能够给出上线下线提示。客户端能够发出连接请求,能编辑发送信息,可以指定发给单人或所有人,能显示聊天人数,上线下线用户等。 二、 通信规范的制定 服务请求规范: 服务器端: (1) 创建一个UDP的套接字,接受来自客户端的广播请求,当请求报文内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”时,接受请求,给客户端发送本服务器TCP聊天室的端口号。 (2) 创建一个主要的TCP协议的套接字负责客户端TCP连接 ,处理它的连接请求事件。 (3)在主要的TCP连接协议的套接字里面再创建TCP套接字保存到动态数组里,在主要的套接字接受请求后 ,就用这些套接字和客户端发送和接受数据。 客户端: (1) 当用户按“连接”按钮时,创建UDP协议套接字,给本地计算机发广播,广播内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”。 (2)当收到服务器端的回应,收到服务器发来的端口号后,关闭UDP连接。根据服务器的IP地址和端口号重新创建TCP连接。 故我思考:客户端一定要知道服务器的一个端口,我假设它知道服务器UDP服务的端口,通过发广播给服务器的UDP服务套接字,然后等待该套接字发回服务器TCP聊天室服务的端口号,IP地址用ReceiveForom也苛刻得到。 通信规范 通信规范的制定主要跟老师给出的差不多,并做了一小点增加: (增加验证用户名是否与聊天室已有用户重复,在服务器给客户端的消息中,增加标志0) ① TCP/IP数据通信 --- “聊天”消息传输格式 客户机 - 服务器 (1)传输“用户名” STX+1+用户名+ETX (2) 悄悄话 STX+2+用户名+”,”+内容+ETX (3) 对所有人说 STX+3+内容+ETX 服务器- 客户机 (0)请求用户名与在线用户名重复 //改进 STX+0+用户名+EXT (1)首次传输在线用户名 STX+1+用户名+ETX (2)传输新到用户名 STX+2+用户名+ETX (3)传输离线用户名 STX+3+用户名+ETX (4)传输聊天数据 STX+4+内容+ETX (注:STX为CHR(2),ETX 为CHR(3)) 三、 主要模块的设计分析 四、 系统运行效果 (要求有屏幕截图) 五、 心得与体会
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值