Tillbaka till lektionslistan

Mobila applikationer med Android: Lektion 9

Idag:
  • "Android-Java" och "vanliga skrivbords-Java"
  • Nätverk, klienter och servrar
  • Nätverkskommunikation i Java: klasserna Socket och ServerSocket
  • Trådar
  • Varför trådar behövs för nätverkskommunikation
  • Trådar i Java: klassen Thread

Klicka på startknappen i den lilla mediaspelaren ovan för att lyssna på lektionen. (Man kan behöva vänta en stund på att ljudfilen laddas ner.) Om mediaspelaren inte syns, eller om det inte fungerar av något annat skäl, kan man klicka här för att ladda ner mp3-filen (ca 37 minuter, ca 17 megabyte). Beroende på hur webbläsaren är konfigurerad kan det kräva ett separat mp3-spelarprogram av något slag.

"Bild" 1: "Android-Java" och "vanliga skrivbords-Java"

  • "Skrivbords-Java" = Java SE = Java Standard Edition
  • "Server-Java" = Java EE = Java Enterprise Edition
  • "Telefon-Java" = Java ME = Java Micro Edition
  • "Android-Java" = ...?
  • JVM = Java Virtual Machine, kör Java-bytekod
  • Dalvik = Androids virtuella maskin
  • Dalvik kör inte vanlig Java-bytekod, och eftersom vi kompilerar med vanliga javac måste bytekoden konverteras först
  • Dalvik är gjord för maskiner med långsam processor och trångt minne
  • Många klasser från skrivbords-java finns med (ex: String, ArrayList, Thread, Socket)
  • Vissa klasser finns inte med (ex: JButton och hela resten av Swing-paketet)
  • Android-specifika klasser: Activity, Intent, android.widget.Button, ...
  • Och så måste man tänka lite annorlunda!

Bild 2: Datornätverk med klienter och servrar

Nätverkskommunikation

Bild 3: Socketar och datagram

Nätverkskommunikation

"Bild" 4: Portar

  • Olika "portar", till exempel port nummer 80 för http och port nummer 26000 för Quake
  • Mjukvaruportar (det går ju bara en nätverkssladd in i datorn)
  • Det står i varje meddelande (datagram) till vilken dator och vilken port det ska
  • En serverdator kan ha flera serverprocesser (dvs "program") igång samtidigt
  • En serverprocess lyssnar på en viss port, till exempel en webbserver som lyssnar på port 80
  • "Porten är öppen" = det finns en process som lyssnar, och svarar på tilltal, på den porten

"Bild" 5: Klientsidan av socketar i Java

  • Klienten måste veta adressen till serverdatorn, till exempel www.aftonbladet.se eller 130.243.104.99
  • Klienten måste veta portnumret, till exempel 80
  • Klientprocessen kopplar upp sig mot servern
  • Vid uppkopplingen skapas en socket (java.net.Socket), som är klientens ände av den uppkopplade förbindelsen.
  • Socketen är dubbelriktad, så data kan skickas både från klienten och från servern.
  • Socketen har två ändar, inte fler.

"Bild" 6: Serversidan av socketar i Java

  • Serverprocessen skapar en "server-socket" (java.net.ServerSocket) som lyssnar på en viss port
  • Nu är porten öppen, och klienter kan koppla upp sig mot den
  • Serverprocessen anropar metoden accept för att vänta på uppkopplingar och "acceptera" dem ("svara i telefonen")
  • Från anropet till accept returneras en "vanlig socket" (java.net.Socket), som är serverns ände av den uppkopplade förbindelsen
  • Serverprocessen kan anropa metoden accept på nytt för att vänta på fler uppkopplingar

"Bild" 7: En enkel server

