Flutter HTTP 请求完整详解

#Flutter HTTP 请求完整详解


1. 前言

Flutter 网络请求分为两种方案:

  1. dart:io HttpClient:Dart 原生内置,无需第三方包,API 繁琐,适合简单临时请求;
  2. dio:第三方成熟网络库,工业级项目标准,支持拦截器、超时、取消、文件上传下载、并发、表单,开发首选。

库对比

方案 优点 缺点 适用场景
HttpClient 无依赖,无需引入包 API底层,代码量大,无拦截器,不支持便捷表单/上传 简单测试、极简工具
dio API简洁、拦截器、统一配置、上传下载、取消请求 需要引入依赖 企业正式项目

2. Dart 原生 HttpClient

无需 pub 依赖,仅导入 dart:iodart:convert

2.1 GET 请求示例

dart 复制代码
import 'dart:io';
import 'dart:convert';

void nativeGet() async {
  // 创建客户端
  HttpClient client = HttpClient();
  try {
    // 打开url
    HttpClientRequest req = await client.getUrl(Uri.parse("https://httpbin.org/get?name=test"));
    // 发送请求
    HttpClientResponse res = await req.close();
    // 读取响应流
    String responseStr = await res.transform(utf8.decoder).join();
    Map data = jsonDecode(responseStr);
    print("返回数据:$data");
  } catch (e) {
    print("请求异常:$e");
  } finally {
    client.close();
  }
}

2.2 POST JSON 请求

dart 复制代码
void nativePost() async {
  HttpClient client = HttpClient();
  Map params = {"username":"admin","pwd":"123456"};
  try {
    HttpClientRequest req = await client.postUrl(Uri.parse("https://httpbin.org/post"));
    // 设置请求头json
    req.headers.set("Content-Type", "application/json");
    // 写入json字符串
    req.write(jsonEncode(params));
    HttpClientResponse res = await req.close();
    String str = await res.transform(utf8.decoder).join();
    print(jsonDecode(str));
  } finally {
    client.close();
  }
}

原生缺点:无统一拦截、无全局 baseUrl、文件上传代码复杂、无便捷取消请求,项目统一封装成本极高。

3. dio 基础配置

3.1 pubspec.yaml 引入依赖

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.0

执行 flutter pub get 安装依赖

3.2 基础全局 Dio 实例创建

dart 复制代码
import 'package:dio/dio.dart' as dio;

// 全局单例dio
d.Dio createDio() {
  final dio.Dio dio = dio.Dio();
  // 全局基础配置
  dio.options = dio.BaseOptions(
    baseUrl: "https://httpbin.org", // 统一域名前缀
    connectTimeout: const Duration(seconds: 10), // 连接超时
    receiveTimeout: const Duration(seconds: 10), // 接收超时
    sendTimeout: const Duration(seconds: 10),
    headers: {
      "Content-Type": "application/json",
      "token": "全局登录token",
    },
  );
  return dio;
}

final dio.Dio http = createDio();

4. dio 常用请求方式

4.1 GET 请求

dart 复制代码
// 方式1:url拼接
await http.get("/get?name=张三&age=20");

// 方式2:params 自动拼接(推荐,自动处理编码)
var res = await http.get("/get", queryParameters: {
  "name": "张三",
  "age": 20
});
print(res.data); // res.data 自动转Map/List
print(res.statusCode); // 状态码 200/404/500

4.2 POST JSON 提交

dart 复制代码
var res = await http.post("/post", data: {
  "username": "test",
  "password": "123456"
});
Map result = res.data;

4.3 POST 表单提交 x-www-form-urlencoded

dart 复制代码
var formData = dio.FormData.fromMap({
  "account": "admin",
  "pwd": "666666"
});
await http.post("/login", data: formData);

4.4 携带自定义请求头

dart 复制代码
await http.get("/user/info", options: dio.Options(
  headers: {
    "Authorization": "Bearer xxx-token",
    "device": "android"
  }
));

5. dio 拦截器

拦截器分为三种:请求拦截、响应拦截、错误拦截

作用:统一添加 token、统一打印日志、统一处理登录过期、统一格式化返回数据

