首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

大请求、请求超时问题

编程知识
2024年08月31日 22:16

耗时很长的请求怎么处理?比如数据量大的。业务逻辑处理时间太久,以至于响应超时

这里的超时响应指的是ReadTimeOut,即发送请求内容完毕到接收响应数据开始的这段时间。普通HTTP请求可能在这段时间没有响应超时。

HTTP分块传输(Chunked Transfer Encoding)中每个数据块的到达都会刷新ReadTimeOut。服务器推送事件(SSE)中服务器会自动发送心跳消息刷新ReadTimeOut。由于这种分块或流式传输的方式每次消息处理的业务量和数据量较小,可以减少超时。

这两种只是让请求方尽快看到结果,数据出来一次就推送一次,并不能减少全部数据处理完毕的时间。而js可以收到一次回调我们的代码,打印或者处理一次,而不是收到全部所有数据后再将控制权交给我们的代码。要分批返回数据,就要求服务端的业务逻辑代码不要一次性处理所有数据,而是分批处理或查询。

http报文分块传输

HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked

4A
[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Charlie"}]
4C
[{"id": 4, "name": "David"}, {"id": 5, "name": "Eve"}, {"id": 6, "name": "Frank"}]
42
[{"id": 7, "name": "Grace"}, {"id": 8, "name": "Helen"}, {"id": 9, "name": "Ian"}]
0

  • 添加Transfer-Encoding: chunked头部表明这是一个分块传输响应
  • 4A、4C、和42是各个块的字节大小(十六进制形式),分别对应第一、二、三块数据的长度。
  • JSON数据紧跟在块大小后。
  • 每个块后面跟一个CRLF。
  • 0后跟一个CRLF,表示数据结束。

这一个的问题在于服务器发送和浏览器接收是什么形式?什么表现?我需要试一下。

  • 服务端
[HttpGet]
public async IAsyncEnumerable<string> Get()
{
	var dataList = new[]
	{
		new { Id = 1, Name = "Alice" },
		new { Id = 2, Name = "Bob" },
		new { Id = 3, Name = "Charlie" }
	};

	foreach (var data in dataList)
	{
 		// 模拟数据处理延迟
		await Task.Delay(2000); // 模拟处理时间
		yield return $"ID: {data.Id}, Name: {data.Name}\n";
	}
}

服务端返回一个异步流。使用了IAsyncEnumerable<T>,kestrel就会为响应头添加分块字段。具体来说kestrel内部会使用await foreach迭代这个方法,等待每个数据块的生成,并一次次推送响应数据

  • 浏览器
async function fetchData() {
    try {
        const response = await fetch('/data');
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder('utf-8');
        const list = document.getElementById('data-list');

        while (true) {
            const { value, done } = await reader.read();
            if (done) break;
            const textChunk = decoder.decode(value, { stream: true });
            const li = document.createElement('li');
            li.textContent = textChunk.trim();
            list.appendChild(li);
        }
    } catch (error) {
        console.error('Fetch Error:', error);
    }
}

看看实际运行效果,每次读取响应体都会有2秒延迟

image

看这个时间解析,第一次读取时,遇到第一个Task.Delay(2000),然后开始响应数据。绿色部分走完,浏览器得到响应第一部分数据,进入蓝色部分。

image

这种只能解决传输慢的问题,让接收方尽早看到数据,但不能加快全部数据响应完成时间。

SSE流式传输

流式传输服务端需要设置特定响应头,然后保持http连接,直接向响应中写数据和推送,而不是返回数据,释放连接。

  • 服务端
public async Task<IActionResult> Stream()
{
	HttpContext.Response.ContentType = "text/event-stream";
	HttpContext.Response.Headers.Add("Cache-Control", "no-cache");
	HttpContext.Response.Headers.Add("Connection", "keep-alive");

	// 周期性推数据
	while (true)
	{
		// 推送模拟数据
		var message = $"data: {System.Text.Json.JsonSerializer.Serialize(new { message = "Hello, world!", timestamp = DateTime.UtcNow })}\n\n";
		await Response.WriteAsync(message);
		await Response.Body.FlushAsync();
		//1S间隔再推送
		await Task.Delay(1000);
	}
}
  • 浏览器
const eventSource = new EventSource('/api/sse/stream');

eventSource.onmessage = function(event) {
	const message = JSON.parse(event.data);
	const messageElement = document.createElement('div');
	messageElement.textContent = `Message: ${message.message}, Timestamp: ${message.timestamp}`;
	document.getElementById('messages').appendChild(messageElement);
};

eventSource.onerror = function(event) {
	console.error('Error:', event);
};

image

不过SSE只能在请求地址中增加参数,没法定义携带的请求头,比如Authorization。

HTTP范围请求

范围请求似乎不是我们手动直接处理,而是浏览器和服务器自动完成的。比如大文件下载断点续传。这种不在乎超时问题,似乎不应该纳入此次讨论范畴。

但是我好奇的是,范围请求的流程。浏览器如何决定下载一个压缩包时发送范围请求还是普通请求?浏览器再最开始如何知道范围大小?这似乎有个探测阶段才行,那么浏览器和服务器是如何互动的?如果有探测,那么服务器怎么知道这是一个探测请求,而不是一个下载请求?

确实有一个探测阶段,使用head方法,而不是常规的get post,仅获取文件大小信息而不下载内容。

  • 浏览器发送的 HEAD 请求
HEAD /example.txt HTTP/1.1
Host: example.com
  • 服务器响应
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 100
Content-Type: text/plain

但探测阶段并不总是存在。当我们点击一个链接,浏览器并不知道这是一个大文件。所以浏览器通常会发一个get请求,直接下载文件,并从头部了解并记录是否支持范围请求Accept-Ranges和文件总大小Content-Length,以便再暂停下载之后,再次点击下载时决定能否换成发送范围请求。

  • 对于静态文件,通常web服务器内置实现了范围请求的响应。
  • 对于由控制器接口提供的文件下载,需要我们自己实现这个action的范围下载逻辑,即取头部范围字段,计算偏移量,设置头部,响应码,返回相应部分数据。
    所以控制器接口考虑断点续传时,就要加一个range分支了。第一个分支供完整下载,第二个分支供范围下载
[HttpGet]
public IActionResult GetFile(string filePath)
{
	var fileInfo = new System.IO.FileInfo(filePath);
	var fileBytes = System.IO.File.ReadAllBytes(filePath);
	//范围请求分支
	if (Request.Headers.ContainsKey("Range"))
	{
		var rangeHeader = HttpContext.Request.Headers["Range"].ToString();
		var range = rangeHeader.Replace("bytes=", "").Split('-');
		long start = long.Parse(range[0]);
		long end = range.Length > 1 ? long.Parse(range[1]) : fileInfo.Length - 1;

		if (start >= fileInfo.Length || end >= fileInfo.Length || start > end)
		{
			return StatusCode(416); // Requested Range Not Satisfiable
		}

		var filePart = fileBytes.Skip((int)start).Take((int)(end - start + 1)).ToArray();
		HttpContext.Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileInfo.Length}");
		HttpContext.Response.Headers.Add("Content-Length", filePart.Length.ToString());

		return File(filePart, "text/plain", enableRangeProcessing: true);
	}
	//完整下载分支
	return File(fileBytes, "text/plain");
}

