TCP, UDP 서버 공부 #1 Nodejs / C로 TCP, UDP 서버 만들어 보기

Explanation

이번 주제는 TCP/UDP 랍니다.
간단하게 이론적으로 TCP는 3-way-handshaking 라거나 흐름 제어라거나 높은 신뢰성을 보장하고, UDP는 비연결형으로 낮은 신뢰성을 가지고 있지만 TCP보다 빠르다. 뭐 이정도만 알고 있는데요. 직접 TCP / UDP 서버를 만들어서 비교해서 이것저것 실험해보면 둘의 차이를 조금 더 잘 알고 이해할 수 있지 않을까 하는 생각이 들어 한번 해보게 되었답니다.
그리고 오늘 적을 내용은 간단한 메세지를 주고 받는 TCP / UDP 서버를 만들어 본 내용을 적어 보려합니다.

2024.09.08 추가사항

Node.js를 사용해서 TCP와 UDP 서버를 만들고 그 특징을 비교한 포스팅을 새로 하였습니다!!
아래의 링크의 글이 조금 더 도움이 될 거 같아요!
링크: /node-js를-사용한-websocket-tcp-udp-서버를-만들어서-tcp와-udp의-차이를-확/

1. UDP Server - Node

우선은 그나마 가장 익숙한 Nodejs를 이용해서 아주 간단하게 UDP 서버를 만들어보면 아래와 같답니다.

// Server
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
const port = 4000;