dart 复制代码
http.interceptors.add(d.InterceptorsWrapper(
  onRequest: (d.RequestOptions options, d.RequestInterceptorHandler handler) {
    // 请求前统一处理
    print("请求地址:${options.path}");
    // 自动追加token
    options.headers["token"] = "local_token_123";
    return handler.next(options);
  },
  onResponse: (d.Response response, d.ResponseInterceptorHandler handler) {
    // 统一解析后端返回格式
    print("响应数据:${response.data}");
    return handler.next(response);
  },
  onError: (d.DioException err, d.ErrorInterceptorHandler handler) {
    // 统一捕获所有网络异常
    print("网络错误:${err.message}");
    // 登录过期跳转登录页
    if (err.response?.statusCode == 401) {
      // 跳转登录逻辑
    }
    return handler.next(err);
  }
));

6 文件上传 & 文件下载

6.1 单文件上传

dart 复制代码
// FormData 携带文件 + 普通参数
d.FormData formData = d.FormData.fromMap({
  "desc": "图片描述",
  "file": await d.MultipartFile.fromFile(
    "/storage/emulated/0/test.png", // 文件本地路径
    filename: "upload.png"
  )
});
await http.post("/upload", data: formData);

6.2 多文件上传

dart 复制代码
List<d.MultipartFile> files = [];
files.add(await d.MultipartFile.fromFile("文件1路径"));
files.add(await d.MultipartFile.fromFile("文件2路径"));
d.FormData data = d.FormData({"files": files});
await http.post("/multi/upload", data: data);

6.3 文件下载

dart 复制代码
String savePath = "/storage/emulated/0/download/app.apk";
await http.download(
  "https://xxx/file.apk",
  savePath,
  onReceiveProgress: (int received, int total) {
    // 下载进度
    double progress = received / total;
    print("下载进度:${progress * 100}%");
  }
);

7 高级功能:超时、取消请求、并发请求

7.1 单次请求单独覆盖超时

dart 复制代码
await http.get("/api", options: d.Options(
  connectTimeout: Duration(seconds: 3)
));

7.2 取消请求(页面销毁终止网络,防内存泄漏)

dart 复制代码
// 创建取消令牌
d.CancelToken cancelToken = d.CancelToken();

// 发起请求
http.get("/long/api", cancelToken: cancelToken).catchError((e){
  if (e is d.DioException && e.type == d.DioExceptionType.cancel) {
    print("请求已手动取消");
  }
});

// 页面关闭时取消请求
cancelToken.cancel("页面退出,取消请求");

7.3 并发请求

dart 复制代码
// 同时发起多个请求,全部完成后再处理
Future.wait([
  http.get("/banner"),
  http.get("/goods/list"),
]).then((List<d.Response> resList) {
  var bannerData = resList[0].data;
  var goodsData = resList[1].data;
});

8 JSON 数据解析

8.1 简易手动解析

后端标准格式示例

json 复制代码
{
  "code": 200,
  "msg": "success",
  "data": {
    "id": 1,
    "username": "张三"
  }
}
dart 复制代码
var res = await http.get("/user/1");
Map json = res.data;
int code = json["code"];
String msg = json["msg"];
Map userInfo = json["data"];
String name = userInfo["username"];

8.2 自动序列化模型类

使用工具 json_serializable 自动生成 fromJson/toJson

  1. 依赖
yaml 复制代码
dependencies:
  json_annotation: ^4.8.1
dev_dependencies:
  build_runner: ^2.4.4
  json_serializable: ^6.7.0
  1. 用户模型示例 UserModel
dart 复制代码
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';

@JsonSerializable()
class UserModel {
  final int id;
  final String username;

  UserModel({required this.id, required this.username});
  factory UserModel.fromJson(Map<String> json) => _$UserModelFromJson(json);
}
  1. 生成命令
bash 复制代码
flutter pub run build_runner build
  1. 使用
dart 复制代码
Map data = res.data["data"];
UserModel user = UserModel.fromJson(data);
print(user.username);

9 高频踩坑与解决方案

9.1 Android 真机无法访问 http 明文接口

Android9+ 默认禁止 HTTP 明文,只允许 HTTPS

