2023'te Unity Networking Ortamı ile ilgili devam ediyor! Bugünkü yazımızda gerçek zamanlı çok oyunculu oyunlarda kullanılan veri iletim protokolleri ele alınacak. makale serim Herkese selamlar! Ben MY.GAMES'in Baş Yazılım Mühendisi Dmitrii Ivashchenko. Farklı ağ etkileşimi seviyelerinde hangi protokollerin mevcut olduğuna kısa bir genel bakışla başlayacağız. İçeriğe Genel Bakış OSI Seviyeleri Geçiş kontrol protokolü TCP İstemcisi TCP Sunucusu Kullanıcı Datagram Protokolü UDP istemcisi UDP Sunucusu Web soketi Websocket İstemcisi Websoket Sunucusu Çözüm OSI Seviyeleri OSI (Açık Sistemler Bağlantısı) modeli, bir iletişim sisteminin işlevlerini soyutlama düzeyleri açısından karakterize eden ve standartlaştıran kavramsal bir modeldir. Bu model 1978 yılında Uluslararası Standardizasyon Örgütü (ISO) tarafından geliştirilmiştir. Yedi katmandan oluşur: : Bu katman ham bitlerin kanal üzerinden iletilmesi ve alınmasıyla ilgilidir. Bu katmandaki protokoller, bit temsilleri, iletim hızları, fiziksel kablolar, kartlar ve konektör tasarımları dahil olmak üzere fiziksel arayüzü ve ortamın özelliklerini tanımlar. Fiziksel Katman : Bu katman, fiziksel ortam üzerinden veri aktarımını sağlar ve fiziksel düzeyde oluşabilecek hataları yönetir. Veri Bağlantı Katmanı : Bu katman, ağlar arasındaki veri aktarımının yolunu (yönlendirmesini) belirler. Ağ Katmanı : Bu katman, noktalar arasındaki mesaj dağıtımını yönetir ve gerektiğinde güvenilirlik sağlar. Aktarım Katmanı : Bu katman, kullanıcılar ve uygulamalar arasında oturum oluşturulmasını, sürdürülmesini ve sonlandırılmasını yönetir. Oturum Katmanı : Bu katman, gönderici ve alıcı tarafındaki veri temsilindeki (kodlama) farklılıklardan veri bağımsızlığını sağlar. Sunum Katmanı : Bu katman, kullanıcının etkileşimde bulunduğu uygulamalarla doğrudan ilişkisi olan protokolleri içerir. Uygulama Katmanı Her katmanın bir öncekinin üzerine inşa edildiğini belirtmekte fayda var. Aktarım katmanının üzerindeki katmanlar (Oturum, Sunum ve Uygulama) oldukça uzmanlaşmıştır ve gerçek zamanlı çok oyunculu oyunu düzenlememize yardımcı olamaz. Bu nedenle, Aktarım Katmanında durmalı ve oyuncular arasında en iyi veri alışverişini sağlamak için TCP ve UDP protokollerini kullanmalıyız. Geçiş kontrol protokolü bağlantı odaklı bir protokoldür; bu, veri alışverişi için bağlantı kuran iki cihaz arasında iletişimin gerçekleştiği anlamına gelir. Bu protokol, iletilen tüm verilerin hedefine doğru sırayla ulaşacağını garanti ettiğinden güvenilirliği sağlar. İletim sırasında herhangi bir veri kaybolursa, TCP, tüm veriler başarıyla iletilinceye kadar isteği otomatik olarak yeniden deneyecektir. TCP Bağlantı kurmak aşağıdaki adımları içerir: İstemci ve sunucu, TCP üzerinden veri alışverişi için yuvalar oluşturur. İstemci, belirtilen hedef bağlantı noktasıyla sunucuya bir SYN (senkronizasyon) segmenti gönderir. Sunucu SYN segmentini kabul eder, kendi soketini oluşturur ve istemciye bir SYN-ACK (senkronizasyon-onay) segmenti gönderir. İstemci SYN-ACK segmentini kabul eder ve bağlantı kurma işlemini tamamlamak için sunucuya bir ACK (onay) segmenti gönderir. Artık güvenilir bir iki yönlü bağlantı kurulmuştur. TCP İstemcisi Aşağıdaki örnek, bir TCP istemcisinin temel uygulamasını gösterir ve belirli veriler ve oyun mantığıyla çalışacak şekilde genişletilebilir. Kod, belirli bir IP adresine ve bağlantı noktasına sahip bir sunucuya bağlanır ve ardından bağlantı üzerinden veri gönderip alır. Sunucudan eşzamansız veri alımı için bir ağ akışı kullanılır. using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class TCPClient : MonoBehaviour { private TcpClient tcpClient; private NetworkStream networkStream; private byte[] receiveBuffer; private void Start() { // Example: Connect to server with IP address 127.0.0.1 (localhost) and port 5555 ConnectToServer("127.0.0.1", 5555); } private void ConnectToServer(string ipAddress, int port) { tcpClient = new TcpClient(); tcpClient.Connect(IPAddress.Parse(ipAddress), port); networkStream = tcpClient.GetStream(); // Start asynchronous operation to receive data from the server receiveBuffer = new byte[tcpClient.ReceiveBufferSize]; networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void ReceiveData(IAsyncResult result) { int bytesRead = networkStream.EndRead(result); byte[] receivedBytes = new byte[bytesRead]; Array.Copy(receiveBuffer, receivedBytes, bytesRead); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from server: " + receivedMessage); // Continue the asynchronous operation to receive data networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); networkStream.Write(sendBytes, 0, sendBytes.Length); networkStream.Flush(); } } yöntemi, belirtilen IP adresi ve bağlantı noktasında sunucuya bağlantı kurar. Sunucudan veri alımı yöntemiyle gerçekleştirilir, veri iletimi ise yöntemiyle yapılır. Alınan veriler kullanılarak konsola gönderilir. ConnectToServer(string ipAddress, int port) ReceiveData(IAsyncResult result) SendData(string message) Debug.Log TCP Sunucusu Aşağıdaki kod örneği Unity'deki basit bir TCP sunucusunu temsil etmektedir. Kod, sunucuyu başlatır, belirtilen bağlantı noktasını dinler ve istemcilerden gelen bağlantıları kabul eder. Bir istemciye bağlandıktan sonra sunucu, ağ akışı aracılığıyla veri gönderip alır. yöntemi, sunucuyu belirtilen bağlantı noktasında başlatır ve gelen bağlantıları dinlemeye başlar. Bir istemci bağlantısı kurulduğunda, istemciden veri alan ve verileri almak için zaman uyumsuz bir işlem başlatan yöntemi yürütülür. StartServer(int port) HandleIncomingConnection(IAsyncResult result) Alınan veriler, yönteminde işlenir. Sunucu, istemciden veri aldıktan sonra gerekli işlemleri gerçekleştirebilir veya verileri istemciye geri gönderebilir. ReceiveData(IAsyncResult result) yöntemi, verileri istemciye ağ akışı aracılığıyla gönderir. Veriler bir bayt dizisine dönüştürülür ve istemciye gönderilir. SendData(string message) using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class TCPServer : MonoBehaviour { private TcpListener tcpListener; private TcpClient connectedClient; private NetworkStream networkStream; private byte[] receiveBuffer; private void Start() { // Example: Start the server on port 5555 StartServer(5555); } private void StartServer(int port) { tcpListener = new TcpListener(IPAddress.Any, port); tcpListener.Start(); Debug.Log("Server started. Waiting for connections..."); // Start accepting client connections asynchronously tcpListener.BeginAcceptTcpClient(HandleIncomingConnection, null); } } yöntemi, gelen bir istemci bağlantısını işlemek için kullanılır. Bağlantıyı kabul ettikten sonra istemciyle veri alışverişi için bir akış elde eder ve istemciden veri almak için eşzamansız bir işlem başlatır. HandleIncomingConnection Daha sonra yöntem, bağlı istemciden alınan arabellek boyutuna göre veri almak için bir arabellek oluşturur. Oluşturulan arabelleği kullanarak akıştan veri okumak için eşzamansız bir işlem başlatılır. Okuma işlemi tamamlandıktan sonra veriler daha ileri işlemler için yöntemine aktarılacaktır. ReceiveData Ayrıca yöntem, bir sonraki istemciyi kabul etme olasılığı için gelen bağlantıları kabul etmek üzere başka bir eşzamansız işlem başlatır. Bir istemci sunucuya bağlandığında, bağlantıyı yönetmek için bu yöntem çağrılacak ve sunucu, sonraki istemcileri eşzamansız olarak kabul etmeye hazır olacaktır. private void HandleIncomingConnection(IAsyncResult result) { connectedClient = tcpListener.EndAcceptTcpClient(result); networkStream = connectedClient.GetStream(); Debug.Log("Client connected: " + connectedClient.Client.RemoteEndPoint); // Start asynchronous operation to receive data from the client receiveBuffer = new byte[connectedClient.ReceiveBufferSize]; networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); // Accept next client connection asynchronously tcpListener.BeginAcceptTcpClient(HandleIncomingConnection, null); } yöntemi, istemciden alınan verileri işlemek için kullanılır. Veri okuma işlemi tamamlandıktan sonra yöntem okunan bayt sayısını kontrol eder. Bayt sayısı sıfırdan küçük veya sıfıra eşitse bu, istemcinin bağlantısının kesildiği anlamına gelir. Bu durumda yöntem istemciyle bağlantıyı kapatır ve yürütmeyi sonlandırır. ReceiveData Okunan bayt sayısı sıfırdan büyükse yöntem, alınan veri için bir bayt dizisi oluşturur ve okunan veriyi bu diziye kopyalar. Daha sonra yöntem, alınan baytları UTF-8 kodlamasını kullanarak bir dizeye dönüştürür ve alınan mesajı konsola çıkarır. private void ReceiveData(IAsyncResult result) { int bytesRead = networkStream.EndRead(result); if (bytesRead <= 0) { Debug.Log("Client disconnected: " + connectedClient.Client.RemoteEndPoint); connectedClient.Close(); return; } byte[] receivedBytes = new byte[bytesRead]; Array.Copy(receiveBuffer, receivedBytes, bytesRead); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from client: " + receivedMessage); // Process received data // Continue the asynchronous operation to receive data networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); networkStream.Write(sendBytes, 0, sendBytes.Length); networkStream.Flush(); Debug.Log("Sent to client: " + message); } yöntemi istemciye veri göndermek için kullanılır. Mesaj dizisini UTF-8 kodlamasını kullanarak bir bayt dizisine dönüştürür ve bu diziyi ağ akışına yazar. Verileri gönderdikten sonra yöntem akışı temizler ve gönderilen mesajı konsola çıkarır. SendData Güvenilirlik büyük bir artı gibi görünse de bu TCP özelliği, gerçek zamanlı çok oyunculu oyunlarda sorun yaratabiliyor. TCP'de veri iletimi, yeniden iletim ve akış kontrolü mekanizması tarafından yavaşlatılabilir ve bu da gecikmelere veya "gecikmelere" yol açabilir. Kullanıcı Datagram Protokolü UDP, teslimatı veya paket sırasını garanti etmeyen daha basit bir protokoldür. Bu, bağlantı kurmak veya kayıp paketleri yeniden iletmek için zaman harcamadığından TCP'den çok daha hızlı olmasını sağlar. Hızı ve basitliği nedeniyle UDP, ağ oyunlarında ve gerçek zamanlı veri iletimi gerektiren diğer uygulamalarda sıklıkla kullanılır. Ancak UDP'yi kullanmak, geliştiricilerin veri aktarımını daha dikkatli yönetmesini gerektirir. UDP teslimatı garanti etmediğinden, kayıp paketlerle veya hatalı gelen paketlerle başa çıkmak için kendi mekanizmalarınızı uygulamanız gerekebilir. UDP İstemcisi Bu kod, Unity'de bir UDP istemcisinin temel uygulamasını gösterir. yöntemi UDP istemcisini başlatır ve onu IP adresi ve bağlantı noktasıyla belirtilen uzak sunucuya bağlar. İstemci, yöntemini kullanarak eşzamansız olarak veri almaya başlar ve yöntemini kullanarak sunucuya bir mesaj gönderir. StartUDPClient BeginReceive SendData using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class UDPExample : MonoBehaviour { private UdpClient udpClient; private IPEndPoint remoteEndPoint; private void Start() { // Example: Start the UDP client and connect to the remote server StartUDPClient("127.0.0.1", 5555); } private void StartUDPClient(string ipAddress, int port) { udpClient = new UdpClient(); remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); // Start receiving data asynchronously udpClient.BeginReceive(ReceiveData, null); // Send a message to the server SendData("Hello, server!"); } private void ReceiveData(IAsyncResult result) { byte[] receivedBytes = udpClient.EndReceive(result, ref remoteEndPoint); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from server: " + receivedMessage); // Continue receiving data asynchronously udpClient.BeginReceive(ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); // Send the message to the server udpClient.Send(sendBytes, sendBytes.Length, remoteEndPoint); Debug.Log("Sent to server: " + message); } } Sunucudan veri alındığında, alınan baytları işleyen ve bunları bir dizeye dönüştüren yöntemi çağrılır. Alınan mesaj daha sonra konsola kaydedilir. İstemci, yeniden çağırarak verileri zaman uyumsuz olarak almaya devam eder. ReceiveData BeginReceive yöntemi, mesajı bayta dönüştürür ve UDP istemcisinin yöntemini kullanarak sunucuya gönderir. SendData Send UDP Sunucusu Bu kod, Unity'deki bir UDP sunucusunun temel uygulamasını gösterir. yöntemi, belirtilen bağlantı noktasındaki UDP sunucusunu başlatır ve yöntemini kullanarak eşzamansız olarak veri almaya başlar. StartUDPServer BeginReceive Bir istemciden veri alındığında, alınan baytları işleyen ve bunları bir dizeye dönüştüren yöntemi çağrılır. Alınan mesaj daha sonra konsola kaydedilir. Sunucu, yeniden çağırarak verileri eşzamansız olarak almaya devam eder. ReceiveData BeginReceive using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class UDPServer : MonoBehaviour { private UdpClient udpServer; private IPEndPoint remoteEndPoint; private void Start() { // Example: Start the UDP server on port 5555 StartUDPServer(5555); } private void StartUDPServer(int port) { udpServer = new UdpClient(port); remoteEndPoint = new IPEndPoint(IPAddress.Any, port); Debug.Log("Server started. Waiting for messages..."); // Start receiving data asynchronously udpServer.BeginReceive(ReceiveData, null); } private void ReceiveData(IAsyncResult result) { byte[] receivedBytes = udpServer.EndReceive(result, ref remoteEndPoint); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from client: " + receivedMessage); // Process the received data // Continue receiving data asynchronously udpServer.BeginReceive(ReceiveData, null); } private void SendData(string message, IPEndPoint endPoint) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); // Send the message to the client udpServer.Send(sendBytes, sendBytes.Length, endPoint); Debug.Log("Sent to client: " + message); } } yöntemi, bir mesaj ve istemcinin adresini ve bağlantı noktasını temsil eden bir alır. Mesajı bayta dönüştürür ve UDP sunucusunun yöntemini kullanarak istemciye gönderir. SendData IPEndPoint Send Oyun geliştirme bağlamında TCP ve UDP arasındaki seçim büyük ölçüde oyununuzun türüne bağlıdır. Oyununuz güvenilir veri dağıtımı gerektiriyorsa ve yanıt süresi kritik bir faktör değilse (örneğin, gerçek zamanlı strateji veya sıra tabanlı oyunlarda), TCP uygun bir seçim olabilir. Öte yandan, oyununuz hızlı veri aktarımı gerektiriyorsa ve bir miktar veri kaybıyla başa çıkabiliyorsa (örneğin, birinci şahıs nişancı veya yarış oyunlarında), o zaman UDP en iyi seçim olabilir. WebSocket , tarayıcı ile sunucu arasında kalıcı bir bağlantı kurulmasına olanak tanıyan bir iletişim protokolüdür. Normal HTTP'den temel farkı, çift yönlü iletişime olanak sağlamasıdır; Sunucu yalnızca istemci isteklerine yanıt vermekle kalmaz, aynı zamanda kendisine mesaj da gönderebilir. WebSocket WebSocket, TCP'ye dayalı uygulama düzeyinde bir protokoldür. Akışları değil mesajları desteklemesi onu normal TCP'den ayırır. WebSocket, performansa bir miktar ek yük getirebilecek ek işlevsellik içerir. Adım adım şu şekilde çalışır: İstemci, "Yükseltme isteği" adı verilen özel bir HTTP isteği gönderir. Bu istek, sunucuya istemcinin WebSocket'e geçmek istediğini bildirir. Sunucu WebSocket'i destekliyorsa ve geçiş yapmayı kabul ediyorsa, WebSocket'e yükseltmeyi onaylayan özel bir HTTP yanıtıyla yanıt verir. Bu mesajların değişiminden sonra istemci ile sunucu arasında kalıcı, çift yönlü bir bağlantı kurulur. Her iki taraf da yalnızca karşı tarafın isteklerine yanıt olarak değil, istedikleri zaman mesaj gönderebilir. Artık istemci ve sunucu istedikleri zaman mesaj gönderip alabilirler. Her WebSocket mesajı, mesajın ne zaman başlayıp bittiğini gösteren "çerçeveler" içine sarılır. Bu, tarayıcı ve sunucunun, karışık bir sırada gelse veya ağ sorunları nedeniyle parçalara ayrılmış olsa bile mesajları doğru şekilde yorumlamasına olanak tanır. Her iki taraf da istediği zaman özel bir "bağlantıyı kapat" çerçevesi göndererek bağlantıyı kapatabilir. Diğer taraf kapatma onayıyla yanıt verebilir ve bundan sonra her iki taraf da diğer verileri göndermeyi derhal bırakmalıdır. Kapatma nedenini belirtmek için durum kodları da kullanılabilir. WebSocket İstemcisi Aşağıdaki kod, C# dilini ve kitaplığını kullanarak Unity'de bir WebSocket istemcisinin uygulanmasına ilişkin bir örnek sağlar. WebSocketSharp Nesnenin başlatılması üzerine çağrılan yönteminde, WebSocket sunucunuzun adresiyle başlatılan yeni bir örneği oluşturulur. Start() WebSocket Bundan sonra ve olay işleyicileri ayarlanır. OnOpen OnMessage using UnityEngine; using WebSocketSharp; public class WebSocketClient : MonoBehaviour { private WebSocket ws; void Start() { ws = new WebSocket("ws://your-websocket-server-url/Auth"); ws.OnOpen += OnOpenHandler; ws.OnMessage += OnMessageHandler; ws.ConnectAsync(); } private void OnOpenHandler(object sender, System.EventArgs e) { var data = "Player1"; ws.Send(data) } private void OnMessageHandler(object sender, MessageEventArgs e) { Debug.Log("WebSocket server said: " + e.Data); } } sunucuyla bağlantı kurulduğunda tetiklenir. Bu örnekte bağlantı kurulduğunda sunucuya "Oyuncu1" yazılı bir mesaj gönderilmektedir. OnOpen sunucudan bir mesaj alındığında tetiklenir. Burada bir mesaj alındığında içeriği konsolda görüntülenir. OnMessage Daha sonra sunucuya eşzamansız olarak bağlanan yöntemi çağrılır. ConnectAsync() WebSocket Sunucusu Aşağıdaki kod WebSocket sunucusu oluşturmanın bir örneğidir. Nesne başlatıldığında çağrılan yönteminde, WebSocket sunucunuzun adresiyle başlatılan yeni bir örneği oluşturulur. Daha sonra yolunda bulunan WebSocket hizmeti sunucuya eklenir. Bundan sonra sunucu yöntemi kullanılarak başlatılır. Start() WebSocketServer /Auth AuthBehaviour Start() using UnityEngine; using WebSocketSharp; using WebSocketSharp.Server; public class WebSocketServer : MonoBehaviour { void Start() { var socket = new WebSocketServer("ws://your-websocket-server-url"); socket.AddWebSocketService<AuthBehaviour>("/Auth"); socket.Start(); } } public class AuthBehaviour : WebSocketBehavior { protected override void OnMessage (MessageEventArgs e) { var playerName = e.Data; Debug.Log("WebSocket client connected: " + playerName); Send("Auth Complete: " + playerName); } } , istemcilerden mesaj alırken sunucu davranışını tanımlayan türetilmiş bir sınıftır. Burada, sunucu bir istemciden mesaj aldığında çağrılan yöntemi geçersiz kılınır. AuthBehaviour WebSocketBehavior OnMessage() Bu yöntemde, önce mesaj metni çıkarılır, daha sonra mesajda geçirilen adı kullanarak hangi istemcinin bağlandığını belirten konsola bir mesaj gönderilir. Daha sonra sunucu, istemciye kimlik doğrulamanın tamamlanmasıyla ilgili bilgileri içeren bir mesaj gönderir. Çözüm Unity oyunları için TCP, UDP ve WebSockets kullanarak nasıl bağlantı oluşturulacağını ve her birinin avantajlarını ve dezavantajlarını tartıştık. UDP'nin hafif doğasına ve TCP'nin güvenilirliğine rağmen, oyunlarda gerçek zamanlı çok oyunculu oyunları düzenlemek için ikisi de iyi bir seçim değildir. UDP bazı verileri kaybedecek ve TCP gerekli iletim hızını sağlayamayacaktır. Bir sonraki makalede UDP kullanarak güvenilir veri aktarımının nasıl organize edileceğini tartışacağız.