HelloServer.java

    1	import java.net.ServerSocket;
    2	import java.net.Socket;
    3	
    4	import java.io.InputStreamReader;
    5	import java.io.OutputStreamWriter;
    6	import java.io.BufferedReader;
    7	import java.io.BufferedWriter;
    8	import java.io.PrintWriter;
    9	import java.io.IOException;
   10	
   11	
   12	public class HelloServer {
   13	    public static final int PORT = 2000;
   14	    public static void main(String[] args) throws IOException {
   15	        ServerSocket s = new ServerSocket(PORT);
   16	        System.out.println("Servern: Lyssnar...");
   17	        Socket socket = s.accept();
   18	        System.out.println("Servern: Uppkoppling accepterad.");
   19	
   20	        BufferedReader from_client = new BufferedReader(new InputStreamReader(socket.getInputStream()));
   21	        PrintWriter to_client = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
   22	        // true: PrintWriter is line buffered
   23	
   24	        while (true) {
   25	            String inline = from_client.readLine();
   26	            System.out.println("Servern: Tog emot '" + inline + "'");
   27	            // Not: inline == "quit"
   28	            if (inline == null || inline.equals("quit"))
   29	                break;
   30	            to_client.println("HELLO, CLIENT! YOU SAID: " + inline);
   31	        }
   32	    } // main
   33	} // HelloServer

"Bild" 8: En enkel klient

HelloClient.java

    1	import java.net.Socket;
    2	import java.net.InetAddress;
    3	
    4	import java.io.InputStreamReader;
    5	import java.io.OutputStreamWriter;
    6	import java.io.BufferedReader;
    7	import java.io.BufferedWriter;
    8	import java.io.PrintWriter;
    9	import java.io.IOException;
   10	
   11	public class HelloClient {
   12	    public static final int PORT = 2000;
   13	    public static void main(String[] args) throws IOException {
   14	        InetAddress addr;
   15	        if (args.length >= 1)
   16	            addr = InetAddress.getByName(args[0]);
   17	        else
   18	            addr = InetAddress.getByName(null);
   19	
   20	        Socket socket = new Socket(addr, PORT);
   21	
   22	        BufferedReader from_server = new BufferedReader(new InputStreamReader(socket.getInputStream()));
   23	        PrintWriter to_server = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
   24	        // true: PrintWriter is line buffered
   25	
   26	        BufferedReader kbd_reader = new BufferedReader(new InputStreamReader(System.in));
   27	
   28	        String line_from_user;
   29	        while (true) {
   30	            System.out.print("Skriv en rad: ");
   31	            line_from_user = kbd_reader.readLine();
   32	            to_server.println(line_from_user);
   33	            String line_from_server = from_server.readLine();
   34	            if (line_from_server == null)
   35	                break;
   36	            System.out.println("Från servern: " + line_from_server);
   37	        }
   38	    } // main
   39	} // HelloClient

Bild 9: Körexempel med HelloClient

HelloClient-dialog

Bild 10: Körexempel med HelloServer

HelloServer-dialog

"Bild" 11: Trådar i Java

ThreeBoringThreads.java

    1	class BoringThread extends Thread {
    2	    static int numberOfThreads = 0;
    3	    int threadNumber = ++numberOfThreads;
    4	
    5	    public void run() {
    6	        int nrReps = 0;
    7	        while (true) {
    8	            System.out.print("Tråd nummer " + threadNumber + " har kört ");
    9	            System.out.println(++nrReps + " varv i loopen.");
   10	        }
   11	    } // run
   12	} // class BoringThread
   13	
   14	public class ThreeBoringThreads {
   15	    public static void main(String[] args) {
   16	        BoringThread t1 = new BoringThread();
   17	        BoringThread t2 = new BoringThread();
   18	        BoringThread t3 = new BoringThread();
   19	        t1.start();
   20	        t2.start();
   21	        t3.start();
   22	    } // main
   23	} // class ThreeBoringThreads

Bild 12: Körexempel med ThreeBoringThreads

ThreeBoringThreads-dialog

Bild 13: Ett körexempel till med ThreeBoringThreads

ThreeBoringThreads-knasdialog

"Bild" 14: Synkroniserade trådar

