Post

NodeJS 소켓 서비스 구현

TCP 서버와 소켓 객체 이해하기

GitBooks NodeJS & MongoDB 위 글에서 이벤트, 스트림, http 모듈에 대한 학습 후 이어서 등장하는 소켓 서비스 구현 관련 내용을 정리하였습니다.

net.Socket 객체

net.Socket 객체를 사용해 소켓 서버와 소켓 객체를 모두 생성할 수 있고, 생성된 서버와 클라이언트는 서로 간에 데이터를 읽고 쓸 수 있습니다. (Socket 객체가 서버와 클라이언트 양쪽에서 사용될 수 있다는 의미입니다. 서버에서는 클라이언트와의 개별 연결을 나타내고, 클라이언트에서는 서버와의 연결을 나타냅니다. Socket 객체 자체가 서버 기능을 수행한다는 뜻은 아닙니다… 원문이 번역체인 것 같네요.)

Socket 객체는 Duplex 스트림을 구성하여 Writable, Readable 스트림의 기능을 모두 제공합니다. 예를 들면, write() 함수로 클라이언트/서버에 쓰기 스트림을 보낼 수 있고, 클라이언트나 서버에서 스트림 데이터를 위한 data 이벤트 핸들러를 사용할 수도 있습니다.

소켓 클라이언트에서 Socket 객체는 net.connect()net.createConnection()이 호출될 때 내부적으로 생성됩니다. 이때 Socket 객체는 서버에 소켓 연결을 표현하기 위해 사용됩니다. Socket 객체를 사용해 연결 모니터링, 서버에 데이터 전송, 서버로부터의 응답 처리 등이 가능합니다.

소켓 서버 상에서 Socket 객체는 클라이언트가 서버에 연결하는 시점이나 연결 이벤트 핸들러에 전달되는 시점에 생성됩니다. 서버에서는 Socket 객체로 클라이언트 연결은 물론, 클라이언트와 주고 받는 데이터도 모니터링합니다.

앞서 말했듯 Socket 객체는 net.connect(), net.createConnection()를 통해 생성(반환)됩니다. 이때, 두 함수는 모두 3가지의 매개변수들을 받을 수 있게 구현되어 있습니다.

  • (options, [connectionListener]) : options에 소켓 연결을 정의한 속성이 들어갑니다.
  • (port, [host], [connectionListener]) : port와 host 값을 직접 전달 인자로 받습니다.
  • (path, [connectionListener]) : Socket 객체 생성 시 사용할 유닉스 소켓의 파일 시스템 위치를 path 전달 인자로 받습니다.
    • unix에서는 모든 것이 이며, 소켓 파일 또한 존재합니다. Unix Domain Socket; UDS - 물리적으로 소켓 파일이 존재하며 파일 시스템에 의해 제어됩니다

소켓 객체가 생성되면 서버에 연결되어 있는 동안 다양한 이벤트를 발생시킵니다. 소켓 서버를 구현함으로서 소켓 연결 수립과 종료, 데이터 읽기와 쓰기 과정에서 방출되는 다양한 이벤트를 처리하는 콜백 함수를 등록할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
const net = require("net");
const client = net.connect({ port: 8107, host: "localhost" }, () => {
  console.log("Client connected");
  client.write("some data \r\n");
});
client.on("data", (data) => {
  console.log(data.toString());
  client.end();
});
client.on("end", () => {
  console.log("Client disconnected");
});
  • net.connect()는 연결되었다는 메시지를 콘솔에 출력하고 특정 문자열 데이터를 서버에 write합니다. 서버 측에서 이 데이터를 입력 스트림으로 받게 되어 "data" 이벤트 핸들러에서 처리할 수 있습니다.
  • 마찬가지로 서버가 송신한 데이터는 클라이언트에서 "data"이벤트 핸들러 콜백 함수에 의해 처리됩니다.
  • client.end()는 연결 종료 프로세스를 시작합니다. 클라이언트에서 서버로 FIN 패킷을 보내고, 클라이언트 자체의 'end' 이벤트를 발생시킵니다. (서버에서도 'end' 이벤트를 보낼 수 있습니다)
    • 서버 측에서는 클라이언트가 end()를 호출하면 'end' 이벤트를 받게 되며, 이는 클라이언트가 연결을 종료하려 한다는 신호입니다.
    • TCP의 전이중(full-duplex) 특성 때문에, 한 쪽에서 end()를 호출해도 즉시 연결이 완전히 끊어지지는 않습니다. 양쪽 모두 연결 종료에 동의해야 완전히 종료됩니다.
1
2
3
4
net.connect({ port: 8107, host: "localhost" }, () => {
  console.log("Client connected");
  client.write("some data \r\n");
});

net.Server 객체

TCP 소켓 서버를 생성하고 데이터 읽기/쓰기가 가능한 연결을 만들기 위해 net.Server 객체를 사용합니다. Server 객체는 여러 Socket 연결을 관리하는 상위 개념이며, 각 클라이언트 연결마다 새로운 Socket 객체를 생성하여 처리합니다.

Server 객체는 net.createServer() 호출시 내부적으로 생성됩니다. 이 객체는 소켓 서버를 나타내고, 연결을 위한 수신 처리 후 서버 연결을 통한 데이터 송신/수신을 가능하게 합니다.

  • net.createServer([options], [connectionListener])
    • options는 소켓 서버 객체 생성시 사용되는 옵션을 지정합니다. (ex. keepAlive, allowHalfOpen, pauseOnConnect, ..)
    • connectionListener 콜백 함수에는 연결된 클라이언트의 Socket 객체가 전달됩니다.
  • 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const net = require("net");

