Xamarin iOS WebException:应用程序在 HttpWebRequest 完成后崩溃

Xamarin iOS WebException: App crashes after HttpWebRequest is complete

问题

所以我们正在创建一个 Xamarin iOS 应用程序,它将连接到 ASP .NET 上的 REST API 运行。为了执行 HttpRequests,它使用我们在 iOS 应用程序和 android 应用程序之间共享的库。

乍一看一切正常。 iOS 应用从我们的库调用异步方法,从服务器接收数据并在 table 视图中正确显示。但是由于某种原因,它不会在连接完成后终止连接,导致一段时间后出现 WebException(从执行请求后立即随机到几分钟后)说 Error getting response stream (ReadDoneAsync2): ReceiveFailure。有趣的是,当我们从控制台应用程序调用库中完全相同的方法时,一切正常。

图书馆代码

这是我们库中的相关代码(我们不知道错误发生在哪里,因为主 UI 线程崩溃了。调试也没有发现任何可疑的东西。因此我将包括所有下面的代码,我们将在 API 调用期间在客户端上执行):

俱乐部供应商:

public class ClubProvider : BaseProvider
{
    /// <summary>
    /// Creates a new <see cref="ClubProvider"/> using the specified <paramref name="uri"/>.
    /// </summary>
    internal ClubProvider(string uri)
    {
        if (!uri.EndsWith("/"))
        {
            uri += "/";
        }
        Uri = uri + "clubs";
    }

    public async Task<ClubListResponse> GetClubListAsync()
    {
        return await ReceiveServiceResponseAsync<ClubListResponse>(Uri);
    }
}

BaseProvider(实际执行HttpWebRequest的地方)

public abstract class BaseProvider
{
    public virtual string Uri { get; private protected set; }

    /// <summary>
    /// Reads the response stream of the <paramref name="webResponse"/> as a UTF-8 string.
    /// </summary>
    private async Task<string> ReadResponseStreamAsync(HttpWebResponse webResponse)
    {
        const int bufferSize = 4096;
        using Stream responseStream = webResponse.GetResponseStream();
        StringBuilder builder = new StringBuilder();
        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = await responseStream.ReadAsync(buffer, 0, bufferSize)) != 0)
        {
            builder.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
        }
        return builder.ToString();
    }

    /// <summary>
    /// Performs the <paramref name="webRequest"/> with the provided <paramref name="payload"/> and returns the resulting <see cref="HttpWebResponse"/> object.
    /// </summary>
    private async Task<HttpWebResponse> GetResponseAsync<T>(HttpWebRequest webRequest, T payload)
    {
        webRequest.Host = "ClubmappClient";
        if (payload != null)
        {
            webRequest.ContentType = "application/json";
            string jsonPayload = JsonConvert.SerializeObject(payload);
            ReadOnlyMemory<byte> payloadBuffer = Encoding.UTF8.GetBytes(jsonPayload);
            webRequest.ContentLength = payloadBuffer.Length;
            using Stream requestStream = webRequest.GetRequestStream();
            await requestStream.WriteAsync(payloadBuffer);
        }
        else
        {
            webRequest.ContentLength = 0;
        }
        HttpWebResponse webResponse;
        try
        {
            webResponse = (HttpWebResponse)await webRequest.GetResponseAsync();
        }
        catch (WebException e)
        {
            /* Error handling
            ....
            */
        }
        return webResponse;
    }

    /// <summary>
    /// Performs the <paramref name="webRequest"/> with the provided <paramref name="payload"/> and returns the body of the resulting <see cref="HttpWebResponse"/>.
    /// </summary>
    private async Task<string> GetJsonResponseAsync<T>(HttpWebRequest webRequest, T payload)
    {
        using HttpWebResponse webResponse = await GetResponseAsync(webRequest, payload);
        return await ReadResponseStreamAsync(webResponse);
    }

    /// <summary>
    /// Performs an <see cref="HttpWebRequest"/> with the provided <paramref name="payload"/> to the specified endpoint at the <paramref name="uri"/>. The resulting <see cref="IResponse"/> will be deserialized and returned as <typeparamref name="TResult"/>.
    /// </summary>
    private protected async Task<TResult> ReceiveServiceResponseAsync<TResult, T>(string uri, T payload) where TResult : IResponse
    {
        HttpWebRequest webRequest = WebRequest.CreateHttp(uri);
        webRequest.Method = "POST";
        string json = await GetJsonResponseAsync(webRequest, payload);
        TResult result = JsonConvert.DeserializeObject<TResult>(json);
        return result;
    }

    /// <summary>
    /// Performs an <see cref="HttpWebRequest"/> to the specified endpoint at the <paramref name="uri"/>. The resulting <see cref="IResponse"/> will be deserialized and returned as <typeparamref name="TResult"/>.
    /// </summary>
    private protected async Task<TResult> ReceiveServiceResponseAsync<TResult>(string uri) where TResult : IResponse
    {
        return await ReceiveServiceResponseAsync<TResult, object>(uri, null);
    }
}

