并行文件上传 XMLHttpRequest 请求以及为啥它们不起作用

Posted

技术标签:

【中文标题】并行文件上传 XMLHttpRequest 请求以及为啥它们不起作用【英文标题】:Parallel file upload XMLHttpRequest requests and why they won't work并行文件上传 XMLHttpRequest 请求以及为什么它们不起作用 【发布时间】:2018-12-01 13:16:20 【问题描述】:

我正在尝试使用 XMLHttpRequest 并行上传许多(目前为 3 个)文件。如果有一些代码可以从许多已删除文件的列表中提取它们,并确保我每时每刻都发送 3 个文件(如果可用)。

这是我的代码,据我所知是标准的:

            var xhr = item._xhr = new XMLHttpRequest();
            var form = new FormData();
            var that = this;

            angular.forEach(item.formData, function(obj) 
                angular.forEach(obj, function(value, key) 
                    form.append(key, value);
                );
            );

            form.append(item.alias, item._file, item.file.name);

            xhr.upload.onprogress = function(event) 
                // ...
            ;

            xhr.onload = function() 
                // ...
            ;

            xhr.onerror = function() 
                // ...
            ;

            xhr.onabort = function() 
                // ...
            ;

            xhr.open(item.method, item.url, true);

            xhr.withCredentials = item.withCredentials;

            angular.forEach(item.headers, function(value, name) 
                xhr.setRequestHeader(name, value);
            );

            xhr.send(form);

查看 Opera 开发者工具中的网络监视器,我发现这有点工作,并且我总是得到 3 个“正在进行中”的文件:

但是,如果我查看请求的进展方式,我会发现 3 个上传中的 2 个(这里是看似长时间运行的)处于“待处理”状态,并且 3 个请求中只有 1 个是真正的一次活跃。这也反映在上传时间中,因为这种并行性似乎没有发生时间改进。

我已经在我的代码中放置了控制台日志,看起来这不是我的代码的问题。

我应该知道的并行上传文件是否有任何浏览器限制?据我所知,AJAX 限制的请求数量比我在这里使用的要高得多......向请求中添加文件会改变事情吗?

【问题讨论】:

似乎是服务器端的错误。浏览器端没有问题。浏览器已经为 3 个请求同时请求握手。您的服务器一次可能只能处理 1 个请求。这就是握手长时间处于待处理状态的原因。您需要增加服务器能够处理的请求数。 @yeshashah 我的服务器是 ISS 并且没有设置限制。我的框架是 ASP.NET,在我的代码中没有任何地方可以锁定/阻止事物。 但是,在您发表评论并经过一番搜索后,我偶然发现了这一点,很可能是这种情况:***.com/a/9016375/2173353。如果这是真的,我将不得不想办法绕过这种行为...... 是的。许多服务器避免同时处理具有相同 sessionId 的请求,以避免写操作时出现竞争条件。 AWS S3 允许直接浏览器上传,其中文件被分成单独的块,每个块同时发送到服务器,然后服务器从 fileId 和 chunkId 以正确的顺序组装它们。你探索类似的东西。 @yeshashah 但是使用块有什么帮助呢?这最终不会有同样的问题吗?同样,这将是多个请求争夺同一个资源(在我的例子中是用户 Session 对象)。当然锁定时间会更小,但这些锁定时间的总和仍将与用于完整文件上传的时间相同(或多或少)。我很惊讶这会缩短上传时间。 我为 ASP 服务器找到的一个解决方案是禁用会话状态:***.com/a/4319204/2873331 Setting EnableSessionState="ReadOnly" will prevent that page from gaining an exclusive lock on the SessionState (but the page itself would have to wait for other non-ReadOnly requests by the user to finish before loading). 不确定它的效果如何。 【参考方案1】:

原来是 ASP.NET 导致了这个问题。 来自同一个 SessionId 的多个请求会被序列化,因为它们会锁定会话对象。

