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



: 통신 프로그램의 절반 이상은 웹과 관련있다. 웹은 다음의 특징을 가진다.

- URI(Uniform Resource Identifier)와 URL(Uniform Resource Locator)로 리소스(자원)의 위치를 지정한다.

- HTTP 프로토콜을 사용한다.

- 리소스는 음악,영상과 같은 미디어뿐만 아니라 관련된 리소스를 처리할 수 있는 HTML 언어로 만들어진 문서를 지원한다.


※ HTTP 프로토콜

: 클라이언트와 서버 사이에 이루어지는 요청과 응답으로 구성된 프로토콜이다. 보통 클라이언트에서 서버에 HTML 문서나 데이터를 요청하면 서버는 요구한 정보를 응답한다. 이와 같이 HTTP 프로토콜은 크게 요청 메시지와 응답 메시지가 존재한다. 요청 메시지는 요청 라인, 헤더, 엔티티로 구성되고 응답 메시지는 상태 라인, 헤더, 언티티로 구성된다. 각 구성 요소의 구분은 CRLF로 한다. 

- 요청 라인, 상태 라인 : 메시지를 하나의 문장으로 요약한 것이다. 만약 서버에 동영상을 저장한다고 하면 요청 메시지는 수백만 바이트 이상이 될 것이다. 서버는 이 요청 메시지를 다 읽고 판단하기에는 시간이 너무 오래 걸린다. 따라서 하나의 문장으로 요약된 요청/상태 라인을 통해 시간을 절약할 수 있다.

- 헤더 : 엔티티 속성, 서버/클라이언트 현재 상태, 요청하는 리소스 정보를 갖는다. 예로 클라이언트가 요청한 리소스가 다른 서버에 있다면, 해당 서버 주소를 헤더에 넣어 알려준다. 또한 클라이언트 리소스 캐시 정보가 오래되었다면 해당 리소스를 바꾸라는 정보를 헤더에 넣어 알려준다.

- 엔티티 : 리소스를 말한다. 


1. HTTP 요청 라인

: "Request-Method SP Request-URI SP HTTP-Version" 으로 표현한다. 예로 "GET http://www.google.com/index.html HTTP/1.1"와 같이 표현한다. SP는 Space 약자로 공백을 나타낸다. 요청 라인은 무엇을 달라 또는 데이터를 어디에 입력하라 등의 명령어로 구성된다. 먼저 Request-Method에 대해서 알아보자. 

- OPTIONS : 현재 웹서버에서 지원하는 메서드의 종류를 알려준다. 응답 메시지 내 "Allow" 헤더에 지원하는 메서드 종류를 나타낸다.

- GET : 요청 URI가 가리키는 리소스를 요청(얻고자)할 때 사용한다. 요청 메시지의 크기는 제한되지만 특정 형식에 맞춰 있어 POST 메서드나 PUT 메서드보다 서버 처리 속도가 빠르다.

- HEAD : GET과 비슷하지만 요청 내용은 상태라인과 헤더 정보로 국한된다. 주로 클라이언트 캐시 정보 갱신 목적으로 이용한다.

- POST : 서버에게 필요한 데이터를 제공하고 서버에서 작업한 결과값을 반환받을 때 사용한다. 새로운 리소스를 업로드하거나 수정할 때 사용한다. 요청 메시지의 크기 제한은 없고 GET에 비해 처리 속도가 떨어진다. 

- PUT : 요청 URI를 사용하여 서버내 리소스로 저장하겠다는 의미로 사용된다. URI가 기존에 존재하면 수정하고 없으면 새롭게 입력한다.

- DELETE : 서버에서 URI가 가리키는 리소스를 삭제하라는 의미로 사용된다.

모든 HTTP 지원 서버는 GET과 POST 메서드를 지원해야 한다. 만약 응답 메시지로 501 오류 코드를 받으면 메서드는 지원 예정이거나 아직 구현되지 않는 메서드를 뜻한다. 400 오류 코드를 받으면 해당 메서드는 지원하지 않는다는 뜻이다. 요청 URI는 절대 URI와 상대 URI로 나뉜다. 상대 URI를 사용한다면 Host 헤더에 호스트와 포트번호를 표기해야 한다. 


