sonumb

Golang 서버와 C 클라이언트 간 통신 본문

개발자 이야기/Go

Golang 서버와 C 클라이언트 간 통신

sonumb 2021. 10. 12. 11:42

1. 개요

C로 개발된 애플리케이션과 go로 작성된 애플리케이션이 TCP 통신해야 할 필요가 있다.
대개 C로 작성된 기존 서버에 새롭게 작성된 go 애플리케이션이 접속하는 형태의 프로젝트가 많을 것이다.
허나 아래에 제안될 코드는 C 클라이언트가 Go로 작성된 서버로 접속하는 형태다.

C 클라이언트는 연결정보 (pid, 실행인자 등등)을 구성하여 접속한 go 서버에 전송, go 서버는 수신된 연결정보를 출력하는 코드다.
(바이트 오더링이 생략될 수 있도록 Handshake 프로토콜을 주고 받았다고 가정한다.)

2. 코드

Golang 서버

아래 코드는, 메시지를 읽어 헤더를 파싱하고 헤더 타입에 따라 로직을 실행한다.
현재 MSG_OPCODE_CONNECT만 있으므로 이를 처리한다.
ConnHandler1()함수는 전송한 패킷 전체를 읽은 뒤, 헤더/바디를 분리하여 처리하고,
ConnHandler2()함수는 헤더 크기 만큼 소켓버퍼에서 읽고, 헤더에 기입된 바디크기만큼 다시 읽은 뒤, 내용을 처리하는 루틴이다.
(ioutil.ReadAll() 함수 내부적으로 버퍼를 할당하며, 성능 하락포인트라고 여겨진다.)

// TCP Server
package main

/*
#include <string.h>
#include <stdint.h>

#define MSG_OPCODE_CONNECT 1

typedef struct _MsgHeader {
    uint8_t OpCode;
    uint8_t reserved1[3];
    uint32_t seqno;
    int32_t  body_len;
} MsgHeader;

typedef struct _MsgBodyConnect {
  int32_t  pid;
  int8_t   proc_type;
  int8_t   reserved1;
  int8_t   do_relaunch;
  int32_t  cmd_line_len;
  char     cmd_line[1];
} MsgBodyConnect;

char get_cmd_line( MsgBodyConnect * connect, int offset )
{
  return connect->cmd_line[offset];
}

typedef union _MsgBody {
    MsgBodyConnect     connect;
    // 다른 메시지들 선언
} MsgBody;

typedef struct _Msg {
  MsgHeader hdr;
  MsgBody   body;
} Msg;

const int32_t MsgHeaderSize = sizeof( MsgHeader );
const int32_t MsgBodySize = sizeof( MsgBody );
const int32_t MsgSize = sizeof( Msg );

Msg msg;
*/
import "C"

import (
    "io"
    "log"
    "net"
    "fmt"
    _ "unsafe"
    "strings"
    "runtime"
    "io/ioutil"
    "bufio"
    "unsafe"
)

func ConnHandler1(conn net.Conn) {
    recvBuf := make([]byte, 8096)

    // 전체 읽기
    for {
        n, err := conn.Read(recvBuf)
        if nil != err {
            if io.EOF == err {
                log.Println(err)
                return
            }
            log.Println(err)
            return
        }

        if n > 0 {
            // 바이트 버퍼에서 구조체로 컨버팅
            goUnsafePtr := C.CBytes(recvBuf)
            var msg *C.struct__Msg = (*C.struct__Msg)(goUnsafePtr)

            // 메시지 타입에 따라 처리
            if msg.hdr.OpCode == C.MSG_OPCODE_CONNECT {
                var body * C.struct__MsgBodyConnect = (* C.struct__MsgBodyConnect)(unsafe.Pointer(&msg.body))

                // get cmd line
                cmdLineLen := int(body.cmd_line_len)
                cmdLine := make([]byte, body.cmd_line_len )
                for i := 0 ; i < cmdLineLen ; i++ {
                    ch := C.get_cmd_line(body, C.int(i))
                    cmdLine[i] = byte(ch)
                }

                delim := "\n"
                cmdLineStr := string(cmdLine)
                argv := strings.Split(cmdLineStr, delim)

                fmt.Println( ">> pid:", body.pid )
                fmt.Println( ">>   cmd line(len:",body.cmd_line_len, "):", argv )
                fmt.Println( body )

                //  수신한 argv 를 통해 재기동하는 루틴
                //go func() {
                //  sttyArgs := syscall.ProcAttr{
                //      "",
                //    []string{},
                //      []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
                //      nil,
                //  }
                //  syscall.ForkExec(argv[0], argv, &sttyArgs )
                //}()
            }
        }
    }
}

