
为什么不能直接用 FileStream 长轮询读取日志文件
因为 FileStream 默认不支持“等待新内容到达”,Read() 在文件末尾会立即返回 0 字节,而非阻塞等待。若用循环 Seek() + Read() 轮询,CPU 占用高、延迟不可控,且容易漏掉瞬间写入的多行内容(比如 Log4net 一次刷盘写入几行)。
真正可行的方式是监听文件变化 + 增量读取,核心依赖:FileSystemWatcher 捕获 Changed 事件(注意:它只通知“文件被修改”,不告诉改了哪几行),再配合偏移量管理做安全续读。
-
FileSystemWatcher的NotifyFilter必须包含NotifyFilters.LastWrite,仅监听FileName或Attributes会丢事件 - Windows 上对大文件(>1GB)或高频写入(如每毫秒一行),
Changed事件可能合并或丢失,需加防抖(例如延迟 100ms 后再触发读取) - 不能在事件回调里直接调用
File.OpenText()—— 文件可能正被写入进程独占锁定,应重试 +IOException捕获
如何用 IAsyncEnumerable<string></string> 实现流式响应
ASP.NET Core 6+ 的 Web API 支持直接返回 IAsyncEnumerable<string></string>,客户端用 text/event-stream(SSE)接收,服务端按行 yield 新内容,无需手动管理连接生命周期。
关键点在于:每次 yield 前必须确认当前读取位置未被截断(日志轮转常见),所以得先用 FileInfo.Length 校验,再从上次偏移处开始读取新增字节,最后按 \n 或 \r\n 切分有效行。
- 响应头必须显式设置:
Response.Headers.Add("Content-Type", "text/event-stream"); - 每行数据要包装成 SSE 格式:
yield return $"data: {line.TrimEnd()}\n\n";(注意双换行) - 避免
StreamReader.ReadLineAsync()直接读 —— 它内部缓冲可能导致“读到一半就 yield”,应改用Stream.ReadAsync()+ 自己解析行边界 - 客户端断连时,
cancellationToken会触发,需及时清理FileSystemWatcher和文件句柄
FileSystemWatcher 和文件锁冲突怎么破
典型报错:System.IO.IOException: The process cannot access the file because it is being used by another process. —— 这是因为日志文件正被 NLog/Log4net 独占打开(FileShare.None),而你的 FileStream 试图以 FileAccess.Read 打开失败。
解法只有两个:一是降级为共享读(FileShare.ReadWrite),二是退化为“无锁轮询”作为兜底。生产环境建议组合使用:
- 优先尝试
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.SequentialScan) - 若抛
IOException,启动后台定时任务(如Task.Run(() => PollLoopAsync())),每 500ms 检查FileInfo.Length是否增长,仅当增长时才尝试打开(仍带重试) - 永远不要用
Thread.Sleep()阻塞主线程;所有等待必须用await Task.Delay()+cancellationToken
客户端如何稳定接收 SSE 并处理断连
浏览器原生 EventSource 会在连接中断后自动重连(默认 3s),但重连时无法携带上次读取偏移,所以服务端必须支持“从某行号/字节位置恢复”。简单方案是让客户端在 URL 中传参,如 /api/tail?path=/var/log/app.log&offset=12345。
更健壮的做法是服务端生成唯一 tailId,首次连接返回该 ID,后续断连重连时带上,服务端查内存字典恢复上下文(注意:跨实例部署需用 Redis 存储偏移)。
- 客户端示例:
const es = new EventSource("/api/tail?path=C%3A%5Clogs%5Capp.log"); es.onmessage = e => console.log(e.data); - 服务端需校验
path参数是否在白名单内(如只允许C:\logs\下的文件),防止路径遍历攻击 - 单个连接最大持续时间建议设限(如 30 分钟),超时后返回
event: timeout\ndata:\n\n并关闭,避免长连接堆积
最易被忽略的是编码问题:日志文件可能是 UTF-8 with BOM、GBK 或 UTF-16,StreamReader 默认用 UTF-8 但不检测 BOM,导致首行乱码。务必用 new StreamReader(stream, Encoding.Default) 或先读前 3 字节判断 BOM 再选编码。








