새로운 강의는 이제 https://memi.dev 에서 진행합니다.
memi가 Vue & Firebase로 직접 만든 새로운 사이트를 소개합니다.

바로가기


Communication(통신) 4. 구현-송신

4 분 소요

바이트로 들어오는 데이터를 이제 명확하게 확인 할 수 있는 준비가 되었으니 실제 프로토콜을 이용해 송/수신 을 구현해보겠다.

송신

수신부를 테스트 하려면 송신부를 먼저 만들어야 된다..

제작된 프로토콜 기반으로 바이트(s)를 만들어보겠다..

가공 펑션을 만들어보자

function makebuf(command,data) {
    // 빈 버퍼를 만든다 start + command + length + data.length + checksum + end
    let buf = Buffer.alloc(1 + 1 + 4 + data.length + 1 + 1); 
    buf[0] = 0x09;
    buf[1] = command;
    buf.writeUInt32LE(data.length,2);
    let chksum = 0;
    let i;
    for(i = 0; i < data.length; i++) {
        buf[6+i] = data[i];
        chksum += data[i];        
    }
    buf[6+i] = chksum;
    buf[6+i+1] = 0x90;
    
    return buf;
}

가공 펑션을 사용해 헥사 스트링을 만들어보자

let send = Buffer.from([0x41,0x42,0x43,0x44]);
let buf = makebuf(0x00,send);
let lh = logh(buf);
console.log(lh);

//이렇게 한줄로 줄일 수도 있다.
console.log(logh(makebuf(0x00,Buffer.from("ABCD"))));

//printed
//-> 0x9 0x0 0x4 0x0 0x0 0x0 0x41 0x42 0x43 0x44 0xa 0x90 

프로토콜 대로 체크를 다시 해보면

0x09 = start 0x00 = command 0x04 0x00 0x00 0x00 = length 0x41 0x42 0x43 0x44 = data 0x0A = checksum 0x90 = end

대부분 이해가 가는 내용이지만 바이트 처리를 처음 해본 사람이라면 궁금한 것이 있을 것이다..

바로 length 일 것이다.. 0x00 0x00 0x00 0x04 가 맞아보이는데…

우선 endian이라는 개념을 꼭 알아야만한다. 사실 프로토콜에도 명시를 해줘야 한다..

해당 링크 참조하여 숙지하도록 한다.. https://ko.wikipedia.org/wiki/엔디언

buf.writeUInt32LE(data.length,2) -> buf[2] 부터 LittleEndian(LE)으로 32비트(4바이트)를 쓰겠다는 의미이다..

대부분의 시스템(PC,임베디드장비,모바일)은 리틀엔디안을 쓰고 있는데 뒤집어서 저장을 하는 것이다..

과거 설계 당시 효율 때문이었다는데.. 관심 있는 사람은 더 찾아보면 재밌을 것 같다.

0x12345678 이라는 변수는 피씨에는 0x78 0x56 0x34 0x12로 저장되어 있는 것이다..

c/c++ 확인법

unsigned long l = 0x12345678;
unsigned char c[4];
memcpy(c,&l,4);
String s = "";
for(int i = 0; i < 4; i++) s += "0x" + IntToHex(c[i],2) + " ";
print(s);
//printed
//0x78 0x56 0x34 0x12 

엔디안의 결론은 한바이트 이상의 정수에서만 필요한 개념이다.

수신

수신부는 복잡하다 한바이트 씩 카운팅 해야하므로 제어변수가 들어가야 할 수 있다..

서버는 클라이언트와 달리 늘 멀티 접속을 고려해야한다.. 접속한 클라이언트 마다 컨테이너를 생성해주고 관리를 해줘야한다..

해당 커넥션이 연결되어 있을 때 그 클라이언트만의 변수가 있어야 된다는 것이다..

c++의 경우 커넥션이 이루어질때 sock = new Conn(dclass); 형태로 메모리 할당이 필요하지만.
디스커넥트일때 메모리 해제도 잊지 말아야한다..

node net module은 간단하게 구현된다.. 그것도 싱글쓰레드 논블러킹으로..