解决方案:android/app/src/main/AndroidManifest.xml 添加配置

xml 复制代码
<application android:usesCleartextTraffic="true" ...>

9.2 iOS http 接口无法请求

Info.plist 添加

xml 复制代码
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

9.3 跨域、401 token 失效

拦截器统一捕获 401,清除本地存储 token,跳转登录页面

9.4 内存泄漏:页面退出请求未取消

页面 dispose 中调用 cancelToken.cancel()

9.5 上传图片过大超时

单独设置上传接口 sendTimeout 延长

9.6 中文乱码

dio 默认自动处理 utf8,极少出现;如出现手动设置 headers Accept-Charset: utf-8

10 项目级完整封装

统一返回格式、统一异常捕获、拦截器、全局 token、封装 get/post/upload

dart 复制代码
import 'package:dio/dio.dart';

// 全局单例工具类
class HttpUtil {
  static final HttpUtil _instance = HttpUtil._internal();
  factory HttpUtil() => _instance;
  late Dio dio;

  HttpUtil._internal() {
    BaseOptions options = BaseOptions(
      baseUrl: "https://httpbin.org",
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
      headers: {"Content-Type": "application/json"},
    );
    dio = Dio(options);
    // 添加拦截器
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        // 全局token
        options.headers["token"] = "local_token";
        print("请求url:${options.path} 参数:${options.data ?? options.queryParameters}");
        return handler.next(options);
      },
      onResponse: (resp, handler) {
        print("响应:${resp.data}");
        return handler.next(resp);
      },
      onError: (err, handler) {
        // 统一错误提示
        String errMsg = "网络异常";
        if (err.type == DioExceptionType.connectionTimeout) {
          errMsg = "连接超时";
        } else if (err.response?.statusCode == 401) {
          errMsg = "登录失效,请重新登录";
          // 跳转登录逻辑
        }
        print("请求错误:$errMsg");
        return handler.next(err);
      },
    ));
  }

  // GET封装
  Future<dynamic> get(String path, {Map? params, CancelToken? token}) async {
    Response res = await dio.get(path, queryParameters: params, cancelToken: token);
    return res.data;
  }

  // POST JSON封装
  Future<dynamic> post(String path, {Map? data, CancelToken? token}) async {
    Response res = await dio.post(path, data: data, cancelToken: token);
    return res.data;
  }

  // 文件上传
  Future<dynamic> upload(String path, MultipartFile file) async {
    FormData formData = FormData.fromMap({"file": file});
    return await dio.post(path, data: formData);
  }

  // 文件下载
  Future download(String url, String savePath, {Function? onProgress}) async {
    return await dio.download(url, savePath, onReceiveProgress: (rec, total) {
      if(onProgress != null) on(rec / total);
    });
  }
}

// 使用
final http = HttpUtil();
void testApi() async {
  var data = await http.get("/get", params: {"name":"测试"});
  print(data);
}

11 拓展

11.1 前置全部依赖

pubspec.yaml

