StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中“出错”的情况下利用一个错误处理器来接收针对当前请求的处理。它们之间的差异在于对“错误”的认定上:ExceptionHandlerMiddleware中间件所谓的错误就是抛出异常;StatusCodePagesMiddleware中间件则将400~599的响应状态码视为错误。更多关于ASP.NET Core的文章请点这里]

目录

一、StatusCodePagesMiddleware

二、阻止处理异常

三、UseStatusCodePages

四、UseStatusCodePagesWithRedirects

五、UseStatusCodePagesWithReExecute

一、StatusCodePagesMiddleware

如下面的代码片段所示,StatusCodePagesMiddleware中间件也采用“标准”的定义方式,针对它的配置选项通过一个对应的对象以Options模式的形式提供给它。

public class StatusCodePagesMiddleware
{
public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options);
public Task Invoke(HttpContext context);
}

除了对错误的认定方式,StatusCodePagesMiddleware中间件和ExceptionHandlerMiddleware中间件对错误处理器的表达也不相同。ExceptionHandlerMiddleware中间件的处理器是一个RequestDelegate委托对象,而StatusCodePagesMiddleware中间件的处理器则是一个Func<StatusCodeContext, Task>委托对象。如下面的代码片段所示,配置选项StatusCodePagesOptions的唯一目的就是提供作为处理器的Func<StatusCodeContext, Task>对象。

public class StatusCodePagesOptions
{
public Func<StatusCodeContext, Task> HandleAsync { get; set; }
}

一个RequestDelegate对象相当于一个Func<HttpContext, Task>类型的委托对象,而一个StatusCodeContext对象也是对一个HttpContext上下文的封装,这两个委托对象并没有本质上的不同。如下面的代码片段所示,除了从StatusCodeContext对象中获取当前HttpContext上下文,我们还可以通过其Next属性得到一个RequestDelegate对象,并利用它将请求再次分发给后续中间件进行处理。StatusCodeContext对象的Options属性返回创建 StatusCodePagesMiddleware中间件时指定的StatusCodePagesOptions对象。

public class StatusCodeContext
{
public HttpContext HttpContext { get; }
public RequestDelegate Next { get; }
public StatusCodePagesOptions Options { get; } public StatusCodeContext(HttpContext context, StatusCodePagesOptions options, RequestDelegate next);
}

由于采用了针对响应状态码的错误处理策略,所以实现在StatusCodePagesMiddleware中间件的错误处理操作只会发生在当前响应状态码为400~599的情况下,如下所示的代码片段就体现了这一点。从下面给出的代码片段可以看出,StatusCodePagesMiddleware中间件除了会查看当前响应状态码,还会查看响应内容及媒体类型。如果响应报文已经包含响应内容或者设置了媒体类型,StatusCodePagesMiddleware中间件将不会执行任何操作,因为这正是后续中间件管道希望回复给客户端的响应,该中间件不应该再画蛇添足。

public class StatusCodePagesMiddleware
{
private RequestDelegate _next;
private StatusCodePagesOptions _options; public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options)
{
_next = next;
_options = options.Value;
} public async Task Invoke(HttpContext context)
{
await _next(context);
var response = context.Response;
if ((response.StatusCode >= 400 && response.StatusCode <= 599) && !response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType))
{
await _options.HandleAsync(new StatusCodeContext(context, _options, _next));
}
}
}

StatusCodePagesMiddleware中间件对错误的处理非常简单,它只需要从StatusCodePagesOptions对象中提取出作为错误处理器的Func<StatusCodeContext, Task>对象,然后创建一个StatusCodeContext对象作为输入参数调用这个委托对象即可。

二、阻止处理异常

通过《呈现错误信息》的内容我们知道,如果某些内容已经被写入响应的主体部分,或者响应的媒体类型已经被预先设置,StatusCodePagesMiddleware中间件就不会再执行任何错误处理操作。由于应用程序往往具有自身的异常处理策略,它们可能会显式地返回一个状态码为400~599的响应,在此情况下,StatusCodePagesMiddleware中间件是不应该对当前响应做任何干预的。从这个意义上来讲,StatusCodePagesMiddleware中间件仅仅是作为一种后备的错误处理机制而已。

更进一步来讲,如果后续的某个中间件返回了一个状态码为400~599的响应,并且这个响应只有报头集合没有主体(媒体类型自然也不会设置),那么按照我们在上面给出的错误处理逻辑来看,StatusCodePagesMiddleware中间件还是会按照自己的策略来处理并响应请求。为了解决这种情况,我们必须赋予后续中间件能够阻止StatusCodePagesMiddleware中间件进行错误处理的功能。

