본문 바로가기

공부/LinuxServer

[Linux Server] Node.js로 TCP 에코 서버 구현하기 (+ 방화벽 설정, 포트 열기)

원격 서버는 AWS lightsail을 이용해서 구현하였습니다.

OS는 cent os 입니다.

목차

1. Node.js 설치 2. 포트 열기 3. 서버 코드 작성
4. 클라이언트 코드 작성
5. 서버 설정

1. Node.js 설치

1-1. epel 설치

1-1-1. epel 설치 확인

yum repolist
또는
yum repolist | grep epel

yum 명령어를 통해 epel 저장소가 있는지 확인한다. (빨간줄로 표시된 부분이 있어야 설치된 상태)

없을 경우 epel 설치를 진행한다.

1-1-2. epel 설치

sudo yum -y install epel-release

epel을 설치한다.

관리자 권한이 필요하므로 sudo 명령어를 사용한다.

-y 옵션은 설치중 나오는 모든 질문에 자동으로 y를 입력하게 한다.

1-2. node.js 설치

1-2-1. node.js 설치

sudo yum -y install nodejs

Node.JS 를 설치한다.

이때 의존성에 의해 libuv, npm등이 자동으로 설치된다.

1-2-2. node.js 및 npm 버전 확인

node -v; rpm -qa | grep node
node -v; rpm -qa | grep npm

2. 포트 열기

2-1. 리눅스 포트 열기

2-1-1. 열려있는 포트 확인

sudo iptables -L

현재 열려있는 포트 확인

2-1-2. 포트 개방하기

sudo iptables -I INPUT -p tcp --dport 3000 -j ACCEPT
// (-I : 대문자 i)

// sudo iptables -I INPUT -p tcp --dport 3000:3010 -j ACCEPT
// 3000~3010 포트 전부 오픈

3000위치에 개방할 포트의 번호를 넣으면 해당 포트가 개방된다.

※ 방화벽 재부팅시 포트 설정이 초기화 되므로 방화벽 사용시 아래의 명령어로 설정 한다.

// 방화벽 포트 열기
sudo firewall-cmd --permanent --zone=public --add-port=3306/tcp

// 방화벽 포트 제거
sudo firewall-cmd --permanent --zone=public --remove-port=3306/tcp

// 방화벽 재시작
sudo firewall-cmd --reload

 

2-2. AWS 포트 허용

AWS 인스턴스 관리 페이지에서 방화벽에 원하는 포트번호를 추가해준다.

3. 서버 코드 작성

var net_server = require('net');

var index = 0;

var server = net_server.createServer(function(client) {

    client.id = index++;

    console.log('-----------------------');

    console.log('Client connection: ' + client.id + ":" + client.remotePort);

    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) {
    	/* data의 타입은 string이다.
        */

        console.log('Received data from client on port %d: %s', client.remotePort, data.toString());

        writeData(client, 'Sending: ' + data.toString() + ' to ' +client.id.toString());

        console.log('  Bytes sent: ' + client.bytesWritten);

        console.log('  sent : ' + client.id);

    });

    client.on('error', function(err) {

        console.log('Socket Error: ', JSON.stringify(err));

    });

    client.on('timeout', function() {

        console.log('Socket Timed out ' + client.id + ':' + client.remotePort);

    });

    client.on('end', function() {

        console.log('Client disconnected ' + client.id + ':' + client.remotePort);

    });

    client.on('close', function() {
        console.log('socket close ' + client.id + ':' + client.remotePort);
        server.getConnections(function(error,count){
                console.log('number of connection = '+ count);
        });

    });
});

// 서버가 3000번 포트를 통해 접속을 받는다.
server.listen(3000, function() {

    console.log('Server listening: ' + JSON.stringify(server.address()));
    
	console.log('Server max connections : ' + server.maxConnections);

    server.on('close', function(){

        console.log('Server Terminated');

    });

    server.on('error', function(err){

        console.log('Server Error: ', JSON.stringify(err));

    });
});

