netty-socketio和Socket.IO实现网页在线聊天功能

1.前端框架 Socket.IO

官方文档:https://socket.io/docs/v4/client-api/#iourl

Socket.IO 是一个库,它支持客户端和服务器之间的低延迟、双向和基于事件的通信。

Socket.IO连接可以通过不同的底层传输建立:

  • HTTP长轮询
  • WebSocket
  • WebTransport
    Socket.IO将自动选择最佳可用选项

优点:

  • 在WebSocket连接无法建立的情况下,连接将退回到HTTP长轮询
  • 当客户端最终断开连接时,为了不使服务器不堪重负,它会自动以指数级的后退延迟重新连接。
  • 当客户端断开连接时,报文将被自动缓冲,并在重新连接时发送。

2. java后端框架 netty-socketio

特点:

  • 支持xhr-polling transport(xhr轮询传输)
  • 支持websocket传输
  • 支持namespaces and rooms
  • 支持返回(接收数据的确认)
  • 支持SSL
  • 支持客户端存储(Memory, Redisson, Hazelcast)
  • 支持跨网络socket节点的分布式广播(Redisson, Hazelcast)
  • 支持OSGi
  • 支持Spring
  • 包含JPMS的Java模块信息。
  • 无锁和线程安全的实现
  • 通过注解进行声明式处理程序配置

3.java服务端实现

3.1 创建maven项目netty-socketio-learn

maven pom.xml配置:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>netty-socketio-learn</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <hutool.version>5.8.11</hutool.version>
        <spring.boot.version>3.3.1</spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- 统一依赖管理 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- netty-socketio-->
        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>2.0.11</version>
        </dependency>

        <!-- hutool-all-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

    <!-- 使用 huawei / aliyun 的 Maven 源,提升下载速度 -->
    <repositories>
        <repository>
            <id>huaweicloud</id>
            <name>huawei</name>
            <url>https://mirrors.huaweicloud.com/repository/maven/</url>
        </repository>
        <repository>
            <id>aliyunmaven</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>

        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>

</project>

3.2 ChatServer实现

java 复制代码
package cn.netty.socketio.learn;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.StrUtil;
import com.corundumstudio.socketio.*;
import com.corundumstudio.socketio.store.MemoryStoreFactory;
import com.corundumstudio.socketio.store.StoreFactory;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * ChatServer
 */
public class ChatServer {

    private static final AtomicInteger USER_NUMBER = new AtomicInteger();
    /**
     * host name
     */
    private static final String HOST_NAME = "localhost";
    /**
     * port
     */
    private static final int PORT = 9093;
    private static final String USER_NAME = "userName";
    private static final String USER_ID = "userId";

