取消HTTP请求时触发java.net.SocketException的原因与处理
HttpClient取消请求抛出SocketException问题解析
问题概述
使用httpClient.SendAsync()以流形式下载文件时,通过CancellationToken取消请求,抛出的是java.net.SocketException: Socket closed异常,而非预期的OperationCanceledException。未手动操作Socket,所有Socket相关逻辑由Android底层HttpClient实现处理。
异常产生原因
这是Xamarin.Android旧版本的特定行为:
- Xamarin.Android的
AndroidClientHandler底层依赖Java的HttpURLConnection实现HTTP请求。 - 当触发
CancellationToken取消请求时,底层Java实现会直接关闭Socket连接,抛出SocketException,而没有将其包装为.NET标准的OperationCanceledException。 - 堆栈跟踪中的提示
[monodroid] Not wrapping exception of type Java.Net.SocketException from method SendAsync. This will change in a future release明确说明,该行为会在后续版本中修复,未来会将Java异常包装为对应的.NET取消异常。
正确处理方式
- 区分取消导致的异常:在捕获异常时,同时检查
CancellationToken的IsCancellationRequested状态,或者判断异常消息是否包含Socket closed,将这类异常当作取消操作处理:catch (Java.Net.SocketException ex) { if (cancelToken.IsCancellationRequested || ex.Message.Contains("Socket closed")) { // 处理取消逻辑,比如提示用户下载已取消 System.Diagnostics.Debug.WriteLine("下载已取消"); } else { // 处理其他Socket错误,比如网络断开 System.Diagnostics.Debug.WriteLine($"网络错误: {ex.Message}"); } } catch (OperationCanceledException) { // 处理标准取消异常(新版本Xamarin会触发) System.Diagnostics.Debug.WriteLine("下载已取消"); } catch (Exception ex) { // 处理其他异常 System.Diagnostics.Debug.WriteLine($"未知错误: {ex.Message}"); } - 升级Xamarin版本:如果项目允许,升级到较新的Xamarin.Android版本,新版本已经修复了异常包装问题,取消请求时会抛出标准的
OperationCanceledException。 - 统一异常处理逻辑:将
SocketException(取消场景)和OperationCanceledException的处理逻辑合并,保证代码在新旧版本下都能正常工作。
是否可忽略该异常
不能直接忽略所有SocketException,但确认是取消操作导致的Socket closed异常可以忽略。需要注意区分:
- 取消操作导致的:属于预期行为,可按取消逻辑处理,无需报错。
- 其他原因导致的Socket异常(如网络中断、服务器断开):属于真实错误,需要处理或提示用户。
代码操作中的问题
给出的代码存在以下优化点:
- 避免频繁创建HttpClient:每次点击都
new HttpClient()会导致Socket资源耗尽,建议复用HttpClient实例(比如声明为类级别的静态变量):private static readonly HttpClient _httpClient = new HttpClient(); - 避免使用async void:按钮点击事件用
async void可能导致异常无法被正确捕获,建议改用ICommand绑定异步命令,或者保留async void但确保异常处理覆盖全面。 - CancellationTokenSource的生命周期管理:当前代码在取消后重置Cts是正确的,但需要确保请求正常完成后也重置Cts,避免下次请求使用已取消的令牌。
完整堆栈跟踪
[monodroid] Not wrapping exception of type Java.Net.SocketException from method `SendAsync`. This will change in a future release. [0:] at Java.Interop.JniEnvironment+InstanceMethods.CallIntMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x00068] in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.g.cs:11725 at Java.Interop.JniPeerMembers+JniInstanceMethods.InvokeVirtualInt32Method (System.String encodedMember, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x00024] in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:229 at Java.Net.HttpURLConnection.get_ResponseCode () [0x00000] in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/monoandroid10/android-31/mcw/Java.Net.HttpURLConnection.cs:511 at Xamarin.Android.Net.AndroidClientHandler+<>c__DisplayClass46_0.<DoProcessRequest>b__2 () [0x00000] in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs:438 at System.Threading.Tasks.Task`1[TResult].InnerInvoke () [0x0000f] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corert/src/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs:534 at System.Threading.Tasks.Task.Execute () [0x00000] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corert/src/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:2319 --- End of stack trace from previous location where exception was thrown --- at Xamarin.Android.Net.AndroidClientHandler.DoProcessRequest (System.Net.Http.HttpRequestMessage request, Java.Net.URL javaUrl, Java.Net.HttpURLConnection httpConnection, System.Threading.CancellationToken cancellationToken, Xamarin.Android.Net.AndroidClientHandler+RequestRedirectionState redirectState) [0x00328] in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs:438 at Xamarin.Android.Net.AndroidClientHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) [0x00286] in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs:287 at System.Net.Http.HttpClient.FinishSendAsyncBuffered (System.Threading.Tasks.Task`1[TResult] sendTask, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationTokenSource cts, System.Boolean disposeCts) [0x0017e] in /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/external/corefx/src/System.Net.Http/src/System/Net/Http/HttpClient.cs:506 at TestHttpClientSocketException.MainPage.startButton_Clicked (System.Object sender, System.EventArgs e) [0x00113] in C:\Projects\TestHttpClientSocketException\TestHttpClientSocketException\TestHttpClientSocketException\MainPage.xaml.cs:43 --- End of managed Java.Net.SocketException stack trace --- java.net.SocketException: Socket closed at java.net.SocketInputStream.read(SocketInputStream.java:203) at java.net.SocketInputStream.read(SocketInputStream.java:139) at com.android.okhttp.okio.Okio$2.read(Okio.java:141) at com.android.okhttp.okio.AsyncTimeout$2.read(AsyncTimeout.java:211) at com.android.okhttp.okio.RealBufferedSource.indexOf(RealBufferedSource.java:316) at com.android.okhttp.okio.RealBufferedSource.indexOf(RealBufferedSource.java:310) at com.android.okhttp.okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:206) at com.android.okhttp.internal.http.Http1xStream.readResponse(Http1xStream.java:395) at com.android.okhttp.internal.http.Http1xStream.readResponseHeaders(Http1xStream.java:144) at com.android.okhttp.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:887) at com.android.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:759) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:499) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:435) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:566)
XAML示例
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="TestHttpClientSocketException.MainPage"> <StackLayout> <Button x:Name="startButton" Text="Download" Clicked="startButton_Clicked" /> <Button x:Name="cancelButton" Text="Cancel" Clicked="cancelButton_Clicked" /> </StackLayout> </ContentPage>
后端代码示例
public partial class MainPage : ContentPage { CancellationTokenSource cancelToken; public MainPage() { InitializeComponent(); this.cancelToken = new CancellationTokenSource(); } private async void startButton_Clicked(object sender, EventArgs e) { HttpResponseMessage response; try { using (var httpClient = new HttpClient()) { HttpMethod method = HttpMethod.Post; string requestUri = "http://0.0.0.0:80/some/uri"; httpClient.Timeout = new TimeSpan(0, 0, 300); HttpContent httpContent = new StringContent("<soapenv:Envelope ..."); HttpRequestMessage req = new HttpRequestMessage(method, requestUri); req.Headers.Add("SOAPAction", "http://schemas.xmlsoap.org/soap/envelope"); req.Method = method; req.Content = httpContent; req.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("text/xml" + "; charset=utf-8"); response = await httpClient.SendAsync(req, this.cancelToken.Token).ConfigureAwait(false); System.Diagnostics.Debug.WriteLine("finished"); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.StackTrace); } } private void cancelButton_Clicked(object sender, EventArgs e) { this.cancelToken.Cancel(); this.cancelToken = new CancellationTokenSource(); } }
内容的提问来源于stack exchange,提问作者testing
相关产品推荐
相关产品推荐