2. HTTP 상태 라인

: "HTTP 버전 SP 상태 코드 SP 이유 문구"로 표현한다. 예로 "HTTP/1.0 200 OK", "HTTP/1.0 404 Not Found" 가 있다. 상태 코드는 요청 메시지에 대한 응답 결과를 세 자리의 정수로 표현한다. 이유 문구는 상태 코드에 대한 보충 설명이다.

3. HTTP 헤더
: 요청 메시지에서 헤더는 일반 헤더, 요청 헤더, 엔티티 헤더로 구성된다. 응답 메시지에서는 일반 헤더, 응답 헤더, 엔티티 헤더로 구성된다.

- 일반 헤더
a. Cache-Control : 캐시 정보를 나타낸다. 캐시는 오청과 응답 횟수를 줄여 네트워크 부하를 감소시키기 위해 사용한다. 캐시는 만기와 유효라는 방법을 사용한다. 만기는 클라이언트에 저장된 문서의 만기일(Expires 헤더)를 확인하고 만기일이 지나지 않았으면 캐시 문서를 활용한다. 유효는 클라이언트 내 캐시와 서버에서 제공하는 문서에 대해 여러 가지 정보를 비교 분석해 유효성을 판단한다. 만약 정보가 다르다면 다시 서버에 문서를 요청하고 서버는 새로운 문서 코드 200을 사용해 전송한다. 만약 변화가 없으면 상태 코드 304로 응답한다.
b. Connection : 요청, 응답 후 연결을 유지할 지를 나타낸다. 연결을 유지한다면 3웨이 핸드 쉐이킹 작업없이 지속적으로 서로 통신한다. Connection 필드가 가질 수 있는 값은 close와 Keep-Alive가 있다. close는 디폴트로 요청, 응답 후 연결을 끊겠다는 것이다. Keep-Alive는 연결을 지속적으로 하겠다는 뜻이고 별도로 Keep-Alive 필드에 지속 시간(timeout)과 횟수(max)를 표시해야 한다.
c. Date : 메시지를 생성한 시간을 나타낸다.
d. Transfer-Encoding : 메시지의 본문에서 사용한 인코딩 방식을 나타낸다. 만약 필드값이 chunked라면 Content-Length를 제공하지 않는다. 대신 엔티티 본문은 사이즈1 CRLF 데이터1 사이즈2 CRLF 데이터2 ...... 사이즈N CRLF 데이터N 0 trailer 형식을 갖는다. 0은 데이터 마지막을 나타낸다. trailer는 chunk 데이터에 대한 HTTP 메시지의 정보를 갖는다. chunked를 사용하면 보안상 좋고 서로 다른 데이터를 동시에 보낼 수 있다.
e. Upgrade : 클라이언트와 서버가 통신 도중에 프로토콜을 변경할 때 사용한다.
f. Warning : 경고의 내용을 알린다.

