해당 포스트는 "안드로이드 통신+보안 프로그래밍" 책의 내용을 요약한 것이다.
: 안드로이드 소켓 프로그램을 작성할 때 주의할 점은 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(); } } } } }
'안드로이드 > 통신' 카테고리의 다른 글
6. URLConnection 사용을 위해 알아야할 HTTP 프로토콜/URI,URL (0) | 2017.07.22 |
---|---|
5. 소켓 채널 통신, NIO 버퍼, 클라이언트/서버 소켓 채널 구현 (0) | 2017.07.21 |
4. UDP 서버 프로그램 구현, 브로드캐스트 통신 (2) | 2017.07.20 |
3. 안드로이드 채팅 서버 소켓 프로그램 구현 (0) | 2017.07.20 |
1. 자바 클라이언트/서버 소켓 프로그램 구현 (1) | 2017.07.19 |