Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/streaming-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,17 @@ A progressive renderer can subscribe to `IPackageParser<T>.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`.
15 changes: 15 additions & 0 deletions src/Exceltk/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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]");
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/Exceltk/Reader/ExcelBinaryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ public void Open(Stream fileStream) {
m_SheetIndex=0;
}

/// <summary>
/// Rewind the workbook BIFF cursor and stream record packages
/// (for progressive consumers / TCP demos).
/// </summary>
internal System.Collections.Generic.IEnumerable<Exceltk.Reader.Package.BinaryPackage> 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;
Expand Down
250 changes: 250 additions & 0 deletions src/Exceltk/Test/TcpStreamingDemo.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
/// End-to-end TCP demo: server sends Excel payload in small chunks; client uses
/// streaming package parsers and prints packages as they complete.
/// </summary>
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;
}

/// <summary>
/// Server streams worksheet XML bytes; client XmlPackageParser emits row/dimension packages live.
/// </summary>
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;
}

/// <summary>
/// Server sends length-prefixed BIFF record packages; client rebuilds BinaryPackage records live.
/// </summary>
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;
}
}
}
1 change: 1 addition & 0 deletions src/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading