해당 포스트는 "안드로이드 통신+보안 프로그래밍" 책의 내용을 요약한 것이다.



: 안드로이드 소켓 프로그램을 작성할 때 주의할 점은 read(), accept(), connect()와 같은 블록 모드로 작동하는 메서드들은 메인 스레드 사용해면 안 된다. 이 점을 주의해서 코딩해야 한다. 단, write 메서드의 경우도 블록 모드로 작동되지만 write의 작동 시간이 짧아 메인 스레드에서 실행해도 상관 없다.


사진 왼쪽은 클라이언트 프로그램이 오른쪽은 서버 프로그램이다. 클라이언트에서 서버와 연결한 후 editText에 문자열을 입력하고 전달을 누르면 서버로 문자열이 전송되고 서버는 다시 클라이언트에게 똑같은 문자열을 전송한다. 


※ 안드로이드 클라이언트 프로그램

참고로 Handler와 HandlerThread에 대한 지식이 없다면 "Handler와 HandlerThread 기본/응용" 포스트를 보고 아래의 소스를 봐라.

public class TcpClientActivity extends Activity { ..... 변수 선언 @Override public void onDestroy() { super.onDestroy(); if (client != null) { //소켓과 스레드를 모두 종료시킨다. Message msg = mServiceHandler.obtainMessage(); msg.what = MSG_STOP; mServiceHandler.sendMessage(msg); } thread.quit(); } @Override public void onCreate(Bundle savedInstanceState) {

.... 변수 초기화 및 UI 설정 thread = new HandlerThread("HandlerThread"); thread.start(); // 루퍼를 만든다. mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); mMainHandler = new Handler() { @Override public void handleMessage(Message msg) { String m; switch (msg.what) { case MSG_CONNECT: m = "정상적으로 서버에 접속하였습니다."; text.setText(m); break; case MSG_CLIENT_STOP: text.setText((String) msg.obj); m = "클라이언트가 접속을 종료하였습니다."; break; case MSG_SERVER_STOP: text.setText((String) msg.obj); m = "서버가 접속을 종료하였습니다."; break; case MSG_START: m = "메세지 전송 완료!"; text.setText((String) msg.obj); break; default: m = "에러 발생!"; text.setText((String) msg.obj); break; } Toast.makeText(TcpClientActivity.this, m, Toast.LENGTH_SHORT).show(); super.handleMessage(msg); } }; connect.setOnClickListener(new OnClickListener() { @Override public void onClick(View V) { ip = eip.getText().toString(); try { port = Integer.parseInt(eport.getText().toString()); } catch (NumberFormatException e) { port = 5000; Log.d(TAG, "포트번호", e); } if (client == null) { try { client = new TCPClient(ip, port); client.start(); } catch (RuntimeException e) { text.setText("IP 주소나 포트번호가 잘못되었습니다.."); Log.d(TAG, "에러 발생", e); } } } }); finish.setOnClickListener(new OnClickListener() { @Override public void onClick(View V) { if (client != null) { Message msg = mServiceHandler.obtainMessage(); msg.what = MSG_CLIENT_STOP; mServiceHandler.sendMessage(msg); } } }); start.setOnClickListener(new OnClickListener() { @Override public void onClick(View V) { if (et.getText().toString() != null) { Message msg = mServiceHandler.obtainMessage(); msg.what = MSG_START; msg.obj = et.getText().toString(); mServiceHandler.sendMessage(msg); } } }); } private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_START: Message toMain = mMainHandler.obtainMessage(); try { networkWriter.write((String) msg.obj); networkWriter.newLine(); networkWriter.flush(); toMain.what = MSG_START; } catch (IOException e) { toMain.what = MSG_ERROR; Log.d(TAG, "에러 발생", e); } toMain.obj = msg.obj; mMainHandler.sendMessage(toMain); break; case MSG_STOP: case MSG_CLIENT_STOP: case MSG_SERVER_STOP: client.quit(); client = null; break; } } } public class TCPClient extends Thread { Boolean loop; SocketAddress socketAddress; String line; private final int connection_timeout = 3000; public TCPClient(String ip, int port) throws RuntimeException { socketAddress = new InetSocketAddress(ip, port); } @Override public void run() { try { socket = new Socket(); socket.setSoTimeout(connection_timeout); //read 메서드가 connection_timeout 시간동안 응답을 기다린다. socket.setSoLinger(true, connection_timeout); //서버와의 정상 종료를 위해서 connection_timeout 시간동안 close 호출 후 기다린다. socket.connect(socketAddress, connection_timeout); networkWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); InputStreamReader i = new InputStreamReader(socket.getInputStream()); networkReader = new BufferedReader(i); Message toMain = mMainHandler.obtainMessage(); toMain.what = MSG_CONNECT; mMainHandler.sendMessage(toMain); loop = true; } catch (Exception e) { loop = false; Message toMain = mMainHandler.obtainMessage(); toMain.what = MSG_ERROR; toMain.obj = "소켓을 생성하지 못했습니다."; mMainHandler.sendMessage(toMain); } while (loop) { try { line = networkReader.readLine(); //readLine()은 블록모드로 작동하기 때문에 별도의 스레드에서 실행한다. if (line == null) //서버에서 FIN 패킷을 보내면 null을 반환한다. break; Runnable showUpdate = new Runnable() { @Override public void run() { text.setText(line); } }; mMainHandler.post(showUpdate); //Runnable 객체를 메인 핸들러로 전달해 UI를 변경한다. } catch (InterruptedIOException e) { } catch (IOException e) { loop = false; // Log.d(TAG, "에러 발생", e); break; } } try { //소켓을 close 하면 null로 설정해서 가비지 컬렉션이 되도록 한다. if (networkWriter != null) { networkWriter.close(); networkWriter = null; } if (networkReader != null) { networkReader.close(); networkReader = null; } if (socket != null) { socket.close(); socket = null; } client = null; if (loop) { //클라이언트에서 연결을 끊지 않고 서버에서 FIN 패킷을 받았을 경우 loop = false; Message toMain = mMainHandler.obtainMessage(); toMain.what = MSG_SERVER_STOP; toMain.obj = "네트워크가 끊어졌습니다."; mMainHandler.sendMessage(toMain); } } catch(IOException e ) { Log.d(TAG, "에러 발생", e); Message toMain = mMainHandler.obtainMessage(); toMain.what = MSG_ERROR; toMain.obj = "소켓을 닫지 못했습니다.."; mMainHandler.sendMessage(toMain); } } public void quit() { loop = false; try { if (socket != null) { socket.close(); socket = null; Message toMain = mMainHandler.obtainMessage(); toMain.what = MSG_CLIENT_STOP; toMain.obj = "접속을 중단합니다."; mMainHandler.sendMessage(toMain); } } catch (IOException e) { Log.d(TAG, "에러 발생", e); } } } }

: 통신 프로그래밍에서 가장 어려운 부분이 비정상적인 상황에서 앱이 제대로 상태를 인지하고 작동하게 만드는 것이다. 서버와 정상적으로 연결 안 될 시 connect() 메서드를 호출한 이후 SocketTimeoutException이나 ConnectException 예외가 발생한다. 또한 socket.setSoLinger 메서드로 종료할 때 일정시간 지연시켰다. 그 이유는 서버가 세션 종료를 정상적으로 하기 위해서 종료를 일정시간동안 지연시키기 때문이다. 만약 지연 시키지 않고 다시 연결하고 종료하는 과정을 몇 번 반복하면 서버 연결과정에서 오류가 발생한다. 여기서 while문을 돌 때 loop 변수를 사용했는 데 interrupt 메서드를 사용하는 게 더 일반적이고 좋다. 

※ 안드로이드 서버 프로그램

: 먼저 서버는 작업량이 많아 리소스를 많이 필요로 하기 때문에 매니패스트에 'singleTask' 또는 'singleInstance'로 실행시키는 게 좋다.

public class TcpServerActivity extends Activity {
    ... 변수 선언

