[C#] Try searching and receiving data over TCP/UDP
Tags: csharp, dotnet
Published: 2023-05-02 16:55:43

## Intro
In this time, I will try using sockets to send and receive data over TCP/UDP.

### Environments
* .NET ver.7.0.105

## TCP
I can use TcpClient to send and receive data over TCP.

* [TcpClient Class(System.Net.Sockets) - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient?view=net-7.0)
* [Use Sockets to send and receive data over TCP - .NET - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/sockets/socket-services)

#### TcpSender.cs
```cs
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace SocketSample.Networks;

public class TcpSender
{
    public async Task SendAsync(IPAddress? ipAddress, string message)
    {
        if(string.IsNullOrEmpty(message))
        {
            return;
        }
        await SendAsync(ipAddress, Encoding.UTF8.GetBytes(message));
    }
    public async Task SendAsync(IPAddress? ipAddress, byte[] data)
    {
        if(ipAddress == null ||
            data.Length <= 0)
        {
            return;
        }
        using var client = new TcpClient();
        await client.ConnectAsync(new IPEndPoint(ipAddress, 13));
        await using var stream = client.GetStream();
        await stream.WriteAsync(data);
    }
}
```

#### TcpReceiver.cs
```cs
using System.Net;
using System.Net.Sockets;

namespace SocketSample.Networks;

public class TcpReceiver
{
    public Action<byte[]>? OnReceived;
    public async Task ReceiveAsync(CancellationToken cancel)
    {
        // To accept access from other machines, I have to set "IPAddress.Any".
        var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
        var listener = new TcpListener(ipEndPoint);
        listener.Start();

        using TcpClient client = listener.AcceptTcpClient();
        await using NetworkStream stream = client.GetStream();
        var buffer = new byte[1_024];
        while(true)
        {
            if(cancel.IsCancellationRequested)
            {
                break;
            }
            var received = await stream.ReadAsync(buffer, 0, buffer.Length, cancel);
            if(received == 0)
            {
                await Task.Delay(100);
                continue;
            }
            OnReceived?.Invoke(buffer);
        }
        listener.Stop();
    }
}
```

## UDP
I can use UdpClient to send and receive data over UDP like TCP.

* [UdpClient Class(System.Net.Sockets) - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.udpclient?view=net-7.0)

#### UdpSender.cs
```cs
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace SocketSample.Networks;

public class UdpSender
{
    public async Task SendAsync(IPAddress? ipAddress, string message)
    {
        if(string.IsNullOrEmpty(message))
        {
            return;
        }
        await SendAsync(ipAddress, Encoding.UTF8.GetBytes(message));
    }
    public async Task SendAsync(IPAddress? ipAddress, byte[] data)
    {
        if(ipAddress == null ||
            data.Length <= 0)
        {
            return;
        }
        var ipEndPoint = new IPEndPoint(ipAddress, 13);
        using var udpClient = new UdpClient();
        await udpClient.SendAsync(data, data.Length, ipEndPoint);
    }
}
```

#### UdpReceiver.cs
```cs
using System.Net;
using System.Net.Sockets;

namespace SocketSample.Networks;

public class UdpReceiver
{
    public Action<byte[]>? OnReceived;
    public async Task ReceiveAsync(CancellationToken cancel)
    {
        // To accept access from other machines, I have to set "IPAddress.Any".
        var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
        using var udpClient = new UdpClient(ipEndPoint);
        while(true)
        {
            if(cancel.IsCancellationRequested)
            {
                break;
            }
            var received = await udpClient.ReceiveAsync(cancel);
            if(received.Buffer.Length > 0)
            {
                this.OnReceived?.Invoke(received.Buffer);
            }
        }
    }
}
```

## Switching roles with command line arguments
Because I don't want to create four applications for sending and receiving data over TCP and UDP, I decided switch the processing with command line arguments.
In this time, I used "System.CommandLine".

I can get the result by EventHandler, but because I want to wait for the result of "RootCommand.Invoke", I used "TaskCompletionSource".

* [Tutorial: Get started with System.CommandLine - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial)
* [TaskCompletionSource<TResult> Class - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1?view=net-7.0)

#### ArgsParser.cs
```cs
using System.Net;
using System.CommandLine;

namespace SocketSample.Commands;

public enum Protocol
{
    Tcp = 0,
    Udp,
}
public enum Role
{
    Send = 0,
    Receive
}
public enum DataType
{
    Text = 0,
}
public record CommandLineArgs(Protocol Protocol, Role Role, DataType DataType, IPAddress? IPAddress, string Value);
public class ArgsParser
{
    public Task<CommandLineArgs> ParseAsync(string[] args)
    {
        // For waiting the result of "RootCommand.Invoke"
        var task = new TaskCompletionSource<CommandLineArgs>();
        // Specify option name(-p), type of input value(string), description("Protocol: ~"), and default value(empty string).
        var protocolOption = new Option<string>(
            name: "-p",
            description: "Protocol: TCP or UDP",
            getDefaultValue: () => "");
        var ipAddressOption = new Option<string>(
            name: "-a",
            description: "IPAddress to send",
            getDefaultValue: () => "");
        var rollOption = new Option<string>(
            name: "-r",
            description: "Role: Send or Receive",
            getDefaultValue: () => "");
        var dataTypeOption = new Option<string>(
            name: "-t",
            description: "DataType: Text or file path",
            getDefaultValue: () => "");
        var valueOption = new Option<string>(
            name: "-v",
            description: "Value to send",
            getDefaultValue: () => "");
        var rootCommand = new RootCommand("TCP/UDP sample");
        rootCommand.AddOption(protocolOption);
        rootCommand.AddOption(ipAddressOption);
        rootCommand.AddOption(rollOption);
        rootCommand.AddOption(dataTypeOption);
        rootCommand.AddOption(valueOption);
        
        rootCommand.SetHandler((protocol, ipAddress, roll, dataType, value) => 
        {
            task.SetResult(new CommandLineArgs(ParseProtocol(protocol), ParseRole(roll),
                ParseDataType(dataType), ParseIPAddress(ipAddress), value));
        },
        protocolOption, ipAddressOption, rollOption, dataTypeOption, valueOption);
        rootCommand.Invoke(args);
        return task.Task;
    }
    private Protocol ParseProtocol(string protocolText)
    {
        switch(protocolText.ToUpper())
        {
            case "TCP":
                return Protocol.Tcp;
            case "UDP":
                return Protocol.Udp;
            default:
                return Protocol.Tcp;
        }
    }
    private Role ParseRole(string roleText)
    {
        switch(roleText.ToUpper())
        {
            case "SEND":
                return Role.Send;
            case "RECEIVE":
                return Role.Receive;
            default:
                return Role.Send;
        }
    }
    private DataType ParseDataType(string dataTypeText)
    {
        switch(dataTypeText.ToUpper())
        {
            case "TEXT":
                return DataType.Text;
            default:
                return DataType.Text;
        }
    }
    private IPAddress? ParseIPAddress(string ipAddressText)
    {
        Console.WriteLine(ipAddressText);
        if(string.IsNullOrEmpty(ipAddressText) ||
            IPAddress.TryParse(ipAddressText, out var ipAddress) == false)
        {
            return null;
        }
        return ipAddress;
    }
}
```

Now, I can call these classes like below.

#### Program.cs
```cs

using SocketSample.Commands;
using SocketSample.Networks;

var parser = new ArgsParser();
var parsedArgs = await parser.ParseAsync(args);
await ExecuteAsync(parsedArgs);

async Task ExecuteAsync(CommandLineArgs args)
{
    var cancel = new CancellationTokenSource();
    switch(args.Protocol)
    {
    case Protocol.Tcp:
        await ExecuteTcpAsync(args);
        break;
    case Protocol.Udp:
        await ExecuteUdpAsync(args);
        break;
    }
}
async Task ExecuteTcpAsync(CommandLineArgs args)
{
    var cancel = new CancellationTokenSource();
    switch(args.Role)
    {
    case Role.Send:
        var sender = new TcpSender();
        await sender.SendAsync(args.IPAddress, args.Value);
        break;
    case Role.Receive:
        var receiver = new TcpReceiver();
        receiver.OnReceived += (data) => {
            Console.WriteLine($"Receive {System.Text.Encoding.UTF8.GetString(data)}");
        };
        await receiver.ReceiveAsync(cancel.Token);
        cancel.Cancel();
        break;
    }
}
async Task ExecuteUdpAsync(CommandLineArgs args)
{
    var cancel = new CancellationTokenSource();
    switch(args.Role)
    {
    case Role.Send:
        var sender = new UdpSender();
        await sender.SendAsync(args.IPAddress, args.Value);
        break;
    case Role.Receive:
        var receiver = new UdpReceiver();
        receiver.OnReceived += (data) => {
            Console.WriteLine($"Receive {System.Text.Encoding.UTF8.GetString(data)}");
        };
        await receiver.ReceiveAsync(cancel.Token);
        cancel.Cancel();
        break;
    }
}
```