Platformanın bütün imkanlarından yararlanmaq üçün
Qeydiyyatdan keç
və ya
Daxil ol
azAzərbaycanca
enİngiliscə
TCP bağlantıların C#-da praktiki qurulması, bir az da socket-lər haqqında
17 oktyabr 2023 22:29
454

TCP bağlantıların C#-da praktiki qurulması, bir az da socket-lər haqqında

Transmission Control Protocol (TCP) şəbəkə rabitəsinin təməl daşıdır. Bu protokol, internet üzərindən rabitə kanalı təmin edir. C#-da şəbəkə proqramları yaratmaq üçün TCP bağlantıların işləmə prinsipini başa düşmək vacibdir. Bu məqalə TCP əlaqənin əsaslarını və onun C# dilində praktiki qurulmasını əhatə edəcək.

Təsvirlər

Müştəri-Qulluqcu Arxitekturası
Müştəri-Qulluqcu Arxitekturası

Müştəri-Qulluqcu Arxitekturası

TCP client-server arxitekturasına əsaslanır. Server müştərilərin qoşulmasını gözləyir və onların sorğularını idarə edir. Müştərilər isə serverlərə qoşulmaqla yanaşı digər müştərilərə müraciət edə bilərlər.

Soket

Soket-lər şəbəkə üzərindən iki maşın arasında əlaqənin qurulması üçün təyin olunan yuvalardı. C# dilinin System.Net.Sockets kitabxanasında həmin yuvalarla işləmək üçün bütün lazımi siniflər var.

TCP protokoldan istifadə edən maşınlar arasında əlaqə yaratmaq üçün ünvanlar və portlar istifadə olunur. Beləliklə bir kommunikasiya yolunun hər iki tərəfində ən azı bir yuva (ünvana və porta uyğun gələn "soket") müəyyən edilir. Hər bir proses “dinləmə” (server) yuvası yarada və onu əməliyyat sisteminin portuna təyin edə bilər. Dinləmə prosesi adətən “yuxu” dövrəsində olur və ancaq yeni müraciət gəldikdə oyanır.

TCP Portlar

TCP, maşında müxtəlif prosesləri ayırd etmək üçün portlardan istifadə edir. Portlar 0-dan 65535-ə qədər dəyişir. Nəzərə alın ki, 1024-dən aşağı olan portlar artıq təyin olunmuş portlar hesab olunur və sistem prosesləri üçün qorunur.

Üçtərəfli ilkin razılaşmanın addımları
Üçtərəfli ilkin razılaşmanın addımları

Üçtərəfli ilkin razılaşma

TCP bağlantısının qurulması üç addımlı prosesdir:

  1. SYN: Müştəri əlaqəni başlamaq üçün serverə SYN (sinxronizasiya) paketini göndərir.
  2. SYN-ACK: Server SYN-i qəbul edir və öz SYN paketini göndərir.
  3. ACK: Müştəri serverin SYN paketini qəbul edir.

…nəticədə müştəri və server arasında bağlantı yaranır.

Struktur

Struktur
Struktur

Program bütövlükdə üç layihədən ibarətdir:

  • Server - bu layihə xüsusi olaraq müştərilərin idarə edilməsi, bildiriş və s. kimi qulluqçu tərəfi əməliyyatları ilə bağlıdır;
  • Common - bu layihədə hər ikisi həm server, həm də müştəri tərəfindən istifadə ediləcək ümumi siniflər və alətlər yerləşir;
  • Client - server(lər)ə qoşulmaq, mesaj göndərmək və s. kimi müştəri tərəfi ilə əlaqəli funksionallıqların layihəsi;

Server

Əvvəla sadə serverin interfeysini təyin edək və gedişat zamanı onu genişləndirək:

namespace Server
public interface IServerTcpMessenger
{
void Start();
void SendMessage(int clientId, string message);
void CloseClient(int clientId);
}

Burada,

Start - dinləmə prosesinə başlamaq üçün bizim giriş nöqtəmizdir;

SendMessage - identifikator ilə müəyyən edilə bilən xüsusi müştəriyə sətir (string) tipli mesaj göndərmək üçün istifadə olunacaq;

CloseClient - nəhayət, müəyyən bir müştəri ilə əlaqəni bağlamaq üçün metod;


