Socket Programlama
Önceki bölümde ağın temel kavramlarını ve TCP/UDP'yi tanıdık. Şimdi TCP soketleriyle gerçek istemci-sunucu uygulamaları yazmayı derinleştiriyoruz. Soket, iki makine (veya aynı makinedeki iki süreç) arasındaki iletişim…
Socket Programlama
Önceki bölümde ağın temel kavramlarını ve TCP/UDP'yi tanıdık. Şimdi TCP soketleriyle gerçek istemci-sunucu uygulamaları yazmayı derinleştiriyoruz. Soket, iki makine (veya aynı makinedeki iki süreç) arasındaki iletişim kanalının iki ucundan biridir. Java'da soketle çalışmak, dosyayla çalışmaya çok benzer: bir giriş akışından okur, bir çıkış akışına yazarsın. Örneklerin hepsi tek bir JVM'de (loopback üzerinde) çalışır; internet gerektirmez.
Soketin anatomisi
İki temel sınıf vardır:
ServerSocket(sunucu tarafı): Bir portu dinler.accept()çağrısı bir istemci bağlanana kadar bloklar ve bağlandığında o istemciyle konuşmak için birSocketdöndürür.Socket(istemci tarafı ve sunucunun istemci-başına soketi): İki uç arasındaki bağlantı.getInputStream()vegetOutputStream()ile veri akar.
// Sunucu ServerSocket sunucu = new ServerSocket(port); Socket istemci = sunucu.accept(); // bağlantı bekle (bloklar) // İstemci Socket soket = new Socket("localhost", port); // bağlan
Akışları metin için BufferedReader/PrintWriter ile sarmak en pratik yoldur (satır satır
oku/yaz). Ham byte için doğrudan stream'leri, nesne için ObjectStream'leri kullanırsın.
Bir protokol tanımlamak
Soket sana ham bir kanal verir; üzerine kendi protokolünü (mesaj formatını) tanımlarsın. En
basit haliyle satır tabanlı bir istek-cevap olabilir. Örnek 1 (./Ornek1.java) bir "hesap
makinesi" sunucusu kurar: istemci "5 + 3" gönderir, sunucu ayrıştırıp "= 8" döner; geçersiz
ifade ve sıfıra bölme gibi hataları da yönetir. HTTP bile özünde böyle metin tabanlı bir istek/
cevap protokolüdür — sadece çok daha zengin kuralları vardır.
Çok istemciye aynı anda hizmet: thread'ler
Burada kritik bir gerçek var: accept() ve read() bloklar. Tek thread'le yazarsan, bir
istemciye hizmet ederken diğerleri bekler. Çözüm, her bağlantıyı ayrı bir thread'de işlemektir:
while (true) { Socket c = sunucu.accept(); // bağlantı bekle new Thread(() -> istemciyiHandle(c)).start(); // ayrı thread'de işle }
Örnek 2 (./Ornek2.java) aynı anda bağlanan 3 istemciyi thread-per-client modeliyle karşılar; her
istemci kendi thread'inde yanıt alır.
Üretimde: Her bağlantı için yeni
Threadoluşturmak pahalıdır; bunun yerine bir thread havuzu (ExecutorService) kullanılır. Ve on binlerce eşzamanlı bağlantıda Java 21'in sanal thread'leri (her bağlantıya ucuz bir sanal thread) oyunu değiştirir — bloklayan basit kodla yüksek eşzamanlılık.
Nesne aktarımı
Metin dışında, yapılandırılmış Java nesnelerini de gönderebilirsin. ObjectOutputStream
nesneyi byte'lara çevirir (serialize), ObjectInputStream karşıda geri kurar (deserialize):
out.writeObject(new SiparisIstegi(101, 3)); SiparisCevabi cevap = (SiparisCevabi) in.readObject();
Örnek 3 (./Ornek3.java) soket üzerinden bir istek/cevap nesne çifti aktarır. Nesnelerin
Serializable olması gerekir.
Uyarı: Java'nın yerleşik serileştirmesi güvenlik ve sürüm-uyumluluğu açısından kırılgandır. Farklı sistemler/diller arası iletişimde günümüzde JSON veya Protobuf gibi taşınabilir formatlar tercih edilir (bunu Spring/REST tarafında JSON ile gördük).
Kaynak yönetimi
Soketler ve akışlar birer kaynaktır; mutlaka kapatılmalıdır. try-with-resources bunu otomatik
yapar (örneklerde try (Socket s = ...)). Kapatılmayan soketler port/bağlantı sızıntısına yol açar.
Özet
ServerSocket/Socket ile istemci-sunucu kurmayı; soket üzerine kendi metin protokolünü
tanımlamayı (Örnek 1); çok istemciye thread'lerle eşzamanlı hizmeti (Örnek 2) ve nesne aktarımını
(Örnek 3) öğrendik; thread havuzu/sanal thread ve serileştirme uyarılarına değindik. Bu, tüm ağ
uygulamalarının (web sunucuları dahil) altında yatan mekanizmadır. Sırada, daha yüksek seviyeli
bir soyutlama: adreslerle çalışmak için URL ve URL işlemleri.
▶ Kod Örnekleri(3)
Ornek1
çalıştırılabilir1// Ornek1: İstek-cevap protokolü — basit bir "hesap makinesi" TCP sunucusu.
2// İstemci "a op b" gönderir, sunucu hesaplayıp sonucu döner. (Tek JVM.)
3// Çalıştırma: java Ornek1.java
4import java.io.BufferedReader;
5import java.io.IOException;
6import java.io.InputStreamReader;
7import java.io.PrintWriter;
8import java.net.ServerSocket;
9import java.net.Socket;
10
11public class Ornek1 {
12
13 public static void main(String[] args) throws Exception {
14 ServerSocket sunucu = new ServerSocket(0);
15 int port = sunucu.getLocalPort();
16 System.out.println("Hesap sunucusu, port: " + port);
17
18 // Sunucu: her satırı "a op b" olarak ayrıştırıp sonucu geri yazar (basit bir PROTOKOL).
19 Thread st = new Thread(() -> {
20 try (Socket c = sunucu.accept();
21 BufferedReader in = new BufferedReader(new InputStreamReader(c.getInputStream()));
22 PrintWriter out = new PrintWriter(c.getOutputStream(), true)) {
23 String satir;
24 while ((satir = in.readLine()) != null) {
25 if (satir.equals("KAPAT")) break;
26 out.println(hesapla(satir));
27 }
28 } catch (IOException e) { System.out.println("sunucu: " + e.getMessage()); }
29 });
30 st.start();
31
32 // İstemci: birkaç istek gönderir, cevapları okur.
33 try (Socket s = new Socket("localhost", port);
34 PrintWriter out = new PrintWriter(s.getOutputStream(), true);
35 BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()))) {
36 for (String istek : new String[]{"5 + 3", "10 * 4", "20 / 0", "9 - 12", "KAPAT"}) {
37 if (istek.equals("KAPAT")) { out.println(istek); break; }
38 out.println(istek);
39 System.out.println(" " + istek + " -> " + in.readLine());
40 }
41 }
42
43 st.join();
44 sunucu.close();
45 System.out.println("""
46
47 --- İstek-cevap protokolü ---
48 Soketin üstünde KENDİ metin protokolünü tanımladık: "sayı op sayı" -> sonuç.
49 Sunucu satır satır okur (readLine), işler, cevabı yazar (println). İstemci tersini yapar.
50 HTTP de aslında bunun gelişmişidir: metin tabanlı istek/cevap satırları.""");
51 }
52
53 static String hesapla(String ifade) {
54 try {
55 String[] p = ifade.split(" ");
56 double a = Double.parseDouble(p[0]), b = Double.parseDouble(p[2]);
57 return switch (p[1]) {
58 case "+" -> "= " + (a + b);
59 case "-" -> "= " + (a - b);
60 case "*" -> "= " + (a * b);
61 case "/" -> b == 0 ? "HATA: sıfıra bölme" : "= " + (a / b);
62 default -> "HATA: bilinmeyen işlem";
63 };
64 } catch (Exception e) { return "HATA: geçersiz ifade"; }
65 }
66}Ornek2
çalıştırılabilir1// Ornek2: Çok istemcili sunucu — her bağlantı ayrı thread'de (thread-per-client).
2// Bloklayan accept()/read(), birden çok istemciyi aynı anda karşılamak için thread ister.
3// Çalıştırma: java Ornek2.java
4import java.io.BufferedReader;
5import java.io.IOException;
6import java.io.InputStreamReader;
7import java.io.PrintWriter;
8import java.net.ServerSocket;
9import java.net.Socket;
10import java.util.concurrent.CountDownLatch;
11
12public class Ornek2 {
13
14 public static void main(String[] args) throws Exception {
15 ServerSocket sunucu = new ServerSocket(0);
16 int port = sunucu.getLocalPort();
17 int istemciSayisi = 3;
18 System.out.println("Çok istemcili sunucu, port: " + port);
19
20 // Sunucu döngüsü: her accept'te yeni bir thread başlat -> istemciler paralel hizmet alır.
21 Thread kabulDongusu = new Thread(() -> {
22 try {
23 for (int i = 0; i < istemciSayisi; i++) {
24 Socket c = sunucu.accept(); // bağlantı bekle
25 new Thread(() -> istemciyiHandle(c)).start(); // ayrı thread'de işle
26 }
27 } catch (IOException ignored) {}
28 });
29 kabulDongusu.start();
30
31 // 3 istemciyi AYNI ANDA başlat; her biri kendi sayısının karesini ister.
32 CountDownLatch bitti = new CountDownLatch(istemciSayisi);
33 for (int i = 1; i <= istemciSayisi; i++) {
34 final int sayi = i;
35 new Thread(() -> {
36 try (Socket s = new Socket("localhost", port);
37 PrintWriter out = new PrintWriter(s.getOutputStream(), true);
38 BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()))) {
39 out.println(sayi);
40 System.out.println(" istemci-" + sayi + " cevap: " + in.readLine());
41 } catch (IOException e) {
42 System.out.println("istemci-" + sayi + " hata: " + e.getMessage());
43 } finally { bitti.countDown(); }
44 }).start();
45 }
46
47 bitti.await();
48 sunucu.close();
49 System.out.println("""
50
51 --- Çok istemcili sunucu ---
52 accept() ve read() BLOKLAR; tek thread'le aynı anda yalnızca bir istemciye hizmet edilir.
53 Çözüm: her bağlantıyı ayrı bir thread'de (veya thread havuzunda) işlemek.
54 Üretimde 'thread-per-client' yerine genelde thread havuzu (ExecutorService) kullanılır;
55 ON BİNLERCE eşzamanlı bağlantı için Java 21 SANAL THREAD'leri idealdir.""");
56 }
57
58 static void istemciyiHandle(Socket c) {
59 try (c;
60 BufferedReader in = new BufferedReader(new InputStreamReader(c.getInputStream()));
61 PrintWriter out = new PrintWriter(c.getOutputStream(), true)) {
62 String satir = in.readLine();
63 int n = Integer.parseInt(satir.trim());
64 out.println(n + "^2 = " + (n * n) + " (thread: " + Thread.currentThread().getName() + ")");
65 } catch (Exception e) {
66 System.out.println("handle hatası: " + e.getMessage());
67 }
68 }
69}Ornek3
çalıştırılabilir1// Ornek3: Soket üzerinden NESNE aktarımı — ObjectOutputStream / ObjectInputStream.
2// Metin yerine yapılandırılmış Java nesneleri gönderip almak.
3// Çalıştırma: java Ornek3.java
4import java.io.ObjectInputStream;
5import java.io.ObjectOutputStream;
6import java.io.Serializable;
7import java.net.ServerSocket;
8import java.net.Socket;
9
10public class Ornek3 {
11
12 // Ağdan geçecek nesneler Serializable olmalı (byte dizisine çevrilebilmeli).
13 record SiparisIstegi(int urunId, int adet) implements Serializable {}
14 record SiparisCevabi(boolean basarili, String mesaj, double tutar) implements Serializable {}
15
16 public static void main(String[] args) throws Exception {
17 ServerSocket sunucu = new ServerSocket(0);
18 int port = sunucu.getLocalPort();
19 System.out.println("Nesne sunucusu, port: " + port);
20
21 Thread st = new Thread(() -> {
22 try (Socket c = sunucu.accept();
23 ObjectInputStream in = new ObjectInputStream(c.getInputStream());
24 ObjectOutputStream out = new ObjectOutputStream(c.getOutputStream())) {
25 // İstek nesnesini doğrudan oku (deserialize).
26 SiparisIstegi istek = (SiparisIstegi) in.readObject();
27 System.out.println(" [sunucu] istek: ürün=" + istek.urunId() + ", adet=" + istek.adet());
28 double tutar = istek.adet() * 450.0;
29 // Cevap nesnesini gönder (serialize).
30 out.writeObject(new SiparisCevabi(true, "sipariş alındı", tutar));
31 } catch (Exception e) { System.out.println("sunucu: " + e.getMessage()); }
32 });
33 st.start();
34
35 try (Socket s = new Socket("localhost", port);
36 ObjectOutputStream out = new ObjectOutputStream(s.getOutputStream());
37 ObjectInputStream in = new ObjectInputStream(s.getInputStream())) {
38 out.writeObject(new SiparisIstegi(101, 3)); // nesne gönder
39 SiparisCevabi cevap = (SiparisCevabi) in.readObject(); // nesne al
40 System.out.println("[istemci] cevap: " + cevap.mesaj() + ", tutar: " + cevap.tutar() + " TL");
41 }
42
43 st.join();
44 sunucu.close();
45 System.out.println("""
46
47 --- Nesne aktarımı (object serialization over socket) ---
48 ObjectOutputStream nesneyi byte'lara çevirip (serialize) gönderir; ObjectInputStream
49 karşıda nesneye geri çevirir (deserialize). Taraflar yapılandırılmış veriyle konuşur.
50 Uyarı: Java serileştirme güvenlik/uyumluluk açısından kırılgandır; servisler arası
51 iletişimde günümüzde genelde JSON/Protobuf gibi taşınabilir formatlar tercih edilir.""");
52 }
53}