如何在C#的HttpListener中获取并保存上传的文件?
解决HttpListener处理文件上传仅获取路径的问题
你现在遇到的问题核心有两个:HTML表单没设置正确的编码类型,同时后端代码没解析multipart/form-data格式的请求体——这是文件上传必须的编码方式。默认的表单编码(application/x-www-form-urlencoded)只会把文件的本地路径作为字符串提交,根本不会传递文件内容。下面是完整的解决方案:
第一步:修正HTML表单
确保你的表单添加enctype="multipart/form-data"属性,并且请求方法为POST:
<form method="POST" enctype="multipart/form-data" action="http://localhost:8080"> <input type="file" name="file"> <button type="submit">上传文件</button> </form>
第二步:修改C#后端代码解析文件内容
原来的代码只是读取了请求体的纯文本,但multipart/form-data格式是分段存储的,需要专门解析每个字段(包括文件)。下面是修改后的完整服务器代码,包含文件解析和保存逻辑:
using System; using System.IO; using System.Net; using System.Text; class SimpleFileUploadServer { // 定义上传目录,会自动创建在程序运行目录下 private static readonly string UploadDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Uploads"); public static void Start() { // 初始化上传目录,不存在则创建 if (!Directory.Exists(UploadDir)) { Directory.CreateDirectory(UploadDir); } HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8080/"); listener.Start(); Console.WriteLine("服务器已启动,监听地址:http://localhost:8080/"); while (true) { HttpListenerContext context = listener.GetContext(); if (context.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) { HandleFileUpload(context.Request); SendSuccessResponse(context.Response); } else { SendErrorResponse(context.Response, "仅支持POST请求", HttpStatusCode.MethodNotAllowed); } } } private static void HandleFileUpload(HttpListenerRequest request) { // 校验请求是否为文件上传类型 if (!request.HasEntityBody || !request.ContentType.StartsWith("multipart/form-data")) { Console.WriteLine("无效的文件上传请求"); return; } // 从Content-Type头中提取分隔符(boundary) string boundary = request.ContentType.Split(';')[1].Trim().Split('=')[1]; byte[] boundaryBytes = Encoding.ASCII.GetBytes($"--{boundary}"); using (Stream bodyStream = request.InputStream) using (BinaryReader reader = new BinaryReader(bodyStream)) { // 跳过第一个分隔符 SkipToBoundary(reader, boundary); // 读取文件头信息,提取文件名 string fileName = ExtractFileName(reader); if (string.IsNullOrEmpty(fileName)) { Console.WriteLine("未获取到上传文件名"); return; } // 拼接文件保存路径 string savePath = Path.Combine(UploadDir, fileName); // 读取文件内容并写入本地 SaveFileContent(bodyStream, savePath, boundaryBytes); Console.WriteLine($"文件上传完成:{savePath}"); } } // 辅助方法:跳转到下一个分隔符位置 private static void SkipToBoundary(BinaryReader reader, string boundary) { while (true) { byte[] line = ReadLine(reader); if (line.Length == 0) continue; string lineStr = Encoding.ASCII.GetString(line); if (lineStr.StartsWith($"--{boundary}")) break; } } // 辅助方法:从请求头中提取文件名 private static string ExtractFileName(BinaryReader reader) { byte[] headerLine; while ((headerLine = ReadLine(reader)).Length > 0) { string header = Encoding.ASCII.GetString(headerLine); if (header.StartsWith("Content-Disposition:")) { int fileNameStart = header.IndexOf("filename=\"") + 10; int fileNameEnd = header.IndexOf("\"", fileNameStart); if (fileNameStart > 9 && fileNameEnd > fileNameStart) { return header.Substring(fileNameStart, fileNameEnd - fileNameStart); } } } return null; } // 辅助方法:读取文件内容并保存到本地 private static void SaveFileContent(Stream bodyStream, string savePath, byte[] boundaryBytes) { using (FileStream fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = bodyStream.Read(buffer, 0, buffer.Length)) > 0) { // 检查是否到达分隔符,避免写入多余的边界内容 int boundaryIndex = Array.IndexOf(buffer, boundaryBytes[0], 0, bytesRead); if (boundaryIndex != -1 && bytesRead >= boundaryBytes.Length) { bool isBoundary = true; for (int i = 0; i < boundaryBytes.Length; i++) { if (buffer[boundaryIndex + i] != boundaryBytes[i]) { isBoundary = false; break; } } if (isBoundary) { fileStream.Write(buffer, 0, boundaryIndex); break; } } fileStream.Write(buffer, 0, bytesRead); } } } // 辅助方法:从流中读取一行(跳过回车符) private static byte[] ReadLine(BinaryReader reader) { MemoryStream ms = new MemoryStream(); byte b; while ((b = reader.ReadByte()) != 0x0A) // 0x0A是换行符\n { if (b != 0x0D) // 跳过回车符\r { ms.WriteByte(b); } } return ms.ToArray(); } // 返回成功响应 private static void SendSuccessResponse(HttpListenerResponse response) { SendResponse(response, "文件上传成功!", HttpStatusCode.OK); } // 返回错误响应 private static void SendErrorResponse(HttpListenerResponse response, string message, HttpStatusCode statusCode) { SendResponse(response, message, statusCode); } // 统一响应处理方法 private static void SendResponse(HttpListenerResponse response, string content, HttpStatusCode statusCode) { response.StatusCode = (int)statusCode; response.ContentType = "text/html; charset=utf-8"; byte[] output = Encoding.UTF8.GetBytes(content); response.ContentLength64 = output.Length; response.OutputStream.Write(output, 0, output.Length); response.OutputStream.Close(); } static void Main(string[] args) { Start(); } }
关键说明
- 表单编码的必要性:
enctype="multipart/form-data"会将表单数据拆分为多个分段,每个分段对应一个表单字段,这样才能完整传递文件的二进制内容。 - Multipart解析逻辑:我们需要从请求头中提取分隔符,然后逐个解析请求体的分段,提取文件名和文件内容,最后写入到指定目录。
- 目录初始化:代码会自动创建Uploads目录,避免因目录不存在导致的保存失败。
- 响应优化:封装了统一的响应方法,避免重复代码,同时支持返回不同状态码的响应。
现在运行服务器,提交表单后,文件就会被保存到程序运行目录下的Uploads文件夹中,控制台也会输出完整的保存路径。
内容的提问来源于stack exchange,提问作者Uvindu Chamath




