← Kursa Dön
📄 Text · 20 min

Java ile Ağ Programlama: Socket, ServerSocket, HttpClient

Bugün Java'nın en güçlü yanlarından birine dalıyoruz: ağ programlama (network programming). İnternette her gün kullandığımız uygulamalar — mesajlaşma, web tarayıcı, online oyunlar — hepsi ağ iletişimi üzerine kurulu.

Bu derste sıfırdan başlayarak TCP/UDP soketlerinden modern HttpClient API'sine kadar geniş bir yelpazede ağ programlamayı öğreneceğiz. Hazırsan başlayalım!


1. Ağ İletişimi Temelleri — IP, Port, TCP ve UDP

Ağ programlamaya geçmeden önce birkaç temel kavramı netleştirelim.

IP Adresi Nedir?

IP adresi (Internet Protocol Address), ağdaki her cihazın benzersiz kimliğidir. Tıpkı evinin sokak adresi gibi düşün — birisi sana bir şey göndermek istiyorsa adresini bilmeli. IPv4 formatında 192.168.1.10 gibi, IPv6 formatında ise 2001:0db8::1 gibi görünür.

localhost veya 127.0.0.1 ise kendi bilgisayarını temsil eder. Geliştirme yaparken çoğunlukla bu adresi kullanırız.

Port Nedir?

Bir IP adresinde binlerce servis çalışabilir. Port numarası, o IP üzerindeki hangi servise ulaşmak istediğini belirler. Bunu apartman daire numarası gibi düşün — IP adresi binayı, port numarası daireyi gösterir.

Port numaraları 0-65535 arasında olabilir. 0-1023 arası "well-known ports" olarak bilinir: HTTP → 80, HTTPS → 443, FTP → 21. Kendi uygulamalarında genelde 1024 üstü portları kullanırsın.

TCP vs UDP

İki ana iletişim protokolü var:

TCP (Transmission Control Protocol): Güvenilir, sıralı veri iletimi sağlar. Verinin karşıya ulaştığını garanti eder. Kayıp paket varsa tekrar gönderir. Web, e-posta, dosya transferi gibi işlerde kullanılır.

UDP (User Datagram Protocol): Hızlı ama garantisiz. Veri ulaştı mı ulaşmadı mı kontrol etmez. Video streaming, online oyunlar, DNS sorguları gibi hızın güvenilirlikten önemli olduğu yerlerde tercih edilir.

🎯 Analoji — Telefon Görüşmesi ve Mektup:

>

TCP'yi bir telefon görüşmesi gibi düşün. Önce bağlantı kurarsın (arama yaparsın), karşı taraf kabul eder (açar), sonra karşılıklı konuşursun ve en sonunda bağlantıyı kapatırsın (kapatırsın). Her söylediğin duyulur, sıralıdır ve onaylanır.

>

UDP ise mektup göndermek gibidir. Zarfı yazar, postaya verirsin. Ulaşıp ulaşmadığını bilemezsin, sırayla gidip gitmediğini kontrol edemezsin. Ama çok hızlıdır — her seferinde bağlantı kurmana gerek yoktur.


2. java.net Paketi — Genel Bakış

Java'nın ağ programlama yeteneklerinin büyük bölümü java.net paketinde yaşar. Bu paket, düşük seviye soket programlamadan yüksek seviye HTTP işlemlerine kadar geniş bir araç seti sunar.

İşte en sık kullanacağın sınıflar:

SınıfGörev
SocketTCP client bağlantısı
ServerSocketTCP server, gelen bağlantıları dinler
DatagramSocketUDP iletişimi
DatagramPacketUDP veri paketi
URLURL adresi temsili
URLConnectionURL'ye bağlantı açma
InetAddressIP adresi temsili
HttpClientModern HTTP client (Java 11+, java.net.http)

Java 11 ile birlikte java.net.http paketi de eklendi. Bu paket, eski HttpURLConnection yerine çok daha modern ve kullanışlı bir HTTP client sunuyor. Dersin ilerleyen bölümlerinde buna detaylıca bakacağız.


3. Socket Sınıfı — TCP Client Oluşturma

Socket sınıfı, bir TCP client bağlantısı oluşturmak için kullanılır. Bir sunucuya bağlanmak istediğinde bu sınıfı kullanırsın. Bağlantı kurulduktan sonra hem veri gönderebilir hem de veri alabilirsin.