ThreeBetterThreads.java

    1	class BetterThread extends Thread {
    2	    static int numberOfThreads = 0;
    3	    int threadNumber = ++numberOfThreads;
    4	
    5	    public void run() {
    6	        int nrReps = 0;
    7	        while (true) {
    8	            synchronized ("fnord") {
    9	                System.out.print("Tråd nummer " + threadNumber + " har kört ");
   10	                System.out.println(++nrReps + " varv i loopen.");
   11	            }
   12	        }
   13	    } // run
   14	} // class BetterThread
   15	
   16	public class ThreeBetterThreads {
   17	    public static void main(String[] args) {
   18	        BetterThread t1 = new BetterThread();
   19	        BetterThread t2 = new BetterThread();
   20	        BetterThread t3 = new BetterThread();
   21	        t1.start();
   22	        t2.start();
   23	        t3.start();
   24	    } // main
   25	} // class ThreeBetterThreads

Bild 15: En trådad chat-server med trådade klienter

Chat-system med flertrådad server och flertrådad klient

Bild 16: Trådar som väntar på inkommande data

Trådar som väntar på inkommande data

Bild 17: När data kommer, skickas de vidare

Data som skickas vidare

Bild 18: Chat-klienten

Hela projektet "ChatClient" kan laddas ner som en Zip-fil: ChatClient.zip

Chat-klientens klasser

"Bild" 19: Klassen ServerListenerThread

ServerListenerThread.java

    1	import java.io.BufferedReader;
    2	import java.io.IOException;
    3	
    4	public class ServerListenerThread extends Thread {
    5	    private final ChatClient owner;
    6	    private final BufferedReader from_server;
    7	    private boolean quit = false;
    8	    
    9	    public ServerListenerThread(ChatClient owner, BufferedReader from_server) {
   10	        this.owner = owner;
   11	        this.from_server = from_server;
   12	    }
   13	    
   14	    public void run() {
   15	        while (! quit) {
   16	            String line_from_server;
   17	            try {
   18	                line_from_server = from_server.readLine();
   19	                if (line_from_server == null) {
   20	                    System.out.println("Fick inga data från servern");
   21	                    quit = true;
   22	                }
   23	                else {
   24	                    System.out.println(line_from_server);
   25	                }
   26	            }
   27	            catch (IOException e) {
   28	                System.out.println("Fel vid mottagning från servern");
   29	                quit = true;
   30	            }
   31	            
   32	        } // while
   33	        owner.please_quit();
   34	    } // run
   35	
   36	    public void please_quit() {
   37	        quit = true;
   38	    }
   39	} // class ServerListenerThread

"Bild" 20: Klassen ChatClient