	@Override
	public void onDestroy(){
		super.onDestroy();
		if (thread != null) {
			new Thread() {
				@Override
				public void run() {
					thread.quit();
					thread = null;
				}
			}.start();
		}
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		final EditText eport =(EditText) findViewById(R.id.editText2);
		start = (Button)findViewById(R.id.button1);
		finish = (Button)findViewById(R.id.button2);
		text = (TextView)findViewById(R.id.textView1);
		start.setEnabled(true);
		finish.setEnabled(false);

		start.setOnClickListener(new OnClickListener(){
			@Override
			public void onClick(View v){
				try {
					port = Integer.parseInt(eport.getText().toString());
				} catch (NumberFormatException  e) {
					port = 6000;
				}

				if (thread == null) {
					try {
						thread = new ServerThread(port);
						thread.start();
						text.setText("서비스가 시작되었습니다.\n");
						start.setEnabled(false);
						finish.setEnabled(true);
					} catch (IOException e) {
						text.setText("Server Thread를 시작하지 못했습니다." + e.toString());
					}
				} else {
					Toast.makeText(v.getContext(), "서비스가 실행되고 있습니다.",
							Toast.LENGTH_SHORT).show();
				}
			}
		});

		finish.setOnClickListener(new OnClickListener(){
			@Override
			public void onClick(View v){
				if (thread  != null) {
					new Thread() {
						@Override
						public void run() {
							thread.quit();
							try {
								sleep(500);
								//서버와 클라이언트 사이의 연결을 종료하기 위해서는
								//클라이언트 프로그램에서 본 것같이 일정 시간이 필요하다.
								if(thread.isAlive()) {
									sleep(2000);
								}
							} catch (InterruptedException e) {}

							thread = null;
							Message m = new Message();
							m.what = QUIT_ID;
							m.obj = ("서비스를 종료합니다.");
							mHandler.sendMessage(m);
						}
					}.start();
				}
			}
		});

		mHandler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				switch (msg.what) {
					case MSG_ID:
						text.append((String) msg.obj + "\n");
						break;
					case QUIT_ID:
						text.setText((String) msg.obj);
						start.setEnabled(true);
						finish.setEnabled(false);
						Toast.makeText(TcpServerActivity.this, (String) msg.obj, Toast.LENGTH_SHORT).show();
					default:
						break;
				}
				super.handleMessage(msg);
			}
		};
	}