来自iOS客户的电话

public async override void ViewDidLoad()
{
    // ...
    ServiceProviderFactory serviceProviderFactory = new ServiceProviderFactory("https://10.0.0.40:5004/api");
    ClubProvider clubProvider = serviceProviderFactory.GetClubProvider();
    ClubListResponse clubListResponse = await clubProvider.GetClubListAsync();
    var clublist = new List<ClubProfileListData>();
    foreach (var entry in clubListResponse.ClubListEntries)
    {
        clublist.Add(new ClubProfileListData(entry.Name, entry.City + "," + entry.Country, entry.MusicGenres.Aggregate(new StringBuilder(), (builder, current) => builder.Append(current).Append(",")).ToString()));
    }
    // ...
}

起初这似乎工作得很好,但后来崩溃并显示以下错误消息:

使用 Wireshark 查看执行的请求表明连接从未终止:

连接保持打开状态,直到应用程序崩溃并且我们关闭模拟器。 有趣的是,该应用程序并不总是立即崩溃。我们将接收到的数据加载到 TableView 中,每隔一段时间应用程序不会在加载数据后崩溃。它只会在我们开始滚动结果时崩溃。这对我们来说没有意义,因为现在应该关闭所有网络流,对吧? (毕竟我们对所有 ResponseStreams 使用 using 语句。因此,当从等待的任务返回时,所有流都应自动处理:C)就好像它会根据需要尝试流式传输数据。

使用控制台应用程序测试库代码

现在很明显的原因可能是我们忘记关闭库中的一些流,但是以下代码成功且没有任何错误:

class Program
{
    static void Main(string[] args)
    {
        new Thread(Test).Start();
        while (true)
        {
            Thread.Sleep(1000);
        }
    }

    public static async void Test()
    {
        ServiceProviderFactory serviceProviderFactory = new ServiceProviderFactory("https://10.0.0.40:5004/api");
        ClubProvider clubProvider = serviceProviderFactory.GetClubProvider();
        ClubListResponse clubListResponse = await clubProvider.GetClubListAsync();
        foreach (ClubListResponseEntry entry in clubListResponse.ClubListEntries)
        {
            Console.WriteLine(entry.Name);
        }
        Console.WriteLine("WebRequest complete!");
        Console.ReadLine();
    }
}

查看捕获的数据包,我们发现连接在请求完成后按预期关闭:

问题

这是为什么呢?为什么我们的库代码在 .NET Core 控制台应用程序中按预期工作,但在 iOS 应用程序调用时无法断开连接?我们怀疑这可能是由于 async/await 调用 ()。但是我们确实遇到了一个异常,所以我们不确定这是否真的是上面链接的问题中描述的同一个错误。现在,在我们重写所有库代码之前,我们想消除导致这种奇怪行为的所有其他可能原因。此外,我们还成功地使用 async 调用来加载一些图像而不会导致应用程序崩溃,所以我们实际上只是在猜测这一点:C

如有任何帮助,我们将不胜感激。

好吧,进一步的测试表明应用程序崩溃实际上 与我们的 API 调用 没有任何关系。问题实际上是我们按需异步加载要显示在数据集旁边的图像。事实证明,在测试图像加载期间,我们的 ListView 中只有大约 20 个条目。这很好,因为 iOS 可以一次加载所有图像。然而,当从我们的 API 加载 1500 多个数据集时,ListView 开始缓冲,仅根据需要加载图像,这就是应用程序崩溃的时候。可能是因为原始图像流不再可用或类似原因。

还有一个有趣的旁注:iOS 确实实际上关闭了与服务器的网络连接,但只有在 100 秒超时后才发送 Windows .NET Core 控制台应用程序立即关闭它。我们从来没有等这么久。好吧 :D