阻止StatusCodePagesMiddleware中间件进行错误处理的功能是借助一个通过IStatusCodePagesFeature接口表示的特性来实现的。如下面的代码片段所示,IStatusCodePagesFeature接口定义了唯一的Enabled属性,StatusCodePagesFeature类型是对该接口的默认实现,它的Enabled属性默认返回True。

public interface IStatusCodePagesFeature
{
bool Enabled { get; set; }
} public class StatusCodePagesFeature : IStatusCodePagesFeature
{
public bool Enabled { get; set; } = true ;
}

StatusCodePagesMiddleware中间件在将请求交付给后续管道之前,会创建一个StatusCodePagesFeature对象,并将其添加到当前HttpContext上下文的特性集合中。在最终决定是否执行错误处理操作的时候,它还会通过这个特性检验后续的某个中间件是否不希望其进行不必要的错误处理,如下所示的代码片段很好地体现了这一点。

public class StatusCodePagesMiddleware
{
...
public async Task Invoke(HttpContext context)
{
var feature = new StatusCodePagesFeature();
context.Features.Set<IStatusCodePagesFeature>(feature); await _next(context);
var response = context.Response;
if ((response.StatusCode >= 400 && response.StatusCode <= 599) && !response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType) && feature.Enabled)
{
await _options.HandleAsync(new StatusCodeContext(context, _options, _next));
}
}
}

下面通过一个简单的实例来演示如何利用StatusCodePagesFeature特性来屏蔽StatusCodePagesMiddleware中间件。在如下所示的代码片段中,我们将针对请求的处理定义在ProcessAsync方法中,该方法会返回一个状态码为“401 Unauthorized”的响应。我们通过随机数让这个方法在50%的概率下利用StatusCodePagesFeature特性来阻止StatusCodePagesMiddleware中间件自身对错误的处理。我们通过调用UseStatusCodePages扩展方法注册的StatusCodePagesMiddleware中间件会直接响应一个内容为“Error occurred!”的字符串。

public class Program
{
private static readonly Random _random = new Random();
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder.Configure(app => app
.UseStatusCodePages(HandleAsync)
.Run(ProcessAsync)))
.Build()
.Run(); static Task HandleAsync(StatusCodeContext context) => context.HttpContext.Response.WriteAsync("Error occurred!"); static Task ProcessAsync(HttpContext context)
{
context.Response.StatusCode = 401;
if (_random.Next() % 2 == 0)
{
context.Features.Get<IStatusCodePagesFeature>().Enabled = false;
}
return Task.CompletedTask;
} }
}

对于针对该应用的请求来说,我们会得到如下两种不同的响应。没有主体内容的响应是通过ProcessAsync方法产生的,这种情况发生在StatusCodePagesMiddleware中间件通过StatusCodePagesFeature特性被屏蔽的时候。有主体内容的响应则是ProcessAsync方法和StatusCodePagesMiddleware中间件共同作用的结果。

HTTP/1.1 401 Unauthorized
Date: Sat, 21 Sep 2019 13:37:31 GMT
Server: Kestrel
Content-Length: 15 Error occurred!
HTTP/1.1 401 Unauthorized
Date: Sat, 21 Sep 2019 13:37:36 GMT
Server: Kestrel
Content-Length: 0

我们在大部分情况下都会调用IApplicationBuilder接口相应的扩展方法来注册StatusCodePagesMiddleware中间件。对于StatusCodePagesMiddleware中间件的注册来说,除了UseStatusCodePages方法,还有其他方法可供选择。

三、UseStatusCodePages

我们可以调用如下所示的3个UseStatusCodePages扩展方法重载来注册StatusCodePagesMiddleware中间件。不论调用哪个重载,系统最终都会根据提供的StatusCodePagesOptions对象调用构造函数来创建这个中间件,而且StatusCodePagesOptions必须具有一个作为错误处理器的Func<StatusCodeContext, Task>对象。

public static class StatusCodePagesExtensions
{
public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app)
=> app.UseMiddleware<StatusCodePagesMiddleware>(); public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options)
=> app.UseMiddleware<StatusCodePagesMiddleware>(Options.Create(options)); public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler)
=> app.UseStatusCodePages(new StatusCodePagesOptions
{
HandleAsync = handler
});
}