    public static void main(String[] args) {
        Configuration config = new Configuration();
        config.setHostname(HOST_NAME);
        config.setPort(PORT);
        // client 存储工厂
        StoreFactory storeFactory = new MemoryStoreFactory();
        config.setStoreFactory(storeFactory);

        // 解决对此重启服务时,netty端口被占用问题 重复使用地址
        config.getSocketConfig().setReuseAddress(true);
        // 支持在线人数
        config.setWorkerThreads(100);
        // tcpNoDelay (boolean):
        // 如果设置为true,则禁用Nagle算法,这可以减少小数据包的延迟。
        // 基本原理:Nagle算法试图将多个小的数据包合并成一个较大的数据包再发送出去,这样可以减少网络中的数据包数量,
        // 从而提高带宽利用率。当应用发送少量数据时,TCP并不会立即将这些数据发送出去,而是等待一段时间(比如等待应用发送更多的数据),直到数据量达到一定阈值或超时后再一起发送
        // tcpSendBufferSize (int):
        // 设置发送缓冲区大小。如果设置为-1,则使用系统默认值。
        // tcpReceiveBufferSize (int):
        // 设置接收缓冲区大小。如果设置为-1,则使用系统默认值。
        // tcpKeepAlive (boolean):
        // 如果设置为true,则启用TCP的保活机制,可以在长时间没有数据传输后检测连接是否仍然有效。
        // soLinger (int):
        // 设置关闭连接前等待未发送完的数据的时间。如果是-1,则立即关闭连接而不等待。
        // reuseAddress (boolean):
        // 如果设置为true,则允许重用地址和端口,即使之前绑定过这个端口也行。
        // acceptBackLog (int):
        // 设置监听队列长度,即内核为该socket保留的已完成三次握手但尚未被accept()接受的连接的最大数目。

        final SocketIOServer server = new SocketIOServer(config);

        // 设置命名空间 前端socket连接地址:http://localhost:9093/chat
        SocketIONamespace socketIONamespace = server.addNamespace("/chat");

        // token验证
        socketIONamespace.addAuthTokenListener((authToken, client) -> {
            // 根据token获取用户信息
            System.out.println("token:" + authToken);
            Integer userId = USER_NUMBER.incrementAndGet();
            // 设置用户信息
            client.set(USER_ID, userId);
            String userName = getUserName(userId);
            client.set(USER_NAME, userName);
            System.out.println(StrUtil.format("token:{},用户:{} 验证成功!", authToken, userName));
            return AuthTokenResult.AuthTokenResultSuccess;
        });
        // 创建链接
        socketIONamespace.addConnectListener(client -> {
            String userName = client.get(USER_NAME);
            System.out.println(StrUtil.format("用户:{} 连接成功!", userName));
        });

        // 用户信息
        socketIONamespace.addEventListener("user", Object.class, (client, data, ackSender) -> {
            String userName = client.get(USER_NAME);
            // 返回用户昵称
            ackSender.sendAckData(userName);
        });

        // 消息事件处理
        socketIONamespace.addEventListener("message", Object.class, (client, data, ackSender) ->
                Opt.ofEmptyAble(socketIONamespace.getAllClients()).ifPresent(clients -> {
                    // 发送消息给所有用户
                    socketIONamespace.getBroadcastOperations().sendEvent("message", data);
                }));

        // 用户主动退出处理
        socketIONamespace.addDisconnectListener(client -> {
            String userName = client.get(USER_NAME);
            System.out.println(StrUtil.format("用户:{} 断开连接!", userName));

        });

        server.start();

    }

    /**
     * 获取用户名称
     *
     * @param userId
     * @return
     */
    private static String getUserName(Integer userId) {
        Assert.notNull(userId);
        return "用户" + userId;
    }
}

4. vue3客户端实现

使用Vue 3和socket.io-client来实现一个网页在线聊天应用:

vue3安装教程:https://socket.io/docs/v4/client-installation/

4.1 核心代码Chat.vue

html 复制代码
<template>
  <div>
    <h1>在线聊天室</h1>
    <ul>
      <li v-for="message in messages" :key="message.id">
        {{ message.nickName }}: {{ message.text }}
      </li>
    </ul>
    <input v-model="newMessage" @keyup.enter="sendMessage">
    <button @click="sendMessage">发送</button>
  </div>
</template>

<script setup>
import {ref, onMounted} from 'vue';
import {Manager} from "socket.io-client";
// 连接地址
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:9093";
// 如果管理器初始化时autoConnect设置为false,则启动新的连接尝试。
const manager = new Manager(URL, { autoConnect: true });
const socket = manager.socket("/chat", { auth: { token: "123" } });

// 数据绑定
const messages = ref([]);
const newMessage = ref('');
const nickName = ref('');

// 初始化 Socket.IO 连接
onMounted(() => {
  // 连接成功事件
  socket.on('connect', () => {
    console.log('Connected to the server');
    socket.emit('user', nickName.value, (response) => {
      nickName.value = response;
    });
  });

  // 接收消息事件
  socket.on('message', (msg) => {
    messages.value.push(msg);
  });

  // 断开连接事件
  socket.on('disconnect', () => {
    console.log('Disconnected from the server');
  });
});

