KARADENİZ TEKNİK ÜNİVERSİTESİ Bilgisayar Mühendisliği Bölümü Bilgisayar Sistemleri Laboratuarı JAVA ile DAĞITIK PROGRAMLAMA 1. Giriş JAVA, ağdaki programların haberleşmesi için TCP ve UDP olmak üzere iki farklı socket türü kullanır. Her iki socket türü de haberleşmede Client-Server ilişkisini kullanır. Bu deneyde TCP socket kullanılarak Server ve Client bilgisayarlar için yazılacak socket programları ile JAVA ile dağıtık programlama anlatılacaktır. Örnek programlar “An Introduction to Network Programming with Java” adlı kitaptan alınmıştır. 2. Server Socket Setlemeleri Burada Client bilgisayar ile haberleşme için gerekli Server Socket setlemeleri anlatılacaktır: 2.1. ServerSocket Nesnesi Türetme servSock isimli ServerSocket nesnesi 1024-65535 arası yani özel amaçlar için ayrılmış numaraların dışında bir port numarası seçerek aşağıdaki gibi oluşturulur: ServerSocket servSock = new ServerSocket(1234); 2.2. Server’ı Bekleme Konumuna Getirme servSock, ServerSocket sınıfına ait accept methodunu kullanarak herhangi bir Client’ın bağlanmasını bekler. Bağlantı kurulduğunda aşağıdaki gibi link isimli bir Socket nesnesi türetilir: Socket link = servSock.accept(); 2.3. Gönderilecek/Alınacak Veriler için Input/Output Stream Setleme Client bilgisayardan gelecek mesajları almak ve ona mesaj göndermek üzere Socket sınıfına ait getInputStream ve getOutputStream methodları kullanılarak input ve output isimli stream nesneleri aşağıdaki gibi türetilir: Scanner input = new Scanner(link.getInputStream()); PrintWriter output = new PrintWriter(link.getOutputStream(),true); 2.4. Veri Gönderme ve Alma Veri gönderme ve alma işlemleri için sırasıyla println ve nextLine methodları kullanılır : output.println(“.....”); String message = input.nextLine(); 2.5. Bağlantıyı Sonlandırma Bağlantı Socket sınıfının close methodu ile sonlandırılır: link.close(); 3. Client Socket Setlemeleri Server ile bağlantı kurma dışındaki Client Socket setlemeleri yukarıda Server için anlatılanlarla ayınıdır. Server ile bağlantı kurmak için link isimli Socket nesnesi Server’ın ismi (veya IP adresi) ve port numarası parametreleri yardımıyla aşağıdaki gibi türetilir: Socket link = new Socket( “ServerName” , 1234); 4. Client-Server Programı Aşağıda örnek bir Client-Server programı ve ekran çıktısı verilmiştir. Öncelikle TCPServer.java programı koşularak Server başlatılır. Sonra TCPClient.java koşularak Server ile bağlantı kurulur. Bu örnekte Client tarafından gönderilen mesajlar Server tarafından sayılır ve “BYE BYE” mesajı geldiğinde bağlantı sonlandırılırken toplam mesaj sayısı yazılır. TCPServer.java import java.io.*; import java.net.*; import java.util.*; public class TCPServer { private static ServerSocket servSock; private static final int PORT = 1234; public static void main(String[] args) { try { servSock = new ServerSocket(PORT); System.out.println("Waiting for connection...\n"); } catch(IOException ioEx) { System.out.println("Unable to attach to port!"); System.exit(1); } do { handleClient(); } while (true); } 2 private static void handleClient() { Socket link = null; String message = null; try { link = servSock.accept(); Scanner input = new Scanner(link.getInputStream()); PrintWriter output = new PrintWriter(link.getOutputStream(),true); int numMessages = 1; message = input.nextLine(); while (!message.equals("BYE BYE")) { System.out.println("Received Message : " + message); numMessages++; message = input.nextLine(); } output.println(numMessages + " messages received."); } catch(IOException ioEx){ ioEx.printStackTrace(); } Finally { try{ System.out.println("Because of Received Message : " + message); System.out.println(" * Closing connection... *"); link.close(); } catch(IOException ioEx){ System.out.println("Unable to disconnect!"); System.exit(1); } } } } TCPClient.java import java.io.*; import java.net.*; import java.util.*; public class TCPClient { private static InetAddress host; private static final int PORT = 1234; public static void main(String[] args) { try{ //host = InetAddress.getByName("ServerName"); host = InetAddress.getLocalHost(); System.out.println("Connected to Server"); } catch(UnknownHostException uhEx) { System.out.println("Host ID not found!"); System.exit(1); } accessServer(); } 3 private static void accessServer() { Socket link = null; try { link = new Socket(host,PORT); Scanner input = new Scanner(link.getInputStream()); PrintWriter output = new PrintWriter(link.getOutputStream(),true); Scanner userEntry = new Scanner(System.in); String message, response; do { System.out.print("Enter message : "); message = userEntry.nextLine(); output.println(message); } while (!message.equals("BYE BYE")); response = input.nextLine(); System.out.println("\nSERVER > "+response); } catch(IOException ioEx) { ioEx.printStackTrace(); } finally { try { System.out.println("* Closing connection... *"); link.close(); } catch(IOException ioEx) { System.out.println("Unable to disconnect!"); System.exit(1); } } } } SERVER CLIENT [0]Waiting for connection... [1]Connected to Server [3]Received Message : MERHABA [2]Enter message : MERHABA [5]Received Message : NASILSIN [4]Enter message : NASILSIN [7]Because of Received Message : BYE BYE [6]Enter message : BYE BYE * Closing connection... * [8]SERVER > 3 messages received. * Closing connection... * 4 5. Multithreading Java ile dağıtık programlamada Server bilgisayara 1 ‘den fazla Client bilgisayarın bağlantı kurması durumunda her bir Client bilgisayar için bir thread başlatılır. Bunun için öncelikle Thread sınıfı miraslanarak bir thread nesnesi türetilir. start methodu ile bu threadin yapacağı işin kodunu barındıran run methodu çağrılır. Çok sayıda thread aynı anda koşarken birbirlerine zaman ayırmaları için sleep methodu kullanılır ve parametre olarak aldığı milisaniye cinsinden zaman kadar askıda durur. Aşağıdaki örnek programda biri 5 kere “Hello”; diğeri de 1-5 arası sayıları ekrana yazan iki threadi çağıran ThreadHelloCount adlı java programı ve çıktısı verilmiştir: ThreadHelloCount.java import java.io.*; public class ThreadHelloCount { public static void main(String[] args) { HelloThread hello = new HelloThread(); CountThread count = new CountThread(); hello.start(); count.start(); } } class HelloThread extends Thread { public void run() { for (int i=0; i<5; i++) { try { System.out.println("Hello!"); sleep((int)(Math.random()*1000)); } catch (InterruptedException interruptEx) System.out.println(interruptEx); } } } class CountThread extends Thread { public void run() { for (int i=0; i<5; i++) { try { System.out.println(i); sleep((int)(Math.random()*1000)); } catch (InterruptedException interruptEx) System.out.println(interruptEx); } } } 5 Hello! 0 1 Hello! 2 3 Hello! Hello! 4 Hello! Yukarıdaki program çıktısına dikkat edilirse threadler farklı sıklıklarla çağrılmıştır. Bunun nedeni sleep methodundaki Math.random()*1000 ifadesidir. Böylece random fanksiyonu ile her bir thread 0 ile 1 saniye arasında değişen farklı sürelerde askıda durmaktadır. Dolayısıyla ThreadHelloCount adlı program her koşuşunda farklı bir çıktı üretecektir. Aşağıda, daha önce anlatılan TCPServer ve TCPClient programlarının multithreading versiyonu verilmiştir. MutiServer.java programı, Thread sınıfını miraslayan ClientHandler sınıfı herbir Client bağlantısında bir thread başlatmaktadır. MultiClient.java programı TCPClient ile hemen hemen aynıdır. MultiServer.java import java.io.*; import java.net.*; import java.util.*; public class MultiServer { private static ServerSocket serverSocket; private static final int PORT = 1234; public static void main(String[] args) throws IOException { try{ serverSocket = new ServerSocket(PORT); } catch (IOException ioEx){ System.out.println("\nUnable to set up port!"); System.exit(1); } do { Socket client = serverSocket.accept(); System.out.println("\nNew client accepted.\n"); ClientHandler handler = new ClientHandler(client); handler.start(); //As usual, method calls run. } while (true); } } class ClientHandler extends Thread { private Socket client; private Scanner input; private PrintWriter output; public ClientHandler(Socket socket) { //Set up reference to associated socket... client = socket; try{ input = new Scanner(client.getInputStream()); output = new PrintWriter( client.getOutputStream(),true); } catch(IOException ioEx){ ioEx.printStackTrace(); } } 6 public void run() { String received; do { //Accept message from client on the socket's input stream... received = input.nextLine(); //Echo message back to client on the socket's output stream... output.println("ECHO: " + received); } while (!received.equals("QUIT")); //Repeat above until 'QUIT' sent by client... try { if (client!=null) { System.out.println("Closing down connection..."); client.close(); } } catch(IOException ioEx) { System.out.println("Unable to disconnect!"); } } } MultiClient.java import java.io.*; import java.net.*; import java.util.*; public class MultiClient { private static InetAddress host; private static final int PORT = 1234; public static void main(String[] args) { try { //host = InetAddress.getByName("ServerName"); host = InetAddress.getLocalHost(); } catch(UnknownHostException uhEx) { System.out.println("\nHost ID not found!\n"); System.exit(1); } sendMessages(); } 7 private static void sendMessages() { Socket socket = null; try { socket = new Socket(host,PORT); Scanner networkInput = new Scanner(socket.getInputStream()); PrintWriter networkOutput = new PrintWriter(socket.getOutputStream(),true); Scanner userEntry = new Scanner(System.in); String message, response; do { System.out.print("Enter message ('QUIT' to exit): "); message = userEntry.nextLine(); networkOutput.println(message); response = networkInput.nextLine(); System.out.println("\nSERVER> " + response); } while (!message.equals("QUIT")); } catch(IOException ioEx) { ioEx.printStackTrace(); } finally { try { System.out.println("\nClosing connection..."); socket.close(); } catch(IOException ioEx) { System.out.println("Unable to disconnect!"); System.exit(1); } } } } 6. Synchronized Thread’ler Farklı threadlerin ortak kullandıkları kaynaklara eşzamanlı erişimleri yanlış sonuçlar üretmeye neden olabilir. O yüzden ortak kaynaklara erişimi engelleyecek bir mekanizmaya ihtiyaç vardır. Java synchronized anahtar kelimesi ile bunu gerçekleştirir. Örneğin aşağıdaki methodda ortak kullanılan sum adlı değişkenin aynı anda farklı threadler tarafından güncellenmemesi için updateSum adlı method synchronized yapılmıştır. public synchronized void updateSum(int amount) { sum+=amount; } Synchronized bir methodu koşan thread’e o anlık bir iş düşmüyorsa wait methodunu çağırarak synchronized method üzerindeki kilidi kaldırır ve böylece diğer threadlerin de o methodu koşmasına izin verir. Eğer bir thread işini tamamlamışsa ve wait konumundaki başka bir thread’in çalışmasını sağlamak istiyorsa notify methodunu kullanır. Wait konumundaki bütün threadlerin çalıştırılması için de notifyall methodu kullanılır. Bu durumda hangi thread’e öncelik verileceğine JVM karar verir. 8 6.1. Producer-Consumer Problemi Bilindiği gibi producer-consumer probleminde producerın ürettiği – consumerın tükettiği ortak kaynak kullanılmaktadır. Burada en önemli problem kaynağa eş zamanlı erişimi engelleyerek tutarlılığı sağlamaktır. Yukarıda da bahsedildiği gibi producer ve consumerın ortak çağırdıkları methodlar synchronized yapılarak tutarlı bir kaynak güncellemesi yapılabilecektir. Producer-consumer uygulaması, kaynak kodlardan “Multi Producer-Consumer” adlı klasörün içindedir. Producer ve consumerın ortak erişeceği, kaynak (resourse) üretme ve tüketme işini yapan addOne ve takeOne methodları, Resourse.java programı içindedir. ResourseServer.java programı öncelikle item isimli bir Resourse nesnesi türetir ve bu nesneyi parametre vererek türettiği Producer türünden producer isimli thread nesnesinde addOne methodunu sürekli çağırarak kaynak üretmeye başlar. ResourseServer adından da anlaşılabileceği gibi aynı zamanda bir server programıdır. Kaynak üreten server programıdır. Kaynak tüketimi de bu servera bağlanan clientlar tarafından yapılacaktır. Bağlantı kurulduğunda ResourseServer programında handler isimli bir ClientThread threadi başlatılır ve clientlerın isteklerine cevap verilir. Client bilgisayar “1” karakteri yolladıkça kaynakları tutan item nesnesinin takeOne methodu çağrılarak kaynak harcanır. “0” karakteri ile de bağlantı sonlandırılır. Kaynak üretimi belli bir MAX (5) değerle sınırlandırılmıştır. Bu değere ulaşıldığında wait methodu çağrılarak clientların kaynağı harcaması beklenir. Ayrıca herhangi bir kaynak üretildiğinde de yine tüketilebilmesi için notifiyall methodu ile clientlara bilgi verilir. 7. Deneye Hazırlık Bölüm 6.1. ‘de anlatılan Producer-Consumer uygulamasında Client bilgisayarda koşacak program Multithreading uygulamasındaki MultiClient.java programına çok benzediği için ayrıca anlatılmamıştır. MultiClient.java programının ismini ConsumerClient.java olarak değiştiriniz ve gerekli değişiklikleri yaparak Producer-Consumer uygulamasının doğru bir şekilde çalışmasını sağlayınız. Yazdığınız programı deneye getiriniz. 8. Deneyin Yapılışı Resource.java programı ekrana addOne methodunda üretilen, takeOne’da da kalan numResources toplam kaynak sayısını yazmaktadır. Bunun yerine Resourse.java programına (“Deneyin Yapılışı” adlı klasördeki basit bir Stack uygulamasından da yararlanarak) Stack ekleyip, addOne methodu çağrıldığında hangi kaynağın üretildiğini “PUSHED ITEM = 55” şeklinde, takeOne çağırıldığında da hangi kaynağın tüketildiğini “POPED ITEM = 55” şeklinde ekrana yazınız. Burada üretilen kaynak numResources değişkeninden farklı olarak 0..100 arası random belirlensin. Dolayısıyla stack’e itilen ve stack’ten çekilen değerler de 0..100 arası olacaktır. Stack’in dolu veya boş olduğu bilgisini de “STACK IS FULL/EMPTY” şeklinde ekrana yazınız. Resourse.java programının başında java.util.*; adlı package’i import etmeyi unutmayınız. ResourseServer.java, ConsumerClient.java ‘ya “YOU POPED = 55” şeklinde stackten çekilen değeri yollasın. ConsumerClient.java programından gelen isteklere (“1” veya “0”) bağlı olarak ResourseServer.java ve ConsumerClient.java programlarının ekran çıktısı aşağıdakine benzer olmalıdır : 9 RESOURSESERVER.JAVA CONSUMERCLIENT.JAVA PUSHED ITEM = 55 New client accepted. POPED ITEM = 55 PUSHED ITEM = 21 POPED ITEM = 21 STACK IS EMPTY PUSHED ITEM = 12 POPED ITEM = 12 STACK IS EMPTY PUSHED ITEM = 73 POPED ITEM = 73 PUSHED ITEM = 51 PUSHED ITEM = 6 POPED ITEM = 6 POPED ITEM = 51 STACK IS EMPTY PUSHED ITEM = 47 POPED ITEM = 47 PUSHED ITEM = 48 PUSHED ITEM = 18 PUSHED ITEM = 17 PUSHED ITEM = 0 PUSHED ITEM = 70 STACK IS FULL Closing down connection... Enter message ('0' to exit): SERVER> YOU POPED = 55 Enter message ('0' to exit): SERVER> YOU POPED = 21 Enter message ('0' to exit): SERVER> YOU POPED = 12 Enter message ('0' to exit): SERVER> YOU POPED = 73 Enter message ('0' to exit): SERVER> YOU POPED = 6 Enter message ('0' to exit): SERVER> YOU POPED = 51 Enter message ('0' to exit): SERVER> YOU POPED = 47 Enter message ('0' to exit): SERVER> Connection closed... Closing connection... Process completed. 1 1 1 1 1 1 1 0 8. Rapor Raporu programların olduğu klasördeki şablon belgeyi kullanarak yazınız. Rapor grup adına yazılacaktır. Her öğrencinin ayrı ayrı rapor yazmasına gerek yoktur. Raporun son teslim tarihi sonraki hafta deney saatidir. Daha geç verilen raporlar değendirilmeyecektir. 9. Değerlendirme Kriterleri Deney notu aşağıdaki kriterlere göre verilecektir: DENEYE HAZIRLIK DENEYİN YAPILIŞI RAPOR 40 PUAN 40 PUAN 20 PUAN 10