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
- Use Sockets to send and receive data over TCP - .NET - Microsoft Learn
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);
}
}
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();
}
}
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);
}
}
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);
}
}
}
}
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
- TaskCompletionSource Class - Microsoft Learn
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;
}
}
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;
}
}
Top comments (3)
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!
Thank you for reading my post.
And thank you for your suggestions.
I will add some code snippets as soon as possible :)
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.