// 发送消息方法
const sendMessage = () => {
  if (newMessage.value.trim() !== '') {
    socket.emit('message', {text: newMessage.value, id: Date.now(), nickName: nickName.value});
    newMessage.value = '';
  }
};
</script>

<style scoped>
ul {
  list-style-type: none;
  padding: 0;
}

li {
  margin-bottom: 5px;
}

input[type="text"] {
  padding: 5px;
  margin-right: 5px;
}

button {
  padding: 5px 10px;
}
</style>

5.运行效果

浏览器1:

浏览器2:

6.spring boot3和netty-socketio整合

6.1 application.yaml配置

yaml 复制代码
server:
  port: 8089

spring:
  application:
    name: chat-server

  main:
    allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。

  # Servlet 配置
  servlet:
    # 文件上传相关配置项
    multipart:
      max-file-size: 16MB # 单个文件大小
      max-request-size: 32MB # 设置总上传的文件大小

  # Jackson 配置项
  jackson:
    serialization:
      write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳
      write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
      write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
      fail-on-empty-beans: false # 允许序列化无属性的 Bean

debug: false

6.2 ChatSocketIOServer.java

java 复制代码
package cn.netty.socketio.learn;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.StrUtil;
import com.corundumstudio.socketio.*;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.corundumstudio.socketio.store.MemoryStoreFactory;
import com.corundumstudio.socketio.store.StoreFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ChatSocketIOServer
 * spring boot 应用启动后立即执行一些初始化任务
 */
@Component
public class ChatSocketIOServer implements CommandLineRunner {

    private final SocketIOServer server;
    private final SocketIONamespace socketIONamespace;


    public ChatSocketIOServer() {
        // 创建 SocketIOServer
        server = new SocketIOServer(buildConfig());
        // 创建 socketIONamespace
        socketIONamespace = server.addNamespace("/chat");
    }

    @Bean
    public EventHandler eventHandler() {
        return new EventHandler(socketIONamespace);
    }

    private static final AtomicInteger USER_NUMBER = new AtomicInteger();
    /**
     * host name
     */
    private static final String HOST_NAME = "localhost";
    /**
     * port
     */
    private static final int PORT = 9093;
    private static final String USER_NAME = "userName";
    private static final String USER_ID = "userId";

    public Configuration buildConfig() {
        Configuration config = new Configuration();
        config.setHostname(HOST_NAME);
        config.setPort(PORT);
        // client 存储工厂
        StoreFactory storeFactory = new MemoryStoreFactory();
        config.setStoreFactory(storeFactory);

        // 解决对此重启服务时,netty端口被占用问题 重复使用地址
        config.getSocketConfig().setReuseAddress(true);
        // 支持在线人数
        config.setWorkerThreads(100);
        // tcpNoDelay (boolean):
        // 如果设置为true,则禁用Nagle算法,这可以减少小数据包的延迟。
        // 基本原理:Nagle算法试图将多个小的数据包合并成一个较大的数据包再发送出去,这样可以减少网络中的数据包数量,
        // 从而提高带宽利用率。当应用发送少量数据时,TCP并不会立即将这些数据发送出去,而是等待一段时间(比如等待应用发送更多的数据),直到数据量达到一定阈值或超时后再一起发送
        // tcpSendBufferSize (int):
        // 设置发送缓冲区大小。如果设置为-1,则使用系统默认值。
        // tcpReceiveBufferSize (int):
        // 设置接收缓冲区大小。如果设置为-1,则使用系统默认值。
        // tcpKeepAlive (boolean):
        // 如果设置为true,则启用TCP的保活机制,可以在长时间没有数据传输后检测连接是否仍然有效。
        // soLinger (int):
        // 设置关闭连接前等待未发送完的数据的时间。如果是-1,则立即关闭连接而不等待。
        // reuseAddress (boolean):
        // 如果设置为true,则允许重用地址和端口,即使之前绑定过这个端口也行。
        // acceptBackLog (int):
        // 设置监听队列长度,即内核为该socket保留的已完成三次握手但尚未被accept()接受的连接的最大数目。

        // ssl
        // config.setKeyStorePassword("test1234");
        // InputStream stream = ChatSocketIOServer.class.getResourceAsStream("/keystore.jks");
        // config.setKeyStore(stream);
        return config;
    }

