首页 > 解决方案 > Xamarin iOS WebException:应用程序在 HttpWebRequest 完成后崩溃

问题描述

问题

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

乍一看,一切正常。iOS 应用程序从我们的库中调用一个异步方法,从服务器接收数据并在表格视图中正确显示它。但是由于某种原因,一旦完成连接,它就不会终止连接,导致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 中,并且每隔一段时间,应用程序在加载数据后不会崩溃。只有当我们开始滚动结果时它才会崩溃。这对我们来说没有意义,因为所有网络流现在都应该关闭,对吧?(毕竟我们使用using了 all 的语句ResponseStreams。因此,当从等待的 Task :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

任何帮助将不胜感激。

标签: c#asynchronousxamarin.ioshttpwebrequesthttpwebresponse

解决方案


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

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


推荐阅读