网络编程(TCP/UDP/HTTP)
大约 9 分钟约 2788 字
网络编程(TCP/UDP/HTTP)
简介
网络编程是 .NET 开发中的重要技能,涵盖传输层的 TCP/UDP 协议和应用层的 HTTP 协议。C# 提供了丰富的网络 API,包括 TcpListener/TcpClient 用于可靠传输、UdpClient 用于高效数据报通信,以及 HttpClient 用于现代 HTTP 通信。掌握这些 API 的正确使用方式,对于构建高性能、可靠的网络应用至关重要。
特点
TCP 编程
TCP 服务器(TcpListener)
/// <summary>
/// 简单的 TCP 回声服务器
/// </summary>
public class TcpEchoServer
{
private readonly int _port;
private TcpListener? _listener;
private CancellationTokenSource? _cts;
public TcpEchoServer(int port = 8888) => _port = port;
public async Task StartAsync()
{
_listener = new TcpListener(IPAddress.Any, _port);
_cts = new CancellationTokenSource();
_listener.Start();
Console.WriteLine($"TCP 服务器已启动,监听端口: {_port}");
try
{
while (!_cts.Token.IsCancellationRequested)
{
var client = await _listener.AcceptTcpClientAsync(_cts.Token);
_ = HandleClientAsync(client, _cts.Token); // 不等待,并发处理
}
}
catch (OperationCanceledException)
{
Console.WriteLine("服务器正在关闭...");
}
}
private async Task HandleClientAsync(TcpClient client, CancellationToken ct)
{
var endpoint = client.Client.RemoteEndPoint;
Console.WriteLine($"客户端已连接: {endpoint}");
try
{
using (client)
await using (var stream = client.GetStream())
{
var buffer = new byte[4096];
while (!ct.IsCancellationRequested && client.Connected)
{
var bytesRead = await stream.ReadAsync(buffer, ct);
if (bytesRead == 0) break; // 客户端断开连接
var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"[{endpoint}] 收到: {message}");
// 回声:将收到的消息原样返回
var response = Encoding.UTF8.GetBytes($"Echo: {message}");
await stream.WriteAsync(response, ct);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"客户端 {endpoint} 异常: {ex.Message}");
}
finally
{
Console.WriteLine($"客户端断开: {endpoint}");
}
}
public void Stop()
{
_cts?.Cancel();
_listener?.Stop();
}
}TCP 客户端(TcpClient)
/// <summary>
/// TCP 客户端
/// </summary>
public class TcpEchoClient
{
private readonly string _host;
private readonly int _port;
public TcpEchoClient(string host = "127.0.0.1", int port = 8888)
{
_host = host;
_port = port;
}
public async Task SendMessageAsync(string message)
{
using var client = new TcpClient();
await client.ConnectAsync(_host, _port);
Console.WriteLine($"已连接到 {_host}:{_port}");
await using var stream = client.GetStream();
// 发送消息
var data = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(data);
Console.WriteLine($"已发送: {message}");
// 接收响应
var buffer = new byte[4096];
var bytesRead = await stream.ReadAsync(buffer);
var response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到响应: {response}");
}
}
// 使用
var server = new TcpEchoServer(8888);
_ = server.StartAsync(); // 启动服务器
await Task.Delay(100); // 等待服务器启动
var client = new TcpEchoClient("127.0.0.1", 8888);
await client.SendMessageAsync("Hello TCP Server!");TCP 消息帧协议
TCP 是流式协议,需要自定义消息边界(帧)来正确解析消息。
/// <summary>
/// 基于长度前缀的消息帧协议
/// 格式:[4字节长度][消息体]
/// </summary>
public static class MessageFraming
{
/// <summary>
/// 发送带长度前缀的消息
/// </summary>
public static async Task SendMessageAsync(Stream stream, string message, CancellationToken ct = default)
{
var payload = Encoding.UTF8.GetBytes(message);
var lengthPrefix = BitConverter.GetBytes(payload.Length); // 4 字节长度
// 先发送长度
await stream.WriteAsync(lengthPrefix, ct);
// 再发送消息体
await stream.WriteAsync(payload, ct);
}
/// <summary>
/// 接收带长度前缀的消息
/// </summary>
public static async Task<string?> ReceiveMessageAsync(Stream stream, CancellationToken ct = default)
{
// 先读取 4 字节长度
var lengthBuffer = new byte[4];
var totalRead = 0;
while (totalRead < 4)
{
var read = await stream.ReadAsync(lengthBuffer.AsMemory(totalRead, 4 - totalRead), ct);
if (read == 0) return null; // 连接关闭
totalRead += read;
}
var messageLength = BitConverter.ToInt32(lengthBuffer, 0);
if (messageLength <= 0 || messageLength > 10 * 1024 * 1024) // 最大 10MB
throw new InvalidDataException($"无效的消息长度: {messageLength}");
// 读取消息体
var messageBuffer = new byte[messageLength];
totalRead = 0;
while (totalRead < messageLength)
{
var read = await stream.ReadAsync(
messageBuffer.AsMemory(totalRead, messageLength - totalRead), ct);
if (read == 0) return null;
totalRead += read;
}
return Encoding.UTF8.GetString(messageBuffer);
}
}UDP 编程
UDP 服务器和客户端
/// <summary>
/// UDP 服务器
/// </summary>
public class UdpServer
{
private readonly int _port;
private UdpClient? _udpClient;
public UdpServer(int port = 9999) => _port = port;
public async Task StartAsync(CancellationToken ct = default)
{
_udpClient = new UdpClient(_port);
Console.WriteLine($"UDP 服务器已启动,监听端口: {_port}");
while (!ct.IsCancellationRequested)
{
try
{
var result = await _udpClient.ReceiveAsync(ct);
var message = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"[{result.RemoteEndPoint}] 收到: {message}");
// 返回响应
var response = Encoding.UTF8.GetBytes($"服务器收到: {message}");
await _udpClient.SendAsync(response, response.Length, result.RemoteEndpoint);
}
catch (OperationCanceledException) { break; }
}
}
}
/// <summary>
/// UDP 客户端
/// </summary>
public class UdpClientApp
{
public static async Task SendAndReceiveAsync(string host, int port, string message)
{
using var udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 5000; // 5秒超时
var endpoint = new IPEndPoint(IPAddress.Parse(host), port);
var data = Encoding.UTF8.GetBytes(message);
await udpClient.SendAsync(data, data.Length, endpoint);
Console.WriteLine($"已发送: {message}");
var result = await udpClient.ReceiveAsync();
var response = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"收到响应: {response}");
}
}
// UDP 广播示例
public static async Task BroadcastAsync(int port, string message)
{
using var udpClient = new UdpClient();
udpClient.EnableBroadcast = true;
var data = Encoding.UTF8.GetBytes(message);
var endpoint = new IPEndPoint(IPAddress.Broadcast, port);
await udpClient.SendAsync(data, data.Length, endpoint);
Console.WriteLine($"已广播: {message}");
}UDP 组播(Multicast)
/// <summary>
/// UDP 组播 - 用于一对多通信
/// </summary>
public class UdpMulticast
{
private const string MulticastGroup = "239.0.0.1";
private const int Port = 9000;
/// <summary>
/// 加入组播组并接收消息
/// </summary>
public static async Task ReceiveAsync(CancellationToken ct = default)
{
using var udpClient = new UdpClient(Port);
udpClient.JoinMulticastGroup(IPAddress.Parse(MulticastGroup));
Console.WriteLine($"已加入组播组 {MulticastGroup}:{Port}");
while (!ct.IsCancellationRequested)
{
var result = await udpClient.ReceiveAsync(ct);
var message = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"[组播] {result.RemoteEndPoint}: {message}");
}
}
/// <summary>
/// 向组播组发送消息
/// </summary>
public static async Task SendAsync(string message)
{
using var udpClient = new UdpClient();
var endpoint = new IPEndPoint(IPAddress.Parse(MulticastGroup), Port);
var data = Encoding.UTF8.GetBytes(message);
await udpClient.SendAsync(data, data.Length, endpoint);
Console.WriteLine($"已发送组播: {message}");
}
}HttpClient 高级用法
基础配置和最佳实践
// 最佳实践:使用 IHttpClientFactory 管理生命周期
var builder = WebApplication.CreateBuilder(args);
// 命名客户端
builder.Services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.Timeout = TimeSpan.FromSeconds(30);
});
// 类型化客户端
builder.Services.AddHttpClient<IGitHubService, GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
// 使用示例
public class GitHubService : IGitHubService
{
private readonly HttpClient _client;
public GitHubService(HttpClient client) => _client = client;
public async Task<Repository?> GetRepositoryAsync(string owner, string repo)
{
var response = await _client.GetAsync($"repos/{owner}/{repo}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Repository>();
}
}
public record Repository(string Name, string FullName, int StargazersCount, string HtmlUrl);弹性 HttpClient(重试、熔断)
// 使用 Polly 实现重试和熔断
// dotnet add package Microsoft.Extensions.Http.Polly
// dotnet add package Polly.Extensions.Http
builder.Services.AddHttpClient<IPaymentService, PaymentService>(client =>
{
client.BaseAddress = new Uri("https://api.payment.com/");
client.Timeout = TimeSpan.FromSeconds(10);
})
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retryCount, context) =>
{
Console.WriteLine($"重试 {retryCount},等待 {timeSpan.TotalSeconds}s");
}))
.AddCircuitBreakerAsync(options =>
{
options.ExceptionsAllowedBeforeBreaking = 5;
options.DurationOfBreak = TimeSpan.FromSeconds(30);
options.OnHalfOpen = () =>
{
Console.WriteLine("熔断器半开,尝试恢复...");
return Task.CompletedTask;
};
});自定义 DelegatingHandler(中间件)
/// <summary>
/// 请求日志中间件
/// </summary>
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger<LoggingHandler> _logger;
public LoggingHandler(ILogger<LoggingHandler> logger) => _logger = logger;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var id = Guid.NewGuid().ToString("N")[..8];
_logger.LogInformation("[{Id}] {Method} {Url}", id, request.Method, request.RequestUri);
var sw = Stopwatch.StartNew();
try
{
var response = await base.SendAsync(request, cancellationToken);
sw.Stop();
_logger.LogInformation("[{Id}] {StatusCode} - {Ms}ms",
id, (int)response.StatusCode, sw.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
sw.Stop();
_logger.LogError(ex, "[{Id}] 请求失败 - {Ms}ms", id, sw.ElapsedMilliseconds);
throw;
}
}
}
/// <summary>
/// 认证中间件 - 自动附加 JWT Token
/// </summary>
public class AuthenticationHandler : DelegatingHandler
{
private readonly ITokenProvider _tokenProvider;
public AuthenticationHandler(ITokenProvider tokenProvider) => _tokenProvider = tokenProvider;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await _tokenProvider.GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
// 注册带中间件的 HttpClient
builder.Services.AddHttpClient("AuthenticatedApi")
.AddHttpMessageHandler<AuthenticationHandler>()
.AddHttpMessageHandler<LoggingHandler>();流式处理和 SSE
/// <summary>
/// 流式下载大文件
/// </summary>
public static async Task DownloadFileAsync(
HttpClient client, string url, string outputPath,
IProgress<long>? progress = null,
CancellationToken ct = default)
{
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? -1;
await using var stream = await response.Content.ReadAsStreamAsync(ct);
await using var fileStream = File.Create(outputPath);
var buffer = new byte[81920]; // 80KB 缓冲区
var totalRead = 0L;
while (true)
{
var bytesRead = await stream.ReadAsync(buffer, ct);
if (bytesRead == 0) break;
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct);
totalRead += bytesRead;
progress?.Report(totalRead);
}
}
/// <summary>
/// 处理 Server-Sent Events (SSE)
/// </summary>
public static async IAsyncEnumerable<string> ReadSSEAsync(
HttpClient client, string url,
[EnumeratorCancellation] CancellationToken ct = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Accept", "text/event-stream");
using var response = await client.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(ct);
using var reader = new StreamReader(stream);
while (!reader.EndOfStream && !ct.IsCancellationRequested)
{
var line = await reader.ReadLineAsync(ct);
if (line == null) continue;
if (line.StartsWith("data: "))
{
yield return line["data: ".length..];
}
}
}优点
缺点
总结
.NET 提供了完善且统一的网络编程 API。对于大多数应用场景,HttpClient 配合 IHttpClientFactory 是最佳选择,支持重试、熔断、日志等横切关注点。对于需要底层控制的自定义协议,TcpListener/TcpClient 和 UdpClient 提供了灵活的传输层通信能力。在使用 TCP 时务必注意消息帧协议的设计,使用 UDP 时需要考虑可靠性补偿机制。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
- 部署主题通常要同时看镜像、容器、卷、网络和宿主机资源。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
- 固定镜像标签,记录端口、挂载目录、环境变量和自启动策略。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
- 使用 latest 导致结果不可复现。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
- 继续补齐 Compose 编排、镜像瘦身、安全扫描和镜像仓库治理。
适用场景
- 当你准备把《网络编程(TCP/UDP/HTTP)》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《网络编程(TCP/UDP/HTTP)》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《网络编程(TCP/UDP/HTTP)》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《网络编程(TCP/UDP/HTTP)》最大的收益和代价分别是什么?
