웹 서버를 생성하고 관리하기 위한 메인 클래스.
DIYables_ESP32_WebServer(int port = 80)
지정된 포트에서 웹 서버 인스턴스를 생성합니다(기본값: 80).
void begin()
void begin(const char* ssid, const char* password)
웹 서버를 시작하고 들어오는 연결을 수신하기 시작합니다.
오버로딩된 버전들:
begin(): 서버만 시작합니다(와이파이는 애플리케이션에서 별도로 연결해야 합니다)
begin(ssid, password): 한 번의 호출로 WiFi에 연결하고 서버를 시작합니다(레거시 방식)
void setNotFoundHandler(RouteHandler handler)
404 Not Found 응답에 대한 사용자 정의 핸들러를 설정합니다.
WiFi 연결 상태와 IP 주소를 시리얼 모니터에 출력합니다.
들어오는 클라이언트 요청을 처리합니다. 이는 메인 루프에서 반복적으로 호출되어야 합니다.
void on(const String &uri, HTTPMethod method, THandlerFunction fn)
void on(const String &uri, THandlerFunction fn)
특정 URI와 HTTP 메서드에 대한 핸들러 함수를 등록합니다.
매개변수:
uri: URI 경로(예: "/", "/led", "/api/data")
method: HTTP 메서드(GET, POST, PUT, DELETE 등)
fn: 경로에 접근했을 때 실행되는 핸들러 함수
참고: 이 라이브러리는 on() 대신 addRoute() 메서드를 사용합니다. 아래에 올바른 사용법이 나와 있습니다.
void addRoute(const String &uri, RouteHandler handler)
특정 URI에 대한 핸들러 함수를 등록합니다. 이것이 라이브러리에서 실제로 사용되는 메서드입니다.
RouteHandler 함수 형식:
라우트 핸들러는 이 정확한 시그니처를 따라야 합니다:
void handlerFunction(WiFiClient& client, const String& method, const String& request,
const QueryParams& params, const String& jsonData)
매개변수:
client: 응답 전송을 위한 WiFiClient 참조
method: 문자열로 된 HTTP 메서드 ("GET", "POST", 등)
request: 전체 요청 URI
params: 쿼리 매개변수 (QueryParams 객체)
jsonData: POST 요청의 JSON 페이로드 (GET은 비어 있음)
핸들러 구현 예시:
기본 GET 핸들러:
void handleHome(WiFiClient& client, const String& method, const String& request,
const QueryParams& params, const String& jsonData) {
if (method == "GET") {
String response = "<html><body><h1>Hello World</h1></body></html>";
server.sendResponse(client, response.c_str());
}
}
void setup() {
server.addRoute("/", handleHome);
}
JSON API 핸들러 (GET 및 POST):
void handleApiData(WiFiClient& client, const String& method, const String& request,
const QueryParams& params, const String& jsonData) {
if (method == "POST") {
if (jsonData.length() == 0) {
client.println("HTTP/1.1 400 Bad Request");
client.println("Content-Type: application/json");
client.println("Connection: close");
client.println();
client.print("{\"status\":\"error\",\"message\":\"No JSON data received\"}");
return;
}
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, jsonData);
if (error) {
client.println("HTTP/1.1 400 Bad Request");
client.println("Content-Type: application/json");
client.println("Connection: close");
client.println();
client.print("{\"status\":\"error\",\"message\":\"Invalid JSON\"}");
return;
}
const char* key = doc["key"] | "none";
String response = "{\"status\":\"success\",\"received_key\":\"" + String(key) + "\"}";
server.sendResponse(client, response.c_str(), "application/json");
} else if (method == "GET") {
String response = "{\"status\":\"success\",\"message\":\"GET request received\"}";
server.sendResponse(client, response.c_str(), "application/json");
} else {
client.println("HTTP/1.1 405 Method Not Allowed");
client.println("Content-Type: application/json");
client.println("Connection: close");
client.println();
client.print("{\"status\":\"error\",\"message\":\"Method not allowed\"}");
}
}
void setup() {
server.addRoute("/api/data", handleApiData);
}
쿼리 매개변수 핸들러:
void handleLedControl(WiFiClient& client, const String& method, const String& request,
const QueryParams& params, const String& jsonData) {
if (method == "GET") {
String action = params.getValue("action");
if (action == "on") {
digitalWrite(LED_PIN, HIGH);
server.sendResponse(client, "LED turned ON");
} else if (action == "off") {
digitalWrite(LED_PIN, LOW);
server.sendResponse(client, "LED turned OFF");
} else {
client.println("HTTP/1.1 400 Bad Request");
client.println("Content-Type: text/plain");
client.println("Connection: close");
client.println();
client.print("Invalid action. Use ?action=on or ?action=off");
}
}
}
void setup() {
server.addRoute("/led", handleLedControl);
}
void sendResponse(WiFiClient& client, const char* content, const char* contentType = "text/html")
클라이언트에게 HTTP 응답을 보낸다.
매개변수:
사용 예시:
server.sendResponse(client, "<h1>Hello World</h1>");
server.sendResponse(client, "{\"status\":\"ok\"}", "application/json");
server.sendResponse(client, "Success", "text/plain");
void enableAuthentication(const char* username, const char* password, const char* realm = "ESP32 Server")
모든 경로에 대해 HTTP 기본 인증을 활성화합니다. 활성화되면 모든 경로에서 인증이 필요합니다.
매개변수:
username: 인증에 사용할 사용자 이름(최대 31자)
password: 인증에 사용할 비밀번호(최대 31자)
realm: 브라우저에 표시되는 인증 영역(최대 63자, 선택 사항)
사용 예시:
server.enableAuthentication("admin", "password123");
server.enableAuthentication("admin", "secure456", "My ESP32 Device");
void disableAuthentication()
인증을 비활성화하여 모든 경로를 다시 공개적으로 접근 가능하게 만듭니다.
사용 예시:
server.disableAuthentication();
bool isAuthenticationEnabled()
인증이 현재 활성화되어 있으면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
사용 예시:
if (server.isAuthenticationEnabled()) {
Serial.println("Authentication is active");
} else {
Serial.println("All routes are public");
}
void send401(WiFiClient& client)
적절한 WWW-Authenticate 헤더를 포함한 401 Unauthorized 응답을 보냅니다. 인증 실패 시 자동으로 호출되지만, 커스텀 핸들러에서 수동으로 사용할 수도 있습니다.
사용 예시:
server.send401(client);
HTTP 헤더와 상태 코드를 더 자세히 제어하려면:
void sendCustomResponse(WiFiClient& client) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: application/json");
client.println("Access-Control-Allow-Origin: *");
client.println("Connection: close");
client.println();
client.print("{\"custom\":\"response\"}");
}
The QueryParams 객체에는 URL에서 구문 분석된 쿼리 매개변수가 포함되어 있습니다:
struct QueryParams {
int count;
struct {
const char* key;
const char* value;
} params[MAX_PARAMS];
}
void handleWithParams(WiFiClient& client, const String& method, const String& request,
const QueryParams& params, const String& jsonData) {
String unit = "C";
for (int i = 0; i < params.count; i++) {
if (String(params.params[i].key) == "unit") {
unit = params.params[i].value;
break;
}
}
String response = "Unit selected: " + unit;
server.sendResponse(client, response.c_str());
}
더 쉽게 매개변수에 접근하기 위한 헬퍼 함수 만들기:
String getParam(const QueryParams& params, const String& key, const String& defaultValue = "") {
for (int i = 0; i < params.count; i++) {
if (String(params.params[i].key) == key) {
return String(params.params[i].value);
}
}
return defaultValue;
}
bool hasParam(const QueryParams& params, const String& key) {
for (int i = 0; i < params.count; i++) {
if (String(params.params[i].key) == key) {
return true;
}
}
return false;
}
void handleLed(WiFiClient& client, const String& method, const String& request,
const QueryParams& params, const String& jsonData) {
String state = getParam(params, "state", "off");
int brightness = getParam(params, "brightness", "100").toInt();
if (state == "on") {
digitalWrite(LED_PIN, HIGH);
server.sendResponse(client, "LED turned ON with brightness " + String(brightness));
} else {
digitalWrite(LED_PIN, LOW);
server.sendResponse(client, "LED turned OFF");
}
}
WebSocketServer wsServer(81);
net::WebSocketServer의 별칭 - 초보자용으로 간소화됨.
net::WebSocket의 별칭 - 웹소켓 연결을 나타냅니다.
웹소켓 서버를 시작합니다.
WebSocket 이벤트를 처리합니다. 메인 루프에서 이 함수를 호출하십시오.
void onConnection([](WebSocket &ws) {
});
새 웹소켓 연결에 대한 콜백을 설정합니다.
void onMessage([](WebSocket &ws, const WebSocket::DataType dataType, const char *message, uint16_t length) {
});
수신되는 웹소켓 메시지에 대한 콜백을 설정합니다.
void onClose([](WebSocket &ws, const WebSocket::CloseCode code, const char *reason, uint16_t length) {
});
WebSocket 연결 종료 시 호출될 콜백을 설정합니다.
void send(const String &message)
void send(const char *message, size_t length)
WebSocket을 통해 메시지를 보냅니다.
웹소켓 연결을 닫습니다.
void broadcastTXT(const char* payload)
void broadcastTXT(const String& payload)
연결된 모든 웹소켓 클라이언트에게 텍스트 메시지를 전송합니다.
void broadcastBIN(const uint8_t* payload, size_t length)
연결된 모든 WebSocket 클라이언트에 이진 데이터를 브로드캐스트합니다.
size_t connectedClients()
현재 연결되어 있는 WebSocket 클라이언트의 수를 반환합니다.
WebSocket 서버가 연결을 활성적으로 수신 대기 중일 때 true를 반환합니다.
연결 종료 사유에 대한 표준 WebSocket 종료 코드.
void setup() {
Serial.begin(9600);
WiFi.begin(ssid, password);
server.begin();
wsServer.begin();
wsServer.onConnection([](WebSocket &ws) {
Serial.print("Client connected from: ");
Serial.println(ws.getRemoteIP());
ws.send("{\"type\":\"welcome\",\"message\":\"Connected to ESP32\"}");
});
wsServer.onMessage([](WebSocket &ws, const WebSocket::DataType dataType,
const char *message, uint16_t length) {
handleWebSocketMessage(ws, message, length);
});
wsServer.onClose([](WebSocket &ws, const WebSocket::CloseCode code,
const char *reason, uint16_t length) {
Serial.println("Client disconnected");
});
}
void handleWebSocketMessage(WebSocket &ws, const char *message, uint16_t length) {
String msg = String(message);
Serial.println("Received: " + msg);
if (msg.indexOf("\"type\":\"led\"") >= 0) {
if (msg.indexOf("\"action\":\"on\"") >= 0) {
digitalWrite(LED_PIN, HIGH);
ws.send("{\"type\":\"led_status\",\"status\":\"on\"}");
} else if (msg.indexOf("\"action\":\"off\"") >= 0) {
digitalWrite(LED_PIN, LOW);
ws.send("{\"type\":\"led_status\",\"status\":\"off\"}");
}
}
String response = "Echo: " + msg;
ws.send(response.c_str());
}
void loop() {
server.handleClient();
wsServer.loop();
static unsigned long lastBroadcast = 0;
if (millis() - lastBroadcast > 5000) {
if (wsServer.connectedClients() > 0) {
float temperature = getTemperature();
String sensorData = "{";
sensorData += "\"type\":\"sensor\",";
sensorData += "\"temperature\":" + String(temperature, 1) + ",";
sensorData += "\"timestamp\":" + String(millis());
sensorData += "}";
wsServer.broadcastTXT(sensorData);
}
lastBroadcast = millis();
}
}
라이브러리는 표준 HTTP 메서드들을 지원합니다:
HTTP_GET (가져오기)
HTTP_POST (생성/전송)
HTTP_PUT (대체/수정)
HTTP_DELETE (삭제)
HTTP_PATCH (부분 수정)
HTTP_HEAD (헤더 조회)
HTTP_OPTIONS (옵션)
const ws = new WebSocket('ws:
ws.onopen = function(event) {
console.log('Connected to ESP32 WebSocket');
document.getElementById('status').textContent = 'Connected';
};
ws.onmessage = function(event) {
console.log('Received:', event.data);
try {
const data = JSON.parse(event.data);
handleEsp32Message(data);
} catch (e) {
console.log('Text message:', event.data);
}
};
ws.onclose = function(event) {
console.log('Disconnected from ESP32');
document.getElementById('status').textContent = 'Disconnected';
setTimeout(connectWebSocket, 3000);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
function controlLED(action) {
if (ws.readyState === WebSocket.OPEN) {
const command = {
type: 'led',
action: action,
timestamp: Date.now()
};
ws.send(JSON.stringify(command));
}
}
function requestSensorData() {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({type: 'get_sensor'}));
}
}
function handleEsp32Message(data) {
switch(data.type) {
case 'sensor':
updateTemperatureDisplay(data.temperature);
break;
case 'led_status':
updateLEDStatus(data.status);
break;
case 'welcome':
console.log('Welcome message:', data.message);
break;
default:
console.log('Unknown message type:', data);
}
}
wsServer.onMessage([](WebSocket &ws, const WebSocket::DataType dataType,
const char *message, uint16_t length) {
String response = "Echo: " + String(message);
ws.send(response.c_str());
});
void processWebSocketCommand(WebSocket &ws, const String& message) {
if (message.indexOf("\"type\":\"led\"") >= 0) {
if (message.indexOf("\"action\":\"on\"") >= 0) {
digitalWrite(LED_PIN, HIGH);
ws.send("{\"type\":\"led_status\",\"status\":\"on\",\"success\":true}");
} else if (message.indexOf("\"action\":\"off\"") >= 0) {
digitalWrite(LED_PIN, LOW);
ws.send("{\"type\":\"led_status\",\"status\":\"off\",\"success\":true}");
}
} else if (message.indexOf("\"type\":\"get_sensor\"") >= 0) {
float temp = getTemperature();
String response = "{\"type\":\"sensor\",\"temperature\":" + String(temp, 1) + "}";
ws.send(response.c_str());
}
}
void setupHeartbeat() {
static unsigned long lastHeartbeat = 0;
if (millis() - lastHeartbeat > 30000) {
if (wsServer.connectedClients() > 0) {
String heartbeat = "{\"type\":\"heartbeat\",\"timestamp\":" + String(millis()) + "}";
wsServer.broadcastTXT(heartbeat);
}
lastHeartbeat = millis();
}
}
웹소켓 연결 실패
웹소켓 서버 포트(기본값: 81)에 접근 가능한지 확인
ESP32의 IP 주소가 올바르고 도달 가능한지 확인
브라우저 개발자 도구를 사용하여 웹소켓 연결 오류를 확인
수신되지 않는 메시지
시리얼 모니터에서 WebSocket 이벤트 로그를 확인
JSON 메시지 형식이 올바른지 확인
JSON을 사용하기 전에 간단한 텍스트 메시지로 테스트합니다
메시지 길이가 버퍼 한도를 넘지 않는지 확인합니다
메모리 사용량이 높습니다
void debugWebSocket() {
Serial.println("=== WebSocket Debug Info ===");
Serial.println("Connected clients: " + String(wsServer.connectedClients()));
Serial.println("Server listening: " + String(wsServer.isListening() ? "Yes" : "No"));
Serial.println("Free memory: " + String(ESP.getFreeHeap()) + " bytes");
Serial.println("Uptime: " + String(millis() / 1000) + " seconds");
Serial.println("============================");
}
void monitorPerformance() {
static unsigned long lastCheck = 0;
static int messageCount = 0;
messageCount++;
if (millis() - lastCheck > 10000) {
Serial.println("Messages/10s: " + String(messageCount));
Serial.println("Clients: " + String(wsServer.connectedClients()));
messageCount = 0;
lastCheck = millis();
}
}
라이브러리는 자리 표시자 대체가 가능한 HTML 템플릿을 지원합니다:
String response = HTML_TEMPLATE;
response.replace("%TEMPERATURE%", String(temperature));
response.replace("%LED_STATUS%", ledStatus ? "ON" : "OFF");
server.send(200, "text/html", response);
일반적인 자리 표시자:
웹 애플리케이션에 대한 교차 출처 요청 활성화:
server.on("/api/data", HTTP_OPTIONS, []() {
server.sendHeader("Access-Control-Allow-Origin", "*");
server.sendHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
server.sendHeader("Access-Control-Allow-Headers", "Content-Type");
server.send(200);
});
server.sendHeader("Access-Control-Allow-Origin", "*");
server.sendHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
server.sendHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
JSON API 개발 간소화:
void sendJsonResponse(int statusCode, const String& json) {
server.send(statusCode, "application/json", json);
}
void sendError(int statusCode, const String& message) {
String error = "{\"error\":\"" + message + "\"}";
sendJsonResponse(statusCode, error);
}
sendJsonResponse(200, "{\"status\":\"success\",\"data\":\"value\"}");
sendError(400, "Invalid request format");
강력한 입력 유효성 검사 구현:
bool isValidJsonAction(const String& action) {
return (action == "on" || action == "off" || action == "toggle");
}
bool validateRequiredFields(const String& jsonData, const String& field) {
return (jsonData.indexOf("\"" + field + "\":") >= 0);
}
server.on("/api/control", HTTP_POST, []() {
if (!server.hasArg("plain")) {
sendError(400, "JSON body required");
return;
}
String body = server.arg("plain");
if (!validateRequiredFields(body, "action")) {
sendError(400, "Missing required field: action");
return;
}
});
ArduinoJson 라이브러리로 복잡한 JSON 처리를 위한:
#include <ArduinoJson.h>
void handleJsonRequest() {
String requestBody = server.arg("plain");
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, requestBody);
if (error) {
sendError(400, "Invalid JSON format");
return;
}
const char* action = doc["action"] | "none";
int value = doc["value"] | 0;
bool enabled = doc["enabled"] | false;
StaticJsonDocument<200> response;
response["status"] = "success";
response["received_action"] = action;
response["received_value"] = value;
String responseString;
serializeJson(response, responseString);
sendJsonResponse(200, responseString);
}
라이브러리는 기본 404 오류 페이지를 제공합니다. 이를 재정의할 수 있습니다:
server.onNotFound([]() {
server.send(404, "text/html", "<h1>Custom 404 Page</h1>");
});
메모리 관리: 플래시 메모리에 저장된 문자열 리터럴에 대해 F() 매크로를 사용합니다
비차단 코드: 서버 차단을 피하기 위해 핸들러 함수를 가볍게 유지합니다
보안: 입력 매개변수를 검증하고 출력을 정화합니다
성능: 적절한 HTTP 상태 코드와 콘텐츠 타입을 사용합니다
웹소켓: 연결 상태를 적절히 처리하고 재연결 로직을 구현합니다
시리얼 디버깅을 활성화하여 서버 활동을 모니터링하십시오:
void setup() {
Serial.begin(9600);
}
void loop() {
server.handleClient();
if (Serial.available()) {
String command = Serial.readString();
Serial.println("Debug: " + command);
}
}
최대 WebSocket 메시지 크기: 메시지당 1KB
최대 동시 WebSocket 연결 수: 4-6개(가용 메모리에 따라 다름)
메시지 분할: 자동으로 처리되지만 성능에 영향을 줄 수 있음
바이너리 메시지 크기: 사용 가능한 램에 의해 제한
연결 시간 초과: 기본값 60초(구성 가능)