원래 서버 수신 코드에 아래와 같이 변수를 추가해본다..

net.createServer(function(sock) { 
    var count = 0;
    console.log('open : ' + sock.remoteAddress);        
    sock.on('close', function() {
        console.log('close : ' + sock.remoteAddress);
    });
    sock.on('error', function(err){
        console.error('error : ' + sock.remoteAddress + ' / ' + err);
    });    
    sock.on('data', function (data) {
        console.log((count++) + ' ' + logh(data));
    });
}).listen(1234);

그리고 여러대의 클라이언트에서 접속하며 데이터를 쏴보면 해당 커넥션의 변수가 각각 증가함을 알수가 있다..

var count 는 각각의 컨테이너에서 잘 지내고 있음을 확인할 수 있는 것이다..

바이트 처리를 위한 최소한의 변수를 만들어 보겠다..

var R = {
    step : 0,
    com : 0, //command
    len : 0,
    d : [] //가변 배열
}

구분편의를 위해 obj형으로 만들었으며 걍 따로 만들어도 된다.

node V8 engine 덕에 저정도면 되는 것이다..
c/c++만 해도 배열 민/맥스로 할지 동적으로 할지 length, count 등 몇개가 더 필요하다.
가비지 컬렉터에게 모든 걸 맡겨놔도 후회 없다.

object 변수에 위에 작성한 tx를 넣어보자

var R = {
    step : 0,
    com : 0, //command
    len : 0,
    d : [],
    tx : (command, data) => {
        let buf = Buffer.alloc(1 + 1 + 4 + data.length + 1 + 1); 
        buf[0] = 0x09;
        buf[1] = command;
        buf.writeUInt32LE(data.length,2);
        let chksum = 0;
        let i;
        for(i = 0; i < data.length; i++) {
            buf[6+i] = data[i];
            chksum += data[i];        
        }
        buf[6+i] = chksum;
        buf[6+i+1] = 0x90;
        sock.write(buf);
    }
}

이제 R.tx() 펑션으로 언제든지 연결된 소켓에 데이터를 보낼 수 있게되었다.

코드 가독성도 나아지고 있다.. :)
tx : (command, data) => { 는 function tx(command, data) { 와 같다.
ecmascript 6 버전 표현력으로 끝에 지저분한 }), 형태를 막아준다.

object 변수에 로깅용 함수도 추가하고 rx를 만들어서 수신할때 한바이트 씩 들어가게 해본다..

net.createServer(function(sock) { 
    var R = {
        step : 0,
        com : 0, //command
        len : 0,
        d : [],
        log : (d) => {
            if(d.length===0) return console.error('data is empty');
            let l = d.length;
            let hs = '';
            for(let i = 0; i < l; i++) {
                let h = d[i].toString(16);
                if(h.length < 2) h = '0' + h;
                hs += '0x' + h + ' ';
            }
            console.log(hs);   
        },
        tx : (command, data) => {
            let buf = Buffer.alloc(1 + 1 + 4 + data.length + 1 + 1); 
            buf[0] = 0x09;
            buf[1] = command;
            buf.writeUInt32LE(data.length,2);
            let chksum = 0;
            let i;
            for(i = 0; i < data.length; i++) {
                buf[6+i] = data[i];
                chksum += data[i];        
            }
            buf[6+i] = chksum;
            buf[6+i+1] = 0x90;
            sock.write(buf);
            R.log(buf);
        },
        rx : (r) => {
            //여기서 한바이트 작업..
        }
    }
    console.log('open : ' + sock.remoteAddress);        
    sock.on('close', function() {
        console.log('close : ' + sock.remoteAddress);
    });
    sock.on('error', function(err){
        console.error('error : ' + sock.remoteAddress + ' / ' + err);
    });    
    sock.on('data', function (data) {
        let l = data.length;
        for(let d = 0; d < l; d++) R.rx(data[d]);
        R.log(data);
    });
}).listen(1234);

이제 좀 가독성이 좋아져서 커넥션 독자적으로 무언 가를 넣고 싶은 마음이 생겼다 :)

결론

이제 R.rx() 코드 작성을 통해 받기만 하면 된다

댓글남기기