İlk baxışdan müştəriləri idarə etmək çox asan görünür, amma bizə axınlıq baxımından daha təhlükəsiz (thread-safe) və etibarlı həll lazımdır. Təhlükəsizliyi təmin etmək üçün ConcurrentDictionary sinfindən istifadə edə bilərik. Müştərilərin idarə edilməsinə gəldikdə isə, Guid-dən istifadəni daha məqsədəuyğun hesab edirəm, çünki o, daha çevikdir, yeniləmə prosesini əl ilə həyata keçirməyə ehtiyac yoxdur və nəhayət, hər hansı bir obyekti unikal şəkildə müəyyən etmək üçün etibarlı üsuldur.

Beləliklə, server artıq fərqli təyin olunur:

namespace Server
public interface IServerTcpMessenger
{
void Start();
void SendMessage(Guid clientUid, string message);
void CloseClient(Guid clientUid);
}


Müştərilərin idarəetmə hissəsini aradan qaldırdıq, indi keçək göndəriləcək mesajın özünə. Məsələ burasındadır ki, müştərilər serverə və ya bir-birinə sadə sətirlər göndərə bilər, lakin gələcəkdə məhdudiyyətlər və ya yeni funksiyalar əlavə olunarsa sadə sətirlər bu prosesi uzada bilər. Beləliklə, indi bizim məqsədimiz məlumatların bölünməsini və qaydaların və ya məhdudiyyətlərin təyin olunmasını asanlaşdıracaq model təyin etməkdir.

namespace Common
public enum TcpMessageType
{
Text,
Notification,
...
}
public class TcpMessage
{
public string Data { get; set; }
public TcpMessageType Type { get; set; }
}

TcpMessageType - bu nömrələnmiş verilənlər tipi mesaj növlərinin diapazonunu asanlıqla genişləndirməyə və konkret halları idarə etməyə imkan verir;

TcpMessage.Data - faktiki mesajı json formatında ehtiva edən sətir tipli xüsusiyyət;


Hər iki tərəfdə daxil olan sorğuları həll etmək üçün bizə daha bir sinif lazımdır. “TcpMessageEventArgs” sinfin köməyi ilə biz bütün lazımi atributları bir konteynerə yığa bilərik.

namespace Common
public class TcpMessageEventArgs : EventArgs
{
public TcpMessage Message { get; set; }
}

Nəzərə alınmalı daha bir şey var, yadda saxlanılan müştərinin fərdi şəkildə idarə olunması. Bunun üçün yeni “MessengerClient” sinfini təyin edək.

namespace Server
public class MessengerClient
{
public bool IsConnected => TcpClient.Connected;
public Guid Uid { get; }
public TcpClient TcpClient { get; }
public MessengerClient(TcpClient tcpClient)
{
Uid = Guid.NewGuid();
TcpClient = tcpClient;
}
public override string ToString()
{
return Uid.ToString();
}
}

Nəhayət server üçün interfeysə uyğun sinif yaradaq

namespace Server
public sealed class ServerTcpMessenger : IServerTcpMessenger
{
private readonly IPEndPoint _endPoint;
private TcpListener _tcpListener;
private Thread _serverThread;
public ConcurrentDictionary<Guid, MessengerClient> ConcurrentClients { get; } = new ConcurrentDictionary<Guid, MessengerClient>();
public ServerTcpMessenger(string address, int port)
{
_endPoint = new IPEndPoint(IPAddress.Parse(address), port);
}
public void Start()
{
_serverThread = new Thread(() =>
{
RunListener();
});
_serverThread.Start();
}
private void RunListener()
{
_tcpListener = new TcpListener(_endPoint);
_tcpListener.Start();
Console.WriteLine("Listening");
while (true)
{
AcceptTcpClient();
}
}
private void AcceptTcpClient()
{
var tcpClient = _tcpListener.AcceptTcpClient();
tcpClient.NoDelay = true;
var messengerClient = new MessengerClient(tcpClient);
var concurrentClient = ConcurrentClients.AddOrUpdate(messengerClient.Uid, messengerClient, (k, v) => v);
}
public void SendMessage(Guid clientUid, TcpMessage tcpMessage)
{
if (tcpMessage is null)
{
throw new ArgumentNullException(nameof(tcpMessage));
}
if (ConcurrentClients.TryGetValue(clientUid, out MessengerClient messengerClient))
{
var sendingThread = new Thread(() =>
{
if (messengerClient.IsConnected)
{
var clientStream = messengerClient.TcpClient.GetStream();
var message = JsonConvert.SerializeObject(tcpMessage);
var buffer = Encoding.Unicode.GetBytes(message);
clientStream.Write(buffer, 0, buffer.Length);
clientStream.Flush();
}
})
{
Name = $"SendingThread-{messengerClient}"
};
sendingThread.Start();
}
else
{
throw new Exception("TcpClientNotFound");
}
}
public void CloseClient(Guid clientUid)
{
if (ConcurrentClients.TryGetValue(clientUid, out MessengerClient messengerClient))
{
messengerClient.TcpClient.Close();
}
else
{
throw new Exception("TcpClientNotFound");
}
}
}

