使用 Socket.IO 创建实时聊天应用程序

使用 Socket.IO 创建实时聊天应用程序

引言

大家好,欢迎来到今天的讲座!今天我们要一起探讨如何使用 Socket.IO 创建一个实时聊天应用程序。如果你曾经想过开发一个像微信、WhatsApp 或者 Slack 这样的实时通讯应用,那么你来对地方了!我们将从零开始,一步步构建一个功能齐全的聊天应用,并且我会尽量让这个过程既有趣又易懂。

在正式开始之前,先让我们简单了解一下什么是 Socket.IO 以及它为什么适合用于实时应用的开发。

什么是 Socket.IO?

Socket.IO 是一个基于 WebSocket 的库,它允许你在浏览器和服务器之间建立全双工通信通道。简单来说,WebSocket 是一种可以在客户端和服务器之间保持持久连接的技术,数据可以双向流动,而不需要像传统的 HTTP 请求那样每次都要重新建立连接。这使得 WebSocket 非常适合实时应用,比如聊天、多人游戏、股票行情更新等。

不过,WebSocket 本身也有一些局限性,比如它不支持断线重连、跨域问题等。而 Socket.IO 正是为了解决这些问题而诞生的。它不仅提供了 WebSocket 的所有功能,还增加了自动重连、跨域支持、广播消息等功能,大大简化了开发者的日常工作。

为什么选择 Socket.IO?

  1. 易于使用:Socket.IO 提供了一个非常简单的 API,让你可以快速上手。
  2. 跨平台支持:无论是 Node.js 服务器还是浏览器客户端,Socket.IO 都有很好的支持。
  3. 自动降级:如果浏览器不支持 WebSocket,Socket.IO 会自动降级到长轮询(Long Polling)等其他技术,确保兼容性。
  4. 社区活跃:Socket.IO 拥有一个庞大的开发者社区,遇到问题时可以很容易找到解决方案。

好了,现在我们已经对 Socket.IO 有了初步的了解,接下来让我们正式进入实战环节,开始构建我们的实时聊天应用吧!


第一步:搭建开发环境

在动手编写代码之前,我们需要先准备好开发环境。别担心,这一步非常简单,只需要安装几个必要的工具和依赖包即可。

1. 安装 Node.js 和 npm

首先,你需要确保你的电脑上已经安装了 Node.jsnpm(Node Package Manager)。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时,而 npm 是 Node.js 的包管理工具,用来安装和管理第三方库。

你可以通过以下命令检查是否已经安装了 Node.js 和 npm:

node -v
npm -v

如果没有安装,可以从 Node.js 官方网站 下载并安装最新版本。安装完成后,再次运行上面的命令,确认安装成功。

2. 创建项目目录

接下来,我们在本地创建一个新的项目目录,并初始化一个空的 Node.js 项目。打开终端,执行以下命令:

mkdir chat-app
cd chat-app
npm init -y

npm init -y 会自动生成一个 package.json 文件,里面包含了项目的配置信息。你可以根据需要修改这个文件,但目前我们保持默认设置即可。

3. 安装依赖包

现在,我们需要安装一些必要的依赖包。首先是 Socket.IO 本身,然后是一个轻量级的 HTTP 服务器库 Express,它可以帮助我们快速搭建一个 Web 服务器。最后,我们还需要安装 Nodemon,这是一个开发工具,可以在代码发生变化时自动重启服务器,方便调试。

在终端中执行以下命令来安装这些依赖包:

npm install express socket.io nodemon

安装完成后,package.json 文件中会自动添加这些依赖项。你可以打开 package.json 查看,确保所有依赖都已正确安装。

4. 修改启动脚本

为了方便使用 Nodemon 启动服务器,我们可以修改 package.json 中的 scripts 字段,添加一个名为 start:dev 的启动脚本。这样我们就可以通过 npm run start:dev 来启动服务器,并且每次代码发生变化时,服务器都会自动重启。

打开 package.json,找到 scripts 字段,添加如下内容:

