个人感觉,长连接短连接是错误的描述,应该是长轮询短轮询

长轮询是一个在过去被广泛使用的概念,正是这种技术让网络感觉像是实时的,我认为了解一点历史会帮助你更好地理解。

目录

互联网简史

Javascript 之后的时代

长轮询的诞生

短轮询技术

长轮询

长轮询技术的实现

长轮询控制器

保持轮询方法

长轮询控制器测试

结论


互联网简史

如果您足够大,那么您就会知道早期的网络非常无聊。我所说的无聊是指静态的,没有活动部件。

它只是在 HTTP 请求/响应模型下以单向方式同步工作。

单向请求/响应范例
请求响应模型

在这个模型中,客户端请求服务器发送一个特定的网页。然后服务器将其发送给客户端。故事到此结束。

如果客户端需要另一个页面,它将发送另一个对该页面数据的请求,服务器将请求的数据作为 HTTP 响应发送。

没有什么花哨的。很简单,很单调。

因此,在 Web 的早期,HTTP 作为一种简单的请求-响应协议是有意义的,因为它旨在根据客户端的需求从服务器提供文档。然后该文档将包含到其他文档的链接(超链接)。

没有什么比 javascript 更好的了。全是 HTML。

Javascript 之后的时代

Javascript 诞生于 1995 年,当时Netscape Communications聘请Brendan Eich在 Netscape Navigator 中实现脚本功能,经过十天的时间,JavaScript 语言诞生了。

用 10 天构建的语言你能做多少——好吧,没什么。它仅用于补充 HTML……例如提供表单验证和动态 HTML 的轻量级插入。

但这为 Web 开发世界提供了一种全新的方式。它正在从静态走向动态时代。网络不再是静态的,它发现了动态改变自身的能力。

在接下来的 5 年里,当浏览器大战升温时,微软诞生了XMLHttpRequest对象。但是,这仅受 Microsoft Internet Explorer 支持。

2006年万维网联盟于 2006 年 4 月 5 日发布了 XMLHttpRequest 对象的工作规范。这赋予了 Web 开发人员权力——他们现在可以发送异步请求从服务器获取数据并更改部分DOM 与新数据。不再需要加载整个页面。

这使得网络更加令人兴奋,不再单调。

这是历史上实时聊天应用程序成为现实的时间。它是使用长轮询机制实现的。

长轮询的诞生

长轮询是出于必要而诞生的。在长轮询之前,人们习惯于使用短轮询技术。

整个网络都在请求/响应协议上工作,因此服务器无法将消息推送到客户端。必须请求数据的始终是客户。

短轮询技术

在简短的轮询技术中,客户端不断向服务器发送请求并请求新数据。如果没有新数据,服务器会发回空响应,但如果服务器有新数据,则它会发回数据。

短轮询技术
短轮询

这似乎是一个工作模型,但它有几个缺点。

明显的缺点是客户端和服务器之间的聊天频率。客户端将继续向服务器发送 HTTP 请求。

处理 HTTP 请求的成本很高。涉及很多处理。

  • 每次需要建立新连接时。
  • 必须解析 HTTP 标头
  • 必须执行对新数据的查询
  • 并且必须生成和交付响应(通常没有提供新数据)。
  • 然后必须关闭连接,并且必须清除所有资源。

现在想象一下上述步骤发生在从每个客户端进入服务器的每个请求。有很多资源因不做任何工作而被浪费(实际上)。

我努力展示下图 (:p) 中涉及的处理、混乱和数据传输。

与多个客户端的短轮询
与多个客户端的短轮询

那么,我们如何改善上述令人讨厌的场景呢?

一个简单的解决方案是长轮询技术。

长轮询

解决方案似乎非常简单。建立一次连接,让他们等待尽可能长的时间。这样同时如果有新的数据到达服务器,服务器可以直接返回响应。通过这种方式,我们绝对可以减少所涉及的请求和响应周期的数量。

让我们借助图像来理解场景。

长轮询技术
长轮询

在这里,每个客户端都向服务器发送请求。服务器检查新数据,如果数据不可用则不会立即发回响应;相反,它会在发送响应之前等待一段时间。同时,如果数据可用,它会发送回带有数据的响应。

通过实施长轮询技术,我们可以轻松减少之前发生的请求和响应周期数(短轮询)。

简而言之,长轮询是一种挂起客户端和服务器之间的连接直到数据可用的技术。

您一定在考虑连接超时问题吗?

有多种方法可以处理这种情况:

  • 等到连接超时,然后再次发送新请求。
  • 在侦听响应时使用 Keep-Alive 标头 – 通过长轮询,客户端可以配置为Keep-Alive 在侦听响应时允许更长的超时时间(通过 标头) – 通常会避免看到超时时间为通常用于指示与服务器通信的问题。
  • 维护客户端请求的状态,并/poll在一段时间后继续将客户端重定向到端点。

长轮询技术比短轮询要好得多,但仍然消耗资源。在下一篇文章中,我将撰写有关 WebSockets 的文章,并了解 WebSockets 如何成为实时应用程序更好的解决方案。