"Start" metodunun daxilində biz serverin işini başlatmaq və əsas proqrama mane olmamaq üçün yeni axın yaradırıq. "RunListener" metodu yeni müştərilər müraciət etdikcə bağlantını qəbul edir. "AcceptTcpClient" metodu müştərinin özünü müştərilər siyahısına xüsusi olaraq əlavə edir. Əvvəlcə planlaşdırıldığı kimi, "SendMessage" metodu server tərəfdən qeyd olunan sətri mesajı uid ilə müəyyən edilmiş müştəriyə göndərir. Nəhayət, "CloseClient” metodu uid ilə müəyyən edilmiş müştərinin bağlantısını ləğv edir.


Müştəri

Eynilə server kimi, müştəri tərəfini də təyin etməyimiz lazımdır

namespace Client
public interface IClientTcpMessenger
{
void Start();
void SendMessage(TcpMessage tcpMessage);
}

Və interfeysə uyğun sinif

namespace Client
public sealed class ClientTcpMessenger : IClientTcpMessenger
{
private TcpClient _tcpClient;
private Thread _clientThread;
private readonly object _startLocker = new();
private readonly IPEndPoint _endPoint;
public delegate void ClientMessageReceivedEventHandler(TcpMessageEventArgs args);
public event ClientMessageReceivedEventHandler MessageReceived;
public ClientTcpMessenger(string address, int port)
{
_endPoint = new IPEndPoint(IPAddress.Parse(address), port);
}
public void Start()
{
lock (_startLocker)
{
_tcpClient = new TcpClient
{
NoDelay = true
};
if (_clientThread == null || !_clientThread.IsAlive)
{
_clientThread = new Thread(() =>
{
RunClient();
})
{
Name = "ClientThread",
};
_clientThread.Start();
}
}
}
private void RunClient()
{
_tcpClient.Connect(_endPoint);
if (_tcpClient.Connected)
{
while (true)
{
var stream = _tcpClient.GetStream();
var buffer = new byte[1024];
var response = new StringBuilder();
var bytes = stream.Read(buffer, 0, buffer.Length);
if (bytes != 0)
{
response.Append(Encoding.Unicode.GetString(buffer, 0, bytes));
while (stream.DataAvailable)
{
bytes = stream.Read(buffer, 0, buffer.Length);
response.Append(Encoding.Unicode.GetString(buffer, 0, bytes));
}
HandleMessageReceive(response.ToString());
}
else
{
break;
}
}
}
_tcpClient.Close();
}
private void HandleMessageReceive(string response)
{
var tcpMessage = JsonConvert.DeserializeObject<TcpMessage>(response);
MessageReceived?.Invoke(new TcpMessageEventArgs
{
Message = tcpMessage
});
}
public void SendMessage(TcpMessage tcpMessage)
{
if (tcpMessage is null)
{
throw new ArgumentNullException(nameof(tcpMessage));
}
if (!_tcpClient.Connected)
{
throw new SocketException((int)SocketError.NotConnected);
}
var stringMessage = JsonConvert.SerializeObject(tcpMessage);
new Thread(() =>
{
if (_tcpClient.Connected)
{
var serverStream = _tcpClient.GetStream();
var buffer = Encoding.Unicode.GetBytes(stringMessage);
serverStream.Write(buffer, 0, buffer.Length);
serverStream.Flush();
}
})
{
Name = "SendingThread"
}.Start();
}
}

