咱就是说,做Web开发的朋友肯定都遇到过这种需求:聊天消息要实时推送、股票数据要实时刷新、多人在线协作要实时同步……这时候如果你还搁那用HTTP轮询疯狂发请求,服务端估计已经在后台骂骂咧咧了(笑)。

今天咱就来聊聊这个让前后端能"随时随地保持通话"的狠角色——WebSocket

WebSocket到底是个啥?

打个比方哈:

HTTP 就像是你给快递站打电话——"喂?我的快递到了没?"(发送请求),快递站说"还没"(返回响应),你就得挂了。过一会你又打过去问,反反复复...每次都得你先主动打电话,对方才能告诉你东西。你要是忘了问,人家也不会主动通知你。

WebSocket 就像是加了个微信好友——互加好友之后,你想起来了就发条消息问问,快递站有消息了也能直接推给你,**双方随时都能给对方发消息**,不用每次都"重新认识"一遍。

说白了,WebSocket就是一个全双工通信协议,让浏览器和服务器之间建立一个持久连接,两边都可以随时给对方发消息,不用再"你问我答"那么死板。

没有WebSocket之前,我们都是怎么凑合的?

在WebSocket出现之前,咱想搞实时通信,就只有这几种"歪门邪道":

轮询(Polling)

说白了就是疯狂骚扰服务器。前端每隔几秒发一个HTTP请求问"有新消息吗?",不管有没有新消息都得发。这就像你每隔5秒打电话问快递站"到了吗到了吗到了吗",快递员怕不是想顺着网线过来打你。

// 轮询:每隔3秒问一次,服务器想打人
setInterval(() => {
  fetch('/api/messages')
    .then(res => res.json())
    .then(data => {
      // 有新消息就更新页面
    });
}, 3000);

问题也很明显:

浪费流量:大部分请求都返回"没有新消息"

延迟高:消息实时性取决于轮询间隔,间隔短了服务器压力大,间隔长了消息延迟高

连接开销大:每次请求都要建立TCP连接、带上一大堆HTTP头

长轮询(Long Polling)

这个稍微聪明点了,请求发过去之后,服务器**hold住不回复**,等有消息了再回复。但本质上还是HTTP,连接断了之后还得重新建立。

这俩方案都有个致命问题:服务器不能主动给前端发消息,必须等前端先来问。这在实际场景中有多蛋疼,做过实时聊天的朋友都懂。

WebSocket:让前后端"双向奔赴"

WebSocket的连接建立过程其实蛮有意思的,它是从HTTP"升级"过来的:

第一步:握手——先走个HTTP过场

客户端先发一个HTTP请求,但这个请求比较特殊,头上写着"我想升级成WebSocket":

// 前端发起WebSocket连接,就这么简单
const socket = new WebSocket('ws://localhost:8080/chat');

socket.onopen = () => {
  console.log('连接上了!咱可以开始唠了');
};

socket.onmessage = (event) => {
  console.log('服务器发来消息:', event.data);
};

socket.onclose = () => {
  console.log('连接断了,可能是网不好或者服务器关了');
};

socket.onerror = (error) => {
  console.log('出错了捏:', error);
};

浏览器会在背后发这么一个HTTP请求:

GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

服务器一看这个请求,"哦,你小子想升级成WebSocket是吧",如果支持WebSocket就会返回:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

看到那个 101 Switching Protocols了吗?这代表握手成功,协议升级完毕!从此以后,这条连接就不再是HTTP了,变成了WebSocket,两边可以自由通信。

第二步:消息互通——想发就发

握手完了之后,连接就一直保持着。前端可以主动发消息:

// 发送消息给服务器
socket.send(JSON.stringify({
  type: 'chat',
  content: '你好呀服务器!',
  to: 'user123'
}));

服务器也能随时主动推给前端,不用等前端先问。这就是全双工的魅力。

服务端怎么整?以SpringBoot为例

后端这边也不复杂,咱拿Java的SpringBoot来演示:

// WebSocket配置类
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

// WebSocket服务类
@Component
@ServerEndpoint("/chat/{userId}")
public class WebSocketServer {

    // 存所有在线用户的连接
    private static ConcurrentHashMap<String, Session> onlineUsers = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {
        onlineUsers.put(userId, session);
        System.out.println("用户 " + userId + " 上线啦!当前在线人数:" + onlineUsers.size());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 收到前端消息,处理一下
        System.out.println("收到消息:" + message);
        // 可以转发给其他用户,实现聊天功能
    }

    @OnClose
    public void onClose(@PathParam("userId") String userId, Session session) {
        onlineUsers.remove(userId);
        System.out.println("用户 " + userId + " 下线了~");
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("出错了:" + error.getMessage());
    }

    // 服务器主动推消息给指定用户
    public void sendToUser(String userId, String message) {
        Session session = onlineUsers.get(userId);
        if (session != null) {
            session.getAsyncRemote().sendText(message);
        }
    }
}

