JavaScript

带你手写一个H5聊天室

2023-07-19  本文已影响0人  h2coder

效果预览

H5-WebSocket.png

项目说明

需要完整代码,可以clone仓库

简介

什么是WebSocket协议

WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

WebSocket的优点

WebSocket的应用场景

前端

前端API说明

WebSocket对象

var Socket = new WebSocket(url, [protocol] );

WebSocket属性

属性 描述
Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值: 0 - 表示连接尚未建立。 1 - 表示连接已建立,可以进行通信。 2 - 表示连接正在进行关闭。 3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

WebSocket事件

事件 回调函数 描述
open socket.onopen 连接建立时触发
message socket.onmessage 客户端接收服务端数据时触发
error socket.onerror 通信发生错误时触发
close socket.onclose 连接关闭时触发

WebSocket方法

方法 描述
socket.send() 使用连接发送数据
socket.close() 关闭连接

检查当前浏览器,是否支持WebSocket

function isSupportWebSocket() {
    return "WebSocket" in window;
}

if (isSupportWebSocket()) {
    console.log('浏览器支持WebSocket!');
} else {
    alert('您的浏览器不支持WebSocket功能');
}

定义服务端连接地址和用户Id

// 目标ip地址
const ipAddress = '127.0.0.1:9001';

// 用户Id
const userId = 10086;

封装一个对象,避免变量和函数污染全局变量

// 封装一个WebSocket对象
const webSocket = {
    // 创建Socket对象
    socket: new WebSocket(`ws://${ipAddress}/ws/${userId}`),
    // 判断浏览器,是否支持WebSocket
    isSupportWebSocket() {
        return "WebSocket" in window;
    },
    // 初始化WebSocket
    setup() {
        // 连接成功
        this.socket.onopen = function () {
            console.log('WebSocket 连接成功...');
        };
        // 接收到服务端的消息
        this.socket.onmessage = function (evt) {
            // 获取消息文本
            var receivedMsg = evt.data;
            console.log('WebSocket 收到消息:' + receivedMsg);
        };
        this.socket.onerror = function () {
            console.log('WebSocket 连接发生错误...');
        };
        // 连接已关闭
        this.socket.onclose = function () {
            console.log('WebSocket 连接已关闭...');
        };
    },
    // 发送消息
    sendMessage(msg) {
        this.socket.send(msg);
    },
    // 关闭连接
    close() {
        this.socket.close();
    }
};

发起连接

// 初始化,并发起连接
if (webSocket.isSupportWebSocket()) {
    console.log('浏览器支持WebSocket!');
    webSocket.setup();
} else {
    alert('您的浏览器不支持WebSocket功能');
}

发送消息

在点击发送按钮时,发送消息给服务端

function handleSend() {
    const content = chatInput.value.trim();
    if (content === '') {
        // alert('请输入你要发送的内容');
        return;
    }
    // 添加我的消息
    addChatMsg2List(content, true);
    // 清空输入框
    chatInput.value = '';
    // 发送消息
    webSocket.sendMessage(content);
}

// 发送按钮的点击事件
sendBtn.addEventListener('click', function (e) {
    handleSend();
});
// 键盘的回车事件
window.addEventListener('keyup', function (e) {
    if (e.key === 'Enter') {
        handleSend();
    }
});

监听服务端消息

// 接收到服务端的消息
this.socket.onmessage = function (evt) {
    // 获取消息文本
    var receivedMsg = evt.data;
    console.log('WebSocket 收到消息:' + receivedMsg);
    // 添加对方的消息
    addChatMsg2List(receivedMsg, false);
};

根据聊天数据,渲染列表页面

function render() {
    const newList = list.map(({ isMe, text }) => {
        if (isMe) {
            return `
                <li class="right">
                <span>${text}</span>
                <img src="./assets/me.png" alt="">
                </li>
        `;
        } else {
            return `
                <li class="left">
                <img src="./assets/you.png" alt="">
                <span>${text}</span>
                </li>
             `;
        }
    });
    chatList.innerHTML = newList.join('');
}

滚动聊天列表到底部

function scollList2Bottom() {
    chatDiv.scrollTop = chatDiv.scrollHeight
}

服务端

添加依赖

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

服务端口

server:
  port: 9001

启动类

@SpringBootApplication
//打印日志注解,是lombok提供的,打印日志时,会带有类全名、方法名、标识哪里输出的日志
@Slf4j
public class AppApplication {
    public static void main(String[] args) {
        SpringApplication.run(AppApplication.class, args);
        log.info("项目启动成功");
    }
}

WebSocket配置类

@Configuration
public class WebSocketConfig {
    /**
     * 注入ServerEndpointExporter,
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

发送消息的方法

String msg = "我是消息内容";
webSocket.sendAllMessage(msg);
webSocket.sendOneMessage(userId, msg);
webSocket.sendMoreMessage(userIds, msg);

WebSocket消息处理类

/**
 * WebSocket
 */
@Component
@Slf4j
// 接口路径 ws://localhost:9001/ws/userId
@ServerEndpoint("/ws/{userId}")
public class WebSocket {
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 用户ID
     */
    private String userId;

    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
     * 注:底下WebSocket是当前类名
     */
    private static final Set<WebSocket> sWebSockets = new CopyOnWriteArraySet<>();

    /**
     * 用来存在线连接用户信息
     */
    private static final Map<String, Session> sSessionPool = new ConcurrentHashMap<>();

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        try {
            this.session = session;
            this.userId = userId;
            sWebSockets.add(this);
            sSessionPool.put(userId, session);
            log.info("【WebSocket消息】有新的连接,总数为:" + sWebSockets.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 群发一个欢迎消息
        sendAllMessage("欢迎光临!");
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            sWebSockets.remove(this);
            sSessionPool.remove(this.userId);
            log.info("【WebSocket消息】连接断开,总数为:" + sWebSockets.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("【WebSocket消息】收到客户端消息:" + message);
        // 马上回复一个消息给客户端
        String replay = String.valueOf(message)
                .replace("吗", "")
                .replace("?", "!")
                .replace("?", "!");
        sendOneMessage(userId, replay);
    }

    /**
     * 发送错误时的处理
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误,原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 发送广播消息(群发)
     */
    public void sendAllMessage(String message) {
        log.info("【WebSocket消息】广播消息:" + message);
        for (WebSocket webSocket : sWebSockets) {
            try {
                if (webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 单点消息
     */
    public void sendOneMessage(String userId, String message) {
        Session session = sSessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【WebSocket消息】 单点消息:" + message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 单点消息(多人)
     */
    public void sendMoreMessage(String[] userIds, String message) {
        for (String userId : userIds) {
            Session session = sSessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【WebSocket消息】 单点消息:" + message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读