Socket ile çalışırken I/O stream'lerini kullanıyoruz. getOutputStream() ile veri gönderiyor, getInputStream() ile veri alıyoruz. Bu stream'leri PrintWriter ve BufferedReader ile sarmalayarak satır satır metin okuma/yazma yapabiliyoruz.

En güvenli yaklaşım try-with-resources kullanmaktır:

try (Socket socket = new Socket("localhost", 8080);
     PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
     BufferedReader reader = new BufferedReader(
         new InputStreamReader(socket.getInputStream()))) {

    writer.println("Merhaba!");
    String reply = reader.readLine();
    System.out.println("Yanıt: " + reply);

} catch (IOException e) {
    System.err.println("Bağlantı hatası: " + e.getMessage());
}

Bu şekilde socket ve stream'ler otomatik olarak kapatılır. Kaynak sızıntısı (resource leak) riskini ortadan kaldırır.

⚠️ Dikkat: new Socket(host, port) constructor'ı çağrıldığında hemen bağlantı kurmaya çalışır. Sunucu çalışmıyorsa veya ulaşılamıyorsa ConnectException fırlatılır. Bunu her zaman try-catch ile yakala!


4. ServerSocket Sınıfı — TCP Server Oluşturma

Bir sunucu yazmak istiyorsan ServerSocket sınıfını kullanırsın. Bu sınıf belirli bir portu dinler ve gelen bağlantı isteklerini kabul eder. Her kabul edilen bağlantı için bir Socket nesnesi döner.

İşte en basit server yapısı:

import java.net.ServerSocket;
import java.net.Socket;
import java.io.*;

ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Sunucu 8080 portunda dinliyor...");

Socket clientSocket = serverSocket.accept(); // Bağlantı gelene kadar bekler
System.out.println("Client bağlandı: " + clientSocket.getInetAddress());

BufferedReader in = new BufferedReader(
    new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);

String message = in.readLine();
System.out.println("Client dedi: " + message);
out.println("Mesaj alındı: " + message);

clientSocket.close();
serverSocket.close();

accept() metodu çok önemli — bu metod bloklayıcıdır (blocking). Yani bir client bağlanana kadar program o satırda durur ve bekler. Client bağlandığında yeni bir Socket döner ve bu socket üzerinden iletişim kurulur.

Gerçek dünyada sunucular bir while(true) döngüsü içinde sürekli bağlantı kabul eder. Ama bu basit yapıda aynı anda sadece bir client ile ilgilenebilirsin — bir sonraki bölümde bunu multi-threading ile çözeceğiz.


5. Basit Echo Server/Client Örneği

Öğrendiklerimizi birleştirelim. Echo server, client'tan gelen mesajı aynen geri gönderir. Basit ama ağ programlamanın temellerini anlamak için mükemmel bir örnek.

Echo Server

import java.net.*;
import java.io.*;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        try (ServerSocket server = new ServerSocket(9090)) {
            System.out.println("Echo Server başladı (port 9090)");
            
            while (true) {
                Socket client = server.accept();
                try (BufferedReader in = new BufferedReader(
                         new InputStreamReader(client.getInputStream()));
                     PrintWriter out = new PrintWriter(
                         client.getOutputStream(), true)) {
                    
                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println("Alındı: " + line);
                        out.println("Echo: " + line);
                    }
                }
                client.close();
            }
        }
    }
}

Echo Client

import java.net.*;
import java.io.*;

public class EchoClient {
    public static void main(String[] args) throws IOException {
        try (Socket socket = new Socket("localhost", 9090);
             PrintWriter out = new PrintWriter(
                 socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             BufferedReader console = new BufferedReader(
                 new InputStreamReader(System.in))) {

            System.out.println("Sunucuya bağlandı. Mesaj yazın:");
            String userInput;
            while ((userInput = console.readLine()) != null) {
                out.println(userInput);
                System.out.println("Sunucu: " + in.readLine());
            }
        }
    }
}

Önce EchoServer'ı çalıştır, sonra ayrı bir terminalde EchoClient'ı başlat. Yazdığın her mesaj sunucuya gidecek ve "Echo: ..." şeklinde geri dönecek. Ağ programlamanın "Hello World"ü budur!

💡 İpucu: PrintWriter'ın ikinci parametresi true olarak ayarlandığında auto-flush aktif olur. Yani println() her çağrıldığında veri hemen gönderilir. Bu olmadan veri buffer'da birikir ve karşı tarafa geç ulaşabilir.


6. Multi-Threaded Server — Her Client İçin Ayrı Thread

Az önce yazdığımız echo server tek seferde sadece bir client'a hizmet verebiliyordu. Gerçek dünyada bu kabul edilemez — düşün ki bir web sunucusu aynı anda sadece bir kişiye cevap verebilse! İşte burada multi-threading devreye giriyor.

Strateji basit: her yeni client bağlantısı için ayrı bir thread oluştur. Ana thread yeni bağlantıları kabul etmeye devam ederken, her client kendi thread'inde bağımsız olarak işlenir.

ClientHandler Sınıfı

import java.net.*;
import java.io.*;

public class ClientHandler implements Runnable {
    private final Socket clientSocket;

    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }

