Spksh/TentacleSoftware.Telnet

Possible support for ReadAsync()

Closed this issue · 5 comments

A use case that I have is I need to read in the data coming back from the server as it's coming in (I will not always get a line feed). The "ReadLineAsync()" obviously waits for the line while data could be pending sometimes for a good while and there's not currently another way to change the behavior. My code might need to be tweaked, but I had changed it to this while testing in order to read the stream as it was coming in and it seemed to work well for what I was doing (I arbitrarily picked 4096 as the buffer length):

var receiveBuffer = new char[4096];
int dataReceivedLength = await _tcpReader.ReadAsync(receiveBuffer, 0, receiveBuffer.Length);
message = new string(receiveBuffer);

Perhaps having a property that would allow WaitForMessage to use one or the other (ReadLineAsync vs. ReadAsync)? The ReadAsync slightly changes what OnMessageReceived would bubble up because the because EOL markers aren't stripped out but the caller should realize that.

As an aside, I love the verify simple and straightforward design of the client class you put together.

Spksh commented

That's interesting.

Do you need each incoming character, or each incoming byte?

As far as I'm aware, the telnet protocol requires an EOL marker between messages; it doesn't operate as a stream. Is this for some kind of debugging?

Your response made me realize I'm mixing metaphors so my suggestion is probably not suited for this project (my requirement may deviate from telnet and in my narrow site on what I was looking for I didn't think about that).

In the end I suppose I care about the characters. I'm connecting to a text based game server (a mud) that is spitting down text in real time but may not always send a line feed at the end (and in the waiting text the player may need to see something that is relevant).

Thank you for your response, I'll close this because it probably falls out of the scope of this.

Spksh commented

Thanks for the ask, it's an interesting requirement.

I'm not sure reusing my event-driven approach would be that performant per-character, but you're probably not dealing with thousands of characters per second.

Let me know if you end up forking this code; there might be a way we can integrate it into the client.

Thanks for the response.

I'll make a fork when I come up with some time and share it with you. Muds are, line by line based MMO's that happen in real time. Although most line's have proper terminators the prompt does not (although I believe it sends a telnet go ahead marker at the end). The prompt contains the current health (hp) of the player, etc. As a result, if the prompt is sent it will hang until something else with a line terminator follows after it. If no one is in the room causing interactions to be sent it could hold onto the prompt for a bit.

"<1000hp 700m 400mv Town Center (NESWU)>" <-- This line hangs waiting to be read until the following line is sent that does have a terminator. If no following line is sent for say.. 30 seconds, the prompt never prints until that time even though it was sent long ago. Some clients support the telnet go ahead option that they use as an indicator to print the waiting text at that marker.

<1000hp 700m 400mv Town Center (NESWU)> kill cogge
Your stab decimates Cogge!
A sea sword draws life from Cogge.
Cogge parries your attack.
Cogge parries your attack.
Cogge dodges your attack.
Cogge has a few scratches.

<1005hp 700m 400mv Town Center (NESWU)>
Your stab decimates Cogge!
A sea sword draws life from Cogge.
Your slash decimates Cogge!
Cogge parries your attack.
Cogge's cleave DISMEMBERS you!
You dodge Cogge's attack.
You parry Cogge's attack.
Cogge has a few scratches.

<959hp 700m 400mv Town Center (NESWU)>
Cogge parries your attack.
Your stab decimates Cogge!
A sea sword draws life from Cogge.
Your slash decimates Cogge!
Cogge parries your attack.
You dodge Cogge's attack.
You parry Cogge's attack.
You dodge Cogge's attack.
Cogge trips you and you go down!
Cogge's trip scratches you.
The cityguard screams and attacks YOU!
You parry the cityguard's attack.
You parry the cityguard's attack.
Cogge has a few scratches.

<959hp 700m 400mv Town Center (NESWU)> peace
Your stab mauls Cogge.
A sea sword draws life from Cogge.
Cogge parries your attack.
You parry Cogge's attack.
You dodge Cogge's attack.
You parry Cogge's attack.
You parry the cityguard's attack.
You parry the cityguard's attack.
Cogge has a few scratches.

<963hp 700m 400mv Town Center (NESWU)>
You call peace to the room.

<963hp 700m 400mv Town Center (NESWU)>

@Spksh

Another aside, you have a comment that states:

// Due to CR/LF platform differences, we sometimes get empty messages if the server sends us over-eager EOL markers
// Because ReadLine*() strips out the EOL characters, the message can end up empty (but not null!)
// I think this is a server implementation problem rather than our problem to solve
// So just handle empty messages in your consumer library

The clarify, I'm almost certain this is a server implementation problem. ReadLineAsync reads a new line when it finds "\r", "\n" or "\r\n". The problem is some server implementations (especially old ones) will write "\n\r" which has the characters flip flopped. In this case, it treats then each as a new line which will have the effect of producing a blank line that should not be there.

Since ReadLineAsync strips the line feeds out it makes it difficult to try to discern or correct the malformed feed with certainty because you don't know what variation you received. Just removing blank lines may not be the ideal solution if there truly were blank lines sent. In my scenario (which maybe specific to me) I went ahead and read all the data in with ReadAsync, tossed out all carriage returns and then raised an event when each new line feed was found.

I'm not sure that fix would work for everyone but it does explain the behavior of ending up with random blank lines.