本章内容:

                一、简单回顾

                二、准备工作

                三、代码编写及测试

                四、代码解释

一、简单回顾

        在上述的章节中我们了解了有关 WebRTC 的一些相关的基本概念,如:信令的核心作用什么是SDPNAT穿透与ICE框架 等相关知识,若是有不了解的朋友,可以去查看上一篇文章

        地址:攻克实时通信:WebRTC / 移动端音视频通话学习之路(第一天)-CSDN博客https://blog.csdn.net/ma__liu/article/details/150563086?spm=1011.2124.3001.6209

二、准备工作

        在编写代码之前,我们需要准备好以下工作,需要准备好一个编程环境,这里小编用的是Java写的,用了Springboot框架(当然也不是只能用Java,其他的编程语言也都是可以的,只不过语法以及调用的方法可能不太一样,这里就只能靠各位去摸索了,不过实现的思路还是一样的!)以及还需要准备一下测试的工具,这里推荐一下Postman这个测试工具,一款蛮强大的测试工具,能够测试后台的接口以及WebSocket连接等等(这里就不细说了,大家可以下载好后自己摸索一番)

Postman下载地址:Download Postman | Get Started for Freehttps://www.postman.com/downloads/

如果实在不想下载的朋友也可以上网查找在线的测试工具,这里给大家推荐一个网站

网站地址:WebSocket在线测试工具https://wstool.js.org/

本章最重要的就是测试代码写好后使用WebSocket是否能连接,以及 “信令”是否能正常工作(原因:WebSocket 承担 WebRTC 信令的传输载体)

三、代码编写及测试

        OKOK👌,在了解完上述的基本内容以及相关的准备工作之后呢,我们开始正式编写代码啦!毕竟 “ 光说不练假把式 ”,要想知道自己是否掌握了先前的理论,写代码无疑是最优解!

        接下来小编会给出自己写的代码,一个简单的 “信令” 实现,仅供各位参考,后续会出如何优化这个 简单的 “信令”

        Maven依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         optional>true</optional>
    </dependency>
</dependencies>    

        WebSocket配置类:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;