ChatClient.java

    1	// ChatClient.java
    2	
    3	import java.io.BufferedReader;
    4	import java.io.BufferedWriter;
    5	import java.io.IOException;
    6	import java.io.InputStreamReader;
    7	import java.io.OutputStreamWriter;
    8	import java.io.PrintWriter;
    9	import java.net.InetAddress;
   10	import java.net.Socket;
   11	
   12	public class ChatClient {
   13	    private static final int PORT = 2001;
   14	    // private static final String DEFAULT_HOST = "basen.oru.se";
   15	    private static final String DEFAULT_HOST = "localhost";
   16	    private boolean quit = false;
   17	
   18	    private final BufferedReader from_user;
   19	    private final BufferedReader from_server;
   20	    private final PrintWriter to_server;
   21	    private final String user_name;
   22	    private final ServerListenerThread server_listener;
   23	    
   24	    public ChatClient(BufferedReader from_user, BufferedReader from_server, PrintWriter to_server, String user_name) {
   25	        this.from_user = from_user;
   26	        this.from_server = from_server;
   27	        this.to_server = to_server;
   28	        this.user_name = user_name;
   29	        this.server_listener = new ServerListenerThread(this, from_server);
   30	        server_listener.start();
   31	    } // ChatClient
   32	
   33	    public void please_quit() {
   34	        quit = true;
   35	    }
   36	
   37	    private void run() {
   38	        System.out.println("Uppkopplad! Loggar in...");
   39	        to_server.println("LOGIN " + user_name);
   40	        System.out.println("Nu kan du chatta! Skriv rader, med ENTER efter varje rad.\n");
   41	        
   42	        while (!quit) {
   43	            try {
   44	                String line_from_user = from_user.readLine();
   45	                // System.out.println("Från tangentbordet: " + line_from_user);
   46	                // System.out.println("Till servern: " + line_from_user);
   47	                to_server.println(line_from_user);
   48	                if (line_from_user.equals("LOGOUT")) {
   49	                    quit = true;
   50	                    server_listener.please_quit();
   51	                }
   52	            }
   53	            catch (IOException e) {
   54	                System.out.println("Kunde inte läsa en rad från användaren.");
   55	                e.printStackTrace();
   56	                quit = true;
   57	            }
   58	        }
   59	        System.out.println("Nedkopplad från servern.");
   60	    } // run
   61	
   62	    public static void main(String[] args) throws IOException {
   63	        System.out.println("Välkommen till chatten!");
   64	        BufferedReader kbd_reader = new BufferedReader(new InputStreamReader(System.in));
   65	        String user_name;
   66	        do {
   67	            System.out.print("Skriv ditt namn: ");
   68	            user_name = kbd_reader.readLine();
   69	        } while (user_name.equals(""));
   70	
   71	        InetAddress addr;
   72	        if (args.length >= 1)
   73	            addr = InetAddress.getByName(args[0]);
   74	        else
   75	            addr = InetAddress.getByName(DEFAULT_HOST);
   76	        int port;
   77	        if (args.length >= 2)
   78	            port = Integer.parseInt(args[1]);
   79	        else
   80	            port = PORT;
   81	        System.out.println("Kopplar upp mot servern...");
   82	        
   83	        Socket socket = new Socket(addr, port);
   84	
   85	        BufferedReader from_server = new BufferedReader(new InputStreamReader(socket.getInputStream()));
   86	        PrintWriter to_server = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
   87	        // true: PrintWriter is line buffered
   88	
   89	        ChatClient cc = new ChatClient(kbd_reader, from_server, to_server, user_name);
   90	        cc.run(); // Use the default execution thread
   91	    } // main
   92	} // class ChatClient

Tillägg 28 september 2012:

Det är kodsnutten new Socket(addr, port) som gör själva uppkopplingen mot servern. I skrivbords-Java-exemplet ovan gör vi det i den så kallade huvudtråden. Vi skapar ingen särskild tråd för det.

I Android bör man inte göra så, utan man bör skapa en särskild tråd när man ska göra någon nätverksaktivitet. Här är huvudtråden den så kallade GUI-tråden, som används för att rita upp användargränssnittet. Om den tråden är upptagen med att koppla upp sig mot en server, eller vänta på data, ser det ut som om appen har hängt sig. I nyare versioner av Android (API-nivå 11 och högre) är det som default förbjudet att göra så, och om man försöker kastas ett NetworkOnMainThreadException.

Läs dokumentet Designing for Responsiveness!

Bild 21: Chat-servern

Hela projektet "ChatServer" kan laddas ner som en Zip-fil: ChatServer.zip

Chat-serverns klasser

"Bild" 22: Klassen ChatServer