由于StatusCodePagesMiddleware中间件最终的目的还是将定制的错误信息响应给客户端,所以可以在注册该中间件时直接指定响应的内容和媒体类型,这样的注册方式可以通过调用如下所示的UseStatusCodePages方法来完成。从如下所示的代码片段可以看出,通过参数bodyFormat指定的实际上是一个模板,它可以包含一个表示响应状态码的占位符({0})。

public static class StatusCodePagesExtensions
{
public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat)
{
return app.UseStatusCodePages(context =>
{
var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode);
context.HttpContext.Response.ContentType = contentType;
return context.HttpContext.Response.WriteAsync(body);
});
}
}

四、UseStatusCodePagesWithRedirects

如果调用UseStatusCodePagesWithRedirects扩展方法,就可以使注册的StatusCodePagesMiddleware中间件向指定的路径发送一个客户端重定向。从如下所示的代码片段可以看出,参数locationFormat指定的重定向地址也是一个模板,它可以包含一个表示响应状态码的占位符({0})。我们可以指定一个完整的地址,也可以指定一个相对于PathBase的相对路径,后者需要包含表示基地址的前缀“~/”。

public static class StatusCodePagesExtensions
{
public static IApplicationBuilder UseStatusCodePagesWithRedirects(this IApplicationBuilder app, string locationFormat)
{
if (locationFormat.StartsWith("~"))
{
locationFormat = locationFormat.Substring(1);
return app.UseStatusCodePages(context =>
{
var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location);
return Task.CompletedTask;
});
}
else
{
return app.UseStatusCodePages(context =>
{
var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
context.HttpContext.Response.Redirect(location);
return Task.CompletedTask;
});
}
}
}

下面通过一个简单的应用来演示针对客户端重定向的错误页面呈现方式。我们在如下所示的应用中注册了一个路由模板为“error/{statuscode}”的路由,路由参数statuscode代表响应的状态码。在作为路由处理器的HandleAsync方法中,我们会直接响应一个包含状态码的字符串。我们调用UseStatusCodePagesWithRedirects方法注册StatusCodePagesMiddleware中间件时将重定义路径设置为“error/{0}”。

public class Program
{
private static readonly Random _random = new Random();
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseStatusCodePagesWithRedirects("~/error/{0}")
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGet("error/{statuscode}", HandleAsync))
.Run(ProcessAsync)))
.Build()
.Run(); static async Task HandleAsync(HttpContext context)
{
var statusCode = context.GetRouteData().Values["statuscode"];
await context.Response.WriteAsync($"Error occurred ({statusCode})");
} static Task ProcessAsync(HttpContext context)
{
context.Response.StatusCode = _random.Next(400, 599);
return Task.CompletedTask;
}
}
}

针对该应用的请求总是得到一个状态码为400~599的响应,StatusCodePagesMiddleware中间件在此情况下会向指定的路径(“~/error/{statuscode}”)发送一个客户端重定向。由于重定向请求的路径与注册的路由相匹配,所以作为路由处理器的HandleError方法会响应下图16-11所示的错误页面。

五、UseStatusCodePagesWithReExecute

除了可以采用客户端重定向的方式来呈现错误页面,还可以调用UseStatusCodePagesWithReExecute方法注册StatusCodePagesMiddleware中间件,并让它采用服务端重定向的方式来处理错误请求。如下面的代码片段所示,当我们调用这个方法的时候不仅可以指定重定向的路径,还可以指定查询字符串。这里作为重定向地址的参数pathFormat依旧是一个路径模板,它可以包含一个表示响应状态码的占位符({0})。

public static class StatusCodePagesExtensions
{
public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, string pathFormat, string queryFormat = null);
}

现在我们对前面演示的这个实例略做修改来演示采用服务端重定向呈现的错误页面。如下面的代码片段所示,我们将针对UseStatusCodePagesWithRedirects方法的调用替换成针对UseStatusCodePagesWithReExecute方法的调用。

public class Program
{
private static readonly Random _random = new Random();
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseStatusCodePagesWithReExecute("/error/{0}")
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGet("error/{statuscode}", HandleAsync))
.Run(ProcessAsync)))
.Build()
.Run(); static async Task HandleAsync(HttpContext context)
{
var statusCode = context.GetRouteData().Values["statuscode"];
await context.Response.WriteAsync($"Error occurred ({statusCode})");
} static Task ProcessAsync(HttpContext context)
{
context.Response.StatusCode = _random.Next(400, 599);
return Task.CompletedTask;
}
}
}