"scripts": {
  "start": "node index.js",
  "start:dev": "nodemon index.js"
}

现在,我们的开发环境已经准备好了!接下来,我们开始编写服务器端代码。


第二步:编写服务器端代码

1. 初始化 Express 服务器

首先,我们需要创建一个简单的 HTTP 服务器。打开编辑器,在项目根目录下创建一个名为 index.js 的文件,并在其中编写以下代码:

// 引入必要的模块
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

// 创建 Express 应用
const app = express();

// 创建 HTTP 服务器
const server = http.createServer(app);

// 创建 Socket.IO 实例
const io = new Server(server, {
  cors: {
    origin: '*',
  }
});

// 设置静态文件目录
app.use(express.static('public'));

// 监听连接事件
io.on('connection', (socket) => {
  console.log('新用户连接:', socket.id);

  // 监听消息事件
  socket.on('chat message', (msg) => {
    console.log('收到消息:', msg);
    io.emit('chat message', msg);  // 广播消息给所有客户端
  });

  // 监听断开连接事件
  socket.on('disconnect', () => {
    console.log('用户断开连接:', socket.id);
  });
});

// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`服务器已启动,正在监听端口 ${PORT}`);
});

这段代码做了几件事情:

  • 使用 Express 创建了一个简单的 HTTP 服务器。
  • 使用 Socket.IO 创建了一个 WebSocket 服务器,并启用了跨域支持(cors: { origin: '*' }),这样前端页面可以从任何域名访问我们的服务器。
  • 设置了一个静态文件目录 public,稍后我们会把前端代码放在这个目录里。
  • 监听了 connection 事件,每当有新用户连接时,都会打印一条日志。
  • 监听了 chat message 事件,当客户端发送消息时,服务器会将消息广播给所有在线用户。
  • 监听了 disconnect 事件,当用户断开连接时,也会打印一条日志。

2. 测试服务器

现在,我们可以通过以下命令启动服务器:

npm run start:dev

如果一切正常,你应该会在终端中看到类似如下的输出:

服务器已启动,正在监听端口 3000

接下来,打开浏览器,访问 http://localhost:3000,你应该会看到一个空白页面。这是因为我们还没有编写前端代码。别担心,接下来我们就来搞定这个问题!


第三步:编写前端代码

1. 创建静态文件目录

在项目根目录下创建一个名为 public 的文件夹,这个文件夹将用于存放前端代码。接下来,我们在 public 文件夹中创建两个文件:index.htmlstyle.css

index.html

这是我们的主页面,包含了一个简单的聊天界面。打开 public/index.html,并编写以下代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>实时聊天应用</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>欢迎来到实时聊天室 🗨️</h1>
    <ul id="messages"></ul>
    <form id="chat-form">
      <input id="message-input" placeholder="输入消息..." autocomplete="off" />
      <button type="submit">发送</button>
    </form>
  </div>

  <!-- 引入 Socket.IO 客户端库 -->
  <script src="/socket.io/socket.io.js"></script>
  <script src="client.js"></script>
</body>
</html>

这段代码创建了一个简单的 HTML 页面,包含了一个标题、一个用于显示消息的 ul 列表,以及一个用于发送消息的表单。我们还引入了 Socket.IO 的客户端库,并加载了一个名为 client.js 的 JavaScript 文件,稍后我们将在这个文件中编写与服务器交互的逻辑。

style.css

为了让页面看起来更美观,我们可以在 public/style.css 中添加一些样式。打开 style.css,并编写以下代码:

body {
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
  margin: 0;
  padding: 0;
}

.container {
  max-width: 600px;
  margin: 50px auto;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
}

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

li {
  padding: 10px;
  border-bottom: 1px solid #ddd;
}

li:last-child {
  border-bottom: none;
}

form {
  display: flex;
  margin-top: 20px;
}

input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  outline: none;
}

button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 10px;
}

button:hover {
  background-color: #0056b3;
}

这段 CSS 代码为页面添加了一些基本的样式,使聊天界面更加美观。你可以根据自己的喜好进一步调整样式。