const server = net.createServer((client) => {
  console.log("Client connected");
  client.on("data", function (data) {
    console.log("Client sent " + data.toString());
  });
  client.on("end", function () {
    console.log("Client disconnected");
  });
  client.write("Hello");
});
server.listen(8107, function () {
  console.log("Server listening for connection");
});
  • net.createServer()의 콜백 함수가 클라이언트 Socket 객체를 전달받게 됩니다.
    • 구체적으로 설명하면:
      • 새 클라이언트가 서버에 연결을 시도하고 성공적으로 이루어질 때마다, 해당 콜백 함수가 실행되고, NodeJS는 해당 콜백 함수에 새로운 net.Socket 객체를 생성하여 넘겨줍니다.
      • 이 새로 생성된 net.Socket 객체가 createServer의 콜백 함수에 전달됩니다.
      • 이 객체는 특정 클라이언트와의 연결을 나타내며, 이를 통해 해당 클라이언트와 데이터를 주고받을 수 있습니다.
      • 이름이 client라 헷갈릴 수 있는데, 특정 클라이언트와의 “연결”을 나타내는 소켓 객체인 것입니다.
  • 클라이언트로부터 받은 데이터를 처리하기 위해 client.on("data") 이벤트 핸들러를, 소켓 종료 처리를 위해 client.one("end") 이벤트 핸들러를 사용합니다.
  • server.listen()을 활용해 8107 포트로의 연결을 수신합니다.

기본 TCP 소켓 구현 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const net = require("net");
function getConnection(connName) {
  const client = net.connect({ port: 8107, host: "localhost" }, function () {
    console.log(connName + " Connected: ");
    console.log("   local = %s:%s", this.localAddress, this.localPort);
    console.log("   remote = %s:%s", this.remoteAddress, this.remotePort);
    this.setTimeout(500);
    this.setEncoding("utf8");
    this.on("data", function (data) {
      console.log(connName + " From Server: " + data.toString());
      this.end();
    });
    this.on("end", function () {
      console.log(connName + " Client disconnected");
    });
    this.on("error", function (err) {
      console.log("Socket Error: ", JSON.stringify(err));
    });
    this.on("timeout", function () {
      console.log("Socket Timed Out");
    });
    this.on("close", function () {
      console.log("Socket Closed");
    });
  });
  return client;
}
function writeData(socket, data) {
  const success = !socket.write(data);
  if (!success) {
    (function (socket, data) {
      socket.once("drain", function () {
        writeData(socket, data);
      });
    })(socket, data);
  }
}
const Dwarves = getConnection("Dwarves");
const Elves = getConnection("Elves");
const Hobbits = getConnection("Hobbits");
writeData(Dwarves, "More Axes");
writeData(Elves, "More Arrows");
writeData(Hobbits, "More Pipe Weed");
  • getConnection():
    • net.connect()의 콜백함수에서 this는 connect 내부에서 생성되고 바인딩 된 socket 객체를 의미합니다.
    • setTimeout, setEcoding은 연결 속성을 설정합니다.
  • writeData():
    • 서버에 데이터를 쓰기 위해 write() 명령을 실행하고, 만약 많은 데이터를 써야 하거나 쓰기가 실패한 경우를 위해 drain 이벤트 핸들러를 설정해 줍니다. 버퍼가 비어있다면 다시 쓰기를 수행하도록 재귀적으로 구현합니다. 또한, 클로저를 사용해 함수 실행이 종료되더라도 소켓과 데이터 값이 보존됩니다.

기본 TCP 소켓 서버 구현 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const net = require("net");
const server = net.createServer(function (client) {
  console.log("Client connection: ");
  console.log("   local = %s:%s", client.localAddress, client.localPort);
  console.log("   remote = %s:%s", client.remoteAddress, client.remotePort);
  client.setTimeout(500);
  client.setEncoding("utf8");
  client.on("data", function (data) {
    console.log(
      "Received data from client on port %d: %s",
      client.remotePort,
      data.toString()
    );
    console.log("  Bytes received: " + client.bytesRead);
    writeData(client, "Sending: " + data.toString());
    console.log("  Bytes sent: " + client.bytesWritten);
  });
  client.on("end", function () {
    console.log("Client disconnected");
    server.getConnections(function (err, count) {
      console.log("Remaining Connections: " + count);
    });
  });
  client.on("error", function (err) {
    console.log("Socket Error: ", JSON.stringify(err));
  });
  client.on("timeout", function () {
    console.log("Socket Timed out");
  });
});
server.listen(8107, function () {
  console.log("Server listening: " + JSON.stringify(server.address()));
  server.on("close", function () {
    console.log("Server Terminated");
  });
  server.on("error", function (err) {
    console.log("Server Error: ", JSON.stringify(err));
  });
});
function writeData(socket, data) {
  const success = !socket.write(data);
  if (!success) {
    (function (socket, data) {
      socket.once("drain", function () {
        writeData(socket, data);
      });
    })(socket, data);
  }
}
  • createServer()로 소켓 서버를 생성하고, 연결 핸들러 콜백을 제공합니다.
  • server.listen()으로 포트 수신을 시작하며, 해당 메소드의 매개변수 콜백 핸들러 안에서 서버 객체의 close와 error 이벤트 핸드러를 추가합니다.
  • 서버는 8107번 포트로 연결을 받고, 데이터를 읽은 후 클라이언트에 데이터 문자열을 write합니다.
This post is licensed under CC BY 4.0 by the author.