	public class ServerThread extends Thread {
		private Boolean loop;
		private ServerSocket server;

		public ServerThread(int port) throws IOException {
			super();
			server = new ServerSocket(port);
			server.setSoTimeout(3000);
			threadList = new ArrayList<EchoThread>();
			loop = true;
		} // 생성자

		@Override
		public void run() {
			while(loop) {
				try {
					Socket sock = server.accept();
					EchoThread thread = new EchoThread(sock);
					thread.start();
					threadList.add(thread);
				} catch ( InterruptedIOException e) {
//           			e.printStackTrace();
				} catch (IOException e) {
					Message m = new Message();
					m.what = QUIT_ID;
					m.obj = ("Server Thread에서 예외가 발생하였습니다." + e.toString());
					mHandler.sendMessage(m);
					break;
				}
			}
			try{
				if(server != null) {
					server.close();
					server = null;
				}
			}catch(Exception e){
				e.printStackTrace();
			}
		}

		public void quit() {
			loop = false;
			if(server != null) {
				try {
					server.close();
					server = null;
				} catch (Exception e) {
					e.printStackTrace();
				}
			}

			for (int i = 0, n = threadList.size() ; i < n; i++) {
				EchoThread t = threadList.remove(0);
				// EchoThread t = threadList.get(i);
				t.quit();
				t.interrupt();
				if (t.isAlive())
					SystemClock.sleep(1000);
			}
		}
	}

	class EchoThread extends Thread{
		private Socket sock;
		private InetAddress inetaddr;
		private OutputStream out;
		private InputStream in;
		private PrintWriter pw;
		private BufferedReader br;

		public EchoThread(Socket sock){
			this.sock = sock;
		} // 생성자	

		@Override
		public void run(){
			try{
				inetaddr = sock.getInetAddress();
				Message m = new Message();
				m.what = MSG_ID;
				m.obj = (inetaddr.getHostAddress() + " 로 부터 접속하였습니다.");
				mHandler.sendMessage(m);

				out = sock.getOutputStream();
				in = sock.getInputStream();
				pw = new PrintWriter(new OutputStreamWriter(out));
				br = new BufferedReader(new InputStreamReader(in));
			} catch(Exception e){
				e.printStackTrace();
			}

			try {
				String line = null;
				while((line = br.readLine()) != null){
					pw.println(line);
					pw.flush();

					Message m2 = new Message();
					m2.what = MSG_ID;
					m2.obj = line;
					mHandler.sendMessage(m2);
				}
			} catch(InterruptedIOException e) {
//   	      		e.printStackTrace();
			} catch(Exception e){
				e.printStackTrace();
			} finally{
				Message m3 = new Message();
				m3.what = MSG_ID;
				m3.obj = (inetaddr.getHostAddress() + "와의 접속이 종료되었습니다.");
				mHandler.sendMessage(m3);
				try {
					threadList.remove(this);

					if (sock != null) {
						sock.close();
						sock = null;
					}
					if (pw != null) {
						pw.close();
						pw = null;
					}
					if (br != null) {
						br.close();
						br = null;
					}
				}catch(Exception e){
					e.printStackTrace();
				}
			}
		} // run

		public void quit() {
			if(sock != null) {
				try {
					sock.close();
					sock = null;
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}



+ Recent posts