[아두이노] [강좌] 50. 와이파이 통신 (4) - WebServer 예제
이번 강좌에서는 WebServer 예제에 대해 알아보자.
우선 웹 서버란?
지난 강좌에서 Server/Client 구조의 흐름에 대해 알아봤었다. 지난 강좌를 참조.
그 순서를 서버의 입장에서 다시 한 번 살펴보자.
① 서버는 클라이언트에서 접속 요청이 오기를 기다린다.
② 클라이언트로부터 접속 요청이 오면 서버는 요청을 수락하고 통신을 준비한다.
③ 클라이언트에서 데이터 요청이 오면 요청 받은 데이터를 전송한다.
④ 데이터 전송이 완료되면 연결을 해제한다.
끝!
클라이언트보다 더 간단하다. 중요한 건 클라이언트의 접속 요청이 있을 때 ②~④번 과정을 진행한다는 것. 그 외에는 그냥 계속 접속을 기다린다.
그럼 예제를 보자. '파일→예제→SparkFun WiFly Shield→WiFly_WebServer' 선택.
'WiFly_WebClient' 예제와 같이 두 개의 탭이 보일 것이다.
'Credentials.h' 탭을 클릭한 후 'passphase' 변수와 'ssid' 변수에 접속할 AP의 SSID와 패스워드를 입력한 후 저장하자. ('Credentials.h'에 대한 내용은 지난 강좌 참조)
다시 'WiFly_WebServer' 탭으로 돌아와서 소스.
WiFly_WebServer.ino |
#include <SPI.h> #include <WiFly.h> #include "Credentials.h" WiFlyServer server(80); void setup() { WiFly.begin(); if (!WiFly.join(ssid, passphrase)) { while (1) { // Hang on failure. } } Serial.begin(9600); Serial.print("IP: "); Serial.println(WiFly.ip());
server.begin(); } void loop() { WiFlyClient client = server.available(); if (client) { boolean current_line_is_blank = true; while (client.connected()) { if (client.available()) { char c = client.read(); if (c == '\n' && current_line_is_blank) { client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println();
for (int i = 0; i < 6; i++) { client.print("analog input "); client.print(i); client.print(" is "); client.print(analogRead(i)); client.println("<br />"); } break; } if (c == '\n') { current_line_is_blank = true; } else if (c != '\r') { current_line_is_blank = false; } } } delay(100); client.stop(); } } |
하나씩 살펴보자. 우선 'WiFly_WebClient' 예제에서와 달리 'WiFlyClient' 객체가 아닌 'WiFlyServer' 객체를 선언하고 있다.
WiFlyServer server(80); |
WiFlyServer의 생성자는 다음과 같다.
WiFlyServer(int port) - 생성자
매개 변수 port - 생성할 서버의 포트 번호 |
WiFlyServer 클래스에는 서버 기능을 만들고 수행하기 위한 함수가 포함되어 있다. 서버 기능을 초기화하고, 클라이언트 접속을 승인하는 등등.
자, 그런데 여기서 포트 번호란 무엇인가?
다음은 네이버 지식 백과에서 설명하고 있는 포트 번호에 대한 내용이다.
포트 번호인터넷의 전송 제어 프로토콜(TCP)이나 사용자 데이터그램 프로토콜(UDP)에서 애플리케이션이 상호 통신을 위해 사용하는 번호. 인터넷상에서 통신은 포트 번호를 사용하도록 되어 있는데 그 범위는 0∼65535이다. 이 중에는 ICANN이 애플리케이션용으로 지정한 기정 포트 번호(well-known port numbers), 회사용의 등록 포트 번호(registered port numbers), 그리고 개별용의 동적 포트 번호(dynamic port numbers)가 있고, 그 범위는 각각 0∼1023, 1024∼49151, 49152∼65535이다. 예를 들면, 5는 원격 작업 입력용, 80은 하이퍼텍스트 전송 규약용, 110은 POP 3용, 1047과 1048은 선사(社)의 NEO Object Request Broker용, 1626은 쇽웨이브용 등이다. [네이버 지식백과]포트 번호 [port number, -番號] (IT용어사전, 한국정보통신기술협회) |
뭐라구..?? TCP, UDP 뭐..??
더 모르겠노! (일베 아님, 대구 사투리임)
그냥 내 맘대로 설명하자면, 네트워크를 사용하는 프로그램의 고유 번호라고 생각하면 된다. IP 주소가 전 네트워크 중의 고유 번호라고 생각한다면, 포트 번호는 한 IP를 가진 시스템 내에서 네트워크를 사용하는 프로그램들의 고유 번호라고 생각하면 쉽다.
왜 포트 번호가 필요한가?
내 컴퓨터에서 생각해보자. 내 컴퓨터는 하나의 IP를 사용하고 있다. 그런데 네트워크를 사용하는 프로그램은 하나가 아니다. Internet Explorer 같은 웹 브라우저도 사용해야 하고, 카카오톡 같은 메신저도 사용하고 있고, N드라이브(최근에 뭐라 이름이 바꼈던데) 같은 클라우드 서비스도 이용하고 있다.
그런데 내 컴퓨터의 IP를 통해 뭔가 데이터가 들어온다. 내 컴퓨터는 그 데이터를 처리해야 하는데 이 데이터가 웹 페이지에서 처리해야 할 데이터인지, 카카오톡에서 처리해야 할 데이터인지를 우선 구분해야 한다. 이 때 사용되는 것이 포트 번호이다.
컴퓨터에서 자주 그리고 널리 사용되는 프로그램들은 지정된 포트 번호를 사용하도록 권고되고 있는데, 그 중 '80'번은 웹 서비스(정확히 말하면 HTTP를 사용하는 웹 서비스)를 위한 번호로 지정되어 있다.
물론 데이터를 보내는 쪽에서도 어느 프로그램으로 보낼 데이터인지를 알려주기 위해 포트 번호를 지정해야 한다. 예를 들어 클라이언트가 포트 번호 '80'으로 지정된 웹 서버에 접속하기 위해서는 서버의 IP주소 또는 도메인 주소와 함께 포트 번호를 함께 지정해줘야 한다는 것.
지난 강좌의 'WiFly_WebClient' 예제에서 'WiFlyClient' 객체를 생성할 때, 생성자의 매개 변수로 "www.google.com"이라는 도메인과 '80'이라는 포트 번호를 사용한 것도 이 때문이다.
당연히 우리가 Internet Explorer나 Chrome 등에서 웹 서버에 접속할 때도 이 포트 번호를 지정해줘야 한다. 우린 지정한 적 없지만, 웹 브라우저는 친절하게도 주소 끝에 포트 번호 '80'을 자동으로 붙여주니까 괜찮아. 다시 말해, 주소창에 "www.google.com"을 입력한 후 엔터를 치면 실제로 요청되는 주소는 "www.google.com:80"이다. 직접 주소 끝에 ":80"을 붙여주면 동일한 결과가 나오지만,N'80' 대신 다른 숫자를 넣는다면 반가운 에러 페이지를 만날 수 있을 것이다.
이 포트 번호는 의무가 아니므로 사용자가 직접 서버를 구축할 때는 변경해도 상관 없다. 위 예제에서 'WiFlyServer' 객체를 만들 때 포트 번호를 변경해도 상관 없다는 말. 단, 그 서버에 접속하기 위해서는 클라이언트 쪽에서도 동일한 포트 번호를 지정해야 한다는 점을 명심하자.
우리는 스마트 폰 등의 웹 브라우저로 접속을 시도할 것이므로 기본 '80'번으로 일단 설정.
하.. 이제 한 줄 했....
그리고는 setup() 함수에서 WiFly 모듈을 초기화한 후 AP에 접속한다.
WiFly.begin(); if (!WiFly.join(ssid, passphrase)) { while (1) { // Hang on failure. } } |
여기서 WiFly 객체는 WiFly 라이브러리 내에 전역으로 선언되어 있는 객체라는 것을 기억하자. 모듈의 전반적인 기능에 대한 함수가 정의되어 있다.
만일 접속에 실패해서 WiFly.join() 함수가 FALSE를 반환한다면 무한 루프로 다음 동작을 막는다. while(1) 함수 앞에 시리얼 메시지를 넣어주면 좋을텐데. 시리얼 초기화는 이 다음이네.
Serial.begin(9600); Serial.print("IP: "); Serial.println(WiFly.ip()); |
시리얼을 초기화 한 후 IP를 출력한다. 앗, 처음 나온 함수!!
const char* WiFly.ip()
매개 변수 없음 반환 값 문자열 형태의 IP 주소 값. 모듈에 할당 된 주소 값이 반환된다. |
모듈이 AP에 성공적으로 접속하게 되면, AP로부터 IP 주소를 부여받는데, 그 IP 주소 값을 문자열 형태로 반환하는 함수이다. Serial.println() 함수로 그 값을 출력하고 있다.
지지난 강좌에서 시리얼 모니터로 커맨드를 주고 받을 때 "get ip"라는 커맨드를 보내면 IP, GW, NM 등등 뜨던거 있잖아요? 그거에요.
이 IP 주소는 클라이언트가 서버에 접속하기 위해 꼭 필요한 정보이므로, 반드시 시리얼 모니터를 이용해 주소 값을 확인해야 한다.
그리고는 'WiFlyServer' 클래스의 begin() 함수를 이용해 서버 기능을 초기화.
server.begin(); |
WiFlyServer.begin()
매개 변수, 반환 값 없음 |
함수 앞에 붙는 객체가 'WiFly'가 아니라 'WiFlyServer' 형태의 객체인 'server'인 것에 유의하자. 서버 기능을 사용하기 위해 자원을 초기화하고, 웹 클라이언트의 접속을 기다리기 위한 모드로 진입한다.
여기까지가 클라이언트의 접속 요청이 있기 전까지 서버가 할 일. 끝!!
클라이언트는 언제 접속 요청하냐고?? 모르지요. 누군가 웹 브라우저 등을 통해 내 IP 주소로 접속을 할 때겠지?
다음 강좌에서 클라이언트가 접속을 요청했을 경우 수행하는 내용(loop() 함수의 내용)과 실제로 업로드 한 후 웹 브라우저에서 서버로 접속(이것이 바로 클라이언트 접속 요청!)해보는 실습을 진행하도록 하겠다. setup() 함수만 했는데 내용이 너무 많아서 끊어 가기.
그럼 다음 강좌에서 만나요, 안녕!!
금방 올께유.