소곤소곤 ad

2015년 1월 13일 화요일

안드로이드 서버 클라이언트 소켓 통신 예제 (Android Java TCP/IP Client/Server Socket Example)

안드로이드 서버 클라이언트 소켓 통신 예제 (Android Java TCP/IP Client/Server Socket Communication Example) 


간단한 통신 프로그램을 짜보려고 인터넷을 다 뒤져봤지만, 제대로 구현된 예제 프로그램을 찾기가 어려웠다.

단순한 에코 프로그램 (echo server) 정도의 구현은 아주 쉽게 작성이 가능하지만, 화면 인터페이스 (UI)에 맞게 사용자가 입력한 내용을 전송한다거나, 소켓에서 받은 데이터로 TextView에라도 표시하려 하면 여러가지 문제에 봉착하고 만다. 실전에 사용하려 하면 어떤 문제들이 생길까? 왜 이런 문제들을 해결해 놓은 예제 프로그램은 찾을 수 없는 것일까?


A. 규칙! 이제 안드로이드의 모든 Network Function은 Main Thread (UI Thread)에서 실행하면 안된다. 


E/AndroidRuntime(673): java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.example/com.example.ExampleActivity}:android.os.NetworkOnMainThreadException

별 생각없이 Socket 함수들을 호출하면 이런 에러를 만나고 만다. 메인 쓰레드에서 네트워크기능을 사용할 수 없다는 뜻이다. 모든 네트워크 기능은 별도의 Thread에서 처리해야 한다. 하지만 여기서부가 또다른 골치아픈 문제의 시작점인것을...


B. 규칙! 메인 쓰레드 (Main Thread = UI Thread)가 아니면 UI를 변경해서는 안된다. 


내가 만든 쓰레드에서 직접 UI의 내용변경을 하면 안되고, 반드시 메인쓰레드가 처리하도록 요청하여야 한다. 이것을 구현하는 방법은 여러가지가 있는데,

a. activity.runOnUiThread()를 이용하는 방법
b. view.post()를 이용하는 방법
c. AsyncTask를 이용하는 방법
d. Message Handler를 이용하는 방법
e. ...

보통의 프로그램이라면 취향껏 고르면 될 일이다. 하지만 소켓 통신 프로그램은 그렇지 못하다!!

왜냐면, 소켓이 이미 하나의 Thread이고, 소켓에서 들어온 데이터를 계속 처리해야 하기 때문에 이 녀석 역시 Thread일 수 밖에 없기 때문이다. 그래서 규칙A와 B가 서로를 방해하는 형태로 계속 꼬여 버린다.

삽질끝에 얻은 가장 단순한 처리는 모든 소켓처리를 1개의 Thread에서 처리하고, UI 와의 통신은 Message Handler를 이용하는 것.

이렇게 구현된 SimpleSocket 클래스는 정말 아주 심플하다!!



import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;

import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class SimpleSocket extends Thread {
	private Socket  mSocket;

	private BufferedReader buffRecv;
	private BufferedWriter buffSend;

	private String  mAddr = "localhost";
	private int     mPort = 8080;
	private boolean mConnected = false;
	private Handler mHandler = null;

	static class MessageTypeClass {
		public static final int SIMSOCK_CONNECTED = 1;
		public static final int SIMSOCK_DATA = 2;
		public static final int SIMSOCK_DISCONNECTED = 3;
	};
	public enum MessageType { SIMSOCK_CONNECTED, SIMSOCK_DATA, SIMSOCK_DISCONNECTED };

	public SimpleSocket(String addr, int port, Handler handler) 
	{
		mAddr = addr;
		mPort = port;
		mHandler = handler;
	}

	private void makeMessage(MessageType what, Object obj)
	{
		Message msg = Message.obtain(); 
		msg.what = what.ordinal();
		msg.obj  = obj;
		mHandler.sendMessage(msg);
	}

	private boolean connect (String addr, int port) 
	{
		try {
			InetSocketAddress socketAddress  = new InetSocketAddress (InetAddress.getByName(addr), port); 
			mSocket = new Socket();
			mSocket.connect(socketAddress, 5000);
		} catch (IOException e) {
			System.out.println(e);
			e.printStackTrace();
			return false;
		}
		return true;
	} 

	@Override
	public void run() {
		if(! connect(mAddr, mPort)) return; // connect failed
		if(mSocket == null)         return;

		try {
			buffRecv = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
			buffSend = new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream()));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		mConnected = true;

		makeMessage(MessageType.SIMSOCK_CONNECTED, "");
		Log.d("SimpleSocket", "socket_thread loop started");

		String aLine = null;

		while( ! Thread.interrupted() ){ try {
			aLine = buffRecv.readLine(); 
			if(aLine != null) makeMessage(MessageType.SIMSOCK_DATA, aLine);
			else break;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}}

		makeMessage(MessageType.SIMSOCK_DISCONNECTED, "");
		Log.d("SimpleSocket", "socket_thread loop terminated");

		try {
			buffRecv.close(); 
			buffSend.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		mConnected = false;
	}

	synchronized public boolean isConnected(){
		return mConnected;
	}

	public void sendString(String str){
		PrintWriter out = new PrintWriter(buffSend, true);
		out.println(str);
	}
}





이 클래스를 사용하려면, 메세지를 처리할 Handler를 하나 구현하고, SimpleSocket을 생성한다음 start()만 해주면 된다. Looper.getMainLooper() 덕택에 UI 변경은 모두 메인쓰레드에서 처리할 수 있다.




public class ChatMain extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.chat_main);

		...
		ed1 = (EditText) findViewById(R.id.editText1);
		Button buttonSend = (Button) findViewById(R.id.button1);

		buttonSend.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				ssocket.sendString(ed1.getText());
				ed1.setText("");
			}
		});

		Handler mHandler = new Handler(Looper.getMainLooper()) {
			@Override
			public void handleMessage(Message inputMessage) {
				switch(inputMessage.what){
				case SimpleSocket.MessageType.SIMSOCK_DATA : 
					String msg = (String) inputMessage.obj;
					Log.d("OUT",  msg);
					// do something with UI
					break;

				case SimpleSocket.MessageType.SIMSOCK_CONNECTED : 
					// do something with UI
					break;

				case SimpleSocket.MessageType.SIMSOCK_DISCONNECTED : 
					// do something with UI
					break;

				}
			}    
		};  

		ssocket = new SimpleSocket("192.168.0.1", 8080, mHandler);
		ssocket.start();

		...
이방식으로 구현해놓으면, 추가로 Thread를 만들지 않고서도 Socket Thread 하나로 모든 소켓통신과 UI 변경을 처리할 수 있다.