    @Override
    public void run() {
        try (BufferedReader in = new BufferedReader(
                 new InputStreamReader(clientSocket.getInputStream()));
             PrintWriter out = new PrintWriter(
                 clientSocket.getOutputStream(), true)) {

            String line;
            while ((line = in.readLine()) != null) {
                System.out.println("[" + Thread.currentThread().getName()
                    + "] Alındı: " + line);
                out.println("Echo: " + line);
            }
        } catch (IOException e) {
            System.err.println("Client hatası: " + e.getMessage());
        } finally {
            try { clientSocket.close(); } catch (IOException ignored) {}
        }
    }
}

Multi-Threaded Server

import java.net.*;
import java.io.*;

public class MultiThreadedServer {
    public static void main(String[] args) throws IOException {
        try (ServerSocket server = new ServerSocket(9090)) {
            System.out.println("Multi-threaded server başladı (port 9090)");

            while (true) {
                Socket client = server.accept();
                System.out.println("Yeni client: "
                    + client.getRemoteSocketAddress());

                Thread thread = new Thread(new ClientHandler(client));
                thread.start();
            }
        }
    }
}

Her accept() çağrısında dönen socket, yeni bir ClientHandler thread'ine verilir. Bu sayede sunucu aynı anda birden fazla client'a hizmet edebilir. Ana thread sadece yeni bağlantıları kabul etmekle meşgul olur.

⚠️ Dikkat: Her client için yeni bir Thread oluşturmak basit ama ölçeklenebilir değildir. Binlerce eşzamanlı bağlantıda thread sayısı patlar. Üretim (production) ortamında ExecutorService ile bir thread pool kullan:

>

``java ExecutorService pool = Executors.newFixedThreadPool(50); // ... pool.submit(new ClientHandler(client)); // thread.start() yerine ``

>

Bu şekilde aynı anda en fazla 50 thread çalışır, gerisini sıraya alır.


7. UDP — DatagramSocket ve DatagramPacket

UDP ile çalışmak TCP'den farklıdır. Bağlantı kurma (handshake) aşaması yoktur. Veriyi bir pakete koyar ve gönderirsin — karşı taraf aldıysa aldı, almadıysa almadı.

UDP'de iki ana sınıf kullanırız: DatagramSocket (iletişim kanalı) ve DatagramPacket (veri paketi).

UDP Gönderici (Sender)

import java.net.*;

public class UDPSender {
    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket();

        String message = "Merhaba UDP!";
        byte[] buffer = message.getBytes();

        InetAddress address = InetAddress.getByName("localhost");
        DatagramPacket packet = new DatagramPacket(
            buffer, buffer.length, address, 9876);

        socket.send(packet);
        System.out.println("Mesaj gönderildi: " + message);
        socket.close();
    }
}

UDP Alıcı (Receiver)

import java.net.*;

public class UDPReceiver {
    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket(9876);
        System.out.println("UDP Receiver dinliyor (port 9876)...");

        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        socket.receive(packet); // Paket gelene kadar bekler

        String received = new String(
            packet.getData(), 0, packet.getLength());
        System.out.println("Alınan mesaj: " + received);
        System.out.println("Gönderen: " + packet.getAddress()
            + ":" + packet.getPort());

        socket.close();
    }
}

UDP'de dikkat edilmesi gereken noktalar:

  • Veri byte[] dizisi olarak gönderilir/alınır. String dönüşümü senin işin.

  • receive() metodu blocking'dir — paket gelene kadar bekler.

  • Pratikte UDP paket boyutunu 8192 byte'ın altında tut.


8. URL ve URLConnection Sınıfları

Düşük seviye soketlerle çalışmak güçlü ama bazen sadece bir web sayfasının içeriğini çekmek istersin. Bu durumda URL ve URLConnection sınıfları işini görür.