function writeData(socket, data){

  var success = socket.write(data);

  if (!success){

      console.log("Client Send Fail : "+data);
      // drain 으로 관리가능

  }
}

vi 등의 텍스트 편집기를 통해 서버 코드 구현. 확장자는 .js(자바스크립트)

4. 클라이언트 코드 작성 (window, 유니티, C#)

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class NodeJsTCPPingpong : MonoBehaviour
{
    private const string IP = "요거는.대상.서버의.IP주소";
    private const int PORT = 3000;

    private const int MAX_BUFFER_SIZE = 65535;

    private int m_nRecvBufferSize = 0;

    private byte[] _sendBuffer;
    private byte[] _rcvBuffer;
    private Socket _socket;

    private Thread _recvListen;

    private int recvSize;

    public string msg;

    public void Awake()
    {
        _rcvBuffer = new byte[MAX_BUFFER_SIZE];
        _sendBuffer = new byte[MAX_BUFFER_SIZE];

        var sendMsg = "echo : ";
        _sendBuffer = Encoding.UTF8.GetBytes(sendMsg);
    }

    public async Task<Socket> Connect()
    {
        try
        {
            if (_socket != null && _socket.Connected) return _socket;
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _socket.SendBufferSize = _socket.ReceiveBufferSize = short.MaxValue;
            _socket.NoDelay = true;
            _socket.ReceiveTimeout = 1000;
            await _socket.ConnectAsync(IP, PORT);

            if (_socket != null)
            {
                if (_recvListen == null)
                {
                    _recvListen = new Thread(DoReceive) {IsBackground = true};
                }

                if (!_recvListen.IsAlive)
                    _recvListen.Start();
            }
        }
        catch (Exception e)
        {
            Debug.LogError(e);
            throw;
        }

        return _socket;
    }

    private void DoReceive()
    {
        do
        {
            recvSize = 0;
            try
            {
                _socket.Send(_sendBuffer, 0, _sendBuffer.Length, SocketFlags.None);///

                int size = MAX_BUFFER_SIZE - m_nRecvBufferSize;
                if (size > 0)
                {
                    recvSize = _socket.Receive(_rcvBuffer, 0, MAX_BUFFER_SIZE, SocketFlags.None);///
                    m_nRecvBufferSize += recvSize;
                    msg = Encoding.Default.GetString(_rcvBuffer);
                }
                else
                {
                    recvSize = 0;
                }
            }
            catch (ObjectDisposedException)
            {
                Debug.LogError("tcp close");
            }
            catch (IOException ioex)
            {
                Debug.LogError("WSACancelBlockCall");
                Debug.LogError(ioex);
            }
            catch (Exception ex)
            {
                Debug.LogError(ex);
            }

            Debug.Log("DataReceive: " + recvSize);
            Debug.Log(msg);

        } while (recvSize > 0);

        Debug.Log("DataReceiveComplete");
        
        _socket.Close();
    }

    private void OnDestroy()
    {
        if (_socket != null)
        {
            try
            {
                _socket.Shutdown(SocketShutdown.Both);
                _socket.Disconnect(false);
            }
            catch (Exception e)
            {
                Debug.LogError(e);
                throw;
            }
            
            _socket.Close();
            Debug.Log("Socket close");
            _socket = null;
        }

        if (_recvListen!=null && _recvListen.IsAlive)
        {
            _recvListen.Abort();
            Debug.Log("Thread abort");
        }
    }
}

서버의 대상 클라이언트가 유니티였기 때문에 C#으로 작성.

5. 소캣 연결 관련 설정

socket.setTimeout(int time); // 타임아웃 시간 설정
socket.setEncoding('utf8');	// 인코딩 설정
socket.setKeepAlive(bool isKeepAlive); // TCP 연결 유지 (ACK 패킷을 주기적으로 보낸다)
socket.setKeepAlive(bool isKeepAlive, int sendTerm); // 패킷 송신 주기 설정 가능