一、什么是长连接?先搞懂通信的“连接本质”

要理解长连接,首先得明确“连接”在网络通信中的含义。我们常说的HTTP、TCP这些协议,本质上是规范了数据在网络中的传输规则,而“连接”则是客户端与服务器之间基于这些协议建立的“通信通道”。

长连接(Long Connection),顾名思义,就是一旦客户端与服务器建立连接后,会刻意保持这个连接的持久性,在连接存续期间,双方可以随时发送数据,无需反复建立和关闭连接。这种连接并非“永久存在”,而是会通过心跳机制等方式维护,直到一方主动关闭或网络异常中断。

举个生活化的例子:如果把通信比作打电话,短连接就是“拨一次号、说一句话、挂掉”,下次说话再重新拨号;而长连接则是“拨通后不挂线,随时可以你一言我一语,直到聊完再挂掉”。显然,对于需要频繁互动的场景,长连接的效率更高。

二、长连接vs短连接:核心差异与适用场景

很多人会混淆长连接与短连接,其实二者的核心差异体现在“连接的生命周期”和“通信效率”上。我们通过一张对比表清晰呈现:

对比维度长连接短连接
连接生命周期建立后持续保持,按需关闭请求响应完成后立即关闭
连接开销一次建立,多次复用,开销低每次通信都要建立/关闭,开销高
实时性高,双方可随时发送数据低,需重新建立连接才能通信
服务器压力长期保持连接,内存占用高连接频繁创建/销毁,CPU开销高
适用场景实时通信(聊天、直播、行情)单次请求(网页浏览、接口查询)

简单来说:短连接适合“一次交互就结束”的场景,比如我们打开一个网页,服务器返回HTML后连接就关闭,后续再刷新才重新建立;而长连接适合“频繁交互、实时性要求高”的场景,比如微信聊天时,你和好友的消息能即时收发,就是因为客户端与微信服务器保持了长连接。

三、长连接的核心技术:如何“维持”连接?

长连接的关键不是“建立连接”——TCP本身就支持建立持久连接——而是“维持连接”。因为网络环境复杂,比如路由器超时、防火墙拦截、网络波动等,都会导致“假死连接”(连接看似存在,实则无法通信)。要解决这个问题,就需要两大核心技术:TCP长连接基础和心跳机制。

1. 基础:TCP的“Keep-Alive”与HTTP长连接

长连接的底层依赖TCP协议的特性。TCP本身是面向连接的协议,一旦通过三次握手建立连接,就会保持直到四次挥手关闭。但默认情况下,TCP连接如果长时间没有数据传输,会被路由器或操作系统主动关闭(超时时间通常是几分钟到几小时)。

为了避免这种情况,TCP提供了“Keep-Alive”(保活)机制:当连接空闲一段时间后,客户端会向服务器发送一个Keep-Alive探测包,如果服务器回复,说明连接正常;如果多次未回复,则判定连接失效,主动关闭。

而我们熟悉的HTTP协议,在1.1版本中默认支持长连接(通过Connection: keep-alive头部实现)。但HTTP长连接本质是“复用TCP连接”,依然是“请求-响应”模式,无法实现服务器主动向客户端推送数据——这就需要更适合实时推送的长连接协议。

2. 进阶:WebSocket——真正的双向长连接

WebSocket是HTML5推出的专门用于长连接的协议,它解决了HTTP长连接“无法主动推送”的痛点,实现了客户端与服务器的全双工通信(双方可同时发送数据)。

WebSocket的工作流程很简洁:

  1. 握手阶段:客户端通过HTTP请求发起握手,请求头中携带Upgrade: websocket和Connection: Upgrade等字段,表明要升级为WebSocket连接;
  2. 建立连接:服务器确认后,返回101状态码表示协议切换成功,此时连接从HTTP升级为WebSocket,底层依然是TCP连接;
  3. 数据传输:连接建立后,双方可通过帧(Frame)格式自由发送数据,无需重复握手;
  4. 关闭连接:一方发送关闭帧,另一方确认后,通过四次挥手关闭连接。