URL sınıfı bir URL adresini temsil eder ve bileşenlerine ayırmanı sağlar:

import java.net.URL;

URL url = new URL("https://api.example.com:8443/users?page=1");

System.out.println("Protocol: " + url.getProtocol()); // https
System.out.println("Host: " + url.getHost());         // api.example.com
System.out.println("Port: " + url.getPort());         // 8443
System.out.println("Path: " + url.getPath());         // /users
System.out.println("Query: " + url.getQuery());       // page=1

URLConnection ile bir URL'ye bağlanıp içeriğini okuyabilirsin:

import java.net.*;
import java.io.*;

URL url = new URL("https://jsonplaceholder.typicode.com/posts/1");
URLConnection conn = url.openConnection();
conn.setConnectTimeout(5000); // 5 saniye bağlantı timeout
conn.setReadTimeout(5000);    // 5 saniye okuma timeout

try (BufferedReader reader = new BufferedReader(
         new InputStreamReader(conn.getInputStream()))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

Bu yöntem çalışır ama biraz eski usul. Java 11 ile gelen HttpClient çok daha temiz ve modern bir alternatif sunuyor. Şimdi ona geçelim.


9. HttpClient (Java 11+) — Modern HTTP Client

Java 11 ile birlikte gelen java.net.http.HttpClient, ağ programlamada devrim niteliğinde bir yenilik. Eski HttpURLConnection'ın karmaşık ve hantal yapısına kıyasla, akıcı (fluent) bir API sunar. HTTP/2 desteği, asenkron istekler ve temiz bir builder pattern'i ile gelir.

HttpClient üç ana sınıftan oluşur:

SınıfGörev
HttpClientHTTP isteklerini gönderen client
HttpRequestGönderilecek istek (URL, method, headers, body)
HttpResponseSunucudan dönen yanıt

Temel kullanım şablonu şöyle:

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
    .GET()  // varsayılan zaten GET
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());

Ne kadar temiz! Builder pattern ile isteği adım adım inşa ediyorsun. BodyHandlers.ofString() ile yanıtı doğrudan String olarak alıyorsun.

HttpClient instance'ı thread-safe'dir ve tekrar kullanılabilir. Genelde uygulama başına bir tane oluşturulur. Her istek için yeni client oluşturmak gereksiz maliyet yaratır.


10. GET ve POST İstekleri HttpClient ile

GET İsteği

GET isteği en basit HTTP çağrısıdır — sunucudan veri çekmek için kullanılır. Header eklemek de çok kolay:

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://jsonplaceholder.typicode.com/users"))
    .header("Accept", "application/json")
    .header("User-Agent", "JavaApp/1.0")
    .GET()
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

System.out.println("Status: " + response.statusCode());
System.out.println("Headers: " + response.headers().map());
System.out.println("Body: " + response.body());

POST İsteği

POST isteği ile sunucuya veri gönderirsin. Body kısmını BodyPublishers ile belirlersin:

HttpClient client = HttpClient.newHttpClient();

String jsonBody = """
    {
        "title": "Yeni Post",
        "body": "Bu bir test mesajıdır.",
        "userId": 1
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

System.out.println("Status: " + response.statusCode()); // 201 Created
System.out.println("Yanıt: " + response.body());

Diğer HTTP metodları da benzer pattern'i takip eder: PUT(BodyPublishers.ofString(...)) ve DELETE() builder'da aynı şekilde çağrılır.

💡 İpucu: Java 15+ kullanıyorsan, JSON string'lerini text blocks (""") ile yazmak çok daha okunabilir. Escape karakterleriyle uğraşmana gerek kalmaz. Yukarıdaki POST örneğinde bunu görüyorsun.


11. JSON Response Parse Etme

API'lerden dönen yanıtlar genelde JSON formatındadır. Java'da JSON parse etmek için birkaç yol var. En popüler kütüphane Gson'dur ama basit durumlar için String işlemleri de kullanılabilir.

Manuel String.indexOf() / substring() ile JSON parse etmek mümkün ama kırılgan ve güvenilmez. Gerçek projelerde asla yapma — bir JSON kütüphanesi kullan.

Gson ile Parse Etme

Gson, Google'ın JSON kütüphanesidir. Maven veya Gradle ile projeye eklenir:

<!-- Maven dependency -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

Kullanımı son derece basit:

import com.google.gson.Gson;

