TCP/IP Internetworking

bash shell 에서 특정 주소 형식을 이용해 socket 연결 기능을 제공하는 것처럼 awk 에서도 아래와 같은 주소 형식의 스트링을 사용하면 자동으로 socket 연결로 인식합니다. 서버쪽 주소는 localport 를 알맞은 값으로 적어주면 되고 hostname 과 remoteport 는 0 으로 두면 됩니다. 클라이언트 주소는 localport 를 0 으로 두고 연결할 hostname 과 remoteport 를 알맞게 설정하면 됩니다. hostname 은 ip 주소로도 사용할 수 있습니다.

  • net-type : /inet4/ 는 IPv4, /inet6/ 는 IPv6, /inet/ 는 system default
  • protocol : tcp 또는 udp

송신만 하거나 수신만 하는 단방향일 경우는 redirection ( <, > ) 을 사용할 수 있지만 양방향 통신일 경우는 coprocess ( |& ) 를 사용해야 합니다. 파이프로 두 개의 외부 명령을 직접 연결해 사용할 수 없듯이 socket 도 직접 외부 명령을 연결해 사용할 수 없습니다.

$ awk -b 'BEGIN{ 
    "cat file1" |& "/inet/tcp/0/some.host.com/8080"
}'
awk: cmd. line:2:     "cat file1" |& "/inet/tcp/0/some.host.com/8080"
awk: cmd. line:2:                    ^ syntax error

임시 웹 서버

디렉토리에 webserver.awkcat.jpg 파일을 복사한 후 awk -f webserver.awk 로 실행하면 됩니다.

각 리소스 ( html, image ) 별로 download 를 위해 tcp 연결을 맺고 끊는 http/1.0 방식입니다.

@load "filefuncs"
@load "readfile"
BEGIN {
    RS = ORS = "\r\n"
    datefmt = "Date: %a, %d %b %Y %H:%M:%S %Z"
    socket = "/inet/tcp/8080/0/0"
    while (socket |& getline > 0) {
        if ( $1 == "GET" ) { 
            print "GET : ", $2 
            $2 = substr($2,2)
            switch ($2) {
                case /\.(jpg|gif|png)$/ :
                    sendImage($2)
                    break
                default :
                    sendHtml()
            }
        }
    }
}
function sendHtml(    a, arr) {
    arr["type"] = "text/html"
    arr["content"] = "\
     <HTML>\n\
     <HEAD><TITLE>Out Of Service</TITLE></HEAD>\n\
     <BODY>\n\
           <H1>This site is temporarily out of service.</H1>\n\
           <P><img src=cat.jpg></P>\n\
     </BODY>\n\
     </HTML>\n\
     "
    arr["length"] = length(arr["content"])
    arr["date"] = strftime(datefmt, systime(), 1)

    send(arr)
}
function sendImage(file,     cmd, a, arr, type) {
    RS="\n"
    cmd = "file -b --mime-type '" file "'"
    cmd | getline type; close(cmd)
    RS="\r\n"
    arr["type"] = type
    stat(file, a)
    arr["length"] = a["size"]
    arr["content"] = readfile(file)
    arr["date"] = strftime(datefmt, systime(), 1)

    send(arr)
}
function send(arr) {
    print "HTTP/1.0 200 OK"                    |& socket
    print arr["date"]                          |& socket
    print "Server: AWK"                        |& socket
    print "Content-Length: " arr["length"]     |& socket
    print "Content-Type: " arr["type"]         |& socket
    print ""                                   |& socket
    print arr["content"]                       |& socket

    print "close socket"
    close(socket)
}

양방향 채팅

위에서 작성한 웹 서버의 경우는 메시지가 전송되는 순서가 request, response 로 일정하기 때문에 하나의 프로세스로도 처리할 수가 있었는데요. 양방향 채팅의 경우는 일정한 순서 없이 임의로 전달되기 때문에 입력과 출력을 따로 처리해줘야 되므로 두 개의 프로세스가 필요합니다.