现在,让我们跳到使用 Spring 引导框架使用 Java 进行长轮询的实现部分。

长轮询技术的实现

写这篇文章的想法来自我在处理客户的一个项目时遇到的一个技术问题。

问题是在请求中执行的任务花费了太多时间来完成。由于漫长的等待,客户端面临连接超时问题。

Websockets 解决方案是不可能的,因为客户端不在我们的控制之下。为了让它发挥作用,我们需要做一些事情。

我想过使用Keep-Alive标头,但正如我所说,这不是正确的做法,因为通常会避免看到超时期限通常用于指示与服务器通信的问题。这会提醒监控系统出现问题。

似乎可行的解决方案是使用任务的唯一 ID 连续重定向到轮询端点。

这有点复杂,但在这里我将通过一个简单的实时聊天 API 向您展示长轮询。

项目的目录结构
项目的目录结构

您唯一应该在这里关注的是LongPollingController.javaLongPollingControllerTest.java

长轮询控制器

让我们看一下LongPollingController.java代码:

package com.example.longpolling.controller;

import com.example.longpolling.Notice;
import com.example.longpolling.model.CustomMessage;
import com.example.longpolling.model.GetMessage;
import lombok.extern.log4j.Log4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@Log4j
@RestController
public class LongPollingController {

    private static final List<CustomMessage> messageStore = new ArrayList<>();


    @Notice(comment = "保存消息", order = "2")
    @PostMapping("/sendMessage")
    public ResponseEntity<List<CustomMessage>> saveMessage(@RequestBody CustomMessage message) {
        log.debug(message);
        message.setId(messageStore.size() + 1);
        messageStore.add(message);
        return ResponseEntity.ok(messageStore);
    }

    @Notice(comment = "获取消息", order = "1")
    @GetMapping("/getMessages")
    public ResponseEntity<List<CustomMessage>> getMessage(GetMessage input) throws InterruptedException {
        if (lastStoredMessage().isPresent() && lastStoredMessage().get().getId() > input.getId()) {
            List<CustomMessage> output = new ArrayList<>();
            for (int index = input.getId(); index < messageStore.size(); index++) {
                output.add(messageStore.get(index));
            }
            return ResponseEntity.ok(output);
        }

        return keepPolling(input);
    }

    @Notice(comment = "保持轮询")
    private ResponseEntity<List<CustomMessage>> keepPolling(GetMessage input) throws InterruptedException {
        Thread.sleep(5000);
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(URI.create("/getMessages?id=" + input.getId() + "&to=" + input.getTo()));
        return new ResponseEntity<>(headers, HttpStatus.TEMPORARY_REDIRECT);
    }

    private Optional<CustomMessage> lastStoredMessage() {
        return messageStore.isEmpty() ? Optional.empty() : Optional.of(messageStore.get(messageStore.size() - 1));
    }

    /**
     * 那么,什么是 长轮询?嗯,它不是来自华沙的高个子 ,这个想法是模仿发布和订阅模式。场景是这样的:
     * 1)浏览器向服务器请求一些数据。
     * 2)服务器没有可用的数据并允许请求挂起。
     * 3)稍后,响应数据可用,服务器完成请求。
     * 4)浏览器一收到数据,就会显示出来,然后立即请求更新。
     * 5)流程现在循环回到第(2)点。
     * 我怀疑 Spring 的人不太热衷于术语“长轮询”,因为他们更正式地将这种技术称为异步请求处理
     * 在查看我上面的长轮询或 异步请求处理 流程时,您可能会猜到会是什么发生在你的服务器上。
     * 每次强制服务器等待数据可用时,都会占用一些宝贵的资源。
     * 如果您的网站很受欢迎并且负载很重,那么等待更新所消耗的资源数量会激增,因此您的服务器可能会耗尽并崩溃。
     * https://dzone.com/articles/long-polling-tomcat-spring
     *
     * @return
     */
    @Notice(comment = "延迟获得结果", order = "3")
    @GetMapping("/getDeferredResult")
    private DeferredResult<String> getDeferredResult() {
        long timeOutInMilliSec = 100000L;
        String timeOutResp = "Time Out.";
        DeferredResult<String> deferredResult = new DeferredResult<>(timeOutInMilliSec, timeOutResp);
        CompletableFuture.runAsync(() -> {
            try {
                // Long polling task; if task is not completed within 100s, timeout response returned for this request
                TimeUnit.SECONDS.sleep(10);
                // set result after completing task to return response to client
                deferredResult.setResult("Task Finished");
            } catch (Exception ignored) {
            }
        });
        return deferredResult;
    }
}

首先看一下/sendMessage端点:

    private static final List<CustomMessage> messageStore = new ArrayList<>();
    
    @Notice(comment = "保存消息", order = "2")
    @PostMapping("/sendMessage")
    public ResponseEntity<List<CustomMessage>> saveMessage(@RequestBody CustomMessage message) {
        log.debug(message);
        message.setId(messageStore.size() + 1);
        messageStore.add(message);
        return ResponseEntity.ok(messageStore);
    }