Aydın məsələdir ki, "Start" metodu əlaqə prosesini başladır və daxilində olan "_startLocker" obyekti yenidən-əlaqə halları baş verdikdə ümumi prosesin thread-safe olmasını təmin edir. Əgər serverdə "RunListener" metodu gələn müştəri müraciətləri üçün idisə, burada “RunClient” metodu daxil olan mesajları izləmək üçün istifadə olunur. Həmin metodun daxilində çağırılan “HandleMessageReceive” metodu artıq qəbul olunan mesajı yoxlayır və proqramçı tərəfindən yazılan məntiqi icra edir. “SendMessage” metodun əvvəlində olan yoxlama şərtləri gələcəkdə genişləndirilə biləcək hissələrdir.

Nəticə

Proqramı işə salmaq üçün əvvəlcə server tərəfini qurmalıyıq, bunu üçün bağlantı portu və mesaj göndərmək üçün sonsuz dövr yazmalıyıq.

namespace Server
static void Main()
{
var messenger = new ServerTcpMessenger("127.0.0.1", 59992);
messenger.Start();
while (true)
{
var message = Console.ReadLine();
if (message == "break")
{
break;
}
messenger.SendMessage(messenger.ConcurrentClients.FirstOrDefault().Key, new TcpMessage
{
Data = message,
Type = TcpMessageType.Text
});
}
}


Digər tərəfdən isə müştəri hissəsi var, burada da serverə mesaj göndərmək üçün əlaqə portunu və sonsuz dövrü yazmalıyıq.

namespace Client
static void Main()
{
var messenger = new ClientTcpMessenger("127.0.0.1", 59992);
messenger.MessageReceived += (args) =>
{
Console.WriteLine($"Server: {args.Message.Data}");
};
messenger.Start();
while (true)
{
var message = Console.ReadLine();
if (message == "break")
{
break;
}
messenger.SendMessage(new TcpMessage
{
Data = message,
Type = TcpMessageType.Text
});
}
}
Soldan sağa: server (qulluqçu), client (müştəri)
Soldan sağa: server (qulluqçu), client (müştəri)

Mürəkkəb əlavələr

Server

  • Müştəridən gələn mesajları qəbul edib, lazımi məntiqi icra etmək üçün “MessageReceivedTrigger” adlı yeni event, “ListenClient” və “HandleMessageReceive” metodlarını əlavə edək;
  • Yeni müştəri qoşulduqda müəyyən bir məntiqi yerinə yetirmək üçün “ClientConnectedTrigger” adlı event əlavə edək və “AcceptTcpClient” metodunda dəyişikliklər edək;
  • Bütün müştərilərə mesaj göndərmək üçün “BroadcastMessage”, bütün bağlantıları ləğv etmək üçün isə “CloseAllClients” metodlarını əlavə edək;
public sealed class ServerTcpMessenger : IServerTcpMessenger
{
...
public delegate void ServerMessageReceivedTrigger(MessengerClient sender, TcpMessageEventArgs args);
public delegate void ServerClientConnectedTrigger(MessengerClient messengerClient);
public event ServerMessageReceivedTrigger MessageReceivedTrigger;
public event ServerClientConnectedTrigger ClientConnectedTrigger;
public ServerTcpMessenger(string address, int port) { ... }
public void Start() { ... }
private void RunListener() { ... }
private void AcceptTcpClient()
{
...
ClientConnectedTrigger?.Invoke(concurrentClient);
var concurrentClientListenThread = new Thread((client) =>
{
ListenClient(client as MessengerClient);
})
{
Name = $"ListenerThread-{concurrentClient}"
};
concurrentClientListenThread.Start(concurrentClient);
}
private void ListenClient(MessengerClient messengerClient)
{
try
{
while (true)
{
var stream = messengerClient.TcpClient.GetStream();
var buffer = new byte[1024];
var message = new StringBuilder();
var bytes = stream.Read(buffer, 0, buffer.Length);
if (bytes != 0)
{
message.Append(Encoding.Unicode.GetString(buffer, 0, bytes));
while (stream.DataAvailable)
{
bytes = stream.Read(buffer, 0, buffer.Length);
message.Append(Encoding.Unicode.GetString(buffer, 0, bytes));
}
HandleMessageReceive(messengerClient, message.ToString());
}
else
{
break;
}
}
}
catch (Exception)
{
return;
}
}
private void HandleMessageReceive(MessengerClient sender, string message)
{
var tcpMessage = JsonConvert.DeserializeObject<TcpMessage>(message);
MessageReceivedTrigger?.Invoke(sender, new TcpMessageEventArgs
{
Message = tcpMessage,
});
}
public void SendMessage(Guid tcpClientUid, TcpMessage tcpMessage) { ... }
public void BroadcastMessage(TcpMessage tcpMessage)
{
if (tcpMessage is null)
{
throw new ArgumentNullException(nameof(tcpMessage));
}
var broadcastingThread = new Thread(() =>
{
var message = JsonConvert.SerializeObject(tcpMessage);
foreach (var client in ConcurrentClients)
{
var messengerClient = client.Value;
if (messengerClient.IsConnected)
{
var clientStream = messengerClient.TcpClient.GetStream();
var buffer = Encoding.Unicode.GetBytes(message);
clientStream.Write(buffer, 0, buffer.Length);
clientStream.Flush();
}
}
})
{
Name = "BroadcastingThread"
};
broadcastingThread.Start();
}
public void CloseClient(Guid clientUid) { ... }
public void CloseAllClients()
{
foreach (var client in ConcurrentClients)
{
client.Value.TcpClient.Close();
}
}
}