WebSocket的优势很明显:开销小(数据帧头部仅2-14字节)、实时性高(支持双向主动推送)、兼容性好(可通过HTTP端口穿透防火墙),是目前实时通信的主流选择。

3. 保障:心跳机制——避免“假死”的关键

无论是TCP的Keep-Alive,还是WebSocket,都离不开心跳机制的补充。因为TCP的Keep-Alive是底层机制,超时时间较长(通常默认2小时),无法满足应用层对“快速检测连接状态”的需求。

应用层的心跳机制逻辑很简单:

  • 客户端每隔固定时间(比如30秒)向服务器发送一个“心跳包”(通常是一个无意义的小数据包,比如{type: “heartbeat”});
  • 服务器收到心跳包后,立即回复一个“心跳响应包”;
  • 如果客户端连续多次(比如3次)未收到响应,或服务器连续多次未收到心跳包,则判定连接失效,主动关闭连接并尝试重连。

心跳间隔的设置很有讲究:太短会增加网络和服务器压力,太长则无法及时检测失效连接。通常会根据业务场景设置10-60秒,比如直播场景对实时性要求高,可设为10秒;普通聊天场景可设为30秒。

四、长连接的典型应用场景:这些场景都在靠它支撑

长连接的核心价值是“实时、高效的持续通信”,因此在需要频繁互动或实时数据更新的场景中被广泛应用,以下是几个典型案例:

1. 即时通信工具(IM)

微信、QQ、企业微信等IM工具是长连接最典型的应用。当你发送一条消息时,客户端通过已建立的WebSocket长连接直接将消息发送到服务器,服务器再通过长连接推送给接收方,整个过程延迟可低至几十毫秒,实现“即时送达”。同时,心跳机制会持续检测连接状态,一旦网络切换(比如从WiFi切到5G),会自动重连,保证通信不中断。

2. 直播与互动娱乐

直播场景中的弹幕、礼物赠送、实时点赞等互动,都依赖长连接实现。比如用户发送一条弹幕,客户端通过长连接将弹幕数据推送到服务器,服务器再广播给当前直播间的所有在线用户,确保所有观众能同步看到弹幕。此外,直播中的“在线人数实时更新”也通过长连接推送实现。

3. 金融实时行情

股票、期货、数字货币等金融场景对实时性要求极高,行情数据的延迟可能直接影响用户决策。长连接在这里的作用是“实时推送行情数据”——当交易所的行情更新后,服务器通过长连接将数据推送给所有订阅的客户端,确保用户能第一时间看到最新的价格、成交量等信息,避免了客户端频繁轮询(反复请求数据)带来的高延迟和高开销。

4. 物联网(IoT)设备通信

物联网场景中,设备(比如智能手表、智能家居、工业传感器)需要持续向服务器上报数据(如心率、温度、运行状态),同时接收服务器的控制指令(如远程开关、参数调整)。长连接在这里的优势是“低功耗、高可靠”——设备只需一次建立连接,即可持续传输数据,避免了频繁建立连接带来的电量消耗,适合电池供电的设备。

五、长连接的挑战与解决方案:如何应对高并发与不稳定?

长连接虽然优势明显,但在实际应用中也会面临两大核心挑战:高并发连接带来的服务器压力和网络不稳定导致的连接中断。我们来看看对应的解决方案:

1. 挑战一:高并发连接——百万级连接如何支撑?

长连接的特点是“连接长期保持”,如果有100万用户同时在线,服务器就需要维持100万条TCP连接。传统的“多线程/多进程”模型会因为线程切换开销大、内存占用高而无法支撑,此时需要采用“IO多路复用”技术。

IO多路复用的核心逻辑是“用一个线程管理多个连接”,通过select、poll、epoll等系统调用,监听多个连接的IO事件(如数据到达、连接关闭),只有当连接有事件发生时才进行处理,从而大幅降低服务器的资源消耗。目前主流的长连接服务器(如Nginx、Node.js、Netty)都基于IO多路复用实现,可轻松支撑百万级并发连接。