见here。

我的解决方法是将会话设置为只读此特定操作。这样,就不需要锁定。 这是我的代码(原代码取自here):

public class CustomControllerFactory : DefaultControllerFactory

    protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
    
        if (controllerType == null)
        
            return SessionStateBehavior.Default;
        

        var actionName = requestContext.RouteData.Values["action"].ToString();
        MethodInfo actionMethodInfo;
        var methods = controllerType.GetMethods(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        actionMethodInfo = methods.FirstOrDefault(x => x.Name == actionName && x.GetCustomAttribute<ActionSessionStateAttribute>() != null);
        if (actionMethodInfo != null)
        
            var actionSessionStateAttr = actionMethodInfo.GetCustomAttributes(typeof(ActionSessionStateAttribute), false)
                .OfType<ActionSessionStateAttribute>()
                .FirstOrDefault();

            if (actionSessionStateAttr != null)
            
                return actionSessionStateAttr.Behavior;
            
        
        return base.GetControllerSessionBehavior(requestContext, controllerType);
    



[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ActionSessionStateAttribute : Attribute

    public SessionStateBehavior Behavior  get; private set; 
    public ActionSessionStateAttribute(SessionStateBehavior behavior)
    
        this.Behavior = behavior;
    


// In your Global.asax.cs
protected void Application_Start(object sender, EventArgs e)

    // .........
    ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));



// You use it on the controller action like that:
[HttpPost]
[Authorize(Roles = "Administrators")]
[ValidateAntiForgeryToken]
[ActionSessionState(SessionStateBehavior.ReadOnly)]
public async Task<ActionResult> AngularUpload(HttpPostedFileBase file)

这是辉煌的结果:

【讨论】:

你检查了这两种机制所花费的总时间吗? @PrakharLondhe 是的,并行执行请求确实减少了总时间。但这取决于您的服务器代码。例如。如果它包含一些其他锁定代码(除了会话锁本身),如果它处理并行请求足够好,如果没有其他瓶颈,如数据库调用等。 你得到的加速是多少?问题是由于上传速度受到限制,与一个请求相比,即使有多个请求仍然会结合到相同的总速度.. 同样在你的耗时字段中,如果你之前看到你的每个请求花费了 500 毫秒,而在下一个 ss 中每个请求花费了将近 5 秒 @PrakharLondhe 在我的情况下,大多数时候上传速度并不是瓶颈,而是我们没有并行处理文件的事实,因为我们无法并行发送文件(我们正在做一些后处理阻止请求,直到文件被处理 - 所以并不是所有的时间都花在实际上传上)。我现在不记得速度提升了,因为已经过去了几年,但理论上你可以实现相当大的速度倍增,因为我们的处理非常繁重。【参考方案2】:

HTTP/1.1 RFC

HTTP/1.1 RFC 的第 8.1.4 节说“单用户客户端不应与任何服务器或代理保持超过 2 个连接。

在此处阅读更多信息:Roundup on Parallel Connections

【讨论】:

下一句是:“这里的关键是“应该”这个词。 Web 客户端不必遵循此准则。”他们没有。并行连接是一种非常简洁的技术,用于在许多地方进行优化,例如 AWS S2 浏览器上传,通过将文件块同时发送到服务器并将其组装在服务器上以加快上传速度。与服务器或代理建立超过 1 个连接没有任何问题。

以上是关于并行文件上传 XMLHttpRequest 请求以及为啥它们不起作用的主要内容,如果未能解决你的问题,请参考以下文章

中止多文件上传 AJAX 请求

15. 利用ajax jquery 上传文件

通过FormData对象可以组装一组用 [XMLHttpRequest]发送请求的键/值对,它可以更灵活方便的发送表单数据。

通过jQuery Ajax使用FormData对象上传文件

通过jQuery Ajax使用FormData对象上传文件

如何从 XMLHttpRequest 获取进度