2023'te Unity Networking Ortamı ile ilgili makale serim devam ediyor! Bugünkü yazımızda gerçek zamanlı çok oyunculu oyunlarda kullanılan veri iletim protokolleri ele alınacak.
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.
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:
Fiziksel Katman : 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.
Veri Bağlantı Katmanı : Bu katman, fiziksel ortam üzerinden veri aktarımını sağlar ve fiziksel düzeyde oluşabilecek hataları yönetir.
Ağ Katmanı : Bu katman, ağlar arasındaki veri aktarımının yolunu (yönlendirmesini) belirler.
Aktarım Katmanı : Bu katman, noktalar arasındaki mesaj dağıtımını yönetir ve gerektiğinde güvenilirlik sağlar.
Oturum 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.
Sunum Katmanı : Bu katman, gönderici ve alıcı tarafındaki veri temsilindeki (kodlama) farklılıklardan veri bağımsızlığını sağlar.
Uygulama Katmanı : Bu katman, kullanıcının etkileşimde bulunduğu uygulamalarla doğrudan ilişkisi olan protokolleri içerir.
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.
TCP 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.
Bağlantı kurmak aşağıdaki adımları içerir:
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(); } }
ConnectToServer(string ipAddress, int port)
yöntemi, belirtilen IP adresi ve bağlantı noktasında sunucuya bağlantı kurar. Sunucudan veri alımı ReceiveData(IAsyncResult result)
yöntemiyle gerçekleştirilir, veri iletimi ise SendData(string message)
yöntemiyle yapılır. Alınan veriler Debug.Log
kullanılarak konsola gönderilir.
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.
StartServer(int port)
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 HandleIncomingConnection(IAsyncResult result)
yöntemi yürütülür.
Alınan veriler, ReceiveData(IAsyncResult result)
yönteminde işlenir. Sunucu, istemciden veri aldıktan sonra gerekli işlemleri gerçekleştirebilir veya verileri istemciye geri gönderebilir.
SendData(string message)
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.
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); } }
HandleIncomingConnection
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.
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 ReceiveData
yöntemine aktarılacaktır.
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); }
ReceiveData
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.
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); }
SendData
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.
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.
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.
Bu kod, Unity'de bir UDP istemcisinin temel uygulamasını gösterir. StartUDPClient
yöntemi UDP istemcisini başlatır ve onu IP adresi ve bağlantı noktasıyla belirtilen uzak sunucuya bağlar. İstemci, BeginReceive
yöntemini kullanarak eşzamansız olarak veri almaya başlar ve SendData
yöntemini kullanarak sunucuya bir mesaj gönderir.
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 ReceiveData
yöntemi çağrılır. Alınan mesaj daha sonra konsola kaydedilir. İstemci, BeginReceive
yeniden çağırarak verileri zaman uyumsuz olarak almaya devam eder.
SendData
yöntemi, mesajı bayta dönüştürür ve UDP istemcisinin Send
yöntemini kullanarak sunucuya gönderir.
Bu kod, Unity'deki bir UDP sunucusunun temel uygulamasını gösterir. StartUDPServer
yöntemi, belirtilen bağlantı noktasındaki UDP sunucusunu başlatır ve BeginReceive
yöntemini kullanarak eşzamansız olarak veri almaya başlar.
Bir istemciden veri alındığında, alınan baytları işleyen ve bunları bir dizeye dönüştüren ReceiveData
yöntemi çağrılır. Alınan mesaj daha sonra konsola kaydedilir. Sunucu, BeginReceive
yeniden çağırarak verileri eşzamansız olarak almaya devam eder.
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); } }
SendData
yöntemi, bir mesaj ve istemcinin adresini ve bağlantı noktasını temsil eden bir IPEndPoint
alır. Mesajı bayta dönüştürür ve UDP sunucusunun Send
yöntemini kullanarak istemciye gönderir.
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, 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.
Aşağıdaki kod, C# dilini ve WebSocketSharp kitaplığını kullanarak Unity'de bir WebSocket istemcisinin uygulanmasına ilişkin bir örnek sağlar.
Nesnenin başlatılması üzerine çağrılan Start()
yönteminde, WebSocket sunucunuzun adresiyle başlatılan yeni bir WebSocket
örneği oluşturulur.
Bundan sonra OnOpen
ve OnMessage
olay işleyicileri ayarlanır.
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); } }
OnOpen
sunucuyla bağlantı kurulduğunda tetiklenir. Bu örnekte bağlantı kurulduğunda sunucuya "Oyuncu1" yazılı bir mesaj gönderilmektedir.
OnMessage
sunucudan bir mesaj alındığında tetiklenir. Burada bir mesaj alındığında içeriği konsolda görüntülenir.
Daha sonra sunucuya eşzamansız olarak bağlanan ConnectAsync()
yöntemi çağrılır.
Aşağıdaki kod WebSocket sunucusu oluşturmanın bir örneğidir.
Nesne başlatıldığında çağrılan Start()
yönteminde, WebSocket sunucunuzun adresiyle başlatılan yeni bir WebSocketServer
örneği oluşturulur. Daha sonra /Auth
yolunda bulunan AuthBehaviour
WebSocket hizmeti sunucuya eklenir. Bundan sonra sunucu Start()
yöntemi kullanılarak başlatılır.
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); } }
AuthBehaviour
, istemcilerden mesaj alırken sunucu davranışını tanımlayan WebSocketBehavior
türetilmiş bir sınıftır. Burada, sunucu bir istemciden mesaj aldığında çağrılan OnMessage()
yöntemi geçersiz kılınır.
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.
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.