Olen tottunut koodaamaan TCP-yhteyksien käsittelyn C/C++-tyyliin, missä Socket:iin voidaan virittää read-callback, jolloin inputtia ei tarvitse vahtia aktiivisesti.
Javassa eikä siis myöskään Androidissa ei ole I/O callback-funktiota. Stack Overflow ja muilla vastaavilla palstoilla on paljon TCP Client -esimerkkejä, mutta en ole löytänyt niistä yhtään, jossa sanoman lähetys ja vastaanotto ovat toisistaan riippumattomia. Sellainen ratkaisu ei siis kelpaa, joka perustuu siihen, että server (tai client) lähettää sanoman johon sitten vastapuolen client (tai server) vastaa.
Sanoman lähettäminen serverille ei ole ongelma. Mutta onko sanoman vastaanottamiseen serveriltä (sanoman lähetyksistä riippumatta) muuta ratkaisua kuin se, että TCP client-socketilta kysellään toistuvasti ajastimen ohjaamana, onko sillä vastaanotettua tekstiä readln()-funktiolla luettavaksi?
readln() ei saa jäädä odottamaan datan saapumista, koska silloin sanomien lähetys ei ole mahdollista.
Oman lisänsä ongelmaan tuo se, että Android ei salli verkkoliikennettä UI-säikeessä. Tämä on ratkaistu löytyneissä esimerkeissä monella eri tavalla enkä osaa valita, mitä kombinaatiota olioista Service, IntentService, AsyncTask, Thread jne. minun pitäisi käyttää.
Verkkoyhteyksien käsittely on jonkin aikaa ollut kiellettyä UI-säikeessä, mikä on ihan hyvä asia. Se tietenkin aiheuttaa vähän lisävirittelyä, mutta on vaivan arvoista.
AsyncTask on tehty sellaisiin tehtäviin, jossa annetaan syöte, tehdään yksi operaatio ja palautetaan arvo. Sitä voi kyllä väärinkäyttää tuollaiseen mitä haet, mutta on parempia vaihtoehtoja.
Thread-luokka on tähän varsin hyvä jos verkkoliikenteen käsittely pysyy vain yhdessä Activityssä (tai sen alaisissa Fragmenteissa). Jos yhteyden pitää pysyä auki eri Activity-luokkien välillä tai ohjelman sulkeuduttua, Service on ainoa mahdollisuus.
Käytännössä ohjelma siis koostuu Activitystä, joka lähettää ja käsittelee dataa, sekä säikeestä, joka on vastuussa liikenteestä. Lisäksi näiden väliseen kommunikointiin voidaan käyttää interfaceja. Interfacet eivät toimi Servicen kanssa, jolloin pitäisi käyttää joko BroadcastManageria tai Handlereita.
Esimerkiksi näin
// ActivityCallback.java interface ActivityCallback { void onNewMessage(byte[] message); } // MyTCPClientThread.java class MyTCPClientThread extends Thread { ActivityCallback callback; InputStream in; OutputStream out; public MyTCPClientThread(ActivityCallback callback) { this.callback = callback; // TODO in = socket.getInputStream(); out = socket.getOutputStream(); } public void run() { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int inByte; // -1 tarkoittaa InputStreamin sulkeutumista while((inByte = in.read()) != -1) { buffer.write(inByte); // Viestin lopetusmerkki vastaanotettu, ilmoita Activitylle ja nollaa bufferi if (inByte == 42) { callback.onNewMessage(buffer.toByteArray()); buffer.reset(); } } } public void sendMessage(byte[] message) { try { outputStream.write(message); } catch (IOException e) { // Yhteys sulkeutunut } } } // MyActivity.java class MyActivity extends Activity implements ActivityCallback { @Override protected void onCreate(Bundle b) { super.onCreate(b); MyTCPClient client = new MyTCPClient(this); Thread clientThread = new Thread(client); clientThread.start(); // Lähetä viesti clientThread.write(...); } @Override public void onNewMessage(byte[] message) { // Käsittely vastaanotettu data } }
Kiitos nopeasta ja selkeästä vastauksesta! Interfacen käyttöä en olisi keksinyt itse, elegantti tapa sanoman välittämiseen.
Kokeilen nyt aluksi tämän avulla, mutta koska haluan yhteyden pysyvän auki mahdollisimman kauan, teen joskus myöhemmin Service-pohjaisen ratkaisun.
Sovellukseni on pikaviestisovellus, jossa viestit syötetään sähköttämällä eli Morse-koodilla. Käyttöliittymässä on painikkeet, joita koskettamalla syntyy pisteitä ja viivoja. Viestit siirtyvät vastaanottajalle yksinkertaisen serverin kautta. Vastaanotetut viestit kuuluvat sähkötyksenä.
Missä määrittelet Socketin? Saan helposti poikkeuksen ~"Network operation in main thread", mutta tämäkään ei auta:
(Koodi on Kotlin:ia. Suosittelen tutustumaan, jos ette vielä tunne)
inner class MyTCPClient : Thread() { init { Log.i(TAG, "MyTcpClient.init") } override fun run() { Log.i(TAG, "MyTcpClient.run") var socket: Socket? = null var input: BufferedReader? = null var output: PrintWriter? = null try { Log.i(TAG, "MyTcpClient.run: Opening Socket...") socket = Socket("192.168.8.101", 2222) input = BufferedReader(InputStreamReader(socket!!.getInputStream())) output = PrintWriter(socket!!.getOutputStream()) Log.i(TAG, "MyTcpClient.run: Socket opened") } catch (e: Exception) { Log.i(TAG, "MyTcpClient.run: $e") } while (true) { val message = input!!.readLine() if (message == null) { break } Log.i(TAG, "MyTcpClient: Received message='$message'") showMessage(message) } } // Ei käänny, koska output ei näy fun sendMessage(message: String) {¨ // try { // output!!.println(message) // } // catch (e: IOException) { // // Yhteys on sulkeutunut // Log.i(TAG, "MyTcpClient: Connection closed") // } } }
Socketin avaus pitää varmaankin tehdä run-metodin sisällä, kuten oletkin tehnyt, sillä luokan constructori taidetaan suorittaa kutsujan säikeessä.
output-muuttujan saat näkyviin kun siirrät muuttujan alustuksen run-metodin sisältä luokan alle.
inner class MyTCPClient : Thread() { var socket: Socket? = null var input: BufferedReader? = null var output: PrintWriter? = null override fun run() { try { socket = Socket(...) ... } ... } fun sendMessage(message : String) { output!!.println(message) } }
Aihe on jo aika vanha, joten et voi enää vastata siihen.