Giriş nöqtəsi

static void Main()
{
var messenger = new ServerTcpMessenger("127.0.0.1", 59992);
messenger.Start();
messenger.ClientConnectedTrigger += (newClient) =>
{
Console.WriteLine(newClient);
};
messenger.MessageReceivedTrigger += (newClient, args) =>
{
Console.WriteLine($"Message by {newClient}:");
Console.WriteLine(args.Message.Data);
};
while (true)
{
var message = Console.ReadLine();
if (message == "break")
{
break;
}
messenger.BroadcastMessage(new TcpMessage
{
Data = message,
Type = TcpMessageType.Text
});
}
}

Müştəri

  • Yeni delegate-lər yaratmadan, bağlantı və səhvləri izləmək üçün “Start” metoduna “onConnected” və “onFail” parametrlərini əlavə edək;
  • Server tərəfi flood etməmək və müştəri tərəfdən yükü aradan qaldırmaq üçün “RunClient” metoduna ləğv olma taymerini əlavə edək;
public sealed class ClientTcpMessenger : IClientTcpMessenger
{
...
public ClientTcpMessenger(string address, int port) { ... }
public void Start(Action onConnected, Action onFail)
{
lock (_startLocker)
{
_tcpClient = new TcpClient
{
NoDelay = true
};
if (_clientThread == null || !_clientThread.IsAlive)
{
_clientThread = new Thread(() =>
{
try
{
RunClient(onConnected, onFail);
}
catch (Exception ex)
{
onFail?.Invoke();
}
})
{
Name = "ClientThread",
};
_clientThread.Start();
}
}
}
private void RunClient(Action onConnected, Action onFail)
{
Thread.Sleep(100);
System.Timers.Timer timer = new(10000)
{
AutoReset = false
};
timer.Elapsed += (s, e) =>
{
if (!_tcpClient.Connected)
{
onFail?.Invoke();
_tcpClient.Close();
_clientThread.Interrupt();
}
};
timer.Start();
_tcpClient.Connect(_endPoint);
...
}
private void HandleMessageReceive(string response) { ... }
public void SendMessage(TcpMessage tcpMessage) { ... }
}


Giriş nöqtəsi

static void Main()
{
var messenger = new ClientTcpMessenger("127.0.0.1", 59992);
messenger.MessageReceived += (args) =>
{
Console.WriteLine("Server:");
Console.WriteLine(args.Message.Data);
};
messenger.Start(() =>
{
Console.WriteLine("Connected");
}, () =>
{
Console.WriteLine("Failed");
});
while (true)
{
var message = Console.ReadLine();
if (message == "break")
{
break;
}
messenger.SendMessage(new TcpMessage
{
Data = message,
Type = TcpMessageType.Text
});
}
}

Nəticə

Soldan sağa/yuxarıdan aşağı: server, müştərilər (1, 2, 3)
Soldan sağa/yuxarıdan aşağı: server, müştərilər (1, 2, 3)


Əvvəldə qeyd etdiyim kimi, C#-da şəbəkə tətbiqlərini yaratmaq üçün TCP bağlantıların əsaslarını bilmək önəmlidir. Optimal performans üçün həmişə təhlükəsizlik, səhvlərin idarə edilməsi və asinxron proqramlaşdırmanı nəzərə alın.

Bu qədər, uğurlar!

P.S. VS-nu ağ fona ancaq strukturu nümayiş etmək üçün çevirmişdim)

6
Müəllif
Şərhlər