아두이노와 무선인터넷을 통한 간단한 서버 구성 두 번째
네트워크 정보를 제공하는 간단한 서버 만들기
이제 직접, 서버를 구현하는 코드를 작성해 보겠습니다. 라이브러리 예제 없이 직접 코딩하겠지만, 어차피 대부분 중복되는 내용이므로, 예제들에서 복사해서 붙여 써도 됩니다. 클라이언트 요청에 대한 서버 응답에 대해서 설명해야 하기에, 되도록 간단한 프로그램으로 구성하겠습니다.
프로그램 구성은 이전 글에서 본 기본 예제와 다른 점이 없습니다.
- 1번에는 헤더 파일 포함, 네트워크 접속 정보 외 각종 선언들이 들어가고,
- 2번은 setup() 함수로 WiFi 연결을 설정합니다.
- 3번은 loop() 함수이고, Client 접속이 들어올 때마다 웹페이지를 전송합니다.
쉬운 구성을 위해 Client로부터 피드백은 받지 않습니다.
우선, 이전 Client 예제와 마찬가지로 헤더 파일 두 개를 포함합니다. WiFi 모듈이 SPI 통신 방식으로 연결되어 있기 때문에 SPI.h 파일을 include 합니다. 또 무선랜 이용을 위해 WiFi101.h 파일도 include 합니다.
그 다음, 접속하려는 무선랜의 SSID 이름과 해당하는 비밀번호를 설정합니다. 라이브러리 예제에서는 char배열형(char[ ])을 사용했었는데, 이번에는 String Object 형을 사용했습니다. 결과는 어차피 동일합니다.
정수형 변수 status 는 WiFi 모듈의 상태값을 저장하기 위한 변수입니다. WL_IDLE_STATUS로 초기화 했는데, 다른 상태값들은 WiFi101 라이브러리 폴더에서 WiFi.h 헤더파일을 보시면 아래와 같이 확인이 가능합니다.
typedef enum {
WL_NO_SHIELD = 255,
WL_IDLE_STATUS = 0,
WL_NO_SSID_AVAIL,
WL_SCAN_COMPLETED,
WL_CONNECTED,
WL_CONNECT_FAILED,
WL_CONNECTION_LOST,
WL_DISCONNECTED,
WL_AP_LISTENING,
WL_AP_CONNECTED,
WL_AP_FAILED,
WL_PROVISIONING,
WL_PROVISIONING_FAILED
} wl_status_t;
그리고, 이어서 WiFiServer 클래스의 인스턴스인 server를 80 포트로 생성합니다. 이렇게 생성된 인스턴스에 begin() 명령만 내리면 서버가 실행되므로 어려울 건 없습니다. 서버 구현에 필요한 코드가 WiFi101 라이브러리로 깔끔하게 제공되기 때문에 필요한 코드만 갖다 쓰면 됩니다.
void setup() 함수 구현
우선 WiFi모듈의 SPI pin을 설정합니다. Arduino MKR1000 보드라면 필요없고, Adafruit Feather M0 보드라면 넣어 줘야 합니다. 그리고, 모니터링과 디버깅을 위해 Serial 통신을 설정합니다.
WiFi 쉴드의 장착 여부를 체크하는 부분입니다. MKR1000이나 Feather M0 보드는 쉴드가 아니지만, WiFi모듈이 정상적으로 작동하는지 체크할 수 있습니다.
WiFi에 연결합니다. Client예제 부터 SimpleWebServerWiFi 예제까지 동일하게 나오는 부분으로 While() 문에 의해 정상적으로 연결될 때까지 무한루프를 돌게 됩니다.
가장 중요한 코드입니다. server.begin(), 이 코드를 빼 놓으면 클라이언트 요청을 받지 않습니다. 우선, WiFi 연결이 완료되면 메시지를 출력하도록 하고, 바로 Server 서비스를 시작합니다.
그런 후, 접속할 주소를 출력해 줍니다. 이 부분은 이전 예제에서 그대로 복사한 것입니다. IPAddress 는 아이피 주소를 담기 위한 Class이고, WiFi.localIP() 함수를 이용하면 서버의 아이피 주소를 알 수 있습니다.
void loop() 함수 구현
loop() 함수를 구현할 차례입니다. 클라이언트 접속을 기다리다가 요청이 들어오면 웹페이지를 전송하고 클라이언트 접속을 끊습니다. 그리고 다시 클라이언트 접속을 기다리며 무한 대기합니다.
- Client 접속 Listening
- Client Request 분석
- Web page Response
- Client 접속 종료
위와 같은 순서로 처리하는데, loop() 함수의 첫번째 코드가 바로 클라이언트가 접속하는지 리스닝하는 명령입니다.
WiFiClient client = server.available();
server는 WiFiServer Class의 Instance입니다. 위에서 80포트로 생성했었습니다. server.available()
함수는 클라이언트 접속이 들어 오는지 체크합니다. 이를 리스닝이라 표현하는데, loop()
함수 내에 있기 때문에 루프를 돌며 계속해서 대기합니다.
클라이언트 접속이 들어오면 available() 함수는 클라이언트 인스턴스를 리턴합니다. WiFiClient Class 인스턴스인 client가 이를 받아 처리하게 됩니다.
if (client) {
}
만약 클라이어트 접속이 들어 오면, server.available()
함수에 의해 클라이언트가 리턴되고, 이를 client 변수가 받습니다. 그래서, 이 client변수를 체크하면 현재 클라이언트 접속이 발생했는지 알 수 있습니다. 접속이 있을 때만 처리하기 위해 IF문으로 묶어 주었습니다.
client.stop()
클라이언트에게 응답을 완료했으면 접속을 종료합니다. 응답 완료후 바로 접속을 끊기 때문에 Connection: close 방식이며 클라이언트 쪽에서 keep-alive 방식으로 요청했어도 무시됩니다.
이제 IF (client)
문 안쪽을 구현하겠습니다.
다시 while()
문이 나와서 반복합니다. 우선, 접속이 성공한 후에도 계속해서 클라이언트와 연결이 유지되고 있는지 체크해야 하는데, 이 부분을 client.connected()
함수가 처리합니다.
또 이전 글에서 보았듯이 클라이언트로부터 전송된 메시지가 버퍼에 도착하면 한 문자씩 읽어서 처리하고, 이를 글자 수만큼 반복해 주어야 하기 때문에 while()
문안에서 처리하도록 구성합니다.
다시 또 IF문이 나오네요!^^; 이제 클라이언트로부터 전송된 문자가 있다면 하나씩 읽어서 처리하는 부분이 필요합니다. 이 부분은 이미 예전 글에서 살펴본 부분이죠! client.avaliable()
함수는 buffer에 도착한 메시지가 있는지 체크해서 반환해 줍니다.
여기까지 구성된 loop()
함수는,
클라이언트 접속이 있을 때까지 리스닝하다가 접속이 발생하면 client 변수에 해당 클라이언트 인스턴스를 반환하고, if (client)
문에 의해서 접속된 client가 있을 때만 처리하기 위해 if문 안쪽으로 들어 갑니다.
IF문 안쪽에서는, 접속이 발생했으므로, 연결이 유지되는지 체크하며 버퍼에 문자가 들어올때마다 한 문자씩 읽어 내서 처리합니다.
이제, 클라이언트로부터 전송된 메시지를 한 문자씩 읽어서 처리하는 부분을 구성해야 하는 데, 이번 예제는 워낙 간단해서 아무런 처리도 할 필요가 없습니다. 요청 메시지가 어떠하든 시종일관 동일한 웹페이지를 제공하기 때문입니다.
단지, 요청 메시지가 끝났을 때에만 응답 메시지를 전송해야 하기 때문에, 요청 메시지의 끝인지 아닌지만 체크하면 되겠습니다.
if (client.connect(server, 80)) {
Serial.println("connected to server");
client.println("GET /data/2.5/weather?id=1835847&APPID=?????????&units=metric&mode=xml HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
}
이 코드는 이전 글에서 작성했던 OpenWeatherMap 날씨 정보 가져오기 예제의 일부분입니다.
openweathermap 서버에게 날씨 정보를 요청하는 클라이언트 요청 메시지인데, 네 번의 client.println()
함수에 의해 처리합니다. 이 요청 메시지의 마지막 줄을 보면 client.println();
즉, 출력 메시지가 없습니다. 그리고, println()
함수는 줄바꿈("\n")을 자동으로 해주기 때문에, 이 마지막 줄은 아무것도 출력하지 않고 줄만 바꾸게 됩니다. 쉽게 말해 "빈줄"입니다.
클라이언트의 Request는 빈줄로 끝나도록 구성합니다. 그래야만 서버가 메시지 끝을 인식할 수 있습니다. 마찬가지로, 서버를 구현하는 입장에서도, 요청 메시지가 빈줄인지 아닌지 체크하면 그 끝을 확인할 수 있습니다. println()
함수는 자동으로 줄바꿈을 해줍니다. 즉, 줄바꿈을 의미하는 기호인 "\n"문자를 줄 끝에 붙여 줍니다.
보통 줄바꿈이라고 하면 두 가지 동작으로 구성됩니다.
- 그 줄 처음으로 커서를 옮긴다. : Carrage Return, CR, \r
- 다음 줄로 커서를 내린다. : Line Feed, LF, \n
예전 타자기 시절에 가져온 방식이라 합니다. \r, \n은 제어문자라고 하며, 이 동작들을 기호로 나타냅니다.
그림에서 보듯이, 먼저 Carrage Retrun에 의해 2번 자리로 커서가 이동되고, Line Feed로 3번 자리로 이동해서 결국, 다음 줄 처음 자리로 커서를 보내게 됩니다. 제어문자를 이용해 표현하면 바로 위 노란색 부분과 같습니다. 즉, 줄을 바꾸기 위해선 \r, \n 두 가지 제어 문자가 연속으로 와야 하는데, Client Request나 Server Response에서 두 제어문자가 따로 올 일은 없기 때문에, 둘 중 하나만 체크하면 됩니다. 이전 예제에서는 '\n'을 체크하고, '\r'은 무시하도록 했는데, 이번 예제에서도 동일하게 구성하겠습니다.
'\n'만 가지고 Client 요청 메시지를 표현하면 아래와 같습니다.
위 요청 메시지를 Client가 요청할 때는 네 줄에 걸쳐서 전송하지만, 실제로 서버 버퍼에 전송된 메시지는 한 줄이라고 보면 됩니다. 그리고, 세 번째 줄과 네 번째 줄은 아래 그림과 같은 모습으로 버퍼에 도착합니다.
그림에서 보듯이, 네 번째 줄은 아무 출력없이 줄바꿈 기호만 오기 때문에, 연속으로 줄바꿈 기호가 두 개 도착합니다. 이 부분을 체크하면 됩니다. 물론, 원래는 Connection: close\r\n\r\n 이겠지요!
bool newLine = false;
우선, Boolean type 변수를 하나 선언합니다. 줄바꿈 기호가 들어 왔었는지 체크하기 위한 변수로, while문 바깥쪽에서 선언하였습니다. 만일, while문 안쪽에서 선언한다면 루프를 돌 때마다 새로 선언하기 때문에 값을 유지할 수 없겠죠!
위 그림의 노란색 부분은 버퍼에 문자가 도착하면 하나씩 읽어 내어 처리하는 코드입니다. client.read() 함수로 한 문자를 읽고 변수 c에 저장하며 또 모니터링을 위해 Serial 모니터로 출력합니다. 그리고, 줄바꿈 기호인지 체크하는 부분이 이어집니다.
연두색 부분이 줄바꿈 기호를 체크 하는 부분입니다. 읽어 온 문자가 줄바꿈 기호라면 어떤 처리를 할 테고, 아니라면 newLine 변수를 false로 만들어서 바로 전 글자가 줄바꿈 기호가 아니라는 것을 기억하도록 합니다. 이 때, '\r' 문자가 아닐 때만 처리하도록 else if문을 구성하였습니다. 이제 '\r' 문자는 항상 무시하게 됩니다.
마지막으로 노란색 부분이 추가되었습니다.
'\n' 제어문자가 들어올 경우, 바로 전에 '\n'문자가 들어 왔었는지 newLine 변수를 통해 체크합니다. newLine 변수는 바로 전 문자가 '\n'제어문자라면 true, 아니라면 false를 가지고 있습니다.('\r' 제어문자는 무시함.) 따라서, '\n' 문자가 들어 왔는데, newLine 변수가 true라면 연속으로 '\n'문자가 들어온 경우이므로 "빈줄"에 해당합니다.
newLine의 값이 false라면 '\n'문자가 들어 왔으므로 true로 값을 설정합니다.
우선, 여기까지 서버를 실행하고, 클라이언트의 Request를 받아 처리하는 소스를 완성했습니다. 아직 클라이언트쪽에 웹 페이지를 전송하는 부분이 없기 때문에, 클라이언트 쪽 웹브라우저에선 일정 시간 후 에러를 출력하겠지만, 시리얼 모니터를 통해서 절차적으론 제대로 돌아가고 있음을 확인 할 수 있습니다.
#include <SPI.h>
#include <WiFi101.h>
String ssid = "TURTLE";
String pass = "yesican1";
int status = WL_IDLE_STATUS;
WiFiServer server(80);
void setup() {
WiFi.setPins(8, 7, 4, 2);
Serial.begin(9600);
delay(1000); // 시리얼모니터 출력 준비를 위한 대기시간
// check for the presence of the shield:
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
while (true); // don't continue
}
while ( status != WL_CONNECTED) { // 연결될 때까지 계속 실행
Serial.print("Attempting to connect to Network named: ");
Serial.println(ssid); // print the network name (SSID);
// Connect to WPA/WPA2 network. Change this line if using open or WEP network:
status = WiFi.begin(ssid, pass);
// wait 2 seconds for connection:
delay(2000);
}
Serial.println("Network connected.");
server.begin();
IPAddress ip = WiFi.localIP();
Serial.print("To see this page in action, open a browser to http://");
Serial.println(ip);
}
void loop() {
WiFiClient client = server.available();
if (client) {
Serial.println("new client");
bool newLine = false;
while (client.connected()) {
if (client.available()) {
char c = client.read();
Serial.write(c);
if (c == '\n') {
if (newLine) {
Serial.println("Client Request Ended!");
Serial.println("Web page transfer Start!");
} else {
newLine = true;
}
} else if (c != '\r') {
newLine = false;
}
}
}
client.stop();
Serial.println("client disonnected");
}
}
다음 글에서는, 클라이언트에 제공할 웹페이지를 구성하고, 테스트를 위해 네트워크 정보 등을 웹페이지로 출력하는 소스를 살펴 보겠습니다.
이상입니다.