对于前面演示的实例,由于错误页面是通过客户端重定向的方式呈现的,所以浏览器地址栏显示的是重定向地址。我们在选择这个实例时采用了服务端重定向,虽然显示的页面内容并没有不同,但是地址栏上的地址是不会发生改变的,如图16-12所示。(S1615)

之所以命名为UseStatusCodePagesWithReExecute,是因为通过这个方法注册的StatusCodePagesMiddleware中间件进行错误处理时,它仅仅将提供的重定向路径和查询字符串应用到当前HttpContext上下文,然后分发给后续管道重新执行。UseStatusCodePagesWithReExecute方法中注册StatusCodePagesMiddleware中间件的实现总体上可以由如下所示的代码片段来体现。

public static class StatusCodePagesExtensions
{
public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, string pathFormat, string queryFormat = null)
{
return app.UseStatusCodePages(async context =>
{
var newPath = new PathString(string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
var formatedQueryString = queryFormat == null ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode); context.HttpContext.Request.Path = newPath;
context.HttpContext.Request.QueryString = newQueryString;
await context.Next(context.HttpContext);
});
}
}

与ExceptionHandlerMiddleware中间件类似,StatusCodePagesMiddleware中间件在处理请求的过程中会改变当前请求上下文的状态,具体体现在它会将指定的请求路径和查询字符串重新应用到当前请求上下文中。为了不影响前置中间件对请求的正常处理,StatusCodePagesMiddleware中间件在完成自身处理流程之后必须将当前请求上下文恢复到原始状态。StatusCodePagesMiddleware中间件依旧采用一个特性来保存原始路径和查询字符串。这个特性对应的接口是具有如下定义的IStatusCodeReExecuteFeature,但是该接口仅仅包含两个针对路径的属性,并没有用于携带原始请求上下文的属性,但是默认实现类型StatusCodeReExecuteFeature包含了这个属性。

public interface IStatusCodeReExecuteFeature
{
string OriginalPath { get; set; }
string OriginalPathBase { get; set; }
} public class StatusCodeReExecuteFeature : IStatusCodeReExecuteFeature
{
public string OriginalPath { get; set; }
public string OriginalPathBase { get; set; }
public string OriginalQueryString { get; set; }
}

在StatusCodePagesMiddleware中间件处理异常请求的过程中,在将指定的重定向路径和查询字符串应用到当前请求上下文之前,它会根据原始的上下文创建一个StatusCodeReExecuteFeature特性对象,并将其添加到当前HttpContext上下文的特性集合中。当整个请求处理过程结束之后,StatusCodePagesMiddleware中间件还会将这个特性从当前HttpContext上下文中移除,并恢复原始的请求路径和查询字符串。如下所示的代码片段体现了UseStatusCodePagesWithReExecute方法的实现逻辑。

public static class StatusCodePagesExtensions
{
public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app,string pathFormat, string queryFormat = null)
{
return app.UseStatusCodePages(async context =>
{
var newPath = new PathString( string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
var formatedQueryString = queryFormat == null ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString); var originalPath = context.HttpContext.Request.Path;
var originalQueryString = context.HttpContext.Request.QueryString; context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
{
OriginalPathBase = context.HttpContext.Request.PathBase.Value,
OriginalPath = originalPath.Value,
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
}); context.HttpContext.Request.Path = newPath;
context.HttpContext.Request.QueryString = newQueryString;
try
{
await context.Next(context.HttpContext);
}
finally
{
context.HttpContext.Request.QueryString = originalQueryString;
context.HttpContext.Request.Path = originalPath;
context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(null);
}
});
}
}

ASP.NET Core错误处理中间件[1]: 呈现错误信息
ASP.NET Core错误处理中间件[2]: 开发者异常页面
ASP.NET Core错误处理中间件[3]: 异常处理器
ASP.NET Core错误处理中间件[4]: 响应状态码页面