fork 하기 전에 server 의 socket |& getline 와 client 의 print "" |& socket 로 먼저 socket 을 연결하는 것은 child 와 parent 프로세스가 동일한 socket 을 사용하기 위해서입니다. 상대방이 종료하게 되면 socket 에 연결 중인 parent 프로세스만 종료되고 child 프로세스는 남게 되므로 종료전에 먼저 kill 외부 명령으로 child pid 를 종료시킵니다.

한가지 주의할 것은 heredoc 으로 작성하면 안되고 quotes 이나 파일로 만들어서 실행해야 됩니다.

############ 먼저 server 를 실행 ############
$ awk '
@load "fork"
BEGIN{
    socket = "/inet/tcp/8080/0/0"
    socket |& getline
    if (( pid = fork() ) == 0 ) {
        while (getline msg < "/dev/stdin" > 0)   # child
            print msg |& socket
    } else {
        while (socket |& getline msg > 0)        # parent
            print msg
        system("kill " pid)
    }
}'

##############  chat client  ##############
$ awk '
@load "fork"
BEGIN{
    socket = "/inet/tcp/0/some.host.com/8080"    # localhost or 0.0.0.0
    print "" |& socket
    if (( pid = fork() ) == 0 ) {
        while (getline msg < "/dev/stdin" > 0)   # child
            print msg |& socket
    } else {
        while (socket |& getline msg > 0)        # parent
            print msg
        system("kill " pid)
    }
}'

바이너리 파일 전송

awk 는 기본적으로 텍스트 데이터를 다루는 프로그램이지만 약간 다른 방식으로 활용을 하여 바이너리 파일 전송을 구현한 것입니다. 이때는 입출력 데이터를 byte 로 다루는 -b 옵션을 사용해야 하고 RS 설정과 RT 변수를 활용합니다.

테스트에 사용한 gawk 버전이 4.1.4 인데 구버전의 경우 정상적으로 작동하지 않을 수 있습니다.

RS=".{1,3}" 형식을 이용하여 일정한 크기로 데이터 읽어들이기 예제

$ echo -n 0123456789 |
awk '{ printf "NR:%s, NF:%s, $0:(%s), RT:(%s)\n", NR, NF, $0, RT  }' RS='.{1,3}'
NR:1, NF:0, $0:(), RT:(012)
NR:2, NF:0, $0:(), RT:(345)
NR:3, NF:0, $0:(), RT:(678)
NR:4, NF:0, $0:(), RT:(9)

바이너리 파일 전송

#################  file receiver  #################
$ awk -b -f - <<\EOF
BEGIN{ RS=".{1,300}"
    while ( getline < "/inet/tcp/8080/0/0" > 0 ) {
        printf "%s", RT > "file1.jpg.copy" 
        sum += length(RT)
        printf "\r%'d bytes received", sum
    }
    print ""
}
EOF
1,637,568 bytes received

##################  file sender  ###################
$ awk -b -f - <<\EOF 
BEGIN{ RS=".{1,300}" 
    while ( getline < "file1.jpg" > 0 ) {
        printf "%s", RT > "/inet/tcp/0/some.host.com/8080"
        sum += length(RT)
        printf "\r%'d bytes sent", sum
    }
    print ""
}
EOF
1,637,568 bytes sent

tar 파일 전송

###############  tar receiver  ################
$ awk -b 'BEGIN{ RS=".{1,300}"
    while ( getline < "/inet/tcp/8080/0/0" > 0 ) {
        printf "%s", RT | "tar xvz"
    }
}'

###############  tar sender  ##################
$ awk -b 'BEGIN{ RS=".{1,300}" 
    while ( "tar cvz ./mydir" | getline ) {
        printf "%s", RT > "/inet/tcp/0/some.host.com/8080"
    }
}'