2018년 7월 17일 화요일

ESP32에서 웹소켓 서버 실행 (Websocket server on ESP32)

ESP32에 웹소켓(web socket) 서버를 만들는 방법을 설명하겠다. 이번에 만드는 웹소켓 서버는 가장 단순한 echo 서버로 클라이언트가 보낸 데이터를 그대로 클라이언트에 되돌려 준다.
또한 작성한 웹소켓 서버를 테스트 하기 위해 간단한 파이썬 클라이언트를 작성했다.

ESP32에서 웹소켓을 사용하기 위해 Websocket 라이브러리를 사용하였다. 이 라이브러리는 일반적으로 아두이노에서 TCP 서버를 만드는데 사용되는 WiFiServer 라이브러리 상에서 동작한다.
이 라이브러리는 ESP8266용으로 만들어 져 아직 공식적으로 ESP32를 지원하지는 않지만 약간만 수정하면 ESP32에서도 잘 동작한다.

라이브러리는 https://github.com/morrissinger/ESP8266-Websocket 에서 다운받을 수 있다.


우측 상단의 녹색 (1)번 버튼 (Clone or download) 을 누르면 풀다운 창이 열리고 그곳에서 Download ZIP을 눌러주면 된다. 그러면 ESP8266-Websocket-master.zip 이라는 파일이 다운로드 될 것이다. 이 파일의 압축을 풀면 ESP8266-Websocket-master 이라는 이름의 폴더가 생기게 된다. .


