티스토리 뷰



이 자료들은 팁스소프트에서 제공하는 [ 알짜배기 ] 프로그램을 이용하면 더 편리하게 볼수 있습니다.
* 알짜배기 프로그램 받기 - 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
스트림(Stream)의 이해 - 바이트 스트림 : http://www.tipssoft.com/bulletin/tb.php/FAQ/928
소켓(Socket) 통신의 이해 - 클라이언트 : http://www.tipssoft.com/bulletin/tb.php/FAQ/930
이 자료에서 사용하는 서버프로그램은 MFC 로 개발된 것으로 화면을 캡쳐하여 JPG 로 압축한 후
클라이언트 프로그램에 전송하는 역할을 합니다. 이 강좌에서는 이러한 서버 프로그램과는 직접적인
관련이 없기때문에 클라이언트를 테스트할 수 있도록 서버 프로그램은 실행파일만 제공하도록
하겠습니다.
서버에서 클라이언트로 전송하는 이미지 정보를 클라이언트 프로그램 화면에 출력하기 위해서는
이미지를 파일에 입출력하는 방법과 이미지를 출력하는 방법을 추가적으로 알아야 합니다.
따라서 이번 강좌에서는 파일 입출력과 이미지 출력을 위한 이미지뷰를 사용하는 간단한 방법을 설명할
것이며, 구현할 클라이언트의 주요 기능이 무엇이고 어떻게 구현했는지를 설명하도록 하겠습니다.
1. 파일 입출력하기
프로그램에서 안드로이드 장치 내부의 메모리에 파일을 저장할 때, 특별한 파일 접근 모드를
명시하지 않으면 기본적으로 다른 프로그램이나 사용자가 해당 모드로 생성한 파일에 접근하지
못하게 하게됩니다. 그리고 해당 프로그램이 삭제되면 그 프로그램이 생성했던 파일들은 함께
지워지게 됩니다.
이처럼 데이터를 프로그램 전용 파일로 내장 메모리에 저장하는 방법은 아래와 같으며 파일에
데이터를 읽고 쓸 때에는 각 용도에 맞는 파일 스트림을 얻어서 해당 스트림을 이용해야 합니다.
1.1 파일 저장하기
파일을 저장할 때에는 FileOutputStream 을 반환하는 openFileOutput 메소드를 사용합니다.
FileOutputStream openFileOutput(String name, int mode);
첫번째 매개 인자인 name 에 파일명을 명시하며, mode 에 명시된 값에 따라 생성되는 파일의
성격이나 이어쓰기 유무가 달라집니다. mode 에 설정할 수 있는 상수 값과 그 의미는 다음과
같습니다.
MODE_PRIVATE : 기본 모드로 파일이 없으면 private 모드로 파일을 생성합니다.
MODE_WORLD_READABLE : 다른 프로그램이 읽을 수 있는 모드로 파일을 생성합니다.
MODE_WORLD_WRITEABLE : 다른 프로그램이 쓸 수 있는 모드로 파일을 생성합니다.
MODE_APPEND : 파일이 존재하는 경우 맨뒤에 데이터를 추가하여 저장합니다.
이 상수값은 Context 추상 클래스에 선언되어 있기 때문에 Context.MODE_PRIVATE 와 같은
형태로 사용해야합니다.
openFileOutput 메소드를 이용해 얻은 파일 출력용 스트림을 다음과 같이 이용하면 파일에
데이터를 쓸 수 있습니다.
// 파일에 쓸 데이터
String data = "Tipssoft.com";
// 해당 프로그램만 접근할 수 있는 test_data.bin 파일명을 가진 파일을 생성하고,
// 생성한 파일에 데이터를 쓸 수 있는 스트림을 반환한다.
FileOutputStream output_stream = openFileOutput("test_data.bin", Context.MODE_PRIVATE);
// 문자열 데이터를 바이트로 변환하여 파일에 저장한다.
output_stream.write(data.getBytes());
// 스트림을 닫는다.
output_stream.close();
1.2 파일 읽어오기
파일을 읽을 때에는 FileInputStream 을 반환하는 openFileInput 메소드를 사용합니다.
FileInputStream openFileInput(String name);
이 함수의 name 매개 인자에 읽고자 하는 파일명을 명시하여 해당 파일을 읽을 수 있는 파일
입력용 스트림을 얻을 수 있으며 아래와 같은 방법으로 데이터를 읽을 수 있습니다.
// test_data.bin 이라는 파일명을 가진 파일을 읽을 수 있는 스트림을 반환한다.
FileInputStream input_stream = openFileInput("test_data.bin");
// 파일 스트림이 읽을 수 있는 파일내의 데이터 크기를 얻어서 바이트 배열을 할당한다.
byte[] data = new byte[input_stream.available()];
// 파일을 읽는다.
input_stream.read(data);
// 바이트 형의 데이터를 String 형으로 변환한다.
String str_data = new String(data);
2. 이미지 출력하기
문자열을 출력할 때 텍스트뷰를 사용하듯이 이미지를 출력할때에는 이미지뷰를 사용해야합니다.
이미지뷰를 사용하기 위해서는 리소스 파일에 다음과 같은 XML 코드를 추가하여야 합니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ImageView
android:id="@+id/id_image"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
2.1 프로젝트 폴더에 추가한 이미지 출력하기
위의 코드는 이미지를 출력할 수 있는 컨트롤을 생성한 것이며, 프로그램이 실행되자마자 원하는
이미지를 출력하고자 하는 경우 프로젝트폴더\res\drawable 폴더 내에 이미지 파일을
넣은 후 다음과 같이 XML 코드를 구성하면 됩니다.
<ImageView
android:id="@+id/id_image"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/image1"
/>
ImageView 요소의 src 속성을 추가하면 drawable 폴더 내에있는 image1 이라는 이미지 파일을
이미지뷰에 출력시킵니다. 이때 image1 은 프로젝트폴더가 컴파일되면서 발견한 이미지 파일의
이름을 R 클래스에 리소스 ID 로 선언한 것이므로 확장자를 빼고, 파일명만 써주어야 합니다.
2.2 openFileOutput 메소드로 파일에 저장한 이미지 출력하기
프로그램에서 이미지를 사용할 때 구현 당시 출력할 이미지가 정해져서 프로젝트 파일 내에
포함될 수 있지만 프로그램이 실행되면서 출력할 이미지가 생기는 경우도 있습니다. 이런 경우
openFileOutput 메소드를 이용하여 이미지 파일로 저장한 후에 다시 입력 스트림으로 읽어 들여서
출력할 수 있습니다.
// id_image 라는 ID 의 ImageView 컨트롤을 얻는다.
ImageView image = (ImageView) findViewById(R.id.id_image);
// image.jpg 파일을 읽을 수 있는 파일 스트림을 얻는다.
FileInputStream is = openFileInput("image.jpg");
// 파일의 이미지 데이터를 출력에 사용하는 Drawable 객체로 얻기위해
// Drawable 클래스의 static 메소드인 createFromStream 메소드를 사용한다.
Drawable draw_image = Drawable.createFromStream(is, "image.jpg");
// 제대로 변환된 Drawable 객체를 ImageView 에 그리도록 한다.
if(draw_image != null) image.setImageDrawable(draw_image);
// 파일 입력 스트림을 닫는다.
is.close();
3. 서버 - 클라이언트의 진행 흐름
PC 에서 서버 프로그램을 실행시켜 서버 시비스가 시작되면 안드로이드용 클라이언트 프로그램을
이용하여 서버에 접속할 수 있습니다. 서버가 전송하는 캡쳐 이미지는 클라이언트가 요청할 때에만
전송하는 것이기때문에 지속적으로 접속 상태를 유지할 필요가 없으므로 클라이언트가 캡쳐 이미지를
전송받기 전에 서버에 접속을 하고, 이미지 전송이 완료되면 서버와의 연결이 해제됩니다.
아래의 그림은 서버와 클라이언트 간의 통신 흐름에 대한 것입니다.
클라이언트는 캡쳐 이미지를 요청하기 전에 서버에 연결을 시도(Connect)하고, 연결이 받아들여지면
(Accept) 서버와 클라이언트 간에는 다음과 같은 메세지들을 주고받게 됩니다.
- REQUEST_CAPTURE_IMAGE
서버에 캡쳐 이미지를 요청합니다. 이 메세지는 이미지를 요청한다는 것 외에 다른 부가적인
정보가 필요하지 않으므로 Boby 없이 Header 만 구성되어 전송됩니다.
- SEND_IMAGE_SIZE
이미지 데이터를 한번에 모두 전송할 수 없기때문에 전체 이미지 크기에 대한 정보를 서버가
클라이언트에게 알려줍니다. 데이터 크기는 int 형 변수에 저장되기때문에 32비트 운영체제를
기준으로 데이터를 송수신하면 Body Size 는 4Byte 입니다.
- SEND_CAPTURE_IMAGE
캡쳐 이미지 정보를 일정 크기로 분할하여 전송할때 사용하는 메세지입니다. 한번에 이미지
데이터를 모두 전송할 수 없기때문에 이 메세지와 함께 분할된 이미지 정보가 반복적으로
클라이언트 프로그램에 전송됩니다. Body 에는 이미지 데이터가 존재하며 Body 의 크기는
Header 의 BodySize 에 저장됩니다.
4. 클라이언트 프로그램의 주요 기능
이번 강좌에서 구현하는 예제는 캡쳐한 서버 화면을 클라이언트 프로그램에서 출력하는 프로그램
입니다. 서버 - 클라이언트 모델을 사용하기 때문에 소켓통신을 이용하여 네트워크를 사용하며,
클라이언트 프로그램에서 서버의 IP를 입력하고, Reflesh 버튼을 누르면 서버에 설정된 영역과 화질과
크기로 JPG 이미지가 생성되어 클라이언트로 전송됩니다.
안드로이드에서 수행되는 프로그램은 화면이 회전되거나 잠시 다른 화면으로 전환될 경우 액티비티가
중지되었다가 다시 시작되는 문제 등 여러가지 예외가 많이 발생하기 때문에 서버와의 연결 상태를
이미지가 수신되는 동안으로 제한 하였습니다.
즉, Reflesh 버튼을 누르면 서버와 연결을 시도하고, 연결이 되면 이미지를 수신하며 수신이 완료되면
바로 연결을 해제합니다. 다음은 실행 화면을 나타내는 그림입니다.
① IP 주소를 입력하는 EditText 입니다. Reflesh 버튼을 누를때마다 서버와의 연결과 해제가
이루어지기 때문에 IP 는 항상 입력되어 있어야 합니다.
② 프로그램의 상황을 알려주는 TextView 입니다. 서버의 이미지 전송상황이나 예외 발생 상황을
출력해줍니다.
< 이미지를 수신중인 경우 >
③ 서버의 캡쳐 이미지를 요청할 때 사용하는 Button 입니다. 버튼을 누르면 서버에 연결되며,
캡쳐된 이미지가 클라이언트로 전송되고, 모두 전송되면 연결이 해제됩니다.
④ 수신한 이미지를 출력하는 ImageView 입니다. 수신이 완료되면 수신한 이미지를 출력합니다.
5. 클라이언트 프로그램 구성하기
클라이언트 프로그램에서 가장 중점적인 부분은 소켓 통신부분이므로 소켓 통신에 대한 이해가
부족하다면 글의 서두에 링크한 이전 강좌를 먼저 보시기 바랍니다. 서버와의 연결과 해제 부분은
이전 자료와 거의 비슷하므로, 데이터를 수신하는 것과 이미지를 출력하는 부분만 설명하도록
하겠습니다.
5.1 데이터의 수신
이미지 데이터는 크기가 크기때문에 서버에서 데이터를 송신할 때 작은 크기로 나누어서 보내야
합니다. 나누어 송신되는 이미지 데이터는 미리 열어둔 파일 출력 스트림을 이용하여 파일에
저장됩니다.
public void onReadStream() throws IOException, Exception
{
byte msg_id;
byte[] size = new byte[2];
// 이미지를 저장할 파일 출력 스트림
FileOutputStream save_file = null;
// 쓰레드에 인터럽트가 설정되지 않은 경우
while (!m_client_thread.isInterrupted()) {
// 메세지 번호를 읽는다.
msg_id = (byte)m_in_stream.read();
// 크기 정보가 저장된 2바이트를 읽는다.
if(m_in_stream.read(size) == 2){
// 안드로이드의 기반인 리눅스와 윈도우즈는 Byte Ordering 이 다르기때문에
// 2바이트의 데이터를 송수신할 때 1바이트씩 값을 바꿔주어야 한다.
int data_size = size[1];
data_size = (data_size & 0x000000FF) << 8;
data_size = data_size | (size[0] & 0xFF);

// 전체 이미지의 크기 정보에 관한 메세지
if(msg_id == NM_SEND_IMAGE_SIZE){
byte file_size[] = new byte[data_size];

// 데이터 크기만큼 데이터를 읽는다.
if(m_in_stream.read(file_size) == data_size){
// 수신한 이미지 사이즈를 0으로 초기화한다.
m_recv_image_size = 0;
// Byte Ordering 을 고려하여 바이트 값을 int 값으로 변환한다.
// 0x12345678 을 윈도우즈 시스템에서 전송했다면 수신시 0x78563412 로 저장되므로
// 순서를 역으로 변환해야한다.
int temp_value = (int)file_size[3];
m_total_image_size = (temp_value & 0x000000FF) << 24;
temp_value = (int)file_size[2];
m_total_image_size |= (temp_value & 0x000000FF) << 16;
temp_value = (int)file_size[1];
m_total_image_size |= (temp_value & 0x000000FF) << 8;
temp_value = (int)file_size[0];
m_total_image_size |= (temp_value & 0x000000FF);
// 파일 출력 스트림을 연다.
save_file = openFileOutput("capture_image.jpg", Context.MODE_PRIVATE);
}
// 서버가 이미지 데이터를 전송한다는 메세지
} else if(msg_id == NM_SEND_CAPTURE_IMAGE) {
int temp = 0, recv_size = 0;

// 데이터 크기만큼 바이트 배열을 생성한다.
byte image_data[] = new byte[data_size];
// 이미지 정보를 읽는다.
temp = m_in_stream.read(image_data);

// 반환값이 0 이하인 경우
if(temp < 0) {
// End of Stream 이므로 정상적으로 쓰레드와 소켓이 종료될 수 있도록
// 파일 출력 스트림을 닫고, 예외를 발생시킨다.
save_file.close();
throw new IOException("End of Stream");

} else if(temp < data_size) { // 반환값이 데이터 크기보다 작은 경우
recv_size = temp;

// 데이터를 모두 읽지 못한것이므로, 읽지 못한만큼 추가적으로 읽는다.
while(recv_size < data_size){
// 1바이트를 읽는다.
temp = m_in_stream.read();

if(temp == -1) {
// End Of Stream 이면 스트림을 닫고, 예외를 발생시킨다.
save_file.close();
throw new IOException("End of Stream");
} else {
// 정상적인 값을 읽은 경우 배열의 읽지 못한 부분에 저장한다.
image_data[recv_size] = (byte)temp;
recv_size++;
}
}
}
// 읽은 데이터를 파일에 쓴다.
save_file.write(image_data);

// 수신한 이미지 사이즈를 조정한다.
m_recv_image_size += data_size;
// 수신 정도를 텍스트뷰에 출력한다.
m_display_string = "recv packet. " + m_recv_image_size + " / "
+ m_total_image_size;
m_text_view.post(m_display_run);

// 서버가 이미지 전송을 완료했다는 메세지
} else if(msg_id == NM_FINISH_IMAGE_DATA) {
// 출력 스트림을 닫는다.
save_file.close();
// 이미지를 이미지뷰에 출력한다.
m_text_view.post(m_image_view_run);
// 성공 메세지를 텍스트뷰에 출력한다.
m_display_string = "Success Reflesh";
m_text_view.post(m_display_run);
// 쓰레드의 종료를 위해 인터럽트를 건다.
m_client_thread.interrupt();

// 서버에서 에러가 발생했다는 메세지
} else if(msg_id == NM_SERVER_SIZE_ERROR) {
// 서버의 에러 발생 여부를 텍스트뷰에 출력한다.
m_display_string = "Please Check Server!! And Change Custom Screen Size!";
m_text_view.post(m_display_run);

// 인터럽트를 걸어 쓰레드와 소켓통신을 종료한다.
m_client_thread.interrupt();
}
}
}
}
5.2 이미지 출력하기
소켓 통신으로 서버에서 이미지를 모두 수신한 후에 이미지를 출력해야하지만 소켓 통신을 할 때
사용하는 쓰레드에서는 ImageView 에 직접적인 접근을 하는 것이 불가능하기 때문에 Runnable
객체를 생성하여 run() 메소드에 ImageView 에 관한 코드를 정의합니다.
// 저장한 이미지 정보를 이미지뷰에 출력하는 루틴을 정의
Runnable m_image_view_run = new Runnable() {
public void run()
{
try{
// id_image 라는 ID 를 가진 이미지뷰를 얻어온다.
ImageView image = (ImageView) findViewById(R.id.id_image);
// capture_image.jpg 파일을 읽을 수 있는 파일 스트림을 얻는다.
FileInputStream is = openFileInput("capture_image.jpg");

// 파일 스트림을 이용해 Drawable 객체를 만들고, 해당 객체로 이미지를
// 이미지뷰에 출력한다.
image.setImageDrawable(Drawable.createFromStream(is, "capture_image.jpg"));

// 파일 스트림을 닫는다.
is.close();
} catch (IOException ie) {
m_text_view.setText(ie.toString());
}
}
};
6. 서버 프로그램 사용하기
PC 에서 동작하는 서버 프로그램은 MFC로 개발되었으며 이번 강좌의 클라이언트 프로그램을
테스트할 수 있도록 실행 파일의 형태로 제공합니다.
서버 프로그램을 실행하면 아래의 그림처럼 서버 컴퓨터가 사용할 수 있는 IP 주소를 선택하는
다이얼로그가 뜨며 이때 원하는 IP를 선택한 후 확인을 누르면 됩니다.
이후에 나오는 아래와 같은 다이얼로그에서는 화면 캡쳐와 전송에 관한 여러가지 항목을 설정해주어야
하며 클라이언트가 캡쳐 화면을 요청하면 설정된 값에 맞게 화면을 캡쳐하여 클라이언트에 전송합니다.
① 클라이언트로 전송되는 이미지는 한번에 보내지 못하고, 일정 크기로 나누어 보내게 되는데 이 때
여러개의 패킷을 한번에 보내지 않고, 패킷과 패킷 사이에 10 ms ~ 100 ms 정도의 대기 시간을
줍니다. 한번에 데이터를 보내면 클라이언트가 데이터 수신에 부담을 가지기 때문에 네트워크
상황과 PC 혹은 안드로이드 단말기의 성능에 따라서 적당한 대기 시간을 설정해주는 것이 좋습니다.
② 캡쳐한 이미지의 선명도를 설정합니다. 선명도가 높아질수록 이미지 데이터의 크기는 커집니다.
③ 캡쳐할 화면을 설정합니다. 전체 화면이 아닌 부분 화면을 캡쳐하고자 하는 경우, 사용자 지정 화면
라디오 버튼을 선택한 후에 우측의 에디트 박스에 좌표를 입력해줍니다. 좌측부터 left, top, right,
bottom 이므로 좌표 설정을 올바르게 하시기 바랍니다.
④ 캡쳐할 영역이 큰 경우 이미지의 크기를 절반으로 축소하는 것이 데이터 크기를 줄이는 것에
도움이 되기때문에 캡쳐 영역이 작은 영역인 경우 100% 크기로 전송하고, 아닌 경우 절반 크기로
축소하여 전송합니다.
⑤ 서버 프로그램의 상태나 클라이언트 프로그램의 접속 여부를 표시합니다.
※ 이 예제의 첨부파일에는 서버 프로그램 실행 파일과 안드로이드용 클라이언트 프로그램을
구성하는 프로젝트 파일이 있습니다.

 

댓글