티스토리 뷰



이 자료들은 팁스소프트에서 제공하는 [ 알짜배기 ] 프로그램을 이용하면 더 편리하게 볼수 있습니다.
* 알짜배기 프로그램 받기 - http://www.tipssoft.com/bulletin/tb.php/QnA/8406
* 안드로이드 강좌 목록 - http://www.tipssoft.com/bulletin/tb.php/old_bbs/501
이번 자료에서는 카메라 액티비티를 호출하여 촬영한 사진을 소켓 통신과 파일 입출력을 이용하여
PC 에서 동작하는 서버 프로그램에 전송하는 예제를 다루도록 하겠습니다. 이 예제를 쉽게 이해하기
위해서는 쓰레드, 스트림, 소켓 그리고 액티비티에 대한 지식이 필요하므로 아래에 링크된 자료를
먼저 보시는게 좋습니다.
쓰레드(Thread) 의 이해 - 기초편 : http://www.tipssoft.com/bulletin/tb.php/FAQ/922
소켓(Socket) 통신의 이해 - 클라이언트 : http://www.tipssoft.com/bulletin/tb.php/FAQ/930
액티비티(Activity)의 이해 : http://www.tipssoft.com/bulletin/tb.php/FAQ/944
사진 촬영 후 이미지뷰에 출력하기 : http://www.tipssoft.com/bulletin/tb.php/FAQ/947
안드로이드용으로 구현된 클라이언트 어플리케이션은 위에 링크된 자료에서 사용한 코드들을 조합하여
구현되었기때문에 추가적으로 코드 구성에 대하여 알아두셔야 할 부분은 없습니다.
이 자료에서 사용하는 서버 프로그램은 MFC 로 개발되었으며, 클라이언트에서 전송하는 사진 데이터를
파일에 저장하고, 전송이 완료되면 화면에 출력시키는 역할을 합니다. 이 강좌에서는 서버 프로그램과
직접적인 관련이 없기때문에 클라이언트를 테스트할 수 있는 실행파일만 제공하도록 하겠습니다.
먼저 서버 - 클라이언트 간에 어떤 진행 흐름이 있는가를 알아본 후, 클라이언트가 어떻게 구성되었는지
설명하고, 서버 프로그램을 어떻게 사용하는지 알아보도록 하겠습니다.
1. 서버 - 클라이언트의 진행 흐름
서버와 클라이언트 간에 데이터를 주고 받기위해서는 서버 프로그램이 먼저 서비스를 개시하고,
클라이언트의 접속을 대기해야합니다. 보통 클라이언트 프로그램은 서버에 접속하여 접속상태를
유지한채 서버에 데이터를 전송하거나 서비스를 요청하게되는데 이 예제의 경우에는 사진을 촬영한
후에만 서버에 접속하여 사진을 전송하면 되기때문에 그 외에는 접속상태를 유지하지 않습니다.
아래의 그림은 서버와 클라이언트 간의 통신 흐름을 나타낸 것입니다.
클라이언트 어플리케이션은 사진 촬영을 하고, 사진을 저장하면 서버 프로그램에 연결(Connect)을
하고 연결이 받아들여지면(Accept) 서버와 클라이언트 간에는 다음과 같은 메세지들을 주고 받습니다.
- SEND_IMAGE_SIZE
서버에 사진 이미지를 전송할 것을 알립니다. 이미지 전송의 개시를 알리면서 해당 데이터의
전체 크기를 프레임의 Body 에 넣어서 전송합니다. 데이터의 크기를 4바이트의 int 형으로 정하면
네트워크 프레임은 아래와 같이 구성됩니다.
- SEND_CAPTURE_IMAGE
전송할 사진 이미지를 일정 크기로 분할하여 전송할 때 사용하는 메세지입니다. 이미지 데이터는
한번에 보내기엔 네트워크가 부담을 느낄 수 있기때문에 이 메세지를 이용하여 분할하여 반복하여
서버 프로그램에 전송합니다. 프레임의 Body 에는 이미지 데이터를 넣고, Body 의 크기를
Header 의 Body Size 에 저장합니다.
- FINISH_IMAGE_DATA
클라이언트가 사진 이미지를 모두 전송하면 더 이상 전송할 데이터가 없다는 의미로 사용하는
메세지 입니다. 서버 프로그램에 상태를 전달하기 위해 사용하는 패킷이므로 Body 는 없습니다.
- SUCCESS_RECV_DATA
서버가 데이터를 무사히 전송받은 경우 클라이언트에 전송하는 메세지입니다. 이 메세지는 서버
프로그램이 클라이언트 프로그램에 완료 상태를 전달하기 위해 사용하는 것이므로 패킷을 사용할
때 Body 를 사용하지 않습니다.
2. 클라이언트 어플리케이션 사용하기
클라이언트 어플리케이션은 메인 액티비티에 서버 프로그램에 사진 정보를 보내주어야하기 때문에
IP 주소를 넣을 수 있는 에디트텍스트 컨트롤을 사용하였으며 카메라로 액티비티를 전환하기위해
버튼 컨트롤을 사용하였습니다.
다음은 어플리케이션의 사용 순서를 그림으로 나타낸 것입니다.
① 메인 액티비티에 있는 에디트텍스트에 서버 IP 주소를 입력하고, capture 버튼을 클릭하여 카메라
액티비티를 호출합니다. 기존 카메라 어플리케이션의 이미지 촬영용 액티비티가 활성화됩니다.
② 카메라 액티비티에서 촬영 버튼을 클릭하여 사진을 촬영하고 이미지를 저장합니다. 저장이
완료되면 카메라 액티비티가 종료되고, 메인 액티비티가 다시 활성화되면서 서버에 저장한 사진
정보를 송신하기 시작합니다. 이 때, 텍스트뷰에 파일의 전송 현황이 표시됩니다.
③ 사진 데이터 전송이 완료되면 텍스트뷰에 완료 메세지가 출력됩니다.
안드로이드용 어플리케이션은 기기의 방향이 전환되어 화면이 변경될 때 액티비티를 파괴하였다가
다시 생성하는데 이 예제는 그 부분에 대한 처리를 하지 않았기때문에 화면 전환시 사진을 전송하지
않을 수도 있습니다. 화면 전환에 대한 처리는 다음에 다루도록 하겠습니다.
3. 클라이언트 어플리케이션 구현하기
이 예제에서 사용하는 여러가지 기능들 중에서 액티비티를 전환하는 것과 카메라 액티비티로 촬영한
사진 정보를 읽고 출력하는 방법은 아래의 링크를 참고하시기 바랍니다.
사진 촬영 후 이미지뷰에 출력하기 : http://www.tipssoft.com/bulletin/tb.php/FAQ/947
클라이언트 어플리케이션의 많은 부분이 지난 강좌들에서 소개한 내용과 많이 중복되기때문에 소켓
통신을 이용하여 서버 프로그램과 데이터를 주고 받는 부분만 따로 설명하도록 하겠습니다.
// 소켓 연결을 수행하는 별도의 쓰레드가 수행하는 메소드
public void run()
{
try {
// IP 주소와 포트 번호를 관리하는 객체를 생성한다.
SocketAddress sock_addr = new InetSocketAddress(m_ip_address, m_port_number);
// 소켓을 생성한다.
m_client_socket = new Socket();

// 수신 버퍼 크기를 1024 바이트로 설정한다.
m_client_socket.setReceiveBufferSize(1024);
// 송신 버퍼 크기를 8192 바이트로 설정한다.
m_client_socket.setSendBufferSize(8192);
// 소켓을 닫을 때 TIME_OUT 에 저장해둔 밀리세컨만큼 대기한 후 닫는다.
m_client_socket.setSoLinger(true, TIME_OUT);

// 서버와 연결을 시도한다. TIME_OUT 시간 내에 응답이 오지 않으면 연결을 포기한다.
m_client_socket.connect(sock_addr, TIME_OUT);

// 연결된 경우
if(m_client_socket != null && m_client_socket.isConnected()){
// 연결 성공 메세지가 텍스트뷰에 출력되도록 m_display_run 인터페이스를
// 메인 쓰레드에 전달한다.
m_display_string = "Try Reflesh!!";
m_text_view.post(m_display_run);

// 소켓의 입력 스트림과 출력 스트림을 얻는다.
m_sock_out_stream = new BufferedOutputStream( m_client_socket.getOutputStream());
m_sock_in_stream = m_client_socket.getInputStream();

// 이미지 데이터를 전송한다.
onSendPicture();
// 서버에서 보내는 데이터를 수신한다.
onReadStream();
}
} catch (Exception e) {
// 최상위 예외 클래스로 쓰레드에서 발생하는 모든 예외를 동일하게 처리한다.
m_display_string = e.toString();
m_text_view.post(m_display_run);
} finally {
try {
if(m_client_socket != null) {
if(m_sock_out_stream != null) {
// 출력스트림을 닫는다.
m_sock_out_stream.close();
m_sock_out_stream = null;
}
if(m_sock_in_stream != null) {
// 출력스트림을 닫는다.
m_sock_in_stream.close();
m_sock_in_stream = null;
}

// 소켓을 닫는다.
m_client_socket.close();
m_client_socket = null;
}
} catch (IOException e) {
m_display_string = e.toString();
m_text_view.post(m_display_run);
}
}
}
// 추가 thread 가 서버에서 데이터를 수신하기위해 사용하는 메소드
public void onReadStream() throws IOException
{
byte msg_id;
byte[] size = new byte[2];

// 쓰레드에 인터럽트가 발생하지 않으면 반복한다.
while (!m_client_thread.isInterrupted()) {
// 메세지 번호를 읽는다.
msg_id = (byte)m_sock_in_stream.read();
// 수신완료 메세지가 맞다면
if(msg_id == NM_SUCCESS_RECV_DATA){
// 크기 정보가 저장된 2바이트를 읽는다.
if(m_sock_in_stream.read(size) == 2){
// 반복문을 나와서 메세지 수신 대기를 종료한다.
break;
}
}
}
}
// 이미지 데이터를 전송하는 메소드
public void onSendPicture()
{
// 소켓이 존재하는 경우
if(m_client_socket != null){
FileInputStream input_stream = null;

try {
// 전체 파일 크기와 프레임 body 사이즈를 저장할 바이트 배열을 생성한다.
byte[] file_size = new byte[4];
byte[] body_size = new byte[2];

// 고유영역에서 파일명이 test_picture.jpg 인 파일의 입력용 파일 스트림 객체를 얻는다.
input_stream = openFileInput("test_picture.jpg");
// Byte Ordering 을 고려하여 바이트 값을 int 값으로 변환한다.
// 0x12345678 을 윈도우즈 시스템에서 전송했다면 수신시 0x78563412 로 저장되므로
// 순서를 역으로 변환해야한다.
int full_size = input_stream.available();
file_size[0] = (byte)(full_size & 0x000000FF);
file_size[1] = (byte)((full_size & 0x0000FF00) >> 8);
file_size[2] = (byte)((full_size & 0x00FF0000) >> 16);
file_size[3] = (byte)((full_size & 0xFF000000) >> 24);
// 파일의 전체 크기를 텍스트뷰에 출력해준다.
m_display_string = "send size : " + full_size;
m_text_view.post(m_display_run);
// 바이트의 사이즈를 저장한다.
// 안드로이드의 기반인 리눅스와 윈도우즈는 Byte Ordering 이 다르기때문에
// 2바이트의 데이터를 송수신할 때 1바이트씩 값을 바꿔주어야 한다.
int data_size = file_size.length;
body_size[0] = (byte)data_size;
body_size[1] = (byte)(data_size >>> 8);
// 데이터 전송 시작의 의미로 파일사이즈를 스트림에 쓴다.
m_sock_out_stream.write(NM_SEND_IMAGE_SIZE);
// 전송할 프레임의 body 사이즈를 스트림에 쓴다.
m_sock_out_stream.write(body_size);
// 파일 크기를 스트림에 쓴다.
m_sock_out_stream.write(file_size);
// 스트림에 쓰여진 정보를 서버로 전송한다.
m_sock_out_stream.flush();
// 이미지 데이터를 저장할 바이트 배열을 생성한다.
byte[] send_data = new byte[MAX_SEND_DATA_SIZE];
int remain_file_size = full_size, read_size;
// 전송할 데이터 크기가 남았다면 계속 반복한다.
while(remain_file_size > 0) {
// 50 ms 를 대기한다.
try {
Thread.sleep(50);
} catch (InterruptedException ie) {
m_display_string = ie.toString();
m_text_view.post(m_display_run);
}

// 남은 데이터 크기가 최대 전송가능 데이터 크기보다 크면
// 전송할 크기를 최대치로 하고, 아니면 남은 크기로 한다.
if(remain_file_size > MAX_SEND_DATA_SIZE) data_size = MAX_SEND_DATA_SIZE;
else data_size = remain_file_size;
// 파일 입력 스트림에서 데이터 크기만큼 읽고, 실제 읽은 크기를 반환받는다.
read_size = input_stream.read(send_data, 0, data_size);

if(read_size < 0) {
// 읽은 데이터 크기가 0보다 작으면 문제가 발생한 것이므로
// 스트림을 닫고, 예외를 발생시킨다.
input_stream.close();
throw new IOException("End of Stream");
} else if(read_size < data_size) {
// 읽을 크기보다 실제 읽은 크기가 작으면..
int temp;
// 읽어야할 크기와 읽은 크기가 같아질 때까지 반복한다.
while(read_size < data_size){
// 1바이트를 읽는다.
temp = input_stream.read();

if(temp == -1) {
// -1이면 소켓을 닫고 예외를 발생시킨다.
input_stream.close();
throw new IOException("End of Stream");
} else {
// -1이 아니면 읽은 값을 배열에 저장한다.
send_data[read_size] = (byte)temp;
// 실제 읽은 크기 값을 증가시킨다.
read_size++;
}
}
}
// 전송할 데이터 크기를 바이트 배열에 알맞게 저장한다.
body_size[0] = (byte)data_size;
body_size[1] = (byte)(data_size >>> 8);
// 이미지 데이터의 전송을 의미하는 메세지를 스트림에 쓴다.
m_sock_out_stream.write(NM_SEND_CAPTURE_IMAGE);
// 이미지 데이터의 크기를 스트림에 쓴다.
m_sock_out_stream.write(body_size);
// 이미지 데이터를 스트림에 쓴다.
m_sock_out_stream.write(send_data, 0, data_size);
// 스트림에 쓰여진 정보를 서버로 전송한다.
m_sock_out_stream.flush();
// 남은 데이터 크기를 조정한다.
remain_file_size -= data_size;
// 현재까지 얼마나 데이터를 전송하였는지 텍스트뷰에 출력해준다.
m_display_string = "send_size : " + full_size + " / " + (full_size - remain_file_size);
m_text_view.post(m_display_run);
}
// 전송이 완료되었음을 서버에 알린다.
body_size[0] = 0;
body_size[1] = 0;
m_sock_out_stream.write(NM_FINISH_IMAGE_DATA);
m_sock_out_stream.write(body_size);
m_sock_out_stream.flush();
// 전송이 완료되었음을 텍스트뷰에 출력해준다.
m_display_string = "Success send Picture!!";
m_text_view.post(m_display_run);
} catch (IOException ie) {
// 예외가 발생하면 텍스트뷰에 예외사항을 출력한다.
m_display_string = ie.toString();
m_text_view.post(m_display_run);
} finally {
// 파일 입력 스트림이 null 이 아니면 스트림을 닫는다.
if(input_stream != null) {
try {
input_stream.close();
} catch(IOException e) {
m_display_string = e.toString();
m_text_view.post(m_display_run);
}
}
}
}
}
4. 서버 프로그램 사용하기
PC 에서 동작하는 서버 프로그램은 MFC로 개발되었으며 이번 강좌의 클라이언트 프로그램을
테스트할 수 있도록 실행 파일의 형태로 제공합니다.
서버 프로그램을 실행하면 아래의 그림처럼 서버 컴퓨터가 사용할 수 있는 IP 주소를 선택하는
다이얼로그가 뜨며 이때 원하는 IP를 선택한 후 확인을 누르면 됩니다.
이후에 나오는 다이얼로그에서는 소켓의 서버 서비스 성공 유무를 콤보 박스에 알려주며, 클라이언트
어플리케이션이 이미지를 전송하면 다음과 같이 수신한 이미지를 출력하게됩니다.
하단의 콤보박스에는 현재 어디에서 클라이언트가 접속하였는지 데이터를 수신하는지 등 소켓 통신에
관한 이벤트를 출력해줍니다. 그리고 다이얼로그 상단의 버튼에는 사진이 저장된 경로가 출력되며,
버튼을 클릭하면 탐색기가 실행되면서 다음의 그림처럼 해당 사진 파일이 있는 폴더로 이동하게
됩니다.
※ 이 예제의 첨부파일에는 서버 프로그램의 실행 파일과 안드로이드용 클라이언트 프로그램을
구성하는 프로젝트 파일이 있습니다.


'프로그램 > 안드로이드 강좌' 카테고리의 다른 글

음성인식을 이용한 발음 테스트 예제  (0) 2012.08.15
음성인식  (0) 2012.08.15
TTS(TextToSpeech)  (0) 2012.08.15
사진촬영 이미지뷰  (0) 2012.08.15
컨텍스트(Context)  (0) 2012.08.15
댓글