func ConnHandler2(conn net.Conn) {
    recvHeaderBuffer := make([]byte, C.MsgHeaderSize)
    var sockReader *bufio.Reader = bufio.NewReader(conn)

    // 헤더 읽기
    for {
        n, err := conn.Read(recvHeaderBuffer)
        if nil != err {
            if io.EOF == err {
                log.Println(err)
                return
            }
            log.Println(err)
            return
        }

        if n > 0 {
            // 바이트 버퍼에서 헤더로 컨버팅
            goUnsafePtr := C.CBytes(recvHeaderBuffer) // = C.struct__Msg(recvBuf)
            var msgHdr *C.struct__MsgHeader = (*C.struct__MsgHeader)(goUnsafePtr) // = C.struct__Msg(recvBuf)

            // 헤더에 기입된 바디크기만큼 읽기
            bodyReadBuf, err := ioutil.ReadAll(io.LimitReader(sockReader, int64(msgHdr.body_len)))
            if err != nil {
                log.Println(err)
                return
            }

            // 메시지 타입에 따라 처리
            if msgHdr.OpCode == C.MSG_OPCODE_CONNECT {
                bodyUnsafePtr := C.CBytes(bodyReadBuf)
                var body * C.struct__MsgBodyConnect = (*C.struct__MsgBodyConnect)(bodyUnsafePtr)

                // get cmd line
                cmdLineLen := int(body.cmd_line_len)
                cmdLine := make([]byte, body.cmd_line_len )
                for i := 0 ; i < cmdLineLen ; i++ {
                    ch := C.get_cmd_line(body, C.int(i))
                    cmdLine[i] = byte(ch)
                }

                delim := "\n"
                cmdLineStr := string(cmdLine)
                argv := strings.Split(cmdLineStr, delim)

                fmt.Println( ">> pid:", body.pid )
                fmt.Println( ">>   cmd line(len:",body.cmd_line_len, "):", argv )
                fmt.Println( body )

                //  수신한 argv 를 통해 재기동하는 루틴
                //go func() {
                //  sttyArgs := syscall.ProcAttr{
                //      "",
                //    []string{},
                //      []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
                //      nil,
                //  }
                //  syscall.ForkExec(argv[0], argv, &sttyArgs )
                //}()
            }
        }
    }
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())

    l, err := net.Listen("tcp", ":5032")
    if nil != err {
        log.Fatalf("fail to bind address to 5032; err: %v", err)
    }
    defer l.Close()

    for {
        conn, err := l.Accept()
        if nil != err {
            log.Printf("fail to accept; err: %v", err)
            continue
        }

        go ConnHandler2(conn)
    }
}

C로 작성된 클라이언트

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#include <string.h>
#include <stdint.h>

#include <string.h>
#include <stdint.h>

#define MSG_OPCODE_CONNECT 1

typedef struct _MsgHeader {
    uint8_t OpCode;
    uint8_t reserved1[3];
    uint32_t seqno;
    int32_t  body_len;
} MsgHeader;

typedef struct _MsgBodyConnect {
  int32_t  pid;
  int8_t   proc_type;
  int8_t   reserved1;
  int8_t   do_relaunch;
  uint8_t  failover_retry_max_count;
  int32_t  cmd_line_len;
  char     cmd_line[1];
} MsgBodyConnect;

char get_cmd_line( MsgBodyConnect * connect, int offset )
{
  return connect->cmd_line[offset];
}

typedef union _MsgBody {
    MsgBodyConnect connect;
    // 다른 바디 선언
} MsgBody;
const int32_t MsgBodySize = sizeof( MsgBody );

typedef struct _Msg {
  MsgHeader hdr;
  MsgBody   body;
} Msg;

const int32_t MsgHeaderSize = sizeof( MsgHeader );
const int32_t MsgSize = sizeof( Msg );

char  msg_buf[1024*16];
Msg * msg = (Msg *)msg_buf;

#define offsetof(s,m)   (size_t)&(((s *)0)->m)

int main( int argc, char ** argv )
{
  int ret;
  struct sockaddr_in client_sockaddr;
  int client_sockfd;
  int writelen;
  int readlen;
  int i = 0;
  char delim[2] = "\n";
  char cmdline[1024] = {};

  for ( i = 0 ; i < argc ; i++ )
    {
      strcat( cmdline, argv[i] );
      strcat( cmdline, delim);
    }

  sleep(1);
  /* create client socket */
  client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (client_sockfd == -1) {
    perror("[C] socket");
    return -1;
  }

  printf("[C] socket\n");

  /* set client_sockaddr */
  memset(&client_sockaddr, 0, sizeof(struct sockaddr_in));
  client_sockaddr.sin_family      = AF_INET;
  client_sockaddr.sin_port        = htons(5032);
  inet_pton(AF_INET, "127.0.0.1", &client_sockaddr.sin_addr.s_addr);

  /* connect */
  printf("[C] connect\n");
  ret = connect(client_sockfd, (struct sockaddr *)&client_sockaddr, sizeof(struct sockaddr_in));
  if (ret == -1) {
    perror("[C] connect");
    return -1;
  }

  /* send */
  printf("[C] send\n");
  msg->hdr.OpCode = MSG_OPCODE_CONNECT;
  msg->body.connect.pid = getpid();
  msg->body.connect.proc_type = 2;
  msg->body.connect.cmd_line_len = strlen(cmdline);
  printf("cmdline(%d):%s\n", msg->body.connect.cmd_line_len, cmdline);
  memcpy( msg->body.connect.cmd_line,
          cmdline,
          msg->body.connect.cmd_line_len );

  msg->hdr.body_len =
    msg->body.connect.cmd_line_len + offsetof(MsgBodyConnect, cmd_line);

  writelen = send( client_sockfd,
                   msg,
                   MsgHeaderSize + msg->hdr.body_len,
                   0 );
  // send() 호출 결과 생략

  close(client_sockfd);

  return 0;
}

결과

테스트 순서는 go 서버 실행 후 C 애플리케이션 실행이다.

>>> Golang 서버 실행 및 결과

$ go run server.go
2021/10/12 11:26:40 &{1 [0 0 0] 0 59}
>> pid: 3555
>>   cmd line(len: 47 ): [./client --arg1 1234 --arg2 client description ]
&{3555 2 0 0 0 47 [46] [47 99 108]}
2021/10/12 11:26:40 EOF

>>> C 애플리케이션 실행 및 결과

$ gcc -g -lc client.c -o client
$ ./client --arg1 1234 --arg2 "client description"
[C] socket
[C] connect
[C] send
cmdline(47):./client
--arg1
1234
--arg2
client description
반응형