ChatServer.java

    1	import java.io.BufferedReader;
    2	import java.io.BufferedWriter;
    3	import java.io.IOException;
    4	import java.io.InputStreamReader;
    5	import java.io.OutputStreamWriter;
    6	import java.io.PrintWriter;
    7	import java.net.ServerSocket;
    8	import java.net.Socket;
    9	
   10	public class ChatServer {
   11	    public static final int PORT = 2001;
   12	    
   13	    public static void main(String[] args) {
   14	        final SimpleLogger logger = new SimpleLogger("ChatServer");
   15	        final MainServerThread server = new MainServerThread();
   16	        server.start();
   17	
   18	        try {
   19	            ServerSocket s;
   20	            s = new ServerSocket(PORT);
   21	            logger.log("Server-socketen: " + s);
   22	            logger.log("Servern lyssnar...");
   23	            
   24	            while(true) {
   25	                logger.log("Väntar på uppkoppling från klient...");
   26	                // Blocks until a connection occurs:
   27	                Socket socket = s.accept();
   28	                logger.log("Uppkoppling från klient accepterad.");
   29	                logger.log("Den nya socketen: " + socket);
   30	                final BufferedReader from_client = new BufferedReader(new InputStreamReader(socket.getInputStream()));
   31	                final PrintWriter to_client = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
   32	                // true: PrintWriter is line buffered
   33	                server.new_client(from_client, to_client);
   34	            }
   35	        } catch (IOException e) {
   36	            logger.log("IOException: " + e);
   37	            e.printStackTrace();
   38	        }
   39	    } // main
   40	} // class ChatServer

"Bild" 23: Klassen ClientListenerThread

ClientListenerThread.java

    1	import java.io.BufferedReader;
    2	import java.io.IOException;
    3	import java.io.PrintWriter;
    4	
    5	public class ClientListenerThread extends Thread {
    6	    private final MainServerThread owner;
    7	    private final BufferedReader from_client;
    8	    final PrintWriter to_client;
    9	    private static int instances = 0;
   10	    private int number = ++instances;
   11	    private final SimpleLogger logger = new SimpleLogger("ConnectionWaiterThread " + number);
   12	    private String user_name = null;
   13	    private boolean quit = false;
   14	
   15	    public ClientListenerThread(MainServerThread owner, BufferedReader from_client, PrintWriter to_client) {
   16	        this.owner = owner;
   17	        this.from_client = from_client;
   18	        this.to_client = to_client;
   19	
   20	    }
   21	
   22	    public void run() {
   23	        while (! quit) {
   24	            String line_from_client;
   25	            try {
   26	                line_from_client = from_client.readLine();
   27	                logger.log("Från klienten: " + line_from_client);
   28	                if (line_from_client == null || line_from_client.equals("LOGOUT")) {
   29	                    owner.client_has_disconneted(this);
   30	                    quit = true;
   31	                }
   32	                else if (line_from_client.equals("SHUTDOWN")) {
   33	                    System.exit(0);
   34	                    // quit = true;
   35	                }
   36	                else if (line_from_client.startsWith("LOGIN ")) {
   37	                    user_name = line_from_client.substring(6);
   38	                }
   39	                else {
   40	                    owner.received_from_client(this, line_from_client);
   41	                }
   42	            }
   43	            catch (IOException e) {
   44	                logger.log("Tappat kontakten med klienten.");
   45	                e.printStackTrace();
   46	                quit = true;
   47	            }
   48	        } // while
   49	        owner.client_has_disconneted(this);
   50	    } // run
   51	
   52	    public String getUserName() {
   53	        if (user_name == null)
   54	            return "Namnlös chattare nummer " + number;
   55	        else
   56	            return user_name;
   57	    }
   58	
   59	    public void send(String line) {
   60	        to_client.println(line);
   61	    }
   62	} // class ClientListenerThread

"Bild" 24: Protokollet mellan klienten och servern

Klienten skickar rader som ser ut så här till servern:
  • LOGIN namn
  • LOGOUT
  • SHUTDOWN
  • chat-rad
Servern skickar rader som ser ut så här till klienten:
  • chat-rad

"Bild" 25: Klassen MainServerThread

