Kirjautuminen

Haku

Tehtävät

Kilpailu

Algoritmikisa
Putka Open 2020 -kisan
2. kierros päättyy klo 23:00!

Keskustelu: Nettisivujen teko: TCP Client Androidille

Sivu 1 / 1

Matti Holopainen [05.12.2017 18:11:53]

#

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ää.

Macro [05.12.2017 19:01:20]

#

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
	}
}

Matti Holopainen [06.12.2017 12:14:19]

#

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ä.

Matti Holopainen [07.12.2017 20:51:12]

#

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")
//            }
        }
    }

Macro [08.12.2017 18:22:03]

#

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)
    }
}

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta