Reputation: 29
I want to implement a network client that handles some specific protocol. The easiest option I see is to write NetworkStream
descendant class that processes protocol-specific features. So I created class with sample protocol: all data is transferred wrapped into [[
and ]]
. Client must receive it transparently.
As a simplest server I run ncat with the following command line
>echo [[pong]] | ncat -l -p 2222
and here's the program (don't judje it too tough - it's a sample and I'm just learning C# - awkward clauses possible)
using System;
using System.Text;
using System.Net.Sockets;
using System.Threading.Tasks;
// Sample network stream that wraps all sent and received data into "[[" and "]]"
class MyNetworkStream : NetworkStream
{
public MyNetworkStream(Socket socket)
: base(socket)
{
}
public override int Read(byte[] buffer, int offset, int count)
{
// Read and unwrap
byte[] wrapperBuf = new byte[2];
byte[] contentBuf = new byte[count];
base.Read(wrapperBuf, 0, wrapperBuf.Length);
int res = base.Read(contentBuf, 0, contentBuf.Length);
contentBuf.AsSpan(0, res - 2).CopyTo(buffer.AsSpan(offset));
return res;
}
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, Object state)
{
throw new Exception("BeginRead is called"); // doesn't happen!
}
}
namespace HelloWorld
{
class Program
{
static async Task MainAsync()
{
Socket sock = new Socket(SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync("localhost", 2222);
MyNetworkStream stm = new MyNetworkStream(sock);
byte[] buf = new byte[1024];
int read = await stm.ReadAsync(buf.AsMemory());
Console.WriteLine(Encoding.UTF8.GetString(buf.AsSpan(0, read))); // prints "[[pong]]"
}
static void Main(String[] args)
{
// Main async loop
MainAsync().GetAwaiter().GetResult();
}
}
}
I succeeded in overriding sync Read
but I want async as well. From what I've read in some articles and also in the sources overriding Begin* should be enough to have *Async working. But it's not: exception is not risen and [[pong]]
is written to console so none of my custom methods is called.
(solved, see edit below) I also tried overriding ReadAsync:
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
throw new Exception("ReadAsync is called"); // doesn't happen
}
but it doesn't get called either.
The questions:
==== Edit ====
issue with ReadAsync was solved by overriding the proper overload.
// NetworkStream has 2 ReadAsync overloads directly calling undeluying socket methods instead of
// referring to BeginRead so we have to override them both.
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
{
return this.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
throw new Exception("ReadAsync is called");
}
initial issue was solved with the hint from Fildor (NetworkStream doesn't call Begin* from its *Async overrides). However I'll likely rework my design to another principle as Markus suggested.
Upvotes: 2
Views: 261
Reputation: 22501
As you'd already proposed, I'd also vote for creating wrapper class that holds a (Network)Stream
and by that following the principle "composition over inheritance". Why?
Inheritance is a very strong relationship. As your example and the search for which ReadAsync
method to override shows, you depend on the implementation of the base class and that it calls the respective ReadAsync
overload. If the base class changes this later on, your code might not work anymore.
If using composition on the other hand, you rely on the outer interface of (Network)Stream
and are much more flexible. Instead of changing the behavior of NetworkStream
, you might be able to create a class, that uses a generic stream as an input and does not rely on NetworkStream
.
public class MyNetworkStream : Stream
{
private readonly Stream _underlyingStream;
public MyNetworkStream(Stream underlyingStream)
{
_underlyingStream = underlyingStream;
}
// ...
}
Thus, you could inject (pass a constructor parameter) any stream into the class and are not restricted to always use a NetworkStream
. This might proove valuable when testing the functionality of your stream.
Above example derives from Stream
as you proposed. The Stream
class is a common denominator so that you can use your class in a variety of scenarios. However, you need to implement the complete interface of a Stream
.
If you want to have a more minimal interface of the class, you could omit inheriting from Stream
and create an interface for the class that reflexts exactly the needs of your scenario.
Besides streams, there are also the *Reader
and *Writer
classes in the System.IO
namespace. From a conceptual point of view, your wrapper class could be a MyProtocolReader
that holds a reference to a stream, reads and adjusts the retrieved contents (e.g. remove the leading and trailing parentheses).
Upvotes: 1