yaml 复制代码
```yaml
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.7.0
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  flutter_riverpod: ^2.5.1
  shared_preferences: ^2.2.2
dev_dependencies:
  hive_generator: ^1.1.5
  build_runner: ^2.4.6

11.2 全局 Loading 弹窗封装

全局单例 Loading,任意页面调用,请求自动开启、结束自动关闭

loading_util.dart

dart 复制代码
import 'package:flutter/material';

class LoadingUtil {
  static final LoadingUtil _instance = LoadingUtil._internal();
  factory LoadingUtil() => _instance;
  LoadingUtil._internal();

  bool _isShow = false;

  // 显示加载弹窗
  void show(BuildContext context, {String msg = "加载中..."}) {
    if (_isShow) return;
    _isShow = true;
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (ctx) {
        return PopScope(
          canPop: false,
          child: Center(
            child: Container(
              padding: EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Colors.black54,
                borderRadius: BorderRadius.circular(10),
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  CircularProgressIndicator(color: Colors.white),
                  SizedBox(height: 10),
                  Text(msg, style: TextStyle(color: Colors.white)),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  // 关闭弹窗
  void dismiss(BuildContext context) {
    if (!_isShow) return;
    _isShow = false;
    Navigator.pop(context);
  }
}

集成到 Dio 拦截器:请求前自动 show,成功 / 失败统一 dismiss

11.3 、请求重试拦截器

实现:网络错误 / 5xx 服务端错误自动重试,可配置重试次数、间隔

interceptor/retry_interceptor.dart

dart 复制代码
import 'package:dio/dio.dart';

class RetryInterceptor extends Interceptor {
  // 最大重试次数
  final int maxRetry;
  // 重试间隔 毫秒
  final int delayMs;

  RetryInterceptor({this.maxRetry = 2, this.delayMs = 1000});

  @override
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    // 不需要重试的场景:取消请求、4xx业务错误、上传下载
    bool needRetry = _shouldRetry(err);
    if (!needRetry) return handler.next(err);

    // 获取当前重试次数
    int retryCount = err.requestOptions.extra["retryCount"] ?? 0;
    if (retryCount >= maxRetry) return handler.next(err);

    // 延迟等待
    await Future.delayed(Duration(milliseconds: delayMs));
    // 重试计数+1
    err.requestOptions.extra["retryCount"] = retryCount + 1;
    // 重新发起请求
    final res = await Dio().fetch(err.requestOptions);
    return handler.resolve(res);
  }

  bool _shouldRetry(DioException err) {
    // 取消请求不重试
    if (err.type == DioExceptionType.cancel) return false;
    // 无网络、连接超时、服务端500以上错误才重试
    if (err.type == DioExceptionType.connectionTimeout ||
        err.type == DioExceptionType.receiveTimeout ||
        err.type == DioExceptionType.sendTimeout ||
        (err.response?.statusCode != null && err.response!.statusCode! >= 500)) {
      return true;
    }
    return false;
  }
}

11.4、Hive 持久化缓存拦截器

思路:

  1. 每个接口 url + 参数 作为唯一缓存 key
  2. 可自定义缓存有效期
  3. 请求优先读缓存,无缓存再走网络;网络成功更新缓存

interceptor/cache_interceptor.dart

dart 复制代码
import 'package:dio/dio.dart';
import 'package:hive/hive';

class CacheInterceptor extends Interceptor {
  // 缓存盒子名称
  static const String cacheBoxName = "api_cache";
  // 默认缓存过期时间 毫秒 30分钟
  final int defaultExpire = 30 * 60 * 1000;

  // 生成唯一缓存key
  String _buildCacheKey(RequestOptions options) {
    String path = options.path;
    Map query = options.queryParameters;
    Map data = options.data ?? {};
    return "$path|$query|$data";
  }

  @override
  Future onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    // 跳过缓存标记:extra["noCache"]=true
    bool noCache = options.extra["noCache"] ?? false;
    if (noCache) return handler.next(options);

    var box = await Hive.openBox(cacheBoxName);
    String key = _buildCacheKey(options);
    var cacheData = box.get(key);
    if (cacheData != null) {
      int cacheTime = cacheData["cacheTime"];
      int expire = options.extra["cacheExpire"] ?? defaultExpire;
      int now = DateTime.now().millisecondsSinceEpoch;
      // 未过期,直接返回缓存数据,不发起网络请求
      if (now - cacheTime < expire) {
        Response cacheResp = Response(
          data: cacheData["data"],
          statusCode: 200,
          requestOptions: options,
        );
        return handler.resolve(cacheResp);
      }
    }
    // 缓存过期/无缓存,正常走网络
    return handler.next(options);
  }

  @override
  Future onResponse(Response response, ResponseInterceptorHandler handler) async {
    RequestOptions opts = response.requestOptions;
    bool noCache = opts.extra["noCache"] ?? false;
    if (!noCache) {
      String key = _buildCacheKey(opts);
      var box = await Hive.openBox(cacheBoxName);
      // 存储数据+当前时间戳
      await box.put(key, {
        "data": response.data,
        "cacheTime": DateTime.now().millisecondsSinceEpoch
      });
    }
    return handler.next(response);
  }
}

11.5 改造 HttpUtil

10.项目级完整封装

http/http_util.dart

dart 复制代码
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive';
import 'cache_interceptor.dart';
import 'retry_interceptor.dart';
import '../util/loading_util.dart';

class HttpUtil {
  static final HttpUtil _ins = HttpUtil._internal();
  factory HttpUtil() => _ins;
  late Dio dio;

  HttpUtil._internal() {
    BaseOptions options = BaseOptions(
      baseUrl: "https://api.example.com",
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
      headers: {"Content-Type": "application/json"},
    );
    dio = Dio(options);

    // 拦截器顺序:缓存 -> 重试 -> 全局请求/响应/错误拦截
    dio.interceptors.add(CacheInterceptor());
    dio.interceptors.add(RetryInterceptor(maxRetry: 2, delayMs: 800));
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (opts, handler) {
        // 自动弹出loading
        LoadingUtil().show(navigatorKey.currentContext!, msg: "加载中...");
        // 全局token
        opts.headers["token"] = Hive.box("user").get("token") ?? "";
        return handler.next(opts);
      },
      onResponse: (resp, handler) {
        LoadingUtil().dismiss(navigatorKey.currentContext!);
        return handler.next(resp);
      },
      onError: (err, handler) {
        LoadingUtil().dismiss(navigatorKey.currentContext!);
        String msg = "网络请求失败";
        if (err.type == DioExceptionType.connectionTimeout) msg = "连接超时";
        if (err.response?.statusCode == 401) {
          // 清除token,跳转登录
          Hive.box("user").delete("token");
        }
        print("接口异常:$msg");
        return handler.next(err);
      },
    ));
  }

  // GET 支持缓存配置 noCache/cacheExpire
  Future get(String path,
      {Map? params, bool noCache = false, int? cacheExpire, CancelToken? token}) async {
    Options opts = Options(extra: {
      "noCache": noCache,
      if (cacheExpire != null) "cacheExpire": cacheExpire
    });
    Response res = await dio.get(path, queryParameters: params, options: opts, cancelToken: token);
    return res.data;
  }

  // POST 默认不读缓存
  Future post(String path, {Map? data, CancelToken? token}) async {
    Options opts = Options(extra: {"noCache": true});
    Response res = await dio.post(path, data: data, options: opts, cancelToken: token);
    return res.data;
  }

  // 文件上传
  Future upload(String path, MultipartFile file) async {
    FormData form = FormData.fromMap({"file": file});
    return await dio.post(path, data: form, options: Options(extra: {"noCache": true}));
  }
}

// 全局navigatorKey 无需传context展示loading
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

main.dart 配置全局 navigatorKey

dart 复制代码
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  await Hive.openBox("user");
  await Hive.openBox("api_cache");
  runApp(
    MaterialApp(
      navigatorKey: navigatorKey,
      home: MyHome(),
    ),
  );
}

11.6 Riverpod 封装全局请求状态(AsyncValue)

核心:利用 Riverpod AsyncNotifier 天然携带 loading /data/error,无需手动维护状态

provider/api_provider.dart

dart 复制代码
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../http/http_util.dart';

// 通用分页/数据接口示例
final userListProvider = AsyncNotifierProvider<UserListNotifier, List<dynamic>>(UserListNotifier.new);

class UserListNotifier extends AsyncNotifier<List<dynamic>> {
  final HttpUtil http = HttpUtil();

  // 刷新接口(下拉刷新调用)
  Future<void> refresh() async {
    state = const AsyncLoading();
    try {
      var res = await http.get("/user/list", noCache: true);
      List list = res["data"];
      state = AsyncData(list);
    } catch (e) {
      state = AsyncError(e, StackTrace.current);
    }
  }

  // 初次加载
  @override
  Future<List<dynamic>> build() async {
    var res = await http.get("/user/list");
    return res["data"];
  }
}

页面使用 Riverpod 状态

dart 复制代码
class UserPage extends ConsumerWidget {
  const UserPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<List<dynamic>> userState = ref.watch(userListProvider);
    return Scaffold(
      appBar: AppBar(title: Text("用户列表")),
      body: userState.when(
        loading: () => SizedBox(), // loading弹窗由http拦截器自动管理
        error: (err, stack) => Center(child: Text("加载失败:$err")),
        data: (list) => ListView.builder(
          itemCount: list.length,
          itemBuilder: (ctx, idx) => ListTile(title: Text(list[idx]["name"])),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(userListProvider.notifier).refresh(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}

单次临时请求(无持久状态)

dart 复制代码
ElevatedButton(
  onPressed: () async {
    final res = await ref.read(HttpUtil()).get("/profile");
  },
  child: Text("获取个人信息"),
)

11.7 Provider + ChangeNotifier 请求状态封装

不使用 Riverpod 项目可选用

store/api_store.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../http/http_util.dart';

class ApiStore extends ChangeNotifier {
  bool loading = false;
  dynamic data;
  String? errorMsg;

  Future fetchUserList() async {
    loading = true;
    errorMsg = null;
    notifyListeners();
    try {
      var res = await HttpUtil().get("/user/list");
      data = res["data"];
    } catch (e) {
      errorMsg = e.toString();
    } finally {
      loading = false;
      notifyListeners();
    }
  }
}

使用页面

dart 复制代码
Consumer<ApiStore>(builder: (ctx, store, child) {
  if (store.errorMsg != null) return Text(store.errorMsg!);
  return ListView.builder(itemCount: store.data.length, itemBuilder: ...);
})

11.8 shared_preferences 简易缓存(轻量替代 Hive)

适合简单字符串缓存,无需复杂对象存储

dart 复制代码
import 'package:shared_preferences/shared_preferences';

class SimpleCache {
  static Future setCache(String key, String value, int expireMin) async {
    var sp = await SharedPreferences.getInstance();
    int time = DateTime.now().millisecondsSinceEpoch;
    await sp.setString("cache_$key", value);
    await sp.setInt("cache_time_$key", time);
    await sp.setInt("cache_expire_$key", expireMin * 60 * 1000);
  }

  static Future<String?> getCache(String key) async {
    var sp = await SharedPreferences.getInstance();
    String? data = sp.getString("cache_$key");
    int cacheTime = sp.getInt("cache_time_$key") ?? 0;
    int expire = sp.getInt("cache_expire_$key") ?? 0;
    int now = DateTime.now().millisecondsSinceEpoch;
    if (now - cacheTime > expire) return null;
    return data;
  }
}

总结

  • 页面进入 → Riverpod AsyncNotifier.build 自动发起请求
  • HttpUtil 拦截器触发:弹出全局 Loading
  • CacheInterceptor 优先读取本地缓存,有缓存直接返回不请求网络
  • 无缓存走真实网络,失败触发 RetryInterceptor 自动重试 2 次
  • 请求成功:更新 Hive 缓存、关闭 Loading、更新 AsyncData 状态渲染列表
  • 请求失败:关闭 Loading、更新 AsyncError 展示错误文案
  • 下拉刷新:调用 notifier.refresh (),设置 noCache 强制拉取最新数据
  • 页面销毁:CancelToken 取消未完成请求,杜绝内存泄漏
相关推荐
OSMeteor1 小时前
在 Hyper-V 中搭建固定 IP、固定网关、可上网且与宿主机互通的 Ubuntu 环境
网络协议·tcp/ip·ubuntu
9765033352 小时前
iOS 上架/审核 4.3a Cocos 2026最新方案解读
flutter·ios·swift·cocos2d·ios开发
abbgogo12 小时前
TCP/IP、OSI 与常见网络协议知识点总结
网络·网络协议·计算机网络
李昊哲小课13 小时前
fastapi sse websocket 奶茶店实时订单看板
人工智能·python·websocket·网络协议·fastapi·sse
FungLeo14 小时前
Flutter 接入 Alice 调试浮窗:一个顶层 final 抢跑,把 release 网络整没了
网络·flutter
abbgogo15 小时前
堆叠、DHCP 与链路聚合总结
网络·网络协议
AI人工智能+电脑小能手20 小时前
【大白话说Java面试题 第216题】【10_网络协议篇】第7题:HTTP 协议和 HTTPS 协议的区别
java·http·网络安全·https·ssl/tls
花生了什么事o1 天前
WebSocket 与 SSE:实时通信方案的选择
网络·websocket·网络协议
想学好C++的oMen1 天前
应用层协议http
网络·网络协议·http