Domanda

I am writing a client library for a model railway controller over TCP. The server is embedded in the control unit.

When the client sends a command to the server, for example set(5, addr[3]) the server responds with a reply header <REPLY set(5, addr[3])>, results for that command and if there was an error.

Because all this stuff is asynchronous, I have to match the replies with the commands. (Even if the client would only send one command and then wait for the response there are serverside events)

To have a good and easy understandable interface to this library I use the async await pattern. This means the client code makes a call like await client.Set(5, "addr", "3") the code continues after the server sends back the response and the response is evaluated by the client code.

I am currently implementing this with an IDictionary<string, EventWaitHandle>where the string is the command and the EventWaitHandle is awaited in the method SendeBefehlAwaitResonse(string befehl) with await Task.Run(() => signal = e.WaitOne(timeout));

Is there a more common way to do that? For the NetworkClient I also first used a EventWaitHandle to wait for new messages to send (and to use my MessageDelay) property. I found out that using an infinite loop calling await Task.Delay(100); has way better performance.

Question:

  • Is there a better way to await server responses? Maybe with Reactive Extensions or some other library?

If I have to rewrite parts of the library this isn't a big deal for me. I write the library mostly for learning purposes. Although the code (and mostly the TCP client) is somehow hacked code, I try my best to give a better and easy to understand structure to the project.

Thanks in advance for your help!

You can find the code here: https://github.com/schjan/RailNet | Message Dispatcher Class

È stato utile?

Soluzione

Rx would probably make your life a lot easier. I would also imagine that it would reduce some of the potential race-conditions in your code and also just end up with a lot less plumbing style code (ie the code to maintain the cache).

If I start off by taking what I think are the key elements of the code (from https://github.com/schjan/RailNet/blob/master/src/RailNet.Clients.Ecos/Basic/NachrichtenDispo.cs) and pull them out into methods I see this.

private bool HasBeginAndEnd(string[] message)
{
    bool isValid = true;

    if (!message[0].StartsWith("<") || !message[0].EndsWith(">"))
        isValid = false;

    if (!message.Last().StartsWith("<END"))
        isValid = false;

    return isValid;
}
private bool IsReplyMessage(string[] message)
{
    return message.Length>0 && message[0].StartsWith("<REPLY ");
}
private BasicAntwort ParseResponse(string[] message)
{
    string header = message[0].Substring(7, message[0].Length - 8);
    return new BasicAntwort(message, header);
}

Using these nice little descriptive methods I can use Rx to create an observable sequence of your responses.

var incomingMessages = Observable.FromEventPattern<MessageReceivedEventArgs>(
    h => _networkClient.MessageReceivedEvent += h,
    h => _networkClient.MessageReceivedEvent -= h)
.Select(x => x.EventArgs.Content)
.Where(HasBeginAndEnd)
.Where(IsReplyMessage)
.Select(ParseResponse);

Cool, now we have an incoming stream/sequence.

Next we want to be able to Issue commands and have the appropriate response returned for it. Rx can do this too.

incomingMessages.Where(reply=>reply.Header == befehl)

To continue we also want to add a timeout, so that if we dont get a response from out command for a given time (8000ms?) we should throw. We can convert this to a task, if we only want the single value, then we can do this with Rx too.

incomingMessages.Where(reply=>reply.Header == befehl)
                .Timeout(TimeSpan.FromSeconds(2))
                .Take(1)
                .ToTask();

Nearly done now. We just want a way to send the command and return the task with the response (or the timeout). No problem. Just subscribe first to our incoming message sequence, to avoid race conditions, then issue the command.

public Task<BasicAntwort> SendCommand(NetworkClient networkClient, string befehl)
{
    //Subscribe first to avoid race condition.
    var result = incomingMessages
                        .Where(reply=>reply.Header == befehl)
                        .Timeout(TimeSpan.FromSeconds(2))
                        .Take(1)
                        .ToTask();

    //Send command
    networkClient.SendMessage(befehl);

    return result;
}   

Here is the entire code as a LinqPad script

    void Main()
    {
        var _networkClient = new NetworkClient();
        var sendCommandTask = SendCommand(_networkClient, "MyCommand");
        BasicAntwort reply = sendCommandTask.Result;
        reply.Dump();
    }


    private static bool HasBeginAndEnd(string[] message)
    {
        bool isValid = true;

        if (!message[0].StartsWith("<") || !message[0].EndsWith(">"))
            isValid = false;

        if (!message.Last().StartsWith("<END"))
            isValid = false;

        return isValid;
    }
    private static bool IsReplyMessage(string[] message)
    {
        return message.Length>0 && message[0].StartsWith("<REPLY ");
    }
    private static BasicAntwort ParseResponse(string[] message)
    {
        string header = message[0].Substring(7, message[0].Length - 8);
        return new BasicAntwort(message, header);
    }

    public IObservable<BasicAntwort> Responses(NetworkClient networkClient)
    {
        return Observable.FromEventPattern<MessageReceivedEventArgs>(
                h => networkClient.MessageReceivedEvent += h,
                h => networkClient.MessageReceivedEvent -= h)
            .Select(x => x.EventArgs.Content)
            .Where(HasBeginAndEnd)
            .Where(IsReplyMessage)
            .Select(ParseResponse);
    }   

    public Task<BasicAntwort> SendCommand(NetworkClient networkClient, string befehl)
    {
        //Subscribe first to avoid race condition.
        var result = Responses(networkClient)
                            .Where(reply=>reply.Header == befehl)
                            .Timeout(TimeSpan.FromSeconds(2))
                            .Take(1)
                            .ToTask();

        //Send command
        networkClient.SendMessage(befehl);

        return result;
    }   


public class NetworkClient
{
public event EventHandler<MessageReceivedEventArgs> MessageReceivedEvent;
public bool Connected { get; set; }
public void SendMessage(string befehl)
{
    var handle = MessageReceivedEvent;
    if(handle!=null){

        var message = new string[3]{"<REPLY " + befehl +">", "Some content", "<END>"};

        handle(this, new UserQuery.MessageReceivedEventArgs(){Content=message});
    }
}
}
public class MessageReceivedEventArgs : EventArgs
{
public string[] Content { get; set; }
}

public class BasicAntwort
{
    public BasicAntwort(string[] message, string header)
    {
        Header = header;
        Message = message;
    }

    public string Header { get; set; }
    public string[] Message { get; set; }
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top