You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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

火山引擎 最新活动