// JSON'a karşılık gelen sınıf
class Post {
    int id;
    String title;
    String body;
    int userId;
}

// Parse etme
Gson gson = new Gson();
String json = response.body();

// Tek obje parse
Post post = gson.fromJson(json, Post.class);
System.out.println("Başlık: " + post.title);
System.out.println("Yazar ID: " + post.userId);

Bir JSON dizisini (array) parse etmek için:

import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

String jsonArray = response.body(); // [{"id":1,...}, {"id":2,...}]

Type listType = new TypeToken<List<Post>>(){}.getType();
List<Post> posts = gson.fromJson(jsonArray, listType);

for (Post p : posts) {
    System.out.println(p.id + ": " + p.title);
}

Gson nested objeler, listeler ve null değerleri sorunsuz handle eder. Alternatif olarak Jackson da çok yaygındır — Spring Boot'ta varsayılan JSON kütüphanesidir.


12. Async HTTP — HttpClient.sendAsync()

Şimdiye kadar yaptığımız tüm HTTP istekleri senkron (synchronous) idi. Yani client.send() çağrıldığında yanıt gelene kadar thread bloklanıyordu. Bu, GUI uygulamalarında veya çok sayıda istek atılması gereken durumlarda sorun yaratır.

HttpClient.sendAsync() metodu, isteği arka planda gönderir ve bir CompletableFuture döner. Ana thread çalışmaya devam edebilir.

import java.net.http.*;
import java.net.URI;
import java.util.concurrent.CompletableFuture;

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
    .build();

CompletableFuture<HttpResponse<String>> future =
    client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

System.out.println("İstek gönderildi, başka işler yapıyorum...");

// Yanıt geldiğinde çalışacak callback
future.thenApply(HttpResponse::body)
      .thenAccept(body -> System.out.println("Yanıt: " + body))
      .join(); // Ana thread'in bitmesini bekle

Birden fazla isteği paralel olarak göndermek de mümkün:

HttpClient client = HttpClient.newHttpClient();

List<URI> urls = List.of(
    URI.create("https://jsonplaceholder.typicode.com/posts/1"),
    URI.create("https://jsonplaceholder.typicode.com/posts/2"),
    URI.create("https://jsonplaceholder.typicode.com/posts/3")
);

List<CompletableFuture<String>> futures = urls.stream()
    .map(uri -> HttpRequest.newBuilder().uri(uri).build())
    .map(req -> client.sendAsync(req,
        HttpResponse.BodyHandlers.ofString()))
    .map(future -> future.thenApply(HttpResponse::body))
    .toList();

// Hepsinin tamamlanmasını bekle
futures.forEach(f -> System.out.println(f.join()));

Bu örnekte üç istek aynı anda gönderilir — sırayla göndermekten çok daha hızlı!

💡 İpucu: CompletableFuture.join() ve get() benzer görünür ama farkları var. join() checked exception fırlatmaz (CompletionException fırlatır), get() ise ExecutionException ve InterruptedException fırlatır. Genelde join() tercih edilir çünkü daha temiz kod yazmanı sağlar.


13. Timeout ve Hata Yönetimi

Ağ programlama, doğası gereği hatalara açıktır. Sunucu çökebilir, ağ kopabilir, DNS çözümlenemeyebilir. Bu yüzden sağlam bir hata yönetimi şart.

Timeout Ayarlama

HttpClient ile timeout ayarlamak çok kolay:

import java.time.Duration;

// Client seviyesinde genel timeout
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();

// İstek seviyesinde timeout
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/data"))
    .timeout(Duration.ofSeconds(10))
    .build();

connectTimeout bağlantı kurma süresini, timeout ise tüm isteğin tamamlanma süresini sınırlar. Socket'lerde ise:

Socket socket = new Socket();
socket.connect(
    new InetSocketAddress("localhost", 8080),
    5000  // 5 saniye bağlantı timeout
);
socket.setSoTimeout(10000); // 10 saniye okuma timeout

Yaygın Exception'lar

Ağ programlamada karşılaşacağın başlıca hatalar:

ExceptionNe Zaman Oluşur
ConnectExceptionSunucuya bağlanılamadığında (sunucu kapalı, port yanlış)
SocketTimeoutExceptionBağlantı veya okuma timeout'u aşıldığında
UnknownHostExceptionDNS çözümlenemediğinde (yanlış hostname)
SocketExceptionBağlantı beklenmedik şekilde kesildiğinde
HttpTimeoutExceptionHttpClient isteği timeout'a uğradığında