2. 编写客户端 JavaScript 代码

接下来,我们在 public 文件夹中创建一个名为 client.js 的文件,用于处理与服务器的通信。打开 client.js,并编写以下代码:

// 连接到 Socket.IO 服务器
const socket = io();

// 获取 DOM 元素
const form = document.getElementById('chat-form');
const input = document.getElementById('message-input');
const messages = document.getElementById('messages');

// 监听表单提交事件
form.addEventListener('submit', (e) => {
  e.preventDefault();  // 阻止表单默认提交行为

  if (input.value.trim() === '') return;  // 如果输入为空,不发送消息

  // 发送消息给服务器
  socket.emit('chat message', input.value);

  // 清空输入框
  input.value = '';
  input.focus();
});

// 监听来自服务器的消息
socket.on('chat message', (msg) => {
  const li = document.createElement('li');
  li.textContent = msg;
  messages.appendChild(li);

  // 滚动到底部
  messages.scrollTop = messages.scrollHeight;
});

这段代码做了几件事情:

  • 使用 io() 函数连接到 Socket.IO 服务器。
  • 获取页面中的表单、输入框和消息列表元素。
  • 监听表单的 submit 事件,当用户点击“发送”按钮时,阻止表单的默认提交行为,并将输入框中的内容作为消息发送给服务器。
  • 监听来自服务器的 chat message 事件,每当服务器广播一条新消息时,将其添加到消息列表中,并自动滚动到底部。

3. 测试聊天功能

现在,我们已经完成了前后端的开发工作。再次启动服务器:

npm run start:dev

打开多个浏览器窗口或标签页,访问 http://localhost:3000,你将会看到一个简单的聊天界面。尝试在不同的窗口中发送消息,看看它们是否能够实时同步显示。如果你看到消息成功发送并显示在所有窗口中,恭喜你,你已经成功创建了一个基本的实时聊天应用!🎉


第四步:添加更多功能

虽然我们已经实现了一个基本的聊天应用,但还有很多可以改进的地方。接下来,我们将为应用添加一些额外的功能,让它更加完善。

1. 显示用户名

为了让聊天更加个性化,我们可以为每个用户分配一个唯一的用户名。我们可以通过在用户首次连接时提示他们输入用户名,并将用户名存储在 Socket.IO 的 socket 对象中。

修改服务器端代码

首先,我们需要在服务器端为每个用户分配一个用户名。打开 index.js,找到 io.on('connection') 回调函数,并进行以下修改:

io.on('connection', (socket) => {
  console.log('新用户连接:', socket.id);

  // 提示用户输入用户名
  socket.emit('request username');

  // 接收用户输入的用户名
  socket.on('set username', (username) => {
    socket.username = username;
    console.log(`${username} 已加入聊天室`);
    io.emit('user joined', `${username} 加入了聊天室`);
  });

  // 监听消息事件
  socket.on('chat message', (msg) => {
    console.log(`${socket.username}: ${msg}`);
    io.emit('chat message', { username: socket.username, message: msg });
  });

  // 监听断开连接事件
  socket.on('disconnect', () => {
    console.log(`${socket.username} 离开了聊天室`);
    io.emit('user left', `${socket.username} 离开了聊天室`);
  });
});

修改客户端代码

接下来,我们需要在客户端提示用户输入用户名,并将用户名发送给服务器。打开 client.js,并在顶部添加以下代码:

// 提示用户输入用户名
socket.on('request username', () => {
  const username = prompt('请输入您的用户名:');
  if (username) {
    socket.emit('set username', username);
  }
});

