DEV Community

Masui Masanori
Masui Masanori

Posted on • Edited on

[C#] Try searching and receiving data over TCP/UDP

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.

TcpSender.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

TcpReceiver.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();
    }
}
Enter fullscreen mode Exit fullscreen mode

UDP

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

UdpSender.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

UdpReceiver.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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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".

ArgsParser.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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, I can call these classes like below.

Program.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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
kalkwst profile image
Kostas Kalafatis

Hey there, thanks for sharing your post with the community! I really appreciate the effort you put into it. I noticed that you included some code snippets, but it would be really helpful if you could add some explanations or comments to them. While a seasoned developer might be able to understand the code, newer members might find it a bit challenging. Adding some explanations or comments would make it a lot easier for everyone to follow along and learn from your post.

Thanks again for sharing, and I look forward to seeing more of your contributions in the future!

Collapse
 
masanori_msl profile image
Masui Masanori

Thank you for reading my post.
And thank you for your suggestions.
I will add some code snippets as soon as possible :)

Collapse
 
hendrik_boszschlenski_71c profile image
Hendrik Boszschlenski

There is one issue regarding the cancellation tokens within the ReceiveAsync methods. While the method itself checks the IsCancellationRequested property and ends the loop gracefully, the Udp and Tcp client's ReceiveAsync methods will throw an OperationCancelledException which will be attached to the returning task, instead of ending the loop gracefully.