如果要更完善一点,为某些下载器提供探测接口,那就还要实现一个head方法。但这可能是很少用到的。

[HttpHead]
public IActionResult HeadFile(string filePath)
{
	var fileInfo = new System.IO.FileInfo(filePath);
	Response.Headers["Content-Length"] = fileInfo.Length.ToString();
	return NoContent(); // 204 No Content
}
From:https://www.cnblogs.com/ggtc/p/18389918
本文地址: http://www.shuzixingkong.net/article/1621
0评论
提交 加载更多评论
其他文章 STL 改造红黑树 模拟封装set和map
改造红黑树 目录改造红黑树适配STL迭代器的红黑树基本结构RBTreeNode__RBTree_iteratorRBTree完整代码封装的set封装的map 在初次看STL中实现红黑树的源码时有些不理解,然后自己尝试对set以RBTree&lt;K,K&gt;的方式封装红黑树的迭代器;实现过程发现,
C++ lambda 引用捕获临时对象引发 coredump 的案例
今天复习前几年在项目过程中积累的各类技术案例,有一个小的 coredump 案例,当时小组里几位较资深的同事都没看出来,后面是我周末查了两三个小时解决掉的,今天再做一次系统的总结,给出一个复现的案例代码,案例代码比较简单,便于学习理解。 1. 简介 原则:临时对象不应该被 lambda 引用捕获,因
C++ lambda 引用捕获临时对象引发 coredump 的案例
SNAT 与 DNAT
本文为博主原创,转载请注明出处: SNAT(Source Network Address Translation,源网络地址转换)和DNAT(Destination Network Address Translation,目标网络地址转换)是网络地址转换(NAT)中的两种重要技术,它们在实现内部网络
[postgres]使用pgbench进行基准测试
前言 pgbench是一种在postgres上进行基准测试的简单程序,一般安装后就会自带。pgbench可以再并发的数据库绘画中一遍遍地进行相同序列的SQL语句,并且计算平均事务率。 测试准备 既然要测postgres,肯定要先有个postgres。安装过程略过。 一些环境信息: postgres版
全网最适合入门的面向对象编程教程:42 Python常用复合数据类型-collections容器数据类型
在Python中,collections模块提供了一组高效、功能强大的容器数据类型,扩展了内置的基础数据类型(如list、tuple、dict等),这些容器数据类型在处理特定问题时,能够提供更简洁、更高效的解决方案。
全网最适合入门的面向对象编程教程:42 Python常用复合数据类型-collections容器数据类型 全网最适合入门的面向对象编程教程:42 Python常用复合数据类型-collections容器数据类型 全网最适合入门的面向对象编程教程:42 Python常用复合数据类型-collections容器数据类型
喜报!Fluent Editor 开源富文本迎来了第一位贡献者!
你好,我是 Kagol,个人公众号:前端开源星球。 2024年8月20日,刚开源一周的富文本 Fluent Editor 迎来了第一位贡献者:zzxming,带大家一起分析下这个 #10 这个PR。
喜报!Fluent Editor 开源富文本迎来了第一位贡献者! 喜报!Fluent Editor 开源富文本迎来了第一位贡献者! 喜报!Fluent Editor 开源富文本迎来了第一位贡献者!
MySQL的索引原理及使用
B+树的最底层叶子节点包含了所有的索引项。从图上可以看到,B+树在查找数据的时候,由于数据都存放在最底层的叶子节点上,所以每次查找都需要检索到叶子节点才能查询到数据。所以在需要查询数据的情况下每次的磁盘的IO跟树高有直接的关系,但是从另一方面来说,由于数据都被放到了叶子节点,
MySQL的索引原理及使用 MySQL的索引原理及使用 MySQL的索引原理及使用
一个操作系统的设计与实现——第23章 快速系统调用
23.1 什么是快速系统调用 系统调用是操作系统为3特权级任务提供服务的一种手段。在32位操作系统中,我们通过中断实现了系统调用。由于系统调用是一个使用非常频繁的机制,且中断也不是专门为系统调用设计的,因此,64位CPU提供了系统调用的专用机制:快速系统调用。 快速系统调用由专用的syscall指令