I'm studying C# and trying to get the code below to parse an incoming JSON recipe string that I convert to an XML document and I'm getting the following error
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
at System.Text.Json.Utf8JsonReader.ConsumeStringAndValidate(ReadOnlySpan`1 data, Int32 idx)
at System.Text.Json.Utf8JsonReader.ConsumeString()
at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
at System.Text.Json.Utf8JsonReader.Read()
at System.Text.Json.JsonDocument.Parse(ReadOnlySpan`1 utf8JsonSpan, Utf8JsonReader reader, MetadataDb& database, StackRowStack& stack)
at System.Text.Json.JsonDocument.Parse(ReadOnlyMemory`1 utf8Json, JsonReaderOptions readerOptions, Byte[] extraRentedBytes)
at System.Text.Json.JsonDocument.Parse(ReadOnlyMemory`1 json, JsonDocumentOptions options)
at System.Text.Json.JsonDocument.Parse(String json, JsonDocumentOptions options)
I'm unsure as to why it's not working as the data I send to parse is a string. It works for simple strings however when sending a 'recipe' it throws the exception. The recipe is just a string that I create an XML document from (learning exercise). The recipe code is valid but it seems the JSON reader has issues parsing this string, I could be wrong in this. Here's the code:
TCP receive data method
private async Task Listen()
{
try
{
while (true)
{
Token.ThrowIfCancellationRequested();
TcpClient client = await server.AcceptTcpClientAsync();
Console.WriteLine("Connected!");
await Task.Run(async () => await HandleDevice(client), Token);
}
}
catch (SocketException e)
{
Console.WriteLine("Exception: {0}", e);
}
}
private async Task HandleDevice(TcpClient client)
{
string imei = String.Empty;
string data = null;
Byte[] bytes = new Byte[80196];
int i;
try
{
using (stream = client.GetStream())
{
while ((i = await stream.ReadAsync(bytes, 0, bytes.Length, Token)) != 0)
{
Token.ThrowIfCancellationRequested();
string hex = BitConverter.ToString(bytes);
data = Encoding.UTF8.GetString(bytes, 0, i);
Console.WriteLine(data);
processData(data);
}
}
}
catch (OperationCanceledException) { }
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.ToString());
}
finally
{
client.Close();
}
}
Process data method
public static void processData(string data)
{
var jsonOptions = new JsonDocumentOptions
{
AllowTrailingCommas = true,
};
using (JsonDocument document = JsonDocument.Parse(data, jsonOptions))
{
JsonElement root = document.RootElement;
// FOR DEBUGGING
// foreach (JsonProperty element in root.EnumerateObject())
//{
// Console.WriteLine($"{element.Name} ValueKind={element.Value.ValueKind} Value={element.Value}");
//}
// Parse the data response
if (root.TryGetProperty("data", out JsonElement contentToParse))
{
if (string.IsNullOrEmpty(contentToParse.GetString()) == false)
{
// Data to parse
if (contentToParse.GetString().Contains("RECIPE:"))
{
string[] recipe = contentToParse.GetString().Split(':');
sendToFront(recipe[1]);
}
else
{
Console.WriteLine("Something else to parse {0}", contentToParse.GetString());
}
}
}
}
}
My incoming JSON String looks like this: {"user": "me", "data": "RECIPE:<?xml version='1.0'..."}
- The response is valid JSON and tested by online validators.
UPDATE: I've inspected the issue via console and it appears that only part of the recipe is received which could be causing the issue. How can I get the full JSON string before sending it to the processData
method?
CodePudding user response:
As mentioned by @StephenCleary, you need some kind of framing mechanism.
A simple one is to just prefix each string with its length when sending it. Then you read the length, and use StreamReader
to read that many bytes.
private async Task HandleDevice(TcpClient client)
{
Char[] length = new Char[2];
Char[] data = new Char[80196];
try
{
using (var stream = client.GetStream())
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
while ((i = await reader.ReadBlockAsync(bytes.AsMemory(), Token)) != 0)
{
var toRead = ((int)length[0]) | (((int)length[1]) << 16);
var charsRead = await reader.ReadBlockAsync(data.AsMemory(0, toRead), Token);
var str = new string(data, 0, charsRead);
Console.WriteLine(str);
if (charsRead != toRead)
throw new EndOfStreamException("Unexpected end of stream");
processData(str);
Array.Clear(data);
}
}
}
catch (OperationCanceledException) { }
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.ToString());
}
finally
{
client.Close();
}
}
There are more efficient implementations, but this should suffice for most purposes.
CodePudding user response:
There's nothing wrong with the JSON parser. It's getting an incomplete string from your socket code.
The best and easiest solution (by far) is to communicate at a higher abstraction; i.e., self-host ASP.NET Core instead of reinventing it using a custom TCP/IP protocol.
But if you must use a custom protocol, you'll need to design it correctly. I have a blog series that may help; in particular note the article on message framing, which is what is causing this particular problem. For a more complete guide, I have a video series on properly implementing a custom protocol using .NET - it is not for the faint of heart.