server.on('message', (message, info) => {
  console.log(`message: ${message.toString()}`);
  console.log(`from address: ${info.address} port: ${info.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`listening ${address.address}:${address.port}`);
});

server.bind(port);
// Client
const dgram = require('dgram');
const toHost = '0.0.0.0';
const toPort = 4000;
const client = dgram.createSocket('udp4');

const dataIdx = 1;
let sum = '';

for(let i=0; i<9216; i++) {
  sum += dataIdx.toString();
}

const data = Buffer.from(sum);

client.send(data, toPort, toHost, (err) => {
  if (err) console.log(err);
  else console.log('ok');
  client.close();
});

생각해보니까, 저는 한번도 UDP 서버를 사용해본 적이 없더라고. 그래서인지 막연히 UDP 서버를 구성하는게 굉장히 복잡할 줄 알았는데 생각보다 Nodejs에서 구성하는 건 엄청 간단했어요.

어떤걸 테스트해야 TCP와 차이를 확실히 알 수 있을까... 생각해봤는데 마땅히 방법이 안떠오르더라고요. 그나마 생각한 방법은 UDP의 패킷은 헤더 8Bytes를 제외하고 최대 크기가 65,527Bytes니까 65,528Bytes의 데이터를 보내보자!

하지만... 결과는...

Error: send EMSGSIZE 0.0.0.0:4000
    at doSend (dgram.js:679:16)
    at defaultTriggerAsyncIdScope (internal/async_hooks.js:301:12)
    at afterDns (dgram.js:625:5)
    at processTicksAndRejections (internal/process/task_queues.js:81:21) {
  errno: 'EMSGSIZE',
  code: 'EMSGSIZE',
  syscall: 'send',
  address: '0.0.0.0',
  port: 4000
}

그냥 오류가 뜹니다.

하지만 그안에서도 별거 아닐 수 있지만, 새롭게 알게 된 사실이라면!
Mac OS 에서는 UDP 패킷의 데이터를 9,217Bytes만 보내도 오류가 출력된답니다.
왜 그런지 찾아보니 맥 기본 설정값이 그렇게 되어 있다고 하더라고요.
참고 링크. https://github.com/jaegertracing/jaeger-client-node/issues/124#issuecomment-324222456

그리고 고민을 하다가 Javascript는 하이 레벨의 언어이고 NodeJS도 굉장히 최근의 플랫폼이다보니 상대적으로 조금 로우한 레벨의 언어로 해봐야겠다! 라는 생각이 들어서 C 언어로 만들어봐야겠다. 했어요.

2. UDP Server - C

우선 관련해서 회사 동생에게 물어봤는데, 회사 동생이 윤성우님의 "열혈 TCP/IP 소켓 프로그래밍" 책을 빌려줘서 아래의 코드는 해당 책에 나오는 예제를 참고해서 약간 수정한 코드입니다.

// Server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[]) {
  int serv_sock;
  char message[1024] = {0};
  socklen_t clnt_adr_sz;
  int str_len;

  struct sockaddr_in serv_adr, clnt_adr;

  if(argc != 2) {
    printf("usage: %s <port>\n", argv[0]);
    exit(1);
  }

  serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
  if(serv_sock == -1) {
    char err_message[] = "UDP socket creation error";
    error_handling(err_message);
  }

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;
  serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_adr.sin_port = htons(atoi(argv[1]));

  if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) {
    char err_message[] = "bind() error";
    error_handling(err_message);
  }

  while(1) {
    clnt_adr_sz = sizeof(clnt_adr);
    str_len = recvfrom(serv_sock, message, 1024, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
    printf("%s", message);
    sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
  }
  close(serv_sock);
  return 0;
}

void error_handling(char *message) {
  fputs(message, stderr);
  exit(1);
}
// Client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(int argc, char *argv[]) {
  int sock;
  char message[1024] = {0};
  char res_message[1024] = {0};
  int str_len;
  socklen_t adr_sz;

  struct sockaddr_in serv_adr, from_adr;

  if(argc != 3) {
    printf("Usage : %s <IP> <port>\n", argv[0]);
    exit(1);
  }

  sock = socket(PF_INET, SOCK_DGRAM, 0);

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;
  serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
  serv_adr.sin_port=htons(atoi(argv[2]));


  char msg[] = "Request message: ";
  fputs(msg, stdout);
  fgets(message, sizeof(message), stdin);

  sendto(sock, message, sizeof(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
  adr_sz = sizeof(from_adr);
  str_len = recvfrom(sock, res_message, 1024, 0, (struct sockaddr*)&from_adr, &adr_sz);
  printf("Response message: %s", res_message);

  return 0;
}

한줄 한줄 코드에 대해 이야기하면 좋을 것 같지만,

(사실 저도 C 언어를 잘 모른 답니다.)

3. TCP Server - C

그리고 비교를 위해 TCP 서버도 만들어봤는데요.
TCP는 아래의 글의 예제를 참고해서 약간 수정한 코드랍니다.
참고 링크. https://medium.com/from-the-scratch/http-server-what-do-you-need-to-know-to-build-a-simple-http-server-from-scratch-d1ef8945e4fa

// Server
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>

#define PORT 6666

int main(int argc, char const *argv[]) {
  int server_fd, new_socket; long valread;
  struct sockaddr_in address;
  int addrlen = sizeof(address);
  char buffer[1024] = {0};

  if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
    perror("In socket");
    exit(EXIT_FAILURE);
  }

  address.sin_family = AF_INET;
  address.sin_addr.s_addr = INADDR_ANY;
  address.sin_port = htons( PORT );

  memset(address.sin_zero, '\0', sizeof address.sin_zero);

  if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
    perror("In bind");
    exit(EXIT_FAILURE);
  }

  if (listen(server_fd, 10) < 0) {
    perror("In listen");
    exit(EXIT_FAILURE);
  }

  while(1) {
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
      perror("In accept");
      exit(EXIT_FAILURE);
    }
    valread = read(new_socket, buffer, 1024);
    printf("%s", buffer);
    write(new_socket, buffer, strlen(buffer));
  }
  return 0;
  close(new_socket);
}
// Client
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 6666

int main(int argc, char const *argv[])
{
    int sock = 0; long valread;
    struct sockaddr_in serv_addr;
    char message[1024] = {0};
    char buffer[1024] = {0};
    int new_socket;

    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    memset(&serv_addr, '0', sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }

    char msg[] = "Request message: ";
    fputs(msg, stdout);
    fgets(message, sizeof(message), stdin);

    write(sock, message, strlen(message));
    valread = read(sock, buffer, 1024);
    printf("Response message: %s", buffer);

    return 0;
}

짜잔!

(굉장히 간단한 코드지만.. 개인적으로는 오랜만의.. 엄청난 삽질의 결과물이랍니다..)
하지만, 역시나.. 비교 테스트 예제를 만들기가 어려운 거 같아요.

우선은 여기까지하고, 다음에 짬짬히 더 공부해서 다음 포스팅에 TCP/UDP의 비교 예제를 포스팅 하도록 하겠습니다.

혹시, 이 가련한 개발자 중생을 위하여 UDP와 TCP의 차이를 확인할 수 있는 예제를 알려주시는 용자님이 계신다면, 성은이 망극할 것 같사옵니다. 주륵..

끝