Building a TCP Socket Server for an Autonomous IoT Lamp
- Published on
Junyoung Yang
I built an autonomous IoT lamp for a national software club competition. The device read ambient brightness with a light sensor, adjusted the lamp brightness automatically, and also needed to let an Android app check the current state and control the lamp manually.
At the time, I did not start the project with a deep understanding of backend development or networking. But I needed sensor value changes to be reflected in the app quickly, and commands from the app also had to be delivered to the device without much delay.
This post is an early project record of why I built a TCP socket server instead of using HTTP polling.
Initial Structure
The structure was simple.
- NodeMCU read the light sensor value and controlled the lamp.
- A Node.js server passed messages between the device and the app.
- The Android app displayed the current state and sent manual control commands.
At first, I also considered sending and receiving state through HTTP requests. But then the app would have to periodically ask the server for the current state, and commands from the app would not feel immediate enough.
Problem
What this project needed was closer to "delivering state changes" than "occasionally checking state."
With HTTP polling, I saw several problems.
- The app had to keep asking the server for the current state.
- If the polling interval was long, updates were slow.
- If the polling interval was short, unnecessary requests increased.
- It was awkward for the server to handle both the device and the app as connected participants.
So I chose TCP sockets, where the connection could stay open and messages could be delivered directly.
Approach
I built a simple TCP server with Node.js net module. The server kept a list of connected clients, and when one client sent a message, the server forwarded it to the other clients.
const net = require('net')
const clients = []
const server = net.createServer((socket) => {
clients.push(socket)
socket.on('data', (data) => {
clients.forEach((client) => {
if (client !== socket) {
client.write(data)
}
})
})
socket.on('close', () => {
const index = clients.indexOf(socket)
if (index !== -1) {
clients.splice(index, 1)
}
})
})
server.listen(3000)
Looking at it now, this code lacks many things: authentication, message format, connection cleanup, and failure recovery. Still, for that project, it was closer to the requirement than HTTP polling.
After Applying
Through this work, I first felt the difference between a request-response style and a connection-oriented style.
HTTP was convenient for simple queries or command requests. But for sensor values and device control, where both sides needed to keep their state aligned, keeping the connection open felt more natural.
Another thing I noticed was that even when the server only passes messages, it still has to manage connections.
- It has to track connected clients.
- It has to clean up closed sockets.
- It has to distinguish senders and receivers.
- It has to avoid crashing when an invalid message arrives.
At the time, I understood it only as "socket communication works." But later, this became a starting point for understanding real-time features.
Takeaway
This project does not show my current main stack. It is closer to an early record of directly trying network communication and real-time behavior.
Still, the lesson was clear.
- First decide whether the problem is about periodically checking state or delivering changes as they happen.
- HTTP is familiar, but it is not always the right fit for every communication problem.
- Once a connection is kept open, the server has to manage connection state.
- Even a simple real-time feature has more to handle when failure cases are considered.
If I built it again now, I would also consider WebSocket, MQTT, message format, authentication, and reconnection strategy. But at the time, implementing a TCP socket server helped me first learn the difference between communication styles.