- 요청 헤더 : 응답 메시지에서 받고자 하는 정보를 담고 있다.
a. Accept : 클라이언트 자신이 처리할 수 있는 응답 메시지의 형태를 지정한다. 예로 "Accept: audio/*;q=0.2 , audio/basic"을 보자. ","는 각 요소를 구분해준다. 따라서 "audo/*;q=0.2"와 "audio/basic"으로 구성되 있다. q는 품질등급을 나타낸다. 전체적인 의미를 살펴보면 응답 메시지의 형태로 audio/basic을 원하지만 품질이 80% 이하이면 audio/*와 같은 형태로 전송해달라는 의미이다.
b. Accept-Charset : 클라이언트가 처리할 수 있는 문자셋을 나타낸다. 참고로 HTTP 포준 문자셋은 'ISO-8859-1'이기 때문에 모든 클라이언트는 해당 문자셋을 지원해야 한다.
c. Accept-Encoding : 서버에게 응답 메시지 엔티티를 압축된 문서로 요청할 때 사용한다.
d. Accept-Language : 응답 메시지를 읽을 때 사용할 수 있는 언어를 표현한다.
e. Authorization : 사용자 인증을 필요로 하는 경우, 사용자의 이름, 암호를 전달하기 위해 사용한다.
f. From : 문서를 요청한 사용자의 e-mail 주소를 전달한다.
g. If-Modified-Since : 요청 URI 리소스가 특정 일자 이후 변경되었다면 변경된 리소르를 전달해달라고 요청할 때 사용한다.
h. User-Agent : 클라이언트 SW 정보를 전달한다. 대표적으로 웹 브라우저 종류가 있다.

- 응답 헤더 : 상태 라인만으로 전달하기 어려운 정보를 클라이언트에게 전달하기 위해 사용한다.
a. Age : 프록시 서버에서 사용하고 원본 서버가 클라이언트에 전달할 데이터를 만든 시간을 나타낸다.
b. Location : 리다이렉트된 문서의 주소를 나타낸다.
c. Retry-After : 상태 코드 503과 함께 데이터 요청을 필드에 나타낸 시간이 경과된 이후 다시 시도하라는 의미로 사용한다. 서버에 작업량이 많거나 유지보수를 하고 있을 때 사용한다.
d. Server : 원본 서버에 사용되고 있는 SW를 나타낸다.
e. WWW-Authenticate : 사용자 인증을 클라이언트에 요구할 때 사용한다. 상태코드 401(Unauthorized) 일 때 필요한 인증사항을 담아 응답 메시지로 전달한다.

- 엔티티 헤더 : 엔티티 본문에 대한 정보를 정의한다.
a. Allow : 지원 가능한 메소드(GET, HEAD, PUT 등)을 알리기 위해 사용한다.
b. Content-Encoding : 엔티티 본문이 압축 또는 암호화되 있을 경우, 해당 인코딩 형식을 수신측에 전달하기 위해 사용한다.
c. Content-Type : 엔티티 본문의 미디어 타입(예로 text/html)이나 문자셋을 표시한다.
d. Content-Length : 엔티티 본문의 길이를 바이트로 나타낸다.
e. Content-Location : 사용자가 요구한 리소스가 어디에 위치해 있는지 알려준다. 리소스의 위치가 변경되어 실제 위치한 URI를 알려줄 때 사용한다.
f. Etag : 웹서버에서 사용하는 리소스 버전을 알려준다.

참고로 엔티티 본문 존재 여부는 Content-Length 또는 Transfer-Encoding 데어 필드의 포함 여부에 따라 결정된다.

ex) 네이버에 데이터를 요청하는 소켓 채널 프로그램
public class HttpGet {

	public static void main(String[] args) {
		SocketChannel server = null;
		WritableByteChannel destination;
		
		String host = "www.naver.com";
		try{
			int port = 80;
			String path = "/";
			
			SocketAddress serverAddress = new InetSocketAddress(host,port);
			server = SocketChannel.open(serverAddress);
			server.configureBlocking(true);
			
			//상대 URI 형식으로 요청 라인 표현
			String request = "GET " +path+" HTTP/1.1\r\n" + "Host: "+host
					+ "\r\n" + "\r\n";
			
			CharBuffer requestChars = CharBuffer.wrap(request);
			
			//HTTP 프로토콜이 지원하는 표준 문자셋으로 변환
			Charset charset = Charset.forName("ISO-8859-1");
			ByteBuffer requestBytes = charset.encode(requestChars);
			
			//HTTP 요청 메시지를 서버에 전송
			server.write(requestBytes);
			
			//화면에 출력할 채널 생성
			destination = Channels.newChannel(System.out);
			ByteBuffer data = ByteBuffer.allocateDirect(32*1024);
			
			//HTTP 응답 메시지의 헤더 정보를 화면에 출력할 지 결정
			boolean Headers = true;
			int responseCode = -1;
			
			while(server.read(data) != -1){
				data.flip();
				
				//상태 코드를 읽는 작업을 수행한다. 상태 라인은 HTTP/1.1과 한 바이트 공백 뒤에
				//상태 코드 3바이트가 나오므로 다음과 같이 한다.
				if(responseCode == -1){
					responseCode = 100*(data.get(9) - '0') +
							10*(data.get(10) - '0') +
							1*(data.get(11) - '0');
				}
				//정상 여부를 확인한다.
				if(responseCode<200 || responseCode >=300)
					System.out.println("HTTP Error: "+responseCode);
				
				//헤더 출력 여부를 결정한다.
				if(!Headers){
					try{
						//헤더와 엔티티는 '\r\n\r\n'으로 구분한다.
						for(;;){
							if((data.get()==13) && (data.get() == 10) &&
									(data.get() == 13) && (data.get() == 10))
								break;
						}
					}catch(BufferUnderflowException e){
						//엔티티를 만나지 못한 상태에서 마지막 위치 limit를 만났을 경우
						//3바이트 앞으로 이동한 후 읽었던 바이트를 전부 삭제한다.
						data.position(data.position()-3);
						data.compact();
						continue;
					}
				}
				
				//버퍼로 부터 데이터를 읽어 화면에 출력한다.
				while(data.hasRemaining())
					destination.write(data);
				data.clear();
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				if(server!=null&& server.isOpen())
					server.close();
			}catch(IOException e){}
		}
	}
}


※ URL, URI

- URL : 인터넷 상에 존재하는 서버의 리소스(파일, 디렉토리) 위치를 가리킨다

- URN : 위치와 무관한 리소스 이름을 나타낸다.

- URI : 인터넷 상에 존재하는 리소스의 위치와 이름을 가리킨다.

: URI 는 URL(위치)와 URN(이름)을 사용한 것이다. 따라서 서버의 리소스는 URI를 통해 접근한다. 하지만 URI과 URL는 인터넷 상에서 별도 구분없이 사용된다. URL는 서버를 가리키기에 사용자 입장에서 알기 쉽지만 이름까지 일일이 알 수는 없기 때문이다. 그래서 대부분 URL을 사용하여 사이트를 지정하면 디폴트 웹 페이지(index.html)과 함께 조건에 맞는 리소스를 제공한다. 


- URL 구성요소 : scheme://<user>:<password>@<host>:<port>/<url-path>?query_string#fragment

1. 스킴(Scheme) : 스키마라고도 부르며 'http', 'ftp' 같은 프로토콜을 나타낸다.

2. 호스트(Host) : 서버를 가리킨다. ex)www.google.com

3. 포트번호(Port) : TCP 포트 번호를 나타낸다. HTTP의 경우 80이 된다.

*안드로이드는 호스트와 포트를 합하여 기관(Authority)로 표현한다.

4. 경로(Path) : 구체적인 자원의 위치를 나타낸다.

5. 쿼리(Query) : 서버에 전달되는 매개변수를 말한다. "name=value&name1=value1" 로 표현한다. 쿼리에 사용하는 value는 US-ASCII(영문자)를 사용해야 되는 보장이 없어 시스템에서 지원하는 Charset으로 인코딩 작업이 필요하다.

6. 프래그먼트(fragment) : 리소스의 상세 부분을 의미한다. 예로 URL이 이북을 가리킨다면 몇 번째 페이지를 언급할 때 사용한다.

* user,password는 보안 문제로 생략한다.


- URL 메서드

1. Object getContent(Class[] type) : URL이 가리키는 위치 내 리소스를 가져와 Object 객체로 반환한다. 매개변수의 타입이 리소스 객체 타입과 다르면 null을 반환한다.

2. Object getContent() : 요청한 리소스를 가져와 Object 객체로 반환한다. 

3. URLConnection openConnection() : URL이 가리키는 호스트의 리소스 주소를 갖는 URLConnection 객체를 반환한다.

4. InputStream openStream() : URL이 가리키는 리소스의 응답 메시지 엔티티를 수신하고 InputStream 객체로 반환한다. HTTP의 헤더나 프로토콜 정보는 제공하지 않는다.


ex)

public class URLReader {
	public static void main(String[] args) throws Exception{
		URL google = new URL("http://www.naver.com/");
		BufferedReader in = new BufferedReader(new InputStreamReader(google.openStream()));
		String line;
		while((line = in.readLine())!=null)
			System.out.println(line);
		in.close();
	}
}
위 코드는 첫 번째 예제에서 Headers=false로 실행한 것과 똑같은 결과가 나온다. HTTP 요청 메시지를 구현하는 것보다 URL 클래스를 사용하면 엔티티를 간단하게 얻을 수 있는 장점이 있고 코드가 많이 간결해지는 장점을 가진다.



+ Recent posts