안드로이드 서버 클라이언트 소켓 통신 예제 (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를 변경해서는 안된다.
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 변경을 처리할 수 있다.