Kapsamlı Hata Yönetimi Örneği

import java.net.http.*;
import java.net.*;
import java.io.IOException;
import java.time.Duration;

public class SafeHttpCall {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.example.com/data"))
            .timeout(Duration.ofSeconds(10))
            .build();

        try {
            HttpResponse<String> response = client.send(
                request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() == 200) {
                System.out.println("Başarılı: " + response.body());
            } else {
                System.err.println("HTTP Hata: " + response.statusCode());
            }
        } catch (HttpTimeoutException e) {
            System.err.println("İstek zaman aşımına uğradı!");
        } catch (ConnectException e) {
            System.err.println("Sunucuya bağlanılamadı: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("I/O hatası: " + e.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("İstek kesildi!");
        }
    }
}

En spesifik exception'ı önce yakala — IOException en son olmalı çünkü diğerlerinin üst sınıfıdır.

⚠️ Dikkat: InterruptedException yakaladığında, Thread.currentThread().interrupt() çağrısını unutma! Bu, thread'in interrupt durumunu geri yükler ve üst katmanlardaki kodun bu durumdan haberdar olmasını sağlar. Bunu atlamak, zor bulunan bug'lara yol açar.


14. Try-with-Resources ile Socket Kapatma

Ağ programlamada kaynak yönetimi kritik önem taşır. Açık kalan soketler, port tükenmesine ve bellek sızıntısına neden olabilir. Java 7'de gelen try-with-resources bu sorunu zarif bir şekilde çözer.

Socket, ServerSocket, DatagramSocket ve tüm I/O stream'leri AutoCloseable arayüzünü implement eder. Bu sayede try-with-resources ile otomatik kapatılabilirler.

Eski usulde (Java 7 öncesi) her kaynağı finally bloğunda tek tek kapatman gerekirdi — hem uzun hem hata yapmaya müsait bir koddu. Try-with-resources ile bu çok daha temiz:

try (Socket socket = new Socket("localhost", 8080);
     BufferedReader reader = new BufferedReader(
         new InputStreamReader(socket.getInputStream()));
     PrintWriter writer = new PrintWriter(
         socket.getOutputStream(), true)) {

    writer.println("Merhaba!");
    String response = reader.readLine();
    System.out.println("Yanıt: " + response);

} catch (IOException e) {
    System.err.println("Hata: " + e.getMessage());
}
// Socket, reader ve writer otomatik kapatılır!

Çok daha temiz, değil mi? try bloğu bittiğinde — normal çıkış veya exception fark etmez — tüm kaynaklar otomatik olarak ters sırayla kapatılır. Önce writer, sonra reader, en son socket.

ServerSocket için de aynı pattern geçerli. İç içe try-with-resources ile hem server socket'ini hem de her client bağlantısını güvenli şekilde yönetebilirsin — dersin önceki bölümlerindeki örneklerde bunu zaten uyguladık.


Özet

Bu derste Java ile ağ programlamanın temellerinden modern HTTP client'a kadar geniş bir yelpazede konuları ele aldık. İşte anahtar noktalar:

  • TCP (Socket/ServerSocket) güvenilir, bağlantı tabanlı iletişim sağlar. Client Socket, server ServerSocket kullanır. accept() bloklayıcıdır ve her bağlantıda yeni bir Socket döner.

  • UDP (DatagramSocket/DatagramPacket) hızlı ama güvenilir olmayan iletişim sunar. Bağlantı kurmadan veri gönderir. Video streaming, oyunlar gibi gecikmenin kritik olduğu alanlarda tercih edilir.

  • Multi-threaded server yapısında her client için ayrı bir thread oluşturulur. Gerçek projelerde ExecutorService ile thread pool kullanmak daha ölçeklenebilirdir.

  • HttpClient (Java 11+) modern, fluent API ile HTTP istekleri atmayı kolaylaştırır. Builder pattern ile request oluşturulur, send() senkron, sendAsync() asenkron çağrı yapar.

  • JSON parse için Gson veya Jackson gibi kütüphaneler kullanılır. Manuel String parse kırılgandır, gerçek projelerde tercih etme.

  • Hata yönetimi ve timeout ağ programlamanın olmazsa olmazıdır. ConnectException, SocketTimeoutException, HttpTimeoutException gibi spesifik hataları yakalayarak kullanıcıya anlamlı mesajlar ver. Tüm soketleri try-with-resources ile kapat.