-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNetworking.html
More file actions
2415 lines (2252 loc) · 153 KB
/
Networking.html
File metadata and controls
2415 lines (2252 loc) · 153 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html><html lang="de"><head>
<title>Variationen zum Thema: Android</title>
<meta name="title" content="Variationen zum Thema: Android">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta charset="UTF-8">
<meta name="description" content="Eine Einführung in mobile Anwendungen">
<meta name="keywords" content="Android,Java,Einführung,Mobile Anwendungen">
<meta name="author" content="Ralph P. Lano">
<meta name="robots" content="index,follow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="book.css">
</head>
<body><center>
<div id="wrap">
<ul class="sidenav">
<p><a href="index.html">Variationen zum Thema</a><a href="index.html">Android</a></p>
<li><a href="Intro.html">Intro</a></li>
<li><a href="UI.html">UI</a></li>
<li><a href="Graphics.html">Graphics</a></li>
<li><a href="Persistence.html">Persistence</a></li>
<li><a href="Sensors.html">Sensors</a></li>
<li><a href="Threading.html">Concurrency</a></li>
<li><a href="Networking.html">Networking</a></li>
<li><a href="Multimedia.html">Multimedia</a></li>
<li><a href="Performance.html">Performance</a></li>
<li><a href="Library.html">Library</a></li>
<li><a href="Services.html">Services</a></li>
<li><a href="Cryptography.html">Cryptography</a></li>
<li><a href="Addenda.html">Addenda</a></li>
</ul>
<div class="content"><p>
<img src="images/Ch7_TicTacToe.png" style="display: block; margin-left: auto; margin-right: auto;width: 226px; height: 363px;" /></p>
<h1>
Networking</h1>
<p>
Was wäre ein Smartphone ohne eine Internetverbindung? Ziemlich langweilig wenn man nicht surfen, whatsappn oder online spielen könnte. Deswegen schreiben wir hier einen einfachen Browser, wir laden Webseiten herunter und auch ein eigener Server ist auf dem Programm. Mit einer kleinen Chatapplikation lernen wir auch wofür man TCP und UDP verwenden kann und was der Unterschied ist. JSON steht kurz auf dem Menu, und wir implementieren einen Wifi- und einen Netzwerkscanner. Dann verbringen wir noch ein bisschen Zeit mit Bluetooth. Als Schmankerl gibts dann unser TicTacToe als Netzwerkspiel und wir steuern unseren Computer mit dem Handy.</p>
<p>
In diesem Kapitel wird es auch ziemlich threadig, soll heißen, dass fast jedes Programm mit Threads arbeitet. Es kann also nix schaden ab und zu mal im Kapitel fünf nachzusehen wenn was unklar ist. Sobald man Netzwerksachen macht, benötigt man viele Permissions. Will man sich es einfach machen, dann erlaubt man mal alles was irgendwo mal gebraucht werden könnte:</p>
<pre style="margin-left: 40px;">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
</pre>
<p>
Das ist natürlich nicht besonders nutzerfreundlich, aber wenn WhatsApp das kann, können wir das alle mal. Wir machen das ja nur aus didaktischen Gründen, WhatsApp aus wirtschaftlichen. Ach ja, für alle Beispiele in diesem Kapitel muss natürlich Wifi eingeschaltet sein.</p>
<p>
.</p>
<h2>
<img alt="" src="images/WebView.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />WebView</h2>
<p>
Als erstes wollen wir ein bisschen im Web browsen. Wie das mit einem Intent geht, haben wir bereits im ersten Kapitel gesehen. Hier wollen wir einen <em>WebView</em> verwenden. Dabei handelt es sich um einen View der HTML und CSS darstellen kann, und der auch bei Androids hauseigenem Browser verwendet wird. Neben einem EditText und Button Widget, fügen wir den WebView in unsere Layoutdatei ein:</p>
<pre style="margin-left: 40px;">
<LinearLayout ... >
...
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</pre>
<p>
Wenn wir nur möchten, dass der WebView eine Webseite lädt und anzeigt, dann genügen die Zeilen,</p>
<pre style="margin-left: 40px;">
WebView webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("http://www.google.com/");</pre>
<p>
Sobald der Nutzer aber auf irgendeinen Link klicken würde, dann würde Android den Default-Browser öffnen und dort den Link anzeigen. Um das zu verhindern, müssen wir die <em>setWebViewClient()</em> Methode des WebViews aufrufen und ihr einen WebViewClient übergeben:</p>
<pre style="margin-left: 40px;">
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.webview_activity);
final EditText et = (EditText) findViewById(R.id.editText);
final WebView webView = (WebView) findViewById(R.id.webView);
<span style="color:#0000ff;"> webView.setWebViewClient(new WebViewClient());</span>
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String sUri = et.getText().toString();
if (!sUri.startsWith("http://")) {
sUri = "http://" + sUri;
}
<span style="color:#0000ff;">webView.loadUrl(sUri);</span>
webView.requestFocus();
}
});
}
</pre>
<p>
Im Unterschied zu dem Browserbeispiel aus dem ersten Kapitel, benötigt unser Beispiel noch die Erlaubnis ins Internet gehen zu dürfen. Das erledigen wir durch die folgende Zeile in unserer AndroidManifest Datei:</p>
<pre style="margin-left: 40px;">
<uses-permission android:name="android.permission.INTERNET" /></pre>
<p>
Interessant ist vielleicht noch, dass wir die Klasse WebViewClient auch erweitern können, und dann das Verhalten unseres Browsers voll unter Kontrolle haben. Wie das geht haben wir im Projekt <em>HelpPages</em> im vierten Kapitel gesehen.</p>
<p>
.</p>
<h2>
<img alt="" src="images/InetAddress.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />InetAddress</h2>
<p>
Wenn wir im Browser "www.google.com" eingeben, dann wissen wir, dass wir auf den Google Server wollen. Unser Smartphone kann aber erst mal mit der Domain "www.google.com" nicht viel anfangen [1]. Es braucht die IP Adresse. Dieses Umwandeln von Domain nach IP Adresse und umgekehrt macht in Java die <em>InetAddress</em> Klasse:</p>
<pre style="margin-left: 40px;">
String networkInfo = "";
InetAddress localAdr = InetAddress.getLocalHost();
networkInfo += "Local IP: " + localAdr.<span style="color:#0000ff;">getHostAddress()</span>;
InetAddress remoteAdr = InetAddress.getByName("www.google.com");
networkInfo += "\nGoogle IP: " + remoteAdr.getHostAddress();
InetAddress[] remoteAdrs = InetAddress.getAllByName("www.google.com");
for (int i = 0; i < remoteAdrs.length; i++) {
networkInfo += "\nGoogle IP" + i + ": " + remoteAdrs[i].getHostAddress();
}
</pre>
<p>
Für einen Reverse-DNS-Lookup verwendet man die <em>getHostName()</em> Methode, die macht aus einer IP Adresse wieder einen Domainnamen. Braucht man eher selten.</p>
<p>
Damit der obige Code ohne weiteres funktioniert, müssen wir zwei Dinge tun: zunächst müssen wir wieder die Erlaubnis haben ins Internet gehen zu dürfen, aber wir müssen zusätzlich noch eine <em>ThreadPolicy</em> setzen [2]:</p>
<pre style="margin-left: 40px;">
StrictMode.ThreadPolicy policy =
new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);</pre>
<p>
Tun wir das nicht, bekommen wir eine <em>NetworkOnMainThreadException</em>. Aus irgendeinem Grund hat es Android nicht so gerne, dass wir aus dem Main Thread heraus auf's Netzwerk zugreifen. Natürlich kann man jetzt immer einen Extra-Thread für's Netzwerk starten. Unsere Beispiele würden dadurch aber so unübersichtlich und unlesbar, dass wir aus didaktischen Gründen darauf verzichten werden. Deswegen müssen wir in all unseren Programmen immer diese zwei Zeilen am Anfang der onCreate() einfügen.</p>
<p>
.</p>
<h2>
<img alt="" src="images/URL.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />URL</h2>
<p>
Sehr häufig wollen wir nur eine simple HTML Seite runterladen. Dafür verwendet man die <em>URL</em> Klasse. Die URL Klasse erlaubt es uns eine HTTP-Verbindung (connection) zu einem Webserver herzustellen, und die liefert uns einen InputStream:</p>
<pre style="margin-left: 40px;">
public static String getWebpage(String address) {
try {
<span style="color:#0000ff;">URL url = new URL(address);</span>
HttpURLConnection con = (HttpURLConnection) url.openConnection();
<span style="color:#0000ff;">InputStream is = con.getInputStream();</span>
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String content = "";
while (true) {
String line = br.readLine();
if (line == null)
break;
content += line;
}
br.close();
con.disconnect();
return content;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
</pre>
<p>
Dieser InputStream ist der gleiche wie wir ihn vom Lesen von Dateien her kennen, dort ist es ein FileInputStream. Deswegen ist der Rest des Codes auch identisch mit dem aus der EditorActivity im fünften Kapitel. Verwenden können wir unsere Methode dann ganz einfach:</p>
<pre style="margin-left: 40px;">
String html = getWebpage("http://www.lano.de"));</pre>
<p>
Mit der URL Klasse kann man noch viel mehr machen als einfach nur Dateien runterzuladen, aber für den Anfang soll das genügen.</p>
<p>
<img alt="" src="images/Ch7_OSSI.png" style="margin-left: 10px; margin-right: 10px; width: 254px; height: 320px;" /></p>
<p>
.</p>
<p>
.</p>
<h2>
<img alt="" src="images/Socket.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />Socket</h2>
<p>
Die URL Klasse baut eine TCP Verbindung auf, läd die gewünschte Seite von einem Server, und schließt die Verbindung wieder. Wenn wir nur eine Seite laden wollen dann genügt das auch. Manchmal möchte man aber mehr als eine Seite laden, oder man möchte die Verbindung offen halten. Dann benötigen wir einen <em>Socket</em>, genauer eine TCP Socket [3].</p>
<p>
Die Socket Klasse ist relativ einfach zu verwenden: man gibt ihr eine IP Adresse und einen Port [4]. Dann sagt man ihr, dass sie sich verbinden soll:</p>
<pre style="margin-left: 40px;">
SocketAddress sockaddr = new InetSocketAddress("www.google.com", 80);
Socket socket = new Socket();
socket.<span style="color:#0000ff;">connect</span>(sockaddr, TIMEOUT);
Log.i("SocketActivity", ""+socket.getLocalAddress());
Log.i("SocketActivity", ""+socket.getRemoteSocketAddress());</pre>
<p>
Wenn es einen interessiert, kann man mit <em>getLocalAddress()</em> die eigene Adresse erfahren und mit <em>getRemoteSocketAddress()</em> die Adresse des Servers.</p>
<p>
Steht unsere TCP Verbindung, können wir mit dem Server reden. Wir müssen natürlich die Sprache des Servers sprechen. Der Google Server spricht HTTP [5], und deswegen schreiben wir in den OutputStream:</p>
<pre style="margin-left: 40px;">
OutputStream os = socket.getOutputStream();
os.write("GET / \r\n".getBytes());
os.flush();</pre>
<p>
Das "GET / \r\n" sagt soviel wie "Gib mir doch Deine Einstiegsseite". Die <em>flush()</em> Methode schickt die Daten schon mal los, sonst würde der Socket nämlich warten, ob wir noch mehr zu sagen haben.</p>
<p>
Jetzt antwortet uns der Server, deswegen müssen wir zuhören, und das machen wir mit dem InputStream, den wir vom Socket bekommen:</p>
<pre style="margin-left: 40px;">
InputStream is = socket.getInputStream();
while (true) {
int len = is.read();
if (len == -1)
break;
tv.append(""+(char) len);
}
</pre>
<p>
Wir lesen vom Socket solange bis nichts mehr kommt. Wir könnten jetzt noch eine zweite Anfrage schicken, und so weiter.</p>
<p>
Wenn wir dann fertig sind, müssen wir alle Türen wieder zu machen, und derer sind drei:</p>
<pre style="margin-left: 40px;">
os.close();
is.close();
socket.close();</pre>
<p>
Und das wars. Natürlich muss man um den ganzen Code einen dicken try-catch Block schreiben, weil da kann ja alles mögliche schief laufen.</p>
<p>
.</p>
<h2>
<img alt="" src="images/TimeServer.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />TimeServer</h2>
<p>
Nach dem Client Socket wollen wir uns jetzt den Server Socket ansehen, der ist auch nicht viel schwieriger. Wir wollen einen kleinen Server schreiben, der aktuelles Datum und Uhrzeit ausgibt. Dazu benötigen wir erst einmal einen <em>ServerSocket</em>:</p>
<pre style="margin-left: 40px;">
ServerSocket server = new ServerSocket(3737);
while (true) {
Socket socket = server.<span style="color:#0000ff;">accept()</span>;
OutputStream os = socket.getOutputStream();
String daytime = new Date().toString();
os.write(daytime.getBytes());
os.close();
socket.close();
}
</pre>
<p>
Der hört auf einem bestimmten Port, in unserem Fall 3737. Dann wartet der Server in der <em>accept()</em> Methode solange bis jemand etwas von ihm will. Man nennt das auch einen "blocking" Call. Versucht jetzt irgend ein Client sich mit dem Server zu verbinden, dann liefert die accept() Methode einen ganz normalen Client Socket als Rückgabewert. Mit dem können wir wie im Socket Beispiel oben arbeiten. In diesem Fall schicken wir einfach Datum und Uhrzeit zurück.</p>
<p>
Bei Servern ist es üblich, da sie blockieren, dass man sie in einem separaten Thread startet:</p>
<pre style="margin-left: 40px;">
public class TimeServerActivity extends Activity <span style="color:#0000ff;">implements Runnable</span> {
@Override
public void onCreate(Bundle savedInstanceState) {
...
<span style="color:#0000ff;">Thread th = new Thread(this);
th.start();</span>
}
@Override
public void run() {
... server code ...
}
}</pre>
<p>
Und das ist der Teil, der Server etwas komplizierter macht, aber nicht viel.</p>
<p>
.</p>
<h2>
<img alt="" src="images/TimeClient.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />TimeClient</h2>
<p>
Jetzt haben wir einen Server, fehlt natürlich noch der Client. Der ist fast identisch zu unserem vorigen Socket Beispiel:</p>
<pre style="margin-left: 40px;">
String daytime = "";
SocketAddress sockaddr = new InetSocketAddress(<span style="color:#0000ff;">IP</span>, 3737);
Socket socket = new Socket();
socket.connect(sockaddr, TIMEOUT);
InputStream is = socket.getInputStream();
while (true) {
int len = is.read();
if (len == -1)
break;
daytime += (char) len;
}
is.close();
socket.close();</pre>
<p>
Hypothetisch könnte man auch einen Browser als Client nehmen, aber die Browser sprechen nur HTTP. Dazu später mehr.</p>
<p>
.</p>
<h2>
<img alt="" src="images/Yo.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />Yo</h2>
<p>
Neben dem TCP Protokoll gibt es auch noch das UDP Protokoll [6]. Da das UDP Protokoll nicht verbindungsorientiert ist, ist es sogar einfacher als das TCP Protokoll. Und, mit dem UDP Protokoll kann man auch Broadcasts versenden, soll heißen man kann eine Nachricht an alle schicken. </p>
<p>
In unserer Yo Anwendung [7] wollen wir ein einfaches "Yo" per UDP Broadcast versenden. Dazu benötigen wir einen <em>DatagramSocket</em>, also eine UDP Sockel:</p>
<pre style="margin-left: 40px;">
private void sendYoToEveryone() {
try {
byte[] data = "Yo".getBytes();
DatagramPacket theOutput =
new DatagramPacket(data, data.length,
Util.getLocalBroadcastAddress(), YO_PORT);
DatagramSocket theSocket = new DatagramSocket();
theSocket.<span style="color:#0000ff;">send</span>(theOutput);
} catch (Exception e) {
e.printStackTrace();
}
}
</pre>
<p>
Über den schicken wir dann ein <em>DatagramPacket</em>, also ein UDP Datenpaket. Man kann das entweder an eine bestimmte IP Adresse schicken, oder eben an einen Broadcast, das macht die Methode <em>getLocalBroadcastAddress()</em> der Util Klasse. DatagramPackets können nicht beliebig groß sein: maximal gehen 65507 Byte, garantiert sind aber nur 512 Byte. Das hat mit dem evtl. Umverpacken von Datenpacketen in Routern, Bridges und Switches zu tun.</p>
<p>
Kommen wir zum UDP Server: auch der sollte wieder als separater Thread laufen. Im Gegensatz zu TCP, wird bei UDP für Client und Server der gleiche Sockel verwendet. Der einzige Unterschied ist, dass wir im ersten Fall die Methode <em>send()</em> verwenden und im zweiten die Methode <em>receive()</em>:</p>
<pre style="margin-left: 40px;">
public void run() {
byte[] buffer = new byte[MAX_PACKET_SIZE];
DatagramSocket server = new DatagramSocket(YO_PORT);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
server.<span style="color:#0000ff;">receive</span>(packet);
String yoMessage = new String(packet.getData(), 0, packet.getLength());
final String msg = "" + packet.getAddress() + ": " + yoMessage;
// reset the length for the next packet
packet.setLength(buffer.length);
}
}
</pre>
<p>
Im Gegensatz zur <em>accept()</em> Methode, blockiert die <em>receive()</em> Methode nicht. D.h. es kann durchaus passieren, dass uns das eine oder andere Packet entwischt, wenn wir nicht gerade zuhören. Das ist der Nachteil von UDP.</p>
<p>
<img alt="" src="images/Ch7_TPCvsUDP.png" style="margin-left: 10px; margin-right: 10px; width: 583px; height: 347px;" /></p>
<p>
.</p>
<p>
Zwei Anmerkungen noch: es stellt sich heraus, dass in einem kabelgebunden Netzwerk so gut wie keine UDP Packet verloren gehen, komischerweise verschwinden aber überraschend viele UDP Packete wenn sie über Wifi geschickt werden. Und leider gibt es einige Smartphones die keine UDP Broadcasts können, bzw. diese unterbinden, z.B. einige Samsung Handys tun sich da anscheinend schwer.</p>
<p>
.</p>
<hr />
<h1>
Review</h1>
<p>
Wir haben die Grundlagen der Netzwerkprogrammierung mit Android Geräten gelegt. Mit dem WebView Widget können wir HTML Seiten anzeigen, mit der URL Klasse Webseiten herunterladen, und mit der InetAddress Klasse DNS Anfragen verschicken. Dann haben wir gesehen wie wir mit der Socket und der ServerSocket Klasse TCP Verbindungen herstellen können, sowohl clientseitig als auch serverseitig. Und im letzten Beispiel haben wir UDP Packete verschickt und empfangen mit Hilfe der DatagramPacket und DatagramSocket Klassen. </p>
<p>
.</p>
<hr />
<h1>
Projekte</h1>
<p>
Was wir bisher gesehen haben, haut einen noch nicht so vom Hocker. Aber wir haben die Grundlagen gelegt für eine ganze Reihen von interessanten Anwendungen. Dazu gehörten Netzwerkscanner, Server- und Chatanwendungen, ein bisschen Bluetooth und natürlich ein Spiel. Am coolsten ist aber wahrscheinlich die RemoteDesktopClient Anwendung.</p>
<p>
.</p>
<h2>
<img alt="" src="images/NetworkScanner.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />NetworkScanner</h2>
<p>
Eine interessante Methode der InetAddress Klasse ist die <em>isReachable()</em> Methode: diese schickt einen ICMP Request an eine bestimmte Adresse, macht also eine 'ping' Anfrage [8]. Wir können uns das für einen kleine Network Scanner zu Nutze machen:</p>
<pre style="margin-left: 40px;">
public void run() {
...
InetAddress myIP = Util.getMyLocalIpAddress();
byte[] localAddresses = myIP.getAddress();
for (int i = 0; i < 256; i++) {
localAddresses[3] = (byte) i;
final InetAddress address = InetAddress.getByAddress(localAddresses);
if (address.<span style="color:#0000ff;">isReachable</span>(TIMEOUT)) {
Log.i("NetworkScannerActivity", address.getHostAddress());
}
}
...
}</pre>
<p>
In dem Code oben gehen wir einfach alle lokalen Adressen durch und schauen ob irgendjemand antwortet. Danach wissen wir welche Rechner es in unserem lokalen Netz gibt (falls diese auf einen 'ping' antworten). Interessanterweise scheint das nur im lokalen Netz zu funktionieren.</p>
<p>
.</p>
<h2>
<img alt="" src="images/AllMyIPs.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />AllMyIPs</h2>
<p>
Solange wir als Client unterwegs sind, kann es uns eigentlich egal sein, was unsere IP Adresse ist. Wenn wir allerdings einen Server auf unserem Smartphone hosten wollen, dann müssten wir schon wissen was unsere IP ist, denn sonst kann sich ja niemand mit unserem Server verbinden.</p>
<p>
Stellt sich heraus, die meisten Smartphones haben mehr als eine IP Adresse. Das kommt daher, dass man sich ja einmal über Wifi mit dem Internet verbinden kann, aber natürlich auch über das mobile Datennetz. Deswegen müssen wir uns zunächst alle Netzwerkkarten (nics) unseres Gerätes geben lassen, und dann von jeder dieser Karten die IP Adressen:</p>
<pre style="margin-left: 40px;">
private String getAllLocalIpAddresses() {
String sIPs = "";
...
for (Enumeration<NetworkInterface> <span style="color:#0000ff;">nics</span> = NetworkInterface.getNetworkInterfaces();
nics.hasMoreElements();) {
NetworkInterface nic = nics.nextElement();
for (Enumeration<InetAddress> <span style="color:#0000ff;">ips</span> = nic.getInetAddresses();
ips.hasMoreElements();) {
InetAddress ip = ips.nextElement();
sIPs += nic.getName();
if (InetAddressUtils.isIPv4Address(ip.getHostAddress())) {
sIPs += " (IPv4):";
} else {
sIPs += " (IPv6):";
}
sIPs += " " + ip.getHostAddress() + "\n";
}
}
...
return sIPs;
}
</pre>
<p>
Je nachdem wie wir uns mit dem Internet verbinden, liefert diese Methode verschiedene Resultate. </p>
<p>
Interessant ist allerdings, dass es sich bei all diesen Adressen um lokale Adressen im Sinne von IP handelt, d.h. sie beginnen entweder mit "192.168.y.z" oder mit "10.x.y.z". Wenn wir unsere wirkliche externe IP Adresse erfahren wollen, müssen wir einen Server draussen im Internet fragen:</p>
<pre style="margin-left: 40px;">
private String getMyExternalIP() {
String webpage = Util.getWebpage("http://wikimusicapp.appspot.com/myip");
if (webpage != null) {
String[] words = webpage.split(" ");
return words[3];
}
return "No external IP available";
}
</pre>
<p>
Wenn ich das auf meinem Handy laufen lasse, dann ist meine externe IP Adresse die "80.187.122.215". Wir können nachsehen, wem die Adresse gehört. Es stellt sich heraus, dass das eine Adresse der Telekom ist, also unseres mobilen Dienstleisters. Im Gegensatz zu unserem DSL oder Kabelanschluss, teilen wir die Adresse mit vielen anderen Leuten. Deswegen können wir leider keinen externen Server auf unseren mobilen Endgeräten hosten. Wenn wir uns allerdings auf Wifi und das lokale Netzwerk beschränken geht das sehr wohl.</p>
<p>
.</p>
<h2>
<img alt="" src="images/TimeClientNIST.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />TimeClientNIST</h2>
<p>
Will man die genaue Uhrzeit wissen, dann kann man beim National Institute of Standards and Technology (NIST) nachfragen [9]. Die betreiben nämlich unter der Adresse "time.nist.gov" einen Server, der sowohl das Time Protocol (RFC-868) auf Port 37 als auch das Daytime Protocol (RFC-867) auf Port 13 zur Verfügung stellt [10]. Mit einer kleinen Modifikation können wir unsere SocketActivity dafür verwenden:</p>
<pre style="margin-left: 40px;">
private long getTimeFromNIST() {
long time = 0;
...
SocketAddress sockaddr = new InetSocketAddress("time.nist.gov", PORT_TIME);
Socket socket = new Socket();
socket.connect(sockaddr, TIMEOUT);
InputStream is = socket.getInputStream();
byte[] buffer = new byte[4];
int len = is.read(buffer, 0, 4);
time = 4294967296l + Util.byteArrayToBigEndianInt(buffer);
is.close();
socket.close();
...
return time;
}</pre>
<p>
Die Zahl die wir bekommen ist die Zeit in Sekunden, die seit dem 1. Januar 1900 vergangen ist. Wenn wir das mit unserer lokalen Zeit vergleichen wollen,</p>
<pre style="margin-left: 40px;">
private long getLocalTime() {
long localTime = (new Date().getTime() - new Date(0, 0, 1).getTime()) / 1000;
return localTime;
}
</pre>
<p>
müssen wir noch 3600 abziehen, da sich die Zeit auf UTC bezieht [11].</p>
<p>
.</p>
<h2>
<img alt="" src="images/JSONBooks.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />JSONBooks</h2>
<p>
Viele Webservices im Netz verwenden die JavaScript Object Notation, kurz JSON [12], so auch Google Books [13]. Wenn wir im Browser auf folgende Adresse gehen,</p>
<pre style="margin-left: 40px;">
https://www.googleapis.com/books/v1/volumes?q=isbn:9780521427067</pre>
<p>
dann bekommen wir folgendes JSON zu sehen:</p>
<pre style="margin-left: 40px;">
{
"kind": "books#volumes",
"totalItems": 1,
"items": [
{
"kind": "books#volume",
...
"volumeInfo": {
"title": "A Mathematician's Apology",
"authors": [
"G. H. Hardy"
],
}
...
}
]
}</pre>
<p>
Man kann das Ganze jetzt von Hand parsen oder man kann es sich etwas einfacher machen und die Java Klasse <em>JSONObject</em> verwenden. Zunächst einmal laden wir das JSON mit der URL Klasse herunter:</p>
<pre style="margin-left: 40px;">
String url = "https://www.googleapis.com/books/v1/volumes?q=isbn:" + isbn;
String json = Util.getWebpage(url);
Book bk = new Book(json);</pre>
<p>
Im Constructor der Book Klasse, verwenden wir dann die JSONObject Klasse:</p>
<pre style="margin-left: 40px;">
class Book {
private String title;
private String author;
public Book(String json) {
try {
JSONObject jsonObject = new JSONObject(json);
JSONArray items = jsonObject.getJSONArray("items");
JSONObject book1 = (JSONObject) items.get(0);
JSONObject volumeInfo = (JSONObject) book1.get("volumeInfo");
title = volumeInfo.getString("title");
JSONArray authors = volumeInfo.getJSONArray("authors");
author = (String) authors.get(0);
} catch (JSONException e) {
e.printStackTrace();
}
}
public String toString() {
return "Book [author=" + author + ", title=" + title + "]";
}
}</pre>
<p>
Wir müssen natürlich grob wissen, wie unser JSON aufgebaut ist, aber ansonsten geht das Parsen relativ einfach von der Hand.</p>
<p>
.</p>
<h2>
<img alt="" src="images/GSONCities.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />GSONCities</h2>
<p>
Noch einfacher geht das Ganze mit der <em>Gson</em> Klasse von Google [14]. Dazu schauen wir das Cities Beispiel aus dem fünften Semester noch einmal an. Als Webservice können wir es unter</p>
<pre style="margin-left: 40px;">
http://wikimusicapp.appspot.com/cities?city=Rome</pre>
<p>
erreichen. Dieser Webservice liefert uns das folgende JSON:</p>
<pre style="margin-left: 40px;">
{'<span style="color:#0000ff;">country</span>':'Italy','<span style="color:#0000ff;">name</span>':'Rome','<span style="color:#0000ff;">latitute</span>':'41 48 N','<span style="color:#0000ff;">longitute</span>':'12 36 E'}</pre>
<p>
Wenn wir nun eine Klasse <em>City</em> wie folgt deklarieren (die Instanzvariablen müssen identisch mit den Bezeichnern im JSON sein),</p>
<pre style="margin-left: 40px;">
class City {
private String <span style="color:#0000ff;">country</span>;
private String <span style="color:#0000ff;">name</span>;
private String <span style="color:#0000ff;">latitute</span>;
private String <span style="color:#0000ff;">longitute</span>;
public City() {
}
public String toString() {
...
}
}</pre>
<p>
dann erlaubt uns Gson ohne große Umschweife daraus eine Klasse zu machen:</p>
<pre style="margin-left: 40px;">
String url = "http://wikimusicapp.appspot.com/cities?city=" + cityName;
String json = Util.getWebpage(url);
Gson gson = new Gson();
City city = gson.fromJson(json, City.class);</pre>
<p>
Einfacher geht's nicht.</p>
<p>
.</p>
<h2>
<img alt="" src="images/WebServer.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />WebServer</h2>
<p>
Inzwischen haben wir alles was nötig ist, um einen eigenen Webserver zu schreiben. Wie beim TimeServer verwenden wir einen ServerSocket. Idealerweise sollte der auf dem HTTP Port 80 hören, aber auf Linux Systemen (und Android ist ein Linux System) darf nur Root auf Ports unterhalb von 1024 zugreifen. Deswegen verwenden wir irgendeinen Port der größer ist. Den ServerSocket starten wir, wie gehabt, in einem separaten Thread:</p>
<pre style="margin-left: 40px;">
public void run() {
...
int threadNr = 0;
ServerSocket server = new ServerSocket(8008);
while (isRunning) {
Socket socket = server.accept();
<span style="color:#0000ff;">(new ConnectionThread(++threadNr, socket)).start()</span>;
}
server.close();
...
}</pre>
<p>
Neu ist jetzt, dass wir einen zweiten Thread starten, den ConnectionThread, sobald sich jemand mit uns verbindet. Der Grund dafür ist, dass unser Server ja mehr als nur eine Seite und mehr als nur einen Client bedienen soll. Wenn also ein Request reinkommt, dann lassen wir einen neuen Thread diesen Request beantworten, der Server selbst kann aber sofort wieder auf neue Requests antworten. Anderfalls, müssten nämlich neue Requests immer erst mal warten bis der alte fertig ist. Das Internet wäre so ziemlich nutzlos, wenn man bei Amazon mit seinem Einkauf warten müsste bis der andere fertig ist. Da kann ich ja gleich in den Laden um die Ecke gehen und dort in der Schlange warten.</p>
<p>
Sehen wir uns als nächstes den <em>ConnectionThread</em> mal etwas genauer an: Im Constructor bekommen wir eine Referenz auf den Sockel zum Client, sowie einen Zähler, der uns sagt wie viele Users schon bei uns waren:</p>
<pre style="margin-left: 40px;">
class ConnectionThread extends Thread {
private int threadNr;
private Socket socket;
public ConnectionThread(int threadNr, Socket socket) {
this.threadNr = threadNr;
this.socket = socket;
}
public void run() {
try {
OutputStream out = socket.getOutputStream();
String http = <span style="color:#0000ff;">createHTTPResponse()</span>;
out.write(http.getBytes());
out.flush();
out.close();
socket.close();
socket = null;
} catch (Exception e) {
e.printStackTrace();
}
}
...
}</pre>
<p>
Der ConnectionThread erweitert die Thread Klasse. Wir hätten natürlich auch das Runnable Interface implementieren können, aber wir wollen ab und zu ja mal was Neues lernen. In der run() Methode geht es zu wie gehabt: wir holen uns den OutputStream vom Socket und schreiben unsere Daten in den Stream. Da das HTTP Protokoll in seiner einfachsten Version zustandslos ist, machen wir den Socket gleich wieder zu. Da wir nur einen einfachen Webserver schreiben wollen, der immer nur die gleiche Seite zurückliefert, interessiert uns der InputStream nicht.</p>
<p>
Kommen wir zum HTTP, also zur <em>createHTTPResponse()</em> Methode: die HTTP Response besteht aus dem HTTP Header und der Payload, in unserem Fall dem HTML:</p>
<pre style="margin-left: 40px;">
private String createHTTPResponse() {
String html = createDummyHTML();
String httpHeader = createHTTPHeader(html.length(), "text/html");
return httpHeader + html;
}</pre>
<p>
Das HTML erzeugen wir in der <em>createDummyHTML()</em> Methode, die natürlich beliebiges HTML erzeugen könnte:</p>
<pre style="margin-left: 40px;">
private String createDummyHTML() {
String html = "<html><body>" + "<h1>Hello from WebServerActivity!</h1>"
+ "<p>You are visitor number "
+ threadNr + ".</p>" + "</body></html>";
return html;
}</pre>
<p>
Den HTTP Header erzeugen wir in der <em>createHTTPHeader()</em> Methode:</p>
<pre style="margin-left: 40px;">
private String createHTTPHeader(long contentLength, String mimeType) {
String httpHeader = "HTTP/1.0 200 OK\r\n"
+ "Server: WebServerActivity 1.0\r\n"
+ "Content-length: " + contentLength + "\r\n"
+ "Content-type: " + mimeType
+ "\r\n\r\n";
return httpHeader;
}</pre>
<p>
Wie so ein HTTP Header auszusehen hat kann man im RFC 2616 [15] nachlesen. Effektiv was der Header sagt ist:</p>
<ol>
<li>
Alles ist o.k.: "HTTP/1.0 200 OK";</li>
<li>
ich heiße: "WebServerActivity 1.0";</li>
<li>
es kommen jetzt <em>contentLength</em> Bytes an Daten, mach Dich bereit;</li>
<li>
und die Daten sind vom Typ "text/html".</li>
</ol>
<p>
Ganz wichtig sind auch die zwei CRLF, also "\r\n\r\n", damit wird das Ende des HTTP Headers markiert, und der Browser weiß dann was danach kommt sind die Daten.</p>
<p>
Eine kleine Anmerkung noch zu unserem Zähler threadNr: der ist natürlich nur hübsches Beiwerk und eigentlich gar nicht nötig. Interessant ist er aber trotzdem: wenn wir nämlich eine Seite mit Firefox herunterladen, dann funktioniert der Zähler wie gewünscht, bei jedem Versuch erhöht sich der Zähler um eins. Verwenden wir aber den Chrome Browser, dann erhöht sich der Count um zwei. D.h. Chrome Nutzer verursachen eine doppelt so hohe Last auf unserem Server wie Firefox Nutzer. Wir sollten natürlich von Chrome Nutzern daher auch doppelt so hohe Gebühren verlangen. Ganz so schlimm ist es aber nicht: Chrome möchte neben dem eigentlich Request immer noch gerne ein Favicon [16] haben. Sobald wir ihm ein Favicon schicken, hört er auf danach zu fragen. Wir schicken ihm aber keins, geht ihn doch nix an was unser Favicon ist.</p>
<p>
.</p>
<h2>
<img alt="" src="images/FileServer.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />FileServer</h2>
<p>
Wie kann man denn Bilder zwischen zwei Smartphones oder einem Smartphone und einem Laptop einfach austauschen? Richtig, wir modifzieren unseren WebServer ein klein wenig und schon geht das. Alles was notwendig ist, ist ein kleiner InputStream im ConnectionThread:</p>
<pre style="margin-left: 40px;">
public void run() {
...
byte[] rawData;
// what do you want?
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String request = br.readLine();
String[] parts = request.split(" ");
String <span style="color:#0000ff;">whatDoYouWant</span> = parts[1];
Log.i("FileServerActivity", "request: " + whatDoYouWant);
File dir = android.os.Environment.getExternalStorageDirectory();
final File downloadFile = new File(dir, whatDoYouWant);
if (!"/".equals(whatDoYouWant) && downloadFile.exists()) {
rawData = getRawData(downloadFile);
} else {
String content = getDirectoryContent(dir);
String header = createHTTPHeader(content.length(), "text/html");
rawData = (header + content).getBytes();
}
// here it is:
OutputStream out = socket.getOutputStream();
out.write(rawData);
out.flush();
out.close();
in.close();
socket.close();
socket = null;
...
}</pre>
<p>
Wir bereiten zunächst ein Bytearray für die Rohdaten vor. Dann fragen wir was unser Nutzer denn haben möchte. Die Information bekommen wir aus dem HTTP GET Request der vom Browser kommt, welcher in der Regel wie folgt aussieht:</p>
<pre style="margin-left: 40px;">
GET /<span style="color:#0000ff;">filename</span> HTTP/1.1</pre>
<p>
Das was nach dem GET kommt ist die Datei die der Nutzer haben möchte. </p>
<p>
Wie weiß denn unser Nutzer was er gerne haben möchte? Bei der ersten Anfrage geben wir ihm einfach eine Liste von Dateien mit der Methode <em>getDirectoryContent()</em>:</p>
<pre style="margin-left: 40px;">
private String getDirectoryContent(File dir) {
String content = "<html><body>";
File[] files = dir.listFiles();
for (File file : files) {
if (file.isFile()) {
content += "<a href='" + file.getName() + "'>" + file.getName() + "</a><br/>";
}
}
content += "</body></html>";
return content;
}</pre>
<p>
Schließlich brauchen wir noch die Methode <em>getRawData()</em> um die Datei in Binärform an den Clientbrowser zu schicken:</p>
<pre style="margin-left: 40px;">
private byte[] getRawData(File downloadFile) {
String mimeType = <span style="color:#0000ff;">"application/octet-stream"</span>; // "text/html"
if (downloadFile.getAbsolutePath().endsWith(".png")) {
mimeType = <span style="color:#0000ff;">"image/png"</span>;
}
byte[] data = new byte[(int) downloadFile.length()];
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(downloadFile);
int read = 0;
while ((read = fis.read(data)) != -1) {
baos.write(data, 0, read);
}
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
String header = createHTTPHeader(downloadFile.length(), mimeType);
byte[] head = header.getBytes();
byte[] buffer = new byte[header.length() + (int) downloadFile.length()];
System.arraycopy(head, 0, buffer, 0, head.length);
System.arraycopy(data, 0, buffer, head.length, data.length);
return buffer;
}</pre>
<p>
Browser verstehen verschiedene MIME Types [17]: z.B. HTML ("text/html"), Bilder ("image/png") oder eben Binärdaten ("application/octet-stream").</p>
<p>
Besonders sicher ist unser FileServer natürlich nicht, jeder der im lokalen Netz ist und unsere IP kennt, kann Dateien von unserem Handy runterladen. Da müsste man sich noch was überlegen.</p>
<p>
.</p>
<h2>
<img alt="" src="images/TCPChat.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />TCPChat</h2>
<p>
Mit unserer FileServer Actiity können wir Dateien austauschen, wie wäre es denn wenn wir Textnachrichten (oder auch Sprachnachrichten) austauschen könnten? Hier wollen wir zwei Versionen implementieren, erst eine One-To-One Version die TCP verwendet, und danach eine One-To-Many, die UDP verwendet. Die Nachrichten sollen "live" übertragen werden, d.h. sobald man auf "Send" klckt soll die Nachricht gesendet werden und beim Gegenüber sofort angezeigt werden. Dazu benötigen wir eine stehende TCP Verbindung. D.h. im Gegensatz zu früher, lassen wir die TCP Verbindung offen, und schließen sie erst wieder wenn unsere Unterhaltung beendet ist.</p>
<p>
Zwei Dinge machen die TCPChatActivity etwas komplizierter als unsere bisherigen Anwendungen: zunächst wissen wir nie genau wann unser Gegenüber eine Nachricht schickt. Das lösen wir durch einen separaten Server-Thread, der auf eingehende Nachrichten wartet. Daraus ergibt sich aber eine anderes Problem: ein Nicht-UI Thread darf eigentlich nicht auf unsere UI zugreifen. Das muss er aber, sonst sehen wir ja nicht was der Andere getippt hat. Man könnte das durch ein runOnUiThrea() lösen, die bessere Lösung ist aber das mit einem AsyncTask zu lösen:</p>
<pre style="margin-left: 40px;">
class ConnectionTask extends AsyncTask<Void, byte[], Boolean> {
private Socket socket;
private InputStream is;
private OutputStream os;
@Override
protected Boolean doInBackground(Void... params) {
...
}
@Override
protected void onProgressUpdate(byte[]... values) {
...
}
}</pre>
<p>
Erinnern wir uns, die <em>doInBackground()</em> Methode wird über die <em>execute()</em> Methode aufgerufen, in ihr "lebt" unser AsyncTask. In der <em>onProgressUpdate()</em> können wir dann auf den UI Thread zugreifen. Der ConnectionTask ist am besten eine lokale Klasse, dann kann er nämlich ohne große Probleme auf Instanzvariablen der übergeordneten UI Klasse zugreifen.</p>
<p>
Bevor wir aber den AsyncTast starten können, müssen wir eine TCP Verbindung zwischen den beiden Gesprächspartnern aufbauen. Dazu ist es zwingend notwendig, dass einer von beiden einen TCP Server in Form eines ServerSockets startet (es sei denn man hat wie WhatsApp einen zentralen Server irgendwo im Internet stehen [18]). In unserer App gibt es daher einen Knopf "Run as Server", der den Server startet. Der andere muss sich jetzt mit dem Server verbinden, dazu muss er natürlich die IP Adresse des Servers kennen. Je nachdem ob wir als Server oder Client starten, müssen wir das dem ConnectionTask mitteilen. Das geht am einfachsten über die <em>runAsServer</em> Instanzvariable:</p>
<pre style="margin-left: 40px;">
public class TCPChatActivity extends Activity {
private boolean <span style="color:#0000ff;">runAsServer</span> = false;
private EditText editMessage;
private TextView textConversation;
...
}</pre>
<p>
Um den ConnectionTask zu starten verwenden wir:</p>
<pre style="margin-left: 40px;">
connectionTask.execute();</pre>
<p>
Abhängig davon ob wir als Server oder als Client unterwegs sind, passieren verschiedene Dinge in der doInBackground() Methode des ConnectionTasks:</p>
<pre style="margin-left: 40px;">
protected Boolean doInBackground(Void... params) {
if (<span style="color:#0000ff;">runAsServer</span>) {
ServerSocket server = null;
server = new ServerSocket(PORT);
socket = server.accept();
} else {
String sIP = editServerIP.getText().toString();
SocketAddress sockaddr = new InetSocketAddress(sIP, PORT);
socket = new Socket();
socket.connect(sockaddr, TIMEOUT);
}
if (socket.isConnected()) {
is = socket.getInputStream();
os = socket.getOutputStream();
receiveMessages();
} else {
Log.e(getLocalClassName(), "socket not connected, should not happen...");
}
...
}</pre>
<p>
Im ersten Fall initialisieren wir einen ServerSocket und lassen ihn auf eingehende Verbindungen warten, im anderen Fall stellen wir eine Verbindung als Client her. Wenn die Verbindung erfolgreich hergestellt wurde, erhalten wir sowohl auf Server- als auch auf Clientseite einen Socket. Von dem lassen wir uns sowohl InputStream als auch OutputStream geben, und verarbeiten diese dann in der <em>receiveMessages()</em> Methode, die identisch ist für Server und Client:</p>
<pre style="margin-left: 40px;">
private void receiveMessages() throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
while (true) {
int read = is.read(buffer, 0, BUFFER_SIZE);
if (read == -1)
break;
// need to make a copy, since this is used in the other thread:
byte[] temp = new byte[read];
System.arraycopy(buffer, 0, temp, 0, read);
<span style="color:#0000ff;">publishProgress</span>(temp);
}
Log.e(getLocalClassName(), "Done: should not happen... ");
}</pre>
<p>
Über die <em>publishProgress()</em> Methode zeigen wir die ankommenden Nachrichten im UI Thread an:</p>
<pre style="margin-left: 40px;">
protected void onProgressUpdate(byte[]... values) {
if (values.length > 0) {
textConversation.setText("you: "
+ new String(<span style="color:#0000ff;">values[0]</span>) + "\r\n"
+ textConversation.getText());
}
}</pre>
<p>
Wir sind fast fertig: was noch fehlt ist das Senden von Nachrichten. Dazu implementieren wir die <em>sendMessage()</em> Methode im ConnectionTask:</p>
<pre style="margin-left: 40px;">
public void sendMessage() {
String message = <span style="color:#0000ff;">editMessage</span>.getText().toString();
try {
if (socket.isConnected()) {
os.write(message.getBytes());
} else {
Log.e(getLocalClassName(), "sendMessage(): Socket is closed");
}
} catch (Exception e) {
Log.e(getLocalClassName(), "sendMessage(): " + e);
}
}</pre>
<p>
Wir greifen dazu auf das EditText Widget im UI Thread zu und schicken den Inhalt über den OutputStream an unseren Gesprächspartner. Das Senden selbst wird durch einen Klick auf den "Send" Button in der UI ausgelöst:</p>
<pre style="margin-left: 40px;">
...
btnSend = (Button) findViewById(R.id.btnSend);
btnSend.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
connectionTask.sendMessage();
textConversation
.setText("me: " + editMessage.getText().toString() + "\r\n"
+ textConversation.getText());
}
});
...
</pre>
<p>
Tada.</p>
<p>
.</p>
<h2>
<img alt="" src="images/UDPChat.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />UDPChat</h2>
<p>
Die UDPChatActivity ist etwas einfacher als die TCP Variante. Das hat damit zu tun, dass wir nicht zwischen Client und Server unterscheiden müssen. Auch benötigen wir nicht die IP Adresse der anderen, da wir einfach einen Broadcast an alle im lokalen Netz schicken.</p>
<p>
Schauen wir uns zunächst die <em>sendMessage()</em> Methode in der UDP Version an:</p>
<pre style="margin-left: 40px;">
private void sendMessage() {
try {
String sMessage = et.getText().toString();
DatagramSocket ds = new DatagramSocket();
InetAddress serverAddr = Util.getLocalBroadcastAddress();
DatagramPacket dp =
new DatagramPacket(sMessage.getBytes(), sMessage.length(),
serverAddr, PORT);
ds.setBroadcast(true);
ds.send(dp);
} catch (Exception e) {
Log.e(getLocalClassName(), "sendMessage(): " + e);
}
}</pre>
<p>
Die sieht zwar ein klein wenig komplizierter aus, aber im Gegensatz zur TCP Version benötigen wir keine Referenzen auf InputStream, OutputStream oder Socket.</p>
<p>
Ähnlich verhält es sich beim Empfangen der Nachrichten, das wir in der doInBackground() Methode erledigen:</p>
<pre style="margin-left: 40px;">
protected Boolean doInBackground(Void... params) {
...
byte[] buffer = new byte[MAX_PACKET_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
DatagramSocket socket = null;
socket = new DatagramSocket(PORT);
while (true) {
...
socket.receive(packet);
InetAddress senderIP = packet.getAddress();
String senderName = "" + senderIP.getAddress()[3];
byte[] temp = new byte[packet.getLength()];
System.arraycopy(buffer, 0, temp, 0, temp.length);
publishProgress(senderName.getBytes(), temp);
...
}
...
return result;
}</pre>
<p>
Da es sich um einen Boadcast handelt, wollen wir den Sender der Nachricht anhand des letzten Bytes seiner IP Adresse, <em>senderIP.getAddress()[3]</em>, indentifizieren.</p>
<p>
.</p>
<h2>
<img alt="" src="images/GameServer.png" style="margin-left: 10px; margin-right: 10px; width: 184px; height: 355px; float: right;" />GameServer</h2>
<p>
Will man ein Netzwerkspiel implementieren, dann müssen sich die Spieler erst einmal finden und verbinden. Es gibt zwar von Google die Wi-Fi Peer-to-Peer API [19], die scheint aber nicht viel zu taugen. Deswegen implementieren wir mal kurz unsere eigene Version.</p>
<p>
Nehmen wir an wir wollen ein Spiel für zwei Spieler schreiben, z.B. TicTacToe. Wir könnten, wie im Webserver Beispiel, einen ServerSocket starten und auf eingehende Verbindungen warten. Dazu müsste der Client aber die IP Adresse des Servers kennen und das ist ja so 20. Jahrhundert. </p>
<p>
Im 21. Jahrhundert starten wir einen UDP Server,</p>
<pre style="margin-left: 40px;">
public void run() {
...
byte[] buffer = new byte[MAX_PACKET_SIZE];
DatagramSocket server = new DatagramSocket(GAME_SERVER_PORT);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
...
server.<span style="color:#0000ff;">receive</span>(packet);
InetAddress address = <span style="color:#0000ff;">packet.getAddress()</span>;
final String msg = address.getHostAddress();
...</pre>
<p>
der auf ankommende UDP Packete hört. Die Spieler senden einfach nur einen UDP Broadcast. Kommt jetzt ein Packet von einem Spieler, dann können wir über <em>getAddress()</em> die IP Adresse des Spielers ermitteln. Wir warten also bis alle Spieler sich bemerkbar gemacht haben,</p>
<pre style="margin-left: 40px;">
<span style="color:#0000ff;">playerSet</span>.add(address);
if (playerSet.size() >= NR_OF_PLAYERS_PER_TEAM) {
int i = 0;
InetAddress[] addresses = new InetAddress[NR_OF_PLAYERS_PER_TEAM];
for (InetAddress playerAddress : playerSet) {
if (i < NR_OF_PLAYERS_PER_TEAM) {
addresses[i] = playerAddress;
}
i++;
}
establishConnectionBetweenPlayers(addresses);
playerSet.clear();
}
// reset the length for the next packet
packet.setLength(buffer.length);
}
...
}</pre>
<p>
und speichern die IP Adressen der Spieler in dem HashSet <em>playerSet</em> ab. Wenn wir alle Spieler haben, dann teilen wir über die <em>establishConnectionBetweenPlayers()</em> Methode allen Spielern die gegenseitigen IP Adressen mit:</p>
<pre style="margin-left: 40px;">
private void establishConnectionBetweenPlayers(final InetAddress[] addresses) {
for (int i = 0; i < addresses.length; i++) {
final InetAddress inetAddress = addresses[i];
Thread th = new Thread(new Runnable() {
@Override
public void run() {
sendAddressesToClient(inetAddress, addresses);
}
private void sendAddressesToClient(final InetAddress inetAddress, final InetAddress[] addresses) {
try {
SocketAddress sockaddr = new InetSocketAddress(inetAddress, GAME_SERVER_PORT);
Socket socket = new Socket();
socket.connect(sockaddr, TIMEOUT);
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(addresses);
oos.close();
os.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
th.start();
}
}
</pre>
<p>
Diese Methode ist ein bisschen fies: sie startet so viele Threads wie es Spieler gibt und schickt jedem Spieler die IP Adressen der anderen. Die Spieler müssen natürlich zuhören.</p>