此外,还可以通过“集群部署+负载均衡”分散压力:将用户连接分配到多个服务器节点,每个节点负责一部分连接,同时通过Redis等中间件实现节点间的数据同步(如IM场景中的用户在线状态同步)。

2. 挑战二:网络不稳定——连接中断如何恢复?

移动网络(如4G/5G)的波动、WiFi切换、设备休眠等场景,都会导致长连接中断。要保证通信的连续性,需要实现“智能重连”机制:

  • 指数退避重连:首次重连间隔短(如1秒),如果失败则逐渐增加间隔(如2秒、4秒、8秒),避免频繁重连给服务器带来压力;
  • 重连时数据缓存:重连期间客户端缓存待发送的消息,重连成功后立即发送,避免消息丢失;
  • 连接状态监听:通过监听网络状态变化(如Android的NetworkInfo、iOS的Reachability),在网络恢复时主动触发重连,无需等待心跳超时。

六、实现

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>长连接通信</title>
    <script src="./markdown.js"></script>
    <link rel="stylesheet" href="./github.css" />
    <script src="./highlight.js"></script>
    <script src="./vue.js"></script>
    <script src="./wav.js"></script>
    <style>
      body {
        background-color: #f5f5f5;
      }

      #app {
        margin-top: 2%;
      }
      ul {
        margin: 0;
        padding: 0;
      }
      li {
        list-style: none;
      }
      a {
        text-decoration: none;
      }
      .title {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 40px;
        background: #ffffff;
        z-index: 10;
        text-align: center;
        line-height: 40px;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 4.27vw;
        color: #000000;
        text-align: center;
        font-style: normal;
        text-transform: none;
      }
      .box {
        width: 100%;
        margin: 50px auto 0;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
      }
      .chat {
        width: 100%;
        height: var(--chat-height); /* 使用自定义属性 */
        border-radius: 5px;
        position: relative;
        overflow: auto;
      }
      .left {
        display: flex;
        margin-bottom: 2vw;
      }
      .robot {
        margin-right: 2.67vw;
        width: 52px;
        height: 52px;
        border-radius: 50%;
        background-color: #2ea0e3;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        font-size: 3.5vw;
        color: #ffffff;
      }

      .left .lcontent,
      .left .lcontent .lcontenttext {
        max-width: 59.47vw;
        padding: 2.67vw;
        background: #ffffff;
        border-radius: 8px 8px 8px 8px;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 4vw;
        color: #333333;
        font-style: normal;
        text-transform: none;
        word-wrap: break-word;
      }
      .left .lcontent .link {
        border-top: 1px solid #dddddd;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 3.47vw;
        color: #999999;
        font-style: normal;
        text-transform: none;
        padding: 2.67vw 0;
      }
      .left .lcontent .link .linktitle,
      .left .lcontent .pic .pictitle {
        margin-bottom: 8px;
      }
      .left .lcontent .pic {
        border-top: 1px solid #dddddd;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 3.47vw;
        color: #999999;
        font-style: normal;
        text-transform: none;
        padding: 2.67vw 0 0 0;
      }
      .left .lcontent .pic .pictext {
        width: 100%;
      }
      .left .lcontent .pic .pictext ul {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
      }
      .left .lcontent .pic .pictext ul li {
        width: 48%;
        height: 40vw;
        border-radius: 8px 8px 8px 8px;
        overflow: hidden;
        margin-bottom: 2.67vw;
      }
      .left .lcontent .pic .pictext ul li img {
        width: 100%;
        height: 100%;
      }
      .titleItem {
        margin-top: 8px;
      }

      .right {
        display: flex;
        justify-content: end;
      }
      .right .rcontent {
        width: auto;
        max-width: 59.47vw;
        padding: 2.67vw;
        overflow-wrap: break-word;
        word-wrap: break-word;
        display: inline-block;
        background: #ce3333;
        border-radius: 8px 8px 8px 8px;
        white-space: pre-line;
        margin: 2vw 2.67vw 2vw auto;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 4vw;
        color: #ffffff;
        font-style: normal;
        text-transform: none;
        word-wrap: break-word;
        white-space: normal;
        display: flex;
        align-items: center;
      }
      .right .user {
        width: 52px;
        height: 52px;
        margin: 0px 0;
        display: flex;
        align-items: center;
        margin-top: 10px;
        background: #999999;
        border-radius: 50%;
        justify-content: center;
        color: #ffffff;
      }
      .entry {
        display: flex;
        width: 100%;
        /* margin-bottom: 20px; */
        text-align: center;
      }
      .entry .entrybox {
        margin-top: 1vh;
        padding: 2.67vw;
        height: 20px;
        line-height: 20px;
        border-radius: 10px 10px 10px 10px;
        border: 2px solid #ed3a40;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 4vw;
        color: #333333;
        font-style: normal;
        text-transform: none;
        width: 70%;
        margin-right: 1vw;
      }
      .paste {
        margin-top: 4.3vw;
        height: 3.47vw;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 3.2vw;
        color: #999999;
        font-style: normal;
        text-transform: none;
      }
      /* 添加模态框的样式 */
      .modal {
        display: none;
        position: fixed;
        z-index: 9999;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgba(0, 0, 0, 0.9);
      }

      .modal-content {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        display: block;
        width: 80%;
      }
      .submit {
        margin-top: 1vh;
        width: 17vw;
        background-color: #ed3a40;
        text-align: center;
        border-radius: 10px 10px 10px 10px;
        color: #ffffff;
        padding: 2.67vw 0;
        height: 24px;
        line-height: 24px;
        font-family: Alibaba PuHuiTi, Alibaba PuHuiTi;
        font-weight: 400;
        font-size: 4vw;
        font-style: normal;
        text-transform: none;
      }
      .disabled {
        pointer-events: none;
        background: #ededed;
        color: #999999;
      }
      .context1 {
        color: #999999 !important;
      }
      .context1 p {
        text-indent: 2em;
      }
    </style>
  </head>

  <body>
    <div id="app">
      <div class="title">AI小助手</div>
      <div class="box">
        <div class="chat" :style="{ '--chat-height': chatHeight }">
          <div class="left">
            <div class="robot">机器人</div>
            <div class="lcontent">
              您好,我是AI小助手,您可以发送想要咨询的内容或问题描述~
            </div>
          </div>
          <!-- 对话区域 -->
          <div v-for="(dialogue, index) in dialogues" :key="index">
            <div class="right" v-if="index % 2 === 0">
              <div
                class="rcontent"
                v-text="formatDialogContent(dialogue)"
              ></div>
              <div class="user">我</div>
            </div>
            <!-- 机器人回复 -->
            <div class="left" v-else>
              <div class="robot">机器人</div>
              <div class="copy">
                <div class="lcontent">
                  <div
                    class="lcontenttext context1"
                    v-html="markdownToHtml(dialogue.result.data[0].answer.split('</think>')[0])"
                  ></div>
                  <div
                    class="lcontenttext context2"
                    v-html="markdownToHtml(dialogue.result.data[0].answer.split('</think>')[1] || '')"
                  ></div>
                  <div
                    class="link"
                    v-if="Array.isArray(dialogue.result.data[0].reference)"
                  >
                    <div class="linktitle">参考文献:</div>
                    <div class="linktext">
                      <div
                        class="titleItem"
                        v-for="(titleItem, m) in dialogue.result.data[0].reference"
                        :key="m"
                      >
                        {{m + 1}}.{{titleItem.title}}
                      </div>
                    </div>
                  </div>
                  <div class="pic" v-if="dialogue.result.data.length > 1">
                    <div class="pictitle" style="padding-top: 10px">
                      参考图片:
                    </div>
                    <div class="pictext">
                      <ul>
                        <li
                          v-for="(item, index) in dialogue.result.imgArry.slice(1)"
                          :key="index"
                        >
                          <img
                            :src="item.answer"
                            alt=""
                            @click="openModal(item.answer)"
                          />
                        </li>
                      </ul>
                    </div>
                  </div>
                </div>
                <div class="paste" @click="copyContent($event)">复制</div>
              </div>
            </div>
          </div>
          <div class="left" v-if="showImg">
            <div class="robot">机器人</div>
            <div class="lcontent">思考中...</div>
          </div>
        </div>
        <div class="entry">
          <textarea
            class="entrybox"
            type="text"
            placeholder="请输入您的问题"
            v-model="userInput"
            @keydown.enter="send"
          >
          </textarea>
          <div
            class="submit"
            @click="send"
            :class="{ 'disabled': userInput === '' }"
          >
            发送
          </div>
        </div>
      </div>

      <!-- 模态框 -->
      <div class="modal" v-show="showModal" @click="closeModal">
        <img class="modal-content" :src="currentImgSrc" alt="" />
      </div>
    </div>

    <script>
      // 初始化 marked 配置
      marked.setOptions({
        highlight: function (code, lang) {
          if (lang && hljs.getLanguage(lang)) {
            return hljs.highlight(code, { language: lang }).value;
          }
          return hljs.highlightAuto(code).value;
        },
        breaks: true, // 支持换行符转 <br>
        gfm: true, // 启用 GitHub Flavored Markdown
      });
      const { createApp, ref, onMounted, nextTick } = Vue;

      createApp({
        setup() {
          // 对话数组
          const dialogues = ref([]);
          // 动画
          const showImg = ref(false);
          // n
          const n = ref(1);
          let sendFlag = true;
          // 用户输入的内容
          const userInput = ref("");
          // 初始窗口高度
          const initialWindowHeight = ref(window.innerHeight);
          // chat 区域的高度
          const chatHeight = ref("76vh");

          // 控制模态框的显示和隐藏
          const showModal = ref(false);
          // 当前点击的图片的src
          const currentImgSrc = ref("");
          const socket = new WebSocket(
            "替换成您的接口"
          );
          let rec = null;
          // 新增:Markdown 转 HTML 方法
          const markdownToHtml = (content) => {
            if (!content) return "";
            // 先处理特殊格式,再转换 Markdown
            const formattedContent = content
              .replace(/\/\/n/g, "\n") // 处理双斜杠换行
              .replace(/\\n/g, "\n"); // 处理单斜杠换行
            return marked.parse(formattedContent);
          };

          const fetchData = async (message) => {
            socket.send(message);
            socket.onmessage = function (event) {
              let message = event.data.replace("data:", "").trim();
              message = message.replace("<think>", "");
              showImg.value = false;
              if (message !== "[done]") {
                let eventobj = JSON.parse(message);

                if (eventobj.result.data.length > 0) {
                  let imgArry = eventobj.result.data.slice(1);
                  eventobj.result.imgArry = imgArry;
                }
                let found = false;
                dialogues.value.forEach((speckItem, index) => {
                  if (eventobj.request_id === speckItem?.request_id) {
                    dialogues.value[index] = eventobj;
                    found = true;
                  }
                });
                if (!found) {
                  dialogues.value.push(eventobj);
                }
              } else {
                sendFlag = true;
              }
            };
          };

          const formatDialogContent = (content) => {
            const formattedContent = content.replace(/\/\/n/g, "\n");
            return formattedContent;
          };

          // 发送用户输入的消息
          const sendMessage = () => {
            const message = userInput.value.trim();
            if (message !== "") {
              dialogues.value.push(message); // 将用户输入的消息添加到对话数组中
              showImg.value = true;
              fetchData(message); // 发送消息到接口获取回复
              userInput.value = ""; // 清空输入框
              // 滚动到底部
              setTimeout(() => {
                const chat = document.querySelector(".chat");
                chat.scrollTop = chat.scrollHeight;
              }, 0);
            }
          };

          const send = (e) => {
            e.preventDefault(); // 阻止默认的换行行为
            if (sendFlag === true) {
              if (userInput.value.trim() !== "") {
                sendFlag = false;
                sendMessage();
              }
            } else {
              const tip = document.createElement("div");
              tip.textContent = "请等待...";
              tip.style.position = "fixed";
              tip.style.top = "20px";
              tip.style.left = "50%";
              tip.style.transform = "translateX(-50%)";
              tip.style.padding = "10px 20px";
              tip.style.backgroundColor = "#333";
              tip.style.color = "#fff";
              tip.style.borderRadius = "5px";
              tip.style.zIndex = "9999";
              document.body.appendChild(tip);
              setTimeout(() => {
                document.body.removeChild(tip);
              }, 2000);
            }
          };

          const copyContent = (event) => {
            const target = event.target;
            const lcontent = target.closest(".copy").querySelector(".lcontent");
            const range = document.createRange();
            range.selectNode(lcontent);
            const selection = window.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);
            try {
              document.execCommand("copy");
              selection.removeAllRanges();
              const tip = document.createElement("div");
              tip.textContent = "复制成功";
              tip.style.position = "fixed";
              tip.style.top = "20px";
              tip.style.left = "50%";
              tip.style.transform = "translateX(-50%)";
              tip.style.padding = "10px 20px";
              tip.style.backgroundColor = "#333";
              tip.style.color = "#fff";
              tip.style.borderRadius = "5px";
              tip.style.zIndex = "9999";
              document.body.appendChild(tip);
              setTimeout(() => {
                document.body.removeChild(tip);
              }, 2000);
            } catch (err) {
              const tip = document.createElement("div");
              tip.textContent = "复制失败,请稍后再试";
              tip.style.position = "fixed";
              tip.style.top = "20px";
              tip.style.left = "50%";
              tip.style.transform = "translateX(-50%)";
              tip.style.padding = "10px 20px";
              tip.style.backgroundColor = "#333";
              tip.style.color = "#fff";
              tip.style.borderRadius = "5px";
              tip.style.zIndex = "9999";
              document.body.appendChild(tip);
              setTimeout(() => {
                document.body.removeChild(tip);
              }, 2000);
              console.log("复制失败: ", err);
            }
          };

          // 打开模态框并设置当前图片的src
          const openModal = (imgSrc) => {
            currentImgSrc.value = imgSrc;
            showModal.value = true;
          };

          // 关闭模态框
          const closeModal = () => {
            showModal.value = false;
          };

          const addWatch = () => {
            // 获取目标元素
            const dialogueBox = document.querySelector(".chat");

            if (dialogueBox) {
              // 创建 MutationObserver 实例
              const observer = new MutationObserver(() => {
                // 内容变化时,滚动到底部
                dialogueBox.scrollTop = dialogueBox.scrollHeight;
              });

              // 配置 MutationObserver 监听子元素和内容变化
              observer.observe(dialogueBox, {
                childList: true, // 监听子元素变化
                subtree: true, // 监听所有后代元素
                characterData: true, // 监听文本内容变化
              });

              // 初始滚动到底部
              dialogueBox.scrollTop = dialogueBox.scrollHeight;
            } else {
              console.error("未找到对话框容器");
            }
          };

          onMounted(() => {
            addWatch();
            // 初始化代码高亮
            document.querySelectorAll("pre code").forEach((block) => {
              hljs.highlightElement(block);
            });
          });

          return {
            dialogues,
            userInput,
            sendMessage,
            n,
            formatDialogContent,
            showImg,
            send,
            copyContent,
            chatHeight,
            showModal,
            currentImgSrc,
            openModal,
            closeModal,
            markdownToHtml, // 导出 Markdown 转换方法
          };
        },
      }).mount("#app");
    </script>
  </body>
</html>

上面引入的文件:在资源里面下载即可,不用积分

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