它是一个非常通用的端点,它接收来自POST请求的输入并将消息存储在消息存储中。为简单起见,我使用ArrayList实例将消息存储在内存中。

message-id 是唯一的键,我们将用它来识别需要在响应中发回的内容。

假设你发送了一个新的 post 请求,message-id 将是最后一条消息的 +1。

此实现的下一部分是/getMessages端点:

    @Notice(comment = "获取消息", order = "1")
    @GetMapping("/getMessages")
    public ResponseEntity<List<CustomMessage>> getMessage(GetMessage input) throws InterruptedException {
        if (lastStoredMessage().isPresent() && lastStoredMessage().get().getId() > input.getId()) {
            List<CustomMessage> output = new ArrayList<>();
            for (int index = input.getId(); index < messageStore.size(); index++) {
                output.add(messageStore.get(index));
            }
            return ResponseEntity.ok(output);
        }
        return keepPolling(input);
    }

该方法需要两个参数:

  • id - 这是它想要消息后的消息 ID。
  • to – 此字段表示消息的目标人。因此,客户端需要发送给他的所有消息。

逻辑的第一部分检查存储在 中的最后一条消息 idmessageStore是否大于询问的 id。如果它更大,则表示自客户端上次检查以来有一条新消息可用。因此,使用在该索引之后收到的消息创建输出并将其发送回作为响应。

保持轮询方法

逻辑的另一部分是停止连接的部分。

    @Notice(comment = "保持轮询")
    private ResponseEntity<List<CustomMessage>> keepPolling(GetMessage input) throws InterruptedException {
        Thread.sleep(5000);
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(URI.create("/getMessages?id=" + input.getId() + "&to=" + input.getTo()));
        return new ResponseEntity<>(headers, HttpStatus.TEMPORARY_REDIRECT);
    }

如果存储中没有新消息,只需等待 5 秒(暂停连接),然后将其重定向回/getMessages端点。如果消息可用,则发送回带有数据的响应,否则重复该过程。

当然你可以通过在发回临时重定向响应之前再次检查它来提高效率,但你明白了,对吧?

我发回重定向标头响应的主要原因是,如果我暂停连接的时间过长,那么它就会超时。我不想要那样。我希望客户一直回来,直到我将数据提供给它。

这个实现非常适合我在我的项目期间所处的那种情况,但你也可以从这种方法中吸取教训,弯曲它并让它为自己工作。

也就是说,如果您想制作一个聊天应用程序,请使用 WebSockets。这是创建实时应用程序的最有效和最现代的方法。

长轮询控制器测试

我还希望您查看测试类以了解 API 的实现。

package com.example.longpolling;

import com.example.longpolling.model.CustomMessage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

import java.time.LocalTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class LongPollingApplicationTests {

    @LocalServerPort
    private int port;

    @Bean
    @Primary
    public TestRestTemplate testRestTemplate() {
        return new TestRestTemplate(TestRestTemplate.HttpClientOption.ENABLE_REDIRECTS, TestRestTemplate.HttpClientOption.ENABLE_COOKIES);
    }

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    public void itShouldSendMessageToTheServer() {
        CustomMessage message = new CustomMessage();
        message.setMsg("Test Message");
        message.setFrom("Foo");
        message.setTo("Bar");
        message.setCreatedAt(LocalTime.now());
        List<CustomMessage> response = restTemplate.postForObject(url("/sendMessage"), message, List.class);

        assertEquals(1, response.size());
    }

    private String url(String path) {
        return String.format("http://localhost:%d/%s", port, path);
    }

    @Test
    public void itShouldPollTheServerUntilItGetsTheResponse() {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(10000);
                CustomMessage message = new CustomMessage();
                message.setMsg("Test Message");
                message.setFrom("Foo");
                message.setTo("Bar");
                message.setCreatedAt(LocalTime.now());
                System.out.println("Sending Message: " + message);
                restTemplate.postForObject(url("/sendMessage"), message, List.class);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        List<CustomMessage> response = restTemplate.getForObject(url("/getMessages?id=0&to=Bar"), List.class);

        System.out.println("Response: " + response);

        assertEquals(1, response.size());
    }
}

在这里,我创建了一个将充当发送者的异步线程。它将等待 10 秒并发送将为名为 Bar 的用户寻址的消息。

并且接收者将发送一个带有当前消息的 id 的 get 请求,即第一次为 0。您将看到连接将建立 10 秒钟。并且一旦发送方发送消息,接收方就会得到响应。

结论

这是长轮询技术的一个非常简单直接的实现。

有很多复杂的事情需要注意,比如维护客户端请求的状态。需要发送到每个客户端并同时维护并发连接的数据。

这种技术非常易于理解,并且可以在许多应用程序中产生生产力。

也就是说,我不希望您在您现在正在构建的任何实时应用程序中实现长轮询。有可用的 WebSockets,它们在双向通信方面非常可行和高效。简而言之,使用 WebSockets 构建实时应用程序。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