From 1a8f6efde212024b3519cd7fa445727673b5a1de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 16:11:21 +0000 Subject: [PATCH] Add TCP server/client streaming package demo Loopback transfer of sheet XML and BIFF record packages; the receiver uses XmlPackageParser / BinaryPackage reconstruction and prints packages as they complete. Invoked via -t tcpstream and wired into test.sh. Co-authored-by: Fan Feilong --- docs/streaming-packages.md | 14 ++ src/Exceltk/Program.cs | 15 ++ src/Exceltk/Reader/ExcelBinaryReader.cs | 14 ++ src/Exceltk/Test/TcpStreamingDemo.cs | 250 ++++++++++++++++++++++++ src/test.sh | 1 + 5 files changed, 294 insertions(+) create mode 100644 src/Exceltk/Test/TcpStreamingDemo.cs diff --git a/docs/streaming-packages.md b/docs/streaming-packages.md index 5cf3a88..9fce4db 100644 --- a/docs/streaming-packages.md +++ b/docs/streaming-packages.md @@ -42,3 +42,17 @@ A progressive renderer can subscribe to `IPackageParser.Parse()` (or Fully forward-only OPC (single `ZipInputStream` pass over a non-seekable network body) is a follow-up: sheet parts are not contiguous with workbook.xml. Seekable streams (files, buffered downloads) are supported via `ZipFile` entry streaming. + +## TCP demo + +Run a loopback server/client that streams sheet XML and BIFF packages: + +```bash +dotnet run --project src/Exceltk/Exceltk.csproj -- -t tcpstream \ + -xlsx src/test/test1.xlsx -biff src/test/test8.xls +``` + +(or from `src/`: `dotnet run --project Exceltk/Exceltk.csproj -- -t tcpstream`) + +The receiver prints packages as they complete (`[xml-stream] row#…`, `[biff-stream] #…`) +and exits with `TCP stream test OK` / `Done!` on success. Wired into `src/test.sh`. diff --git a/src/Exceltk/Program.cs b/src/Exceltk/Program.cs index 2058b07..cbfc7da 100644 --- a/src/Exceltk/Program.cs +++ b/src/Exceltk/Program.cs @@ -10,6 +10,20 @@ internal class Program { [STAThread] private static void Main(string[] args) { var cmd=new CommandParser(args); + if (cmd["t"] == "tcpstream") { + string root = Directory.GetCurrentDirectory(); + string xlsx = cmd["xlsx"] ?? cmd["xls"] ?? Path.Combine("test", "test1.xlsx"); + string xls = cmd["biff"] ?? Path.Combine("test", "test8.xls"); + if (!Path.IsPathRooted(xlsx)) { + xlsx = Path.Combine(root, xlsx); + } + if (!Path.IsPathRooted(xls)) { + xls = Path.Combine(root, xls); + } + Environment.ExitCode = Test.TcpStreamingDemo.Run(xlsx, xls); + return; + } + var r = InitConfig(cmd); if (r) { Xls2MarkDown(cmd); @@ -154,6 +168,7 @@ private static void Xls2MarkDown(CommandParser cmd) { Console.WriteLine("3. Pretty (column-aligned) markdown: Exceltk -t md -pretty -xls file"); Console.WriteLine("4. MultiMarkdown HTML tables (merged cells): Exceltk -t md -mmd -xls file"); Console.WriteLine("Note: -t cm (clipboard GUI) was removed after 0.0.9; use 0.0.9 if needed."); + Console.WriteLine("5. TCP streaming package demo: Exceltk -t tcpstream [-xlsx file.xlsx] [-biff file.xls]"); } } } diff --git a/src/Exceltk/Reader/ExcelBinaryReader.cs b/src/Exceltk/Reader/ExcelBinaryReader.cs index 4a6376e..ed7cf85 100644 --- a/src/Exceltk/Reader/ExcelBinaryReader.cs +++ b/src/Exceltk/Reader/ExcelBinaryReader.cs @@ -94,6 +94,20 @@ public void Open(Stream fileStream) { m_SheetIndex=0; } + /// + /// Rewind the workbook BIFF cursor and stream record packages + /// (for progressive consumers / TCP demos). + /// + internal System.Collections.Generic.IEnumerable StreamPackages() { + if (m_stream == null) { + yield break; + } + m_stream.Seek(0, SeekOrigin.Begin); + foreach (var package in m_stream.Parse()) { + yield return package; + } + } + public DataSet AsDataSet() { if (!m_isValid) { return null; diff --git a/src/Exceltk/Test/TcpStreamingDemo.cs b/src/Exceltk/Test/TcpStreamingDemo.cs new file mode 100644 index 0000000..5087ab2 --- /dev/null +++ b/src/Exceltk/Test/TcpStreamingDemo.cs @@ -0,0 +1,250 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Exceltk.Reader; +using Exceltk.Reader.Binary; +using Exceltk.Reader.Package; +using Exceltk.Reader.Parser; +using Exceltk.Reader.Xml; + +namespace Exceltk.Test { + /// + /// End-to-end TCP demo: server sends Excel payload in small chunks; client uses + /// streaming package parsers and prints packages as they complete. + /// + internal static class TcpStreamingDemo { + private const int ChunkSize = 96; + private const int ChunkDelayMs = 2; + + public static int Run(string xlsxPath, string xlsPath) { + Console.WriteLine("=== TCP streaming package demo ==="); + + bool xmlOk = RunXmlSheetDemo(xlsxPath); + bool biffOk = RunBiffPackageDemo(xlsPath); + + if (xmlOk && biffOk) { + Console.WriteLine("TCP stream test OK"); + Console.WriteLine("Done!"); + return 0; + } + + Console.WriteLine("TCP stream test FAILED (xml={0}, biff={1})", xmlOk, biffOk); + return 1; + } + + /// + /// Server streams worksheet XML bytes; client XmlPackageParser emits row/dimension packages live. + /// + private static bool RunXmlSheetDemo(string xlsxPath) { + Console.WriteLine("-- XML sheet over TCP: {0}", Path.GetFileName(xlsxPath)); + byte[] sheetXml = LoadFirstWorksheetXml(xlsxPath); + if (sheetXml == null || sheetXml.Length == 0) { + Console.WriteLine("ERROR: could not load worksheet XML"); + return false; + } + + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + + int rowsSeen = 0; + int dimSeen = 0; + Exception clientError = null; + + var clientTask = Task.Run(() => { + try { + using (var client = new TcpClient()) { + client.Connect(IPAddress.Loopback, port); + using (NetworkStream net = client.GetStream()) { + var settings = new XmlReaderSettings { + CloseInput = false, + IgnoreComments = true, + IgnoreWhitespace = false + }; + using (XmlReader xmlReader = XmlReader.Create(net, settings)) { + var parser = new XmlPackageParser(xmlReader); + foreach (XmlPackage package in parser.Parse()) { + if (package is XmlDimensionPackage dim) { + dimSeen++; + Console.WriteLine("[xml-stream] dimension ref={0}", dim.Ref); + } else if (package is XmlRowPackage row) { + rowsSeen++; + if (rowsSeen <= 15 || rowsSeen % 25 == 0) { + Console.WriteLine("[xml-stream] row#{0} cells={1} r={2}", + rowsSeen, row.Cells.Count, row.RowIndexAttribute); + } + } else if (package is XmlSheetDataEndPackage) { + Console.WriteLine("[xml-stream] sheetData end"); + } else if (package is XmlWorksheetStartPackage ws) { + Console.WriteLine("[xml-stream] worksheet ns={0}", ws.NamespaceUri); + } + } + } + } + } + } catch (Exception ex) { + clientError = ex; + } + }); + + using (TcpClient serverClient = listener.AcceptTcpClient()) + using (NetworkStream serverNet = serverClient.GetStream()) { + // Progressive send: small chunks so the client parser advances dynamically. + for (int offset = 0; offset < sheetXml.Length; offset += ChunkSize) { + int len = Math.Min(ChunkSize, sheetXml.Length - offset); + serverNet.Write(sheetXml, offset, len); + serverNet.Flush(); + Thread.Sleep(ChunkDelayMs); + } + } + + listener.Stop(); + clientTask.Wait(TimeSpan.FromSeconds(30)); + + if (clientError != null) { + Console.WriteLine("ERROR xml client: {0}", clientError.Message); + return false; + } + if (rowsSeen <= 0) { + Console.WriteLine("ERROR: no XmlRowPackage received"); + return false; + } + + Console.WriteLine("-- XML OK (rows={0}, dimensions={1})", rowsSeen, dimSeen); + return true; + } + + /// + /// Server sends length-prefixed BIFF record packages; client rebuilds BinaryPackage records live. + /// + private static bool RunBiffPackageDemo(string xlsPath) { + Console.WriteLine("-- BIFF packages over TCP: {0}", Path.GetFileName(xlsPath)); + + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + + int packagesSeen = 0; + Exception clientError = null; + + var clientTask = Task.Run(() => { + try { + using (var client = new TcpClient()) { + client.Connect(IPAddress.Loopback, port); + using (NetworkStream net = client.GetStream()) { + var recordReader = new ExcelBinaryReader(); + var lenBuf = new byte[4]; + while (ReadExact(net, lenBuf, 4)) { + int size = BitConverter.ToInt32(lenBuf, 0); + if (size <= 0) { + break; // end marker + } + var body = new byte[size]; + if (!ReadExact(net, body, size)) { + throw new EndOfStreamException("truncated BIFF package"); + } + XlsBiffRecord record = XlsBiffRecord.GetRecord(body, 0, recordReader); + if (record == null) { + continue; + } + packagesSeen++; + // BinaryPackage = the record itself + BinaryPackage package = record; + if (packagesSeen <= 20 || packagesSeen % 200 == 0) { + Console.WriteLine("[biff-stream] #{0} {1} size={2}", + packagesSeen, package.GetType().Name, record.Size); + } + } + } + } + } catch (Exception ex) { + clientError = ex; + } + }); + + using (FileStream fs = File.Open(xlsPath, FileMode.Open, FileAccess.Read)) { + var excelReader = new ExcelBinaryReader(); + excelReader.Open(fs); + using (TcpClient serverClient = listener.AcceptTcpClient()) + using (NetworkStream serverNet = serverClient.GetStream()) { + foreach (BinaryPackage package in excelReader.StreamPackages()) { + var record = (XlsBiffRecord)package; + byte[] bytes = record.Bytes; + int size = record.Size; + byte[] len = BitConverter.GetBytes(size); + serverNet.Write(len, 0, 4); + serverNet.Write(bytes, 0, size); + serverNet.Flush(); + Thread.Sleep(ChunkDelayMs); + } + // end marker + serverNet.Write(BitConverter.GetBytes(0), 0, 4); + serverNet.Flush(); + } + excelReader.Close(); + } + + listener.Stop(); + clientTask.Wait(TimeSpan.FromSeconds(60)); + + if (clientError != null) { + Console.WriteLine("ERROR biff client: {0}", clientError.Message); + return false; + } + if (packagesSeen <= 0) { + Console.WriteLine("ERROR: no BIFF packages received"); + return false; + } + + Console.WriteLine("-- BIFF OK (packages={0})", packagesSeen); + return true; + } + + private static byte[] LoadFirstWorksheetXml(string xlsxPath) { + using (FileStream fs = File.Open(xlsxPath, FileMode.Open, FileAccess.Read)) { + var zip = new ZipWorker(); + if (!zip.Open(fs, ownsStream: false)) { + return null; + } + try { + // Prefer sheet1; fall back to first worksheets/*.xml entry path used by test files. + Stream sheet = zip.GetWorksheetStream(1) + ?? zip.GetWorksheetStream("worksheets/sheet1.xml") + ?? zip.GetWorksheetStream("/xl/worksheets/sheet1.xml"); + if (sheet == null) { + // Some workbooks start at sheet2 for the visible sheet — try a few ids. + for (int id = 2; id <= 5 && sheet == null; id++) { + sheet = zip.GetWorksheetStream(id); + } + } + if (sheet == null) { + return null; + } + using (sheet) + using (var ms = new MemoryStream()) { + sheet.CopyTo(ms); + return ms.ToArray(); + } + } finally { + zip.Dispose(); + } + } + } + + private static bool ReadExact(Stream stream, byte[] buffer, int count) { + int offset = 0; + while (offset < count) { + int n = stream.Read(buffer, offset, count - offset); + if (n <= 0) { + return false; + } + offset += n; + } + return true; + } + } +} diff --git a/src/test.sh b/src/test.sh index 446aed9..f32ddae 100755 --- a/src/test.sh +++ b/src/test.sh @@ -30,3 +30,4 @@ run -t md -mmd -xls test/test6_crossline.xlsx run -t md -xls test/test14_late_columns.xlsx run -t md -xls test/test10_nostyles.xlsx run -t md -xls test/test8_issue8_hyperlinks.xlsx +run -t tcpstream -xlsx test/test1.xlsx -biff test/test8.xls