@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final SignalingHandler signalingHandler;

    public WebSocketConfig(SignalingHandler signalingHandler) {
        this.signalingHandler = signalingHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册WebSocket处理器,允许跨域访问
        registry.addHandler(signalingHandler, "/signaling")
                .setAllowedOrigins("*");
    }
}

        SignalingHandler类:

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class SignalingHandler extends TextWebSocketHandler {
    // 存储客户端ID与WebSocketSession的映射
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        try {
            // 解析收到的JSON消息
            Message signalingMessage = objectMapper.readValue(message.getPayload(), Message.class);
            System.out.println("Received message: " + signalingMessage);

            // 处理注册消息
            if ("register".equals(signalingMessage.getType())) {
                String clientId = signalingMessage.getFrom();
                if (clientId != null && !clientId.isEmpty()) {
                    sessions.put(clientId, session);
                    System.out.println("Client registered: " + clientId);

                    // 发送注册成功响应
                    sendResponse(session, "register", "success", "Registered successfully");
                } else {
                    sendResponse(session, "error", "failed", "Client ID is required for registration");
                }
            }
            // 处理离开消息
            else if ("leave".equals(signalingMessage.getType())) {
                String clientId = signalingMessage.getFrom();
                sessions.remove(clientId);
                System.out.println("Client left: " + clientId);
                sendResponse(session, "leave", "success", "Left successfully");
            }
            // 转发其他类型的信令消息
            else if (signalingMessage.getTo() != null && !signalingMessage.getTo().isEmpty()) {
                WebSocketSession targetSession = sessions.get(signalingMessage.getTo());
                if (targetSession != null && targetSession.isOpen()) {
                    targetSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(signalingMessage)));
                    System.out.println("Message forwarded from " + signalingMessage.getFrom() + " to " + signalingMessage.getTo());
                } else {
                    sendResponse(session, "error", "failed", "Target client not found or not connected");
                }
            } else {
                sendResponse(session, "error", "failed", "Invalid message format");
            }
        } catch (Exception e) {
            System.err.println("Error handling message: " + e.getMessage());
            sendResponse(session, "error", "failed", "Error processing message: " + e.getMessage());
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 移除断开连接的客户端
        sessions.entrySet().removeIf(entry -> entry.getValue().equals(session));
        System.out.println("Connection closed. Remaining clients: " + sessions.size());
    }

    // 发送响应消息
    private void sendResponse(WebSocketSession session, String type, String status, String message) throws IOException, IOException {
        Message response = new Message();
        response.setType(type);
        response.setContent("{\"status\":\"" + status + "\",\"message\":\"" + message + "\"}");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
    }
}

        消息实体类:

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Message {
    private String type;
    private String from;
    private String to;
    private String content;

    @Override
    public String toString() {
        return "Message{" +
                "type='" + type + '\'' +
                ", from='" + from + '\'' +
                ", to='" + to + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}

代码到这里,一个简单的 “信令” 就写好啦,各位可以直接复制到自己的代码中测试一下,查看是否能够成功使用,下边小编开始测试,这里我使用的测试工具是Postman和一个在线的测试网站,在上述的 “准备工作”中有提到,用两个不同的测试工具模拟一下两台设备的通信

第一步:代码写完之后肯定是要能成功跑起来啊,不然还怎么拿测试工具测试,代码写完后运行,控制台输出以下内容则表示成功运行了,如下图所示:        

第二步:先拿Postman测试是否能连接的上后台,这里我的路径是 ws://localhost:8080/signaling,如果有连接不上的,看一下是不是自己的端口对不上,springboot默认使用8080端口(当然要是你自己修改过的话,就改为自己使用的端口就好),如果端口没问题就看一下路径是否出错了,路径是代码中自己定义的,以下是成功连接上的提示,如下图所示:

下边这个是在线的测试网址,也使用同样的地址连接上我们的后台,这样我们就可以模拟两台设备通信了

第三步:注册一个用户,使用Postman测试,(注意要在 在线测试网站也注册一个)后续的在线网站测试的截图我就不放出来了,都是一样的流程,如下图所示:        

第四步:给对方发送offer,使用Postman给在线测试网站发送一个offer或者是用在线测试网站给Postman发送一条offer(都行),如下图所示Postman成功收到了来自client2(测试网站)的offer

测试到这里,我们写的一个简单的 “信令” 就算是能使用了,各位可以自己优化一下,后续我也会出一篇关于优化的文章(但可能就是比较久了,毕竟小编也还在学)

四、代码解释

        经过上边的测试,我们的代码跑是跑起来了,但代码写的是什么,可能会有人不理解(这里您要是大佬或者是有经验的开发者就不用浪费自己宝贵的时间观看啦,要是小编解释的有错误的地方可以在下边评论中提出来😁😁😁)

        一、WebSocket配置类

                1、这个类实现了 WebSocketConfigure 接口,通过构造函数注入 SignalingHandler,这是WebSocket的核心依赖

                2、之后需要实现一个 registerWebSocketHandlers 方法,重写这个方法来注册 WebSocket处理,这是配置WebSocket端点和处理器映射的核心

        二、SinglelingHandler类

                1 、SinglelingHandler:

                        继承自 Spring 的 TextWebSocketHandler,专门处理文本类型的 WebSocket 消息,在 WebRTC 场景中主要负责以下这些事情:

                        1、管理客户端连接状态(注册、断开、在线列表)

                        2、解析客户端发送的信令消息(如注册、呼叫、应答、ICE 候选者等)

                        3、在客户端之间转发信令(如 A 向 B 发起呼叫,由该处理器转发 A 的请求给 B)

                        4、处理异常并向客户端返回响应(如 “目标用户不在线”“消息格式错误”)

                2、sessions映射表:

                        用 ConcurrentHashMap 存储用户的ID 和 会话,后续只需要通过ID就能查找到对应的会话,ObjectMapper 是J ackson 库的 JSON 处理工具,用于处理把客户端发送的 JSON 字符串(如 {"type":"offer", "from":"clientA", "to":"clientB"})解析为 Java 对象(Message)再把 Message 对象转化为 JSON 发送给客户端

                3、handleTextMessage方法:

                        当客户端通过 WebSocket 发送文本消息(信令)时,该方法被自动调用,负责解析消息、处理业务逻辑(注册 / 转发等)、返回响应

                        按类型处理:

                                register类型:客户端注册自己的 ID(如 “clientA”),后端将其加入在线列表,后续可通过该 ID 找到它的连接。

                                level类型:客户端主动下线,后端从在线列表移除它。

                                其他类型(如 offer / answer):WebRTC 信令消息,后端根据 to 字段找到目标客户端,转发消息(核心作用:让两个客户端能通过后端 “间接对话”)

                4、afterConnectionClosed 方法:用户处理客户端断开连接

                        当用户的浏览器或者网络异常的时候,该方法被调用,清理不在线的用户

                5、sendRespond 方法:用于处理向客户端发送响应的信息

                        当用户注册成功后,后端会发送一个注册成功的响应

        以上就是代码的解释,要是有哪里说错了请大家指出,这里就不要问我为什么不解释Message类嘞,因为确实没啥好说的,好啦,本章就到这里,各位可以动手试一下是否能够成功实现

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