代码不长吧?就几个注解的事儿。不过这里有个小坑要提醒一下:

如果WebSocket服务类里用了Spring注入的Bean(比如@Service),直接@Autowired是行不通的,因为WebSocket是多实例的。解决办法是通过ApplicationContext去拿,或者把需要用的Bean在配置类里初始化好传进去。

实际开发中,更推荐用STOMP

说实话,原生WebSocket虽然简单,但直接用起来还是有点"原始"——消息格式得自己定、订阅分发得自己写、重连机制也得自己策。咱在正经项目里更推荐用STOMP协议,做一层封装。

STOMP(Simple Text Oriented Messaging Protocol)就是在WebSocket之上又加了一层消息协议,支持发布-订阅模式,用起来特别爽:

// 前端使用STOMP(配合stomp.js和sockjs)
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

stompClient.connect({}, () => {
  // 订阅某个频道,像关注UP主一样
  stompClient.subscribe('/topic/chatroom', (message) => {
    console.log('收到群聊消息:', JSON.parse(message.body));
  });

  // 订阅私聊频道
  stompClient.subscribe('/user/queue/private', (message) => {
    console.log('收到私聊消息:', JSON.parse(message.body));
  });

  // 发消息
  stompClient.send('/app/chat', {}, JSON.stringify({
    content: '大家好呀!',
    roomId: 'room001'
  }));
});
// 后端SpringBoot + STOMP配置
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 客户端订阅的前缀,服务器会广播到这些路径
        config.enableSimpleBroker("/topic", "/queue");
        // 客户端发送消息的前缀,会路由到@MessageMapping
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();  // 兼容不支持WebSocket的浏览器
    }
}

// 消息处理Controller,跟写普通Controller一样简单
@Controller
public class ChatController {

    @MessageMapping("/chat")
    @SendTo("/topic/chatroom")  // 广播到所有订阅者
    public ChatMessage broadcast(ChatMessage message) {
        return message;  // 直接广播出去
    }

    @MessageMapping("/private-chat")
    public void privateChat(ChatMessage message, SimpMessageHeaderAccessor headerAccessor) {
        // 通过SimpMessagingTemplate发给指定用户
        messagingTemplate.convertAndSendToUser(
            message.getTo(), "/queue/private", message
        );
    }
}

用STOMP之后,发送消息就像写Controller接口一样爽。点对点聊天、群聊广播、消息确认这些功能全都给你安排得明明白白的。

不过得注意一点:STOMP是基于帧的消息格式,如果你对接的不是STOMP客户端(比如第三方设备用原生WebSocket),就会有不兼容的问题。这个要看具体场景选择。

心法口诀

特性 HTTP轮询 WebSocket
通信方向 只能客户端主动 双向通信
连接方式 每次请求新建连接 建立后保持连接
实时性 看轮询间隔脸色 毫秒级
服务器压力 每次都要握手,累死 一次握手长期保持
适用场景 普通的增删改查、表单提交 聊天、推送、实时协作、游戏

WebSocket也会掉链子——咱得兜底

WebSocket虽好,但它不是万能的:

1. 网络断了怎么办?—— 前端一定要做心跳检测 + 自动重连机制。隔一段时间发个ping,要是pong没回来就得重连。

// 简单的心跳检测示例
let heartbeatInterval;

socket.onopen = () => {
  // 每30秒发个心跳
  heartbeatInterval = setInterval(() => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'ping' }));
    }
  }, 30000);
};

socket.onclose = () => {
  clearInterval(heartbeatInterval);
  // 自动重连
  setTimeout(() => reconnect(), 3000);
};

2. 代理/防火墙可能拦截 —— 有些公司的网络环境会干掉WebSocket连接,这时候可以用SockJS降级成HTTP长轮询,保证基本可用。

3. 服务器重启所有连接都会断 —— 这个比较鸡贼啊,得做好断线重连的体验优化。消息可以先存本地,连上了再发。

4. 分布式部署时Session不共享 —— WebSocket连接是跟服务器实例绑定的,如果有多台服务器,A服务器上的用户要给B服务器上的用户发消息就走不通了。这时候就需要引入消息中间件(比如RabbitMQ、Redis Pub/Sub)来做跨服务器的消息传递。

总结

WebSocket这个技术,说白了就是让HTTP从一问一答进化成了随时交流。它最大的贡献就是让服务器也能主动找前端搭话,这样才能做出真正实时交互的Web应用。

在实际项目中,咱一般不会裸写原生WebSocket,而是套一层STOMP,配合SpringBoot的`@MessageMapping`,写起来跟普通接口差别不大,很轻松。

以上是个人的一些经验分享,希望能帮到正在捣腾实时通信的朋友们。如果有哪里有什么错误的地方也请大佬们指出,咱一起进步!

本文完结撒花!!!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