MainServerThread.java

    1	import java.io.BufferedReader;
    2	import java.io.PrintWriter;
    3	import java.util.LinkedList;
    4	
    5	// In the chat application, this doesn't really need to be a thread
    6	
    7	public class MainServerThread extends Thread {
    8	    final SimpleLogger logger = new SimpleLogger("MainServerThread");
    9	    private final LinkedList<ClientListenerThread> clients = new LinkedList<ClientListenerThread>();
   10	
   11	    public synchronized void new_client(BufferedReader from_client, PrintWriter to_client) {
   12	        final ClientListenerThread c = new ClientListenerThread(this, from_client, to_client);
   13	        c.start();
   14	        clients.add(c);
   15	    }
   16	
   17	    public synchronized void client_has_disconneted(ClientListenerThread c) {
   18	        clients.remove(c);
   19	    }
   20	    
   21	    public void received_from_client(ClientListenerThread originator, String line_from_client) {
   22	        String sender_name = originator.getUserName();
   23	        IncomingMessage m = new IncomingMessage(line_from_client, sender_name, originator);
   24	        put(m);
   25	    }
   26	
   27	    private class IncomingMessage {
   28	        public final String line;
   29	        public final ClientListenerThread originator;
   30	        public IncomingMessage(String line, String sender_name, ClientListenerThread originator) {
   31	            this.line = line;
   32	            this.originator = originator;
   33	        }
   34	    }
   35	
   36	    private LinkedList<IncomingMessage> inqueue = new LinkedList<IncomingMessage>();
   37	    
   38	    public synchronized void put(IncomingMessage m) {
   39	        inqueue.addLast(m);
   40	        notify();
   41	        logger.log("Efter put, kö nu " + inqueue.size());
   42	    }
   43	
   44	    public synchronized IncomingMessage get() {
   45	        while (inqueue.isEmpty()) {
   46	            try {
   47	                wait();
   48	            } catch (InterruptedException e) {
   49	                logger.log("wait interrupted");
   50	                e.printStackTrace();
   51	            }
   52	        }
   53	        IncomingMessage b = inqueue.getFirst();
   54	        inqueue.removeFirst();
   55	        notify();
   56	        logger.log("Efter get, kö nu " + inqueue.size());
   57	        return b;
   58	    }
   59	
   60	    private boolean quit = false;
   61	
   62	    public void run() {
   63	        while (! quit) {
   64	            IncomingMessage m = get(); // Will wait for a message
   65	            final String line = m.line;
   66	            final ClientListenerThread originator = m.originator;
   67	            final String sender_name = originator.getUserName();
   68	            for (ClientListenerThread receiver : clients) {
   69	                if (receiver != originator)
   70	                    receiver.send(sender_name + ": " + line);
   71	            }
   72	        }
   73	    } // run
   74	} // class MainServerThread

Tillägg 6 december 2015: Det finns ett fel i programkoden ovan. Loopen på rad 68-71, som går igenom alla klienterna i listan clients och skickar meddelandet till var och en av dem, borde inneslutas i en synchronized (this) { }. Annars kan metoderna new_client och client_has_disconneted anropas från en annan tråd och råka ändra på innehållet i listan clients samtidigt som vi går igenom listan. Då kraschar servertråden med ConcurrentModificationException, och chatservern kommer att sluta vidarebefordra meddelanden.

"Bild" 26: SimpleLogger

SimpleLogger.java

    1	import java.text.SimpleDateFormat;
    2	import java.util.Date;
    3	
    4	public class SimpleLogger {
    5	    private final String program_name;
    6	    private final SimpleDateFormat date_format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z");
    7	
    8	    public SimpleLogger(String program_name) {
    9	        this.program_name = program_name;
   10	    }
   11	
   12	    public void log(String message) {
   13	        Date now = new Date();
   14	        String formatted_date = date_format.format(now);
   15	        String line = program_name + " " + formatted_date + ": " + message;
   16	        System.out.println(line);
   17	    }
   18	} // class SimpleLogger

Bild 27: En grafisk chat-klient i Java SE

En grafisk chat-klient i Java SE

Bild 28: En grafisk chat-klient i Android

En grafisk chat-klient i Android

Tillbaka till lektionslistan


Thomas Padron-McCarthy (thomas.padron-mccarthy@oru.se), 6 december 2015