    /**
     * EventHandler
     */
    public static class EventHandler {
        private final SocketIONamespace socketIONamespace;

        public EventHandler(SocketIONamespace socketIONamespace) {
            this.socketIONamespace = socketIONamespace;
        }

        @OnConnect
        public void onConnect(SocketIOClient client) {
            String userName = client.get(USER_NAME);
            System.out.println(StrUtil.format("用户:{} 连接成功!", userName));
        }

        @OnDisconnect
        public void onDisconnect(SocketIOClient client) {
            String userName = client.get(USER_NAME);
            System.out.println(StrUtil.format("用户:{} 断开连接!", userName));
        }

        @OnEvent("user")
        public void onEventUser(SocketIOClient client, Object data, AckRequest ackSender) {
            String userName = client.get(USER_NAME);
            // 返回用户昵称
            ackSender.sendAckData(userName);
        }

        @OnEvent("message")
        public void onEventMessage(SocketIOClient client, Object data, AckRequest ackSender) {
            Opt.ofEmptyAble(socketIONamespace.getAllClients()).ifPresent(clients -> {
                // 发送消息给所有用户
                socketIONamespace.getBroadcastOperations().sendEvent("message", data);
            });
        }
    }


    @Override
    public void run(String... args) throws Exception {
        // token验证
        socketIONamespace.addAuthTokenListener((authToken, client) -> {
            Integer userId = USER_NUMBER.incrementAndGet();
            // 设置用户信息
            client.set(USER_ID, userId);
            String userName = getUserName(userId);
            client.set(USER_NAME, userName);
            System.out.println(StrUtil.format("token:{},用户:{} 验证成功!", authToken, userName));
            return AuthTokenResult.AuthTokenResultSuccess;
        });
        socketIONamespace.addListeners(eventHandler());
        server.start();
    }

    /**
     * 获取用户名称
     *
     * @param userId
     * @return
     */
    private String getUserName(Integer userId) {
        Assert.notNull(userId);
        return "用户" + userId;
    }
}

6.3 ChatServerApplication.java

java 复制代码
package cn.netty.socketio.learn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * ChatServerApplication
 */
@SpringBootApplication
public class ChatServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ChatServerApplication.class, args);
    }
}

启动ChatServerApplication效果和ChatServer一样

相关推荐
晴子呀6 分钟前
Spring底层原理大致脉络
java·后端·spring
只吹45°风13 分钟前
Java-ArrayList和LinkedList区别
java·arraylist·linkedlist·区别
阿华的代码王国20 分钟前
【JavaEE】多线程编程引入——认识Thread类
java·开发语言·数据结构·mysql·java-ee
黑蛋同志21 分钟前
array和linked list的区别
java
andrew_121927 分钟前
腾讯 IEG 游戏前沿技术 一面复盘
java·redis·sql·面试
寻求出路的程序媛34 分钟前
JVM —— 类加载器的分类,双亲委派机制
java·jvm·面试
这孩子叫逆36 分钟前
35. MyBatis中的缓存失效机制是如何工作的?
java·spring·mybatis
骆晨学长37 分钟前
基于SpringBoot的校园失物招领系统
java·spring boot
汇匠源37 分钟前
零工市场小程序:保障灵活就业
java·小程序·零工市场
计算机编程-吉哥40 分钟前
计算机毕业设计 二手图书交易系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试
java·spring boot·毕业设计·毕业论文·计算机毕业设计选题·计算机毕业设计开题报告·二手图书交易系统