Example: Android bi-directional network socket using AsyncTask
Most of the network socket examples I found for Android were one directional only. I needed a solution for a bi-directional data stream. I eventually learned of the AsyncTask. This example shows how to get data from a socket and send data back to it. Due to the blocking nature of a socket that is receiving data, that blocking needs to run in a thread other than the UI thread.
For the sake of example, this code connects to a webserver. Pressing the "Start AsyncTask" button will open the socket. Once the socket is open, the web server waits for a request. Pressing the "Send Message" button will send a request to the server. Any response from the server will be displayed in the TextView. In the case of http, a web server will disconnect from the client once all the data has been sent. For other TCP data streams, the connection will stay up until one side disconnects.
Screenshot:
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.exampleasynctask"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
res\layout\main.xml:
<?xml version="1.0" e开发者_开发百科ncoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button android:id="@+id/btnStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start AsyncTask"></Button>
<Button android:id="@+id/btnSend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send Message"></Button>
<TextView android:id="@+id/textStatus" android:textSize="24sp" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Status Goes Here" />
</LinearLayout>
src\com.exampleasynctask\MainActivity.java:
package com.exampleasynctask;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
Button btnStart, btnSend;
TextView textStatus;
NetworkTask networktask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnStart = (Button)findViewById(R.id.btnStart);
btnSend = (Button)findViewById(R.id.btnSend);
textStatus = (TextView)findViewById(R.id.textStatus);
btnStart.setOnClickListener(btnStartListener);
btnSend.setOnClickListener(btnSendListener);
networktask = new NetworkTask(); //Create initial instance so SendDataToNetwork doesn't throw an error.
}
private OnClickListener btnStartListener = new OnClickListener() {
public void onClick(View v){
btnStart.setVisibility(View.INVISIBLE);
networktask = new NetworkTask(); //New instance of NetworkTask
networktask.execute();
}
};
private OnClickListener btnSendListener = new OnClickListener() {
public void onClick(View v){
textStatus.setText("Sending Message to AsyncTask.");
networktask.SendDataToNetwork("GET / HTTP/1.1\r\n\r\n");
}
};
public class NetworkTask extends AsyncTask<Void, byte[], Boolean> {
Socket nsocket; //Network Socket
InputStream nis; //Network Input Stream
OutputStream nos; //Network Output Stream
@Override
protected void onPreExecute() {
Log.i("AsyncTask", "onPreExecute");
}
@Override
protected Boolean doInBackground(Void... params) { //This runs on a different thread
boolean result = false;
try {
Log.i("AsyncTask", "doInBackground: Creating socket");
SocketAddress sockaddr = new InetSocketAddress("192.168.1.1", 80);
nsocket = new Socket();
nsocket.connect(sockaddr, 5000); //10 second connection timeout
if (nsocket.isConnected()) {
nis = nsocket.getInputStream();
nos = nsocket.getOutputStream();
Log.i("AsyncTask", "doInBackground: Socket created, streams assigned");
Log.i("AsyncTask", "doInBackground: Waiting for inital data...");
byte[] buffer = new byte[4096];
int read = nis.read(buffer, 0, 4096); //This is blocking
while(read != -1){
byte[] tempdata = new byte[read];
System.arraycopy(buffer, 0, tempdata, 0, read);
publishProgress(tempdata);
Log.i("AsyncTask", "doInBackground: Got some data");
read = nis.read(buffer, 0, 4096); //This is blocking
}
}
} catch (IOException e) {
e.printStackTrace();
Log.i("AsyncTask", "doInBackground: IOException");
result = true;
} catch (Exception e) {
e.printStackTrace();
Log.i("AsyncTask", "doInBackground: Exception");
result = true;
} finally {
try {
nis.close();
nos.close();
nsocket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
Log.i("AsyncTask", "doInBackground: Finished");
}
return result;
}
public void SendDataToNetwork(String cmd) { //You run this from the main thread.
try {
if (nsocket.isConnected()) {
Log.i("AsyncTask", "SendDataToNetwork: Writing received message to socket");
nos.write(cmd.getBytes());
} else {
Log.i("AsyncTask", "SendDataToNetwork: Cannot send message. Socket is closed");
}
} catch (Exception e) {
Log.i("AsyncTask", "SendDataToNetwork: Message send failed. Caught an exception");
}
}
@Override
protected void onProgressUpdate(byte[]... values) {
if (values.length > 0) {
Log.i("AsyncTask", "onProgressUpdate: " + values[0].length + " bytes received.");
textStatus.setText(new String(values[0]));
}
}
@Override
protected void onCancelled() {
Log.i("AsyncTask", "Cancelled.");
btnStart.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
Log.i("AsyncTask", "onPostExecute: Completed with an Error.");
textStatus.setText("There was a connection error.");
} else {
Log.i("AsyncTask", "onPostExecute: Completed.");
}
btnStart.setVisibility(View.VISIBLE);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
networktask.cancel(true); //In case the task is currently running
}
}
The SendDataToNetwork
task runs in the main ui thread, meaning it will crash a Honeycomb or higher app due to NetworkOnMainThreadException
Fatal exception. Here's what my SendDataToNetwork
looks like to avoid this issue:
public boolean sendDataToNetwork(final byte[] cmd) {
if (_nsocket.isConnected()) {
Log.i(TAG, "SendDataToNetwork: Writing received message to socket");
new Thread(new Runnable() {
public void run() {
try {
_nos.write(cmd);
} catch (Exception e) {
e.printStackTrace();
Log.i(TAG, "SendDataToNetwork: Message send failed. Caught an exception");
}
}
}).start();
return true;
}
Log.i(TAG, "SendDataToNetwork: Cannot send message. Socket is closed");
return false;
}
Your SendDataToNetwork
does not run on the same thread as doInBackground()
. There is a possibility that SendDataToNetwork
would start sending data before socket is ready.
To avoid all this just use SendDataToNetwork
to save data and signal to background thread that data is ready to be sent.
Since there is possibility that user can press button multiple times, while the old data is still being sent, you should have synchronized Queue inside NetworkTask. Then:
- Background thread sets up the socket connection and then goes to sleep (via wait()).
- On button press,
SendDataToNetwork
adds data to queue and wakes up the background thread (vianotify()
). - When background thread wakes up, it first checks the
finish
flag. If set, it closes connections and exits. If not it reads data from Queue, sends it to network and goes back to sleep. - You should have
finish()
method which sets afinish
flag (atomic variable, like boolean) and wakes the background thread. This is a way to gracefully exit the background thread.
Take a look at how thread synchronization is done: http://www.jchq.net/tutorial/07_03Tut.htm
More interactive example
Similar to the OP's, but you can control host, port and message + there is a popup error notification if the connection failed.
Usage 1:
- get Android and a Linux desktop on a LAN
- find the IP of the desktop with
ifconfig
- run
netcat -l 12345
on a terminal - on Android, fill in the IP of the desktop
- click contact server
- on the terminal, type the reply, and hit
Ctrl + D
- it appears on the
output:
section
Usage 2:
- hostname
google.com
- port
80
- Message:
"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n"
Note that some HTTP servers won't close after the reply expecting further requests, and the application will hang until they timeout. Such servers expect you to parse the Content-Width
header and close yourself.
If the connection fails, an alert message is shown to the user on a dialog.
Code
Add to AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET" />
And the main activity is:
import android.app.Activity;
import android.app.AlertDialog;
import android.app.IntentService;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Main extends Activity {
final static String TAG = "AndroidCheatSocket";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
TextView textView;
final String defaultHostname = "192.168.0.";
textView = new TextView(this);
textView.setText("hostname / IP:");
linearLayout.addView(textView);
final EditText hostnameEditText = new EditText(this);
hostnameEditText.setText(defaultHostname);
hostnameEditText.setSingleLine(true);
linearLayout.addView(hostnameEditText);
textView = new TextView(this);
textView.setText("port:");
linearLayout.addView(textView);
final EditText portEditText = new EditText(this);
portEditText.setText("12345");
portEditText.setSingleLine(true);
linearLayout.addView(portEditText);
textView = new TextView(this);
textView.setText("data to send:");
linearLayout.addView(textView);
final EditText dataEditText = new EditText(this);
dataEditText.setText(String.format("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", defaultHostname));
linearLayout.addView(dataEditText);
final TextView replyTextView = new TextView(this);
final ScrollView replyTextScrollView = new ScrollView(this);
replyTextScrollView.addView(replyTextView);
final Button button = new Button(this);
button.setText("contact server");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
button.setEnabled(false);
new MyAsyncTask(Main.this, replyTextView, button).execute(
hostnameEditText.getText().toString(),
portEditText.getText().toString(),
dataEditText.getText().toString());
}
});
linearLayout.addView(button);
textView = new TextView(this);
textView.setText("output:");
linearLayout.addView(textView);
linearLayout.addView(replyTextScrollView);
this.setContentView(linearLayout);
}
private class MyAsyncTask extends AsyncTask<String, Void, String> {
Activity activity;
Button button;
TextView textView;
IOException ioException;
MyAsyncTask(Activity activity, TextView textView, Button button) {
super();
this.activity = activity;
this.textView = textView;
this.button = button;
this.ioException = null;
}
@Override
protected String doInBackground(String... params) {
StringBuilder sb = new StringBuilder();
try {
Socket socket = new Socket(
params[0],
Integer.parseInt(params[1]));
OutputStream out = socket.getOutputStream();
out.write(params[2].getBytes());
InputStream in = socket.getInputStream();
byte buf[] = new byte[1024];
int nbytes;
while ((nbytes = in.read(buf)) != -1) {
sb.append(new String(buf, 0, nbytes));
}
socket.close();
} catch(IOException e) {
this.ioException = e;
return "error";
}
return sb.toString();
}
@Override
protected void onPostExecute(String result) {
if (this.ioException != null) {
new AlertDialog.Builder(this.activity)
.setTitle("An error occurrsed")
.setMessage(this.ioException.toString())
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
} else {
this.textView.setText(result);
}
this.button.setEnabled(true);
}
}
}
On GitHub with build boilerplate.
I've also posted an Android server example at: https://stackoverflow.com/a/35745834/895245
Tested on Android 5.1.1, Sony Xperia 3 D6643.
精彩评论