ASP.NET Core错误处理中间件[4]: 响应状态码页面的更多相关文章

  1. ASP.NET Core错误处理中间件[1]: 呈现错误信息

    NuGet包"Microsoft.AspNetCore.Diagnostics"中提供了几个与异常处理相关的中间件.当ASP.NET Core应用在处理请求过程中出现错误时,我们可 ...

  2. ASP.NET Core错误处理中间件[2]: 开发者异常页面

    <呈现错误信息>通过几个简单的实例演示了如何呈现一个错误页面,该过程由3个对应的中间件来完成.下面先介绍用来呈现开发者异常页面的DeveloperExceptionPageMiddlewa ...

  3. ASP.NET Core错误处理中间件[3]: 异常处理器

    DeveloperExceptionPageMiddleware中间件错误页面可以呈现抛出的异常和当前请求上下文的详细信息,以辅助开发人员更好地进行纠错诊断工作.ExceptionHandlerMid ...

  4. ASP.NET Core URL Rewrite中间件

    URL重写是基于一个或多个预置规则修改请求URL的行为.URL重写在资源位置和访问地址之间创建了一种抽象,这样二者之间就减少了紧密的联系.URL重写有多种适用的场景: 临时或永久移动或替换服务器资源, ...

  5. ASP.NET Core 中的中间件

    前言   由于是第一次写博客,如果您看到此文章,希望大家抱着找错误.批判的心态来看. sky! 何为中间件? 在 ASP.NET Framework 中应该都知道请求管道.可参考:浅谈 ASP.NET ...

  6. ASP.NET Core 3.1 中间件

    参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 ...

  7. 翻译 - ASP.NET Core 基本知识 - 中间件(Middleware)

    翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0 中间件是集成 ...

  8. 理解ASP.NET Core - 错误处理(Handle Errors)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...

  9. asp.net core 自定义异常处理中间件

    asp.net core 自定义异常处理中间件 Intro 在 asp.net core 中全局异常处理,有时候可能不能满足我们的需要,可能就需要自己自定义一个中间件处理了,最近遇到一个问题,有一些异 ...

随机推荐

  1. 阿里不允许使用 Executors 创建线程池!那怎么使用,怎么监控?

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 五常大米好吃! 哈哈哈,是不你总买五常大米,其实五常和榆树是挨着的,榆树大米也好吃, ...

  2. 【软件测试 Python自动化】全网最全大厂面试题,看完以后你就是面试官!

    前言 为了让大家更好的理解和学习投入到Python自动化来找到一份好的资料也是学习过程中,非常重要的一个点.你的检索能力越强,你就会越容易找到最合适你的资料. 有需要的小伙伴可以复制群号 313782 ...

  3. 第四章 Sentinel--服务容错

    我们接着承接上篇继续讲下去 : 第三章 Nacos Discovery–服务治理,开始第四篇的学习 第四章 Sentinel–服务容错 4.1 高并发带来的问题 在微服务架构中,我们将业务拆分成一个个 ...

  4. SpringBoot从入门到精通教程(五)

    上节,我们讲了 SpringBoot 如何使用MyBatis 今天我们讲讲 Springboot Logo自定义的问题, 我们在启动 SpringBoot 时,控制台会打印 SpringBoot Lo ...

  5. .net下com调用支持x86/x64

    起因 项目涉及u3d/wpf端的渲染图形合成,采用了开源项目spout,为了便捷,采用了spout的com版本作为c#端的调用 项目调整后,细节已经捋清楚了. 但是考虑桌面应用采用anypc,根据运行 ...

  6. matplotlib学习日记(十)-划分画布的主要函数

    (1)函数subplot()绘制网格区域中的几何形状相同的子区布局 import matplotlib.pyplot as plt import numpy as np '''函数subplot的介绍 ...

  7. MacOS下解决宿主机和docker容器之间网络互通

    docker在Mac下使用非常方便,官网提供了desktop版本的可视化软件,甚至还十分贴心地集成好了k8s套件.然而如果想同时部署和使用多个容器,每个容器不使用127.0.0.1地址,而是各自有ip ...

  8. C#自定义TemplateImage使用模板底图,运行时根据用户或产品信息生成海报图(1)

    由于经常需要基于固定的一个模板底图,生成微信小程序分享用的海报图,如果每次都调用绘图函数,手动编写每个placeholder的填充,重复而且容易出错,因此,封装一个TemplateImage,用于填充 ...

  9. 关于django的坑(一)

    关于django orm 的坑: 关于设置数据库表自动更新 django的orm关于更新数据库的方法有update和save两种方法.想要表中自动更新需要一下几个条件: 使用 DateTimeFiel ...

  10. 本地缓存高性能之王Caffeine

    前言 随着互联网的高速发展,市面上也出现了越来越多的网站和app.我们判断一个软件是否好用,用户体验就是一个重要的衡量标准.比如说我们经常用的微信,打开一个页面要十几秒,发个语音要几分钟对方才能收到. ...