이 폴더의 이름을 ESP8266-Websocket (뒷쪽의 "-master' 부분을 제거)로 만들어 준 다음 다시 ZIP 파일로 압축한다

아두이노의 메뉴에서 스케치->라이브러리 포함하기->.ZIP 라이브러리 추가 를 선택 해 준다.



앞축파일을 찾아 선택해서 설치 해 준다.


이제 라이브러리 설치가 완료되었다. 윈도우의 경우 디폴트 위치가 C:\Users\[username]\Documents\Arduino\libraries 가 된다. Mac의 경우/Users/[username]/Docunemts/Arduino/libraries

라이브러리 설치는 끝났지만 그냥 컴파일을 하면 에러가 발생하므로 ESP32에서 사용할 수 있도록 약간의 수정을 해 준다.

라이브러리가 설치된 폴더를 보면 위와 같은 파일이 있는데 그 중 MD5.c, MD5.h 파일을 수정해 줘야 한다.


MD5.c 파일을 오픈 한 화면이다. 파일에서 위의 빨간색 부분 (함수 이름)을 다음과 같이 바꿔주면 된다.


MD5.h 도 유사하게 이름을 바꿔주면 된다.



이제 아두이노 메뉴에서 파일->예제->ESP8266-Websocket->WebsocketClient_Demo 를 열어본다.
예제 파일을 컴파일 해 보면 ESP8266WiFi.h 파일을 찾을 수 없다는 에러가 발생한다. 그러므로 첫번째 줄의 #include <ESP8266WiFi.h> 를 #include <WiFi.h> 로 바꿔줘야 한다.
또한 SSID HERE, PASSWORD HERE 부분에 자신이 사용할 공유기 정보를 넣어줘야 한다.



만일 공유기 ssid가 wifitestap 이고 암호가 ap1234567890 라고 하면 다음과 같이 변경해주면 된다.


샘플코드에서는 analogRead 값을 서버로 보내고 있는데 여기서는 테스트 목적이라 그냥 "Hello" 라는 스트링을 보내도록 수정하였다.


이제 ESP32 보드에 업로드 하고 시리얼 모니터로 결과를 확인할 수 있다.


테스트용 에코서버에 연결되면 반복적으로 "Hello"라는 스트링을 보내고 서버에서 리턴 된 문자를 출력한 것이다.

ESP32용 websocket 라이브러리 설치가 확인되었으면 이제 파이선으로 테스트용 웹소켓 클라이언트를 만들어 보겠다.

여기서는 websocket-client 모듈을 사용한다. 파이선에서 모듈 설치는 pip를 사용해 간단히 해결할 수 있다.

$ pip install websocket-client

모듈을 사용하기 위해 먼저 import 해 줘야 한다.

import websocket
import time

import 한 다음 WebSocket 클래스의 오브젝트를 만들어 준다.

ws = websocket.WebSocket()

이제 만들어 진 오브젝트의 connect() 메소드를 사용해 웹소켓 서버에 쉽게 접속할 수 있다. 메소드를 호출할 때 접속할 서버의 주소를 파라미터로 넘겨주면 되는데 웹소켓을 사용해 접속할 것이기 때문에 주소는 "ws://[server ip]/" 형태로 지정해 줘야 한다. 여기서 server ip는 웹서버가 실행되는 ESP32의 ip 주소를 넣어주면 된다.

테스트 웹소켓 서버로 확인해보고 싶으면 ws://echo.websocket.org/ 를 사용하면 된다.

ws.connect("ws://echo.websocket.org/")

이제 서버에 연결되었기 때문에 send 메소드로 원하는 데이터를 보낼 수 있다.

ws.send("Hello world")

recv 메소드로 서버에서 데이터를 받아 올 수 있다.

result = ws.recv()

서버에 연결을 끊을 때는 close 메소드를 사용하면 된다.

result = ws.close()

간단한 테스트 코드는 아래와 같다.

import websocket
import time

ws = websocket.WebSocket()
ws.connect ("ws://echo.websocket.org/")

i=0
nrOfMsgs = 10

while i<nrOfMsgs:
  ws.send("Msg no. "+str(i))
  result = ws.recv()
  print (result)
  i=i+1
  time.sleep(1)
ws.close()


이제 ESP32에 웹소켓 서버를 만들어 보겠다.
먼저 WebSocketServer.h 를 include 해 줘야 한다.

#include <WiFi.h>
#include <WebSocketServer.h>

다음으로 WiFiServer 클래스 오브젝트 인스턴스로 TCP 서버를 만들어 줘야 한다. 웹소켓 서버는 이 서버 위에서 동작한다. 오브젝트 인스턴스의 파라미터는 옵션으로 서버 포트를 지정하는데 사용한다. 디폴트 값이 80이라 따로 지정하지 않아도 상관없지만 여기서는 명시적으로 80번 포트를 지정해 줬다.

WiFiServer server(80);

그리고 WebSocketServer 클래스의 오브젝트 인스턴스도 만들어 줘야 한다.

WebSocketServer webSocketServer;

마지막으로 공유기 접속을 위한 정보를 지정해 줘야 한다.

const char* ssid = "wifitestap";
const char *password = "ap1234567890";

setup() 함수에서는 우선 무선랩에 접속 후 server 인스턴스의 begin 메소드를 호출해 초기화 해 준다.

void setup()
{
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to the WiFi");
  Serial.println(WiFi.localIP());

  server.begin(80);
  delay(100);
}

loop() 함수에서는 클라이언트와의 접속과 데이터 교환을 처리한다.

먼저 incoming connection이 있는가 확인하기 위해 WiFiServer 오브젝트의 available 메소드를 호출한다. available 메소드는 incoming connection이 있으면 WiFiClient 오브젝트를 리턴한다.

WiFiClient client = server.available();

주의할 점은 아직 웹소켓 클라이언트 레벨이 아니고 TCP 클라이언트 레벨에 있다는 것이다.

TCP 클라이언트가 연결되어 있는지 확인하기 위해 위에서 리턴된 WiFiClient 오브젝트의 connected 메소드를 호출한다. 또한 WebSocketServer 오브젝트에 WiFiClient 오브젝트를 파라미터로 해서 handshake 메소드를 호출해서 프로토콜 핸드쉐이크를 처리하도록 한다.

핸드쉐이크가 성공하면 true를 리턴한다. 이 값이 확인 돼야 클라이언트와 실제 통신을 진행할 수가 있게 된다.


  if (client.connected() && webSocketServer.handshake(client)) {
    // 클라이언트와 통신 코드
  }

여기서는 간단한 에코서버를 만드는 것이기 때문에 클라이언트에서 받은 데이터를 저장할 버퍼가 필요한데 메소드는 String 타입을 사용한다.

String data;

클라이언트가 아무 때나 접속을 끊을 수 있기 때문에 while 루프를 돌면서 접속이 끊어지지 않았는가 확인해야 한다.

또한 루프의 각 iteration 사이에 약간의 딜레이가 필요하다. 그렇지 않으면 첫번째 몇 바이트를 받은 다음 데이서 수신을 멈출 수 있기 때문에 매우 중요하다.

그러므로 루프의 구조는 다음과 같다.

if (client.connected() && webSocketServer.handshake(client)) {
  String data;
  while (client.connected()) {
    // handle communication
    delay(10);  // delay needed for receiving data correctly
  }
}

이제 while 루프 안에서 webSocketServer 오브젝트의 getData 메소드를 호출해 데이터를 수신한다. 이 메소드는 파라미터가 없고 수신한 데이터를 String 타입으로 리턴한다.

    data = webSocketServer.getData();

데이터를 수신하고 나면 그 내용을 시리얼 포트로 출력하고 클라이언트로 되돌려 보낸다.

클라이언트로 데이터를 보낼 땐 webSocketServer 오브젝트의 sendData 메소드를 호출한다.

클라이언트가 아무 데이터도 보내지 않는 경우도 있을 수 있으므로 데이터 버퍼의 길이를 먼저 확인한다. 그러므로 클라이언트에 데이터를 돌려보내기 위해서는 버퍼에 있는 데이터 길이가 0보다 커야만 한다.

    if (data.length() > 0) {
      Serial.println(data);
      webSocketServer.sendData(data);
    }


위에서 설명하며 작성한 전체 코드는 다음과 같다.

#include <WiFi.h>
#include <WebSocketServer.h>
WiFiServer server(80);
WebSocketServer webSocketServer;
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";
void setup()
{
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to the WiFi network");
  Serial.println(WiFi.localIP());
  server.begin()
  delay(1000);
}
void loop()
{
  WiFiClient client = server.available();
  
  if (client.connected() && webSocketServer.handshake(client)) {
    String data;
    while (client.connected()) {
      data = webSocketServer.getData();
      if (data.length() > 0) {
        Serial.println(data);
        webSocketServer.sendData(data);
      }
      delay(10);   // Delay needed for receiving data correctly
    }
    Serial.println("The client disconnected");
    delay(100);
  }
  delay(100);
}

작성한 코드를 테스트 하려면 먼저 ESP32에 업로드 해서 실행시킨다. 그럼 ESP32는 무선랜에 접속하고 local IP를 시리얼 포트로 출력해 준다.


여기서 ESP32의 IP address는 192.168.0.14 이다.
이제 파이썬 코드의 주소 부분을 ESP32의 IP address로 바꿔준다.




주소를 변경했으면 python으로 작성한 클라이언트를 실행시켜 주면 된다.


파이썬 클라이언트(우측 화면)에서 Msg no.x 라는 문자열을 웹소켓을 통해 ESP32로 보내면 ESP32(좌측화면)가 수신한 데이터를 시리얼 포트에 출력하고 그 내용을 클라이언트로 되돌려 보내 준다. 파이썬 클라이언트는 서버에서 받은 데이터 내용을 화면에 출력한 것이다.


댓글 1개:

  1. 웹소켓 클라이언트를 2개 이상 접속하면 웹소켓 서버가 The client is disconnected
    메세지를 남기고 끊어지는데 다중 접속이 안되는지요?

    답글삭제