// 接收来自服务器的用户加入通知
socket.on('user joined', (msg) => {
  const li = document.createElement('li');
  li.className = 'system-message';
  li.textContent = msg;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

// 接收来自服务器的用户离开通知
socket.on('user left', (msg) => {
  const li = document.createElement('li');
  li.className = 'system-message';
  li.textContent = msg;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

// 接收来自服务器的消息
socket.on('chat message', (data) => {
  const li = document.createElement('li');
  li.innerHTML = `<strong>${data.username}:</strong> ${data.message}`;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

添加样式

为了让系统消息(如用户加入和离开的通知)与普通消息区分开来,我们可以在 style.css 中为系统消息添加一些特殊的样式:

.system-message {
  color: #999;
  font-style: italic;
}

2. 添加时间戳

为了让聊天记录更加清晰,我们可以在每条消息旁边显示发送的时间戳。我们可以通过 JavaScript 的 Date 对象获取当前时间,并将其格式化为友好的格式。

修改客户端代码

打开 client.js,找到 socket.on('chat message') 回调函数,并进行以下修改:

// 接收来自服务器的消息
socket.on('chat message', (data) => {
  const li = document.createElement('li');
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  li.innerHTML = `<strong>${data.username}:</strong> ${data.message} <span class="timestamp">(${time})</span>`;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

添加样式

为了让时间戳更加显眼,我们可以在 style.css 中为时间戳添加一些样式:

.timestamp {
  color: #999;
  font-size: 0.8em;
}

3. 添加私聊功能

为了让用户之间可以进行私聊,我们可以在服务器端实现一个私聊机制。私聊的消息只会发送给指定的用户,而不是广播给所有人。

修改服务器端代码

打开 index.js,找到 socket.on('chat message') 回调函数,并在其下方添加以下代码:

// 监听私聊消息事件
socket.on('private message', ({ to, message }) => {
  const recipient = io.sockets.sockets.get(to);
  if (recipient) {
    recipient.emit('private message', { from: socket.username, message });
    console.log(`${socket.username} 私聊给 ${recipient.username}: ${message}`);
  } else {
    console.log(`找不到用户 ${to}`);
  }
});

修改客户端代码

接下来,我们需要在客户端添加一个私聊功能。我们可以通过在表单中添加一个“私聊对象”的输入框,用户可以输入对方的用户名来进行私聊。打开 client.js,并在 form.addEventListener('submit') 回调函数中添加以下代码:

// 监听表单提交事件
form.addEventListener('submit', (e) => {
  e.preventDefault();

  if (input.value.trim() === '') return;

  // 检查是否是私聊消息
  const isPrivate = input.value.startsWith('@');
  if (isPrivate) {
    const [to, ...rest] = input.value.split(' ');
    const message = rest.join(' ');

    // 发送私聊消息
    socket.emit('private message', { to: to.slice(1), message });

    // 在本地显示私聊消息
    const li = document.createElement('li');
    li.innerHTML = `<strong>你 -> ${to.slice(1)}:</strong> ${message}`;
    messages.appendChild(li);
  } else {
    // 发送普通消息
    socket.emit('chat message', input.value);
  }

  input.value = '';
  input.focus();
});

// 接收私聊消息
socket.on('private message', (data) => {
  const li = document.createElement('li');
  li.innerHTML = `<strong>${data.from} -> 你:</strong> ${data.message}`;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

修改 HTML

为了让用户更容易输入私聊对象,我们可以在表单中添加一个提示信息。打开 index.html,找到表单部分,并进行以下修改:

<form id="chat-form">
  <input id="message-input" placeholder="输入消息(@用户名 发送私聊)..." autocomplete="off" />
  <button type="submit">发送</button>
</form>

总结

恭喜你,经过以上步骤,你已经成功创建了一个功能丰富的实时聊天应用!我们不仅实现了基本的聊天功能,还添加了用户名、时间戳和私聊功能。当然,这只是一个起点,你可以根据自己的需求继续扩展和优化这个应用。

如果你想要进一步提升应用的性能和用户体验,这里有一些可以考虑的方向:

  • 用户认证:为用户提供注册和登录功能,确保每个用户的唯一性和安全性。
  • 消息存储:将聊天记录保存到数据库中,以便用户可以在重新连接时查看历史消息。
  • 文件传输:允许用户发送图片、视频等多媒体文件。
  • 表情符号:为用户提供表情符号选择,丰富聊天体验。
  • 群组聊天:创建多个聊天室,用户可以选择加入不同的群组进行讨论。

希望这篇讲座对你有所帮助,祝你在实时应用开发的道路上越走越远!如果你有任何问题或建议,欢迎随时联系我。再见,期待下次再聊!👋


附录:完整代码

index.js(服务器端)

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: '*',
  }
});

app.use(express.static('public'));

io.on('connection', (socket) => {
  console.log('新用户连接:', socket.id);

  socket.emit('request username');

  socket.on('set username', (username) => {
    socket.username = username;
    console.log(`${username} 已加入聊天室`);
    io.emit('user joined', `${username} 加入了聊天室`);
  });

  socket.on('chat message', (msg) => {
    console.log(`${socket.username}: ${msg}`);
    io.emit('chat message', { username: socket.username, message: msg });
  });

  socket.on('private message', ({ to, message }) => {
    const recipient = io.sockets.sockets.get(to);
    if (recipient) {
      recipient.emit('private message', { from: socket.username, message });
      console.log(`${socket.username} 私聊给 ${recipient.username}: ${message}`);
    } else {
      console.log(`找不到用户 ${to}`);
    }
  });

  socket.on('disconnect', () => {
    console.log(`${socket.username} 离开了聊天室`);
    io.emit('user left', `${socket.username} 离开了聊天室`);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`服务器已启动,正在监听端口 ${PORT}`);
});

index.html(前端)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>实时聊天应用</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>欢迎来到实时聊天室 🗨️</h1>
    <ul id="messages"></ul>
    <form id="chat-form">
      <input id="message-input" placeholder="输入消息(@用户名 发送私聊)..." autocomplete="off" />
      <button type="submit">发送</button>
    </form>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="client.js"></script>
</body>
</html>

client.js(前端)

const socket = io();

const form = document.getElementById('chat-form');
const input = document.getElementById('message-input');
const messages = document.getElementById('messages');

socket.on('request username', () => {
  const username = prompt('请输入您的用户名:');
  if (username) {
    socket.emit('set username', username);
  }
});

socket.on('user joined', (msg) => {
  const li = document.createElement('li');
  li.className = 'system-message';
  li.textContent = msg;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

socket.on('user left', (msg) => {
  const li = document.createElement('li');
  li.className = 'system-message';
  li.textContent = msg;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

socket.on('chat message', (data) => {
  const li = document.createElement('li');
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  li.innerHTML = `<strong>${data.username}:</strong> ${data.message} <span class="timestamp">(${time})</span>`;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

socket.on('private message', (data) => {
  const li = document.createElement('li');
  li.innerHTML = `<strong>${data.from} -> 你:</strong> ${data.message}`;
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
});

form.addEventListener('submit', (e) => {
  e.preventDefault();

  if (input.value.trim() === '') return;

  const isPrivate = input.value.startsWith('@');
  if (isPrivate) {
    const [to, ...rest] = input.value.split(' ');
    const message = rest.join(' ');

    socket.emit('private message', { to: to.slice(1), message });

    const li = document.createElement('li');
    li.innerHTML = `<strong>你 -> ${to.slice(1)}:</strong> ${message}`;
    messages.appendChild(li);
  } else {
    socket.emit('chat message', input.value);
  }

  input.value = '';
  input.focus();
});

style.css(样式)

body {
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
  margin: 0;
  padding: 0;
}

.container {
  max-width: 600px;
  margin: 50px auto;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
}

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

li {
  padding: 10px;
  border-bottom: 1px solid #ddd;
}

li:last-child {
  border-bottom: none;
}

form {
  display: flex;
  margin-top: 20px;
}

input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  outline: none;
}

button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 10px;
}

button:hover {
  background-color: #0056b3;
}

.system-message {
  color: #999;
  font-style: italic;
}

.timestamp {
  color: #999;
  font-size: 0.8em;
}

希望你喜欢这篇讲座,祝你在实时应用开发的道路上越走越远!如果有任何问题或建议,欢迎随时联系我。再见,期待下次再聊!👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注