Goal
My goal is to create a TCP server/client class that can be universally reused in various private projects. It should work stable and does not need to be super fast, but rather light-weight to even work on a Raspberry Pi, for example. One TCP connection at the time would be enough for me.
Question
Since I am somewhat new to C# and not an expert on TCP-communications, I would like to know, if the "lock-mechanism" is correctly implemented and if there is any better way of handling errors. In addition to that, I would be glad to hear general improvements to the code or my coding style.
Code
In addition to the code below, I have a link to my project on GitHub as well as a link to the testing program that I created for this class/lib. NOTE: The class is targeted to .NET Standard 2.0
using System;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Net;
using System.Collections;
namespace TcpConnection_Lib
{
public class TcpConnection : IDisposable
{
//fields and properties:
private TcpClient client;
private TcpListener listener;
private Thread ListenThread;
private Thread TcpReaderThread;
public string RemoteEndpointAddress { get; private set; }
private readonly Queue ReceivedStringQueue = new Queue();
public bool TcpIsConnected
{
get
{
if (client != null)
{
return client.Connected;
}
else
{
return false;
}
}
}
private readonly byte[] receiveBuffer = new byte[4096];
private readonly object syncLock = new object();
//methods:
public bool Connect(string IP, int port)
{
try
{
bool successFlag = false;
lock (syncLock)
{
try
{
client = new TcpClient();
client.Connect(IP, port);
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
if (TcpReaderThread != null)
{
TcpReaderThread.Abort();
TcpReaderThread = null;
}
TcpReaderThread = new Thread(ReadData)
{
IsBackground = true
};
TcpReaderThread.Start();
successFlag = true;
}
catch { }
}
return successFlag;
}
catch
{
return false;
}
}
public bool Disconnect()
{
try
{
lock (syncLock)
{
try
{
if (TcpReaderThread != null)
{
TcpReaderThread.Abort();
TcpReaderThread = null;
}
if (client != null)
{
client.Client.Close();
client.Close();
client = null;
}
if (ReceivedStringQueue.Count > 0)
{
ReceivedStringQueue.Clear();
}
}
catch { }
}
return true;
}
catch
{
return false;
}
}
public bool Send(string sendString)
{
try
{
bool successFlag = false;
lock (syncLock)
{
try
{
client.Client.Send(ASCIIEncoding.ASCII.GetBytes(sendString));
successFlag = true;
}
catch { }
}
return successFlag;
}
catch
{
return false;
}
}
public string GetReceivedString()
{
try
{
string returnString = "";
lock (ReceivedStringQueue.SyncRoot)
{
try
{
if (ReceivedStringQueue.Count > 0)
{
returnString = ReceivedStringQueue.Dequeue().ToString();
}
}
catch { }
}
return returnString;
}
catch
{
return "";
}
}
public bool Listen(int port)
{
try
{
IPEndPoint ipLocalEndPoint = new IPEndPoint(IPAddress.Any, port);
listener = new TcpListener(ipLocalEndPoint);
listener.Start(port);
if (ListenThread != null)
{
ListenThread.Abort();
ListenThread = null;
}
ListenThread = new Thread(ListeningMethod)
{
IsBackground = true
};
ListenThread.Start();
return true;
}
catch
{
return false;
}
}
public void Dispose()
{
try
{
lock (syncLock)
{
try
{
Disconnect();
if (listener != null)
{
listener.Stop();
listener = null;
}
if (client != null)
{
client.Close();
client = null;
}
if (ListenThread != null)
{
ListenThread.Abort();
ListenThread = null;
}
if (TcpReaderThread != null)
{
TcpReaderThread.Abort();
TcpReaderThread = null;
}
if (ReceivedStringQueue.Count > 0)
{
ReceivedStringQueue.Clear();
}
}
catch { }
}
GC.SuppressFinalize(this);
}
catch { }
}
private void ListeningMethod()
{
try
{
while (true)
{
try
{
client = listener.AcceptTcpClient();
RemoteEndpointAddress = client.Client.RemoteEndPoint.ToString();
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
if (TcpReaderThread != null)
{
TcpReaderThread.Abort();
TcpReaderThread = null;
}
TcpReaderThread = new Thread(ReadData)
{
IsBackground = true
};
TcpReaderThread.Start();
}
catch
{
if (listener != null)
{
listener.Stop();
listener = null;
}
break;
}
}
}
catch { }
}
private void ReadData()
{
try
{
int bytesRead = 0;
while (true)
{
if (!client.Connected)
{
break;
}
bytesRead = client.GetStream().Read(receiveBuffer, 0, receiveBuffer.Length);
if (bytesRead == 0)
{
break;
}
CopyReceived(Encoding.ASCII.GetString(receiveBuffer, 0, bytesRead));
}
}
catch { }
}
private void CopyReceived(string receivedData)
{
try
{
lock (ReceivedStringQueue.SyncRoot)
{
try
{
ReceivedStringQueue.Enqueue(receivedData);
}
catch { }
}
}
catch { }
}
}
}
EDIT (as VisualMelon suggested):
The API has following interfaces:
methods:
- bool Connect(string IP, int port) - returns true, if the client could connect to the server
- bool Disconnect() - returns true, if all connections could be successfully closed
- bool Listen(int port) - returns true, if the listener could be successfully started
- bool Send(string sendString) - returns true, if the string could be successfully sent
- string GetReceivedString() - returns the received string or an empty string, if nothing got received
- void Dispose() - runs Disconnect() and disposes everything
properties:
- RemoteEndpointAddress - address of the client that connected to the server
- TcpIsConnected - is true, if a TCP client is connected
A simple demonstration of a server implementation with my API:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TcpConnection_Lib;
namespace Tcp_Lib_Server_Tester
{
class Program
{
static TcpConnection connection = new TcpConnection();
static void Main(string[] args)
{
connection.Listen(23); //starts a TCP listener on port 23
while (!connection.TcpIsConnected) //wait until a client connects
{
System.Threading.Thread.Sleep(100);
}
Console.Write("remote endpoint address: " + connection.RemoteEndpointAddress + Environment.NewLine); //prints the remote endpoint IP to the console
bool successfulSendFlag = connection.Send("Hello world!"); //the server sends "Hello World!" to the remote peer and returns if the operation was successful
if (successfulSendFlag)
{
Console.Write("String successfully sent." + Environment.NewLine);
}
else
{
Console.Write("String NOT successfully sent." + Environment.NewLine);
}
string tempReceiveString = "";
while (tempReceiveString == "")
{
tempReceiveString = connection.GetReceivedString(); //returns the received string or an empty string, if nothing got received so far
}
Console.Write("Received: " + tempReceiveString + Environment.NewLine); //prints the received string to the console
Console.ReadLine(); //keeps the console alive
}
}
}
(The client implementation would use "Connect(ip, port)" instead of "Listen(port)" and you could not print the "RemoteEndpointAddress".)
While writing this sample code I discovered a hidden bug. If you check the RemoteEndpointAddress
exactly after the moment where a client connects to the server, then the RemoteEndpointAddress
would be null (therefore I implemented the Sleep()
in the first while-loop of the API-example-code). My first proposed solution would be to lock-in the "get" of the TcpIsConnected
-property as well as the inner part of the while-loop in the ListeningMethod()
, but this would block the TcpIsConnected
-property forever since AcceptClient()
is a blocking method. This means that I have to find a better way to fix this bug...
About the "lock-in" mechanism: I don't have any special use case other than preventing bugs (like the one mentioned above).
EDIT (as dfhwze suggested):
Main goals:
- learn C# and OOP
- learn about TCP
- improve my coding style and architecture
- create a universal library for any future projects that may come up, but nothing specific (theoretical uses: sending and receiving commands between single-board computers)
- create a complete bugfree, well tested and good to read code
Single client instance:
Thank you for pointing that out, because I totally overlooked this limitation!
Server or Client:
This class should either be used as a client or as a server, but not both at the same time.