Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

이번 장에서는 네트워크 프로그래밍을 위한 기본적인 기술들을 살펴볼 것이다. 다루는 내용은 호스트와 서비스 주소를 다루는 법과 TCP,UDP 통신에 대한 것들이다. 그리고 GO API를 이용해서 TCP와 UCP 기반의 네트워크 프로그램을 개발하는 방법도 살펴볼 것이다.

소개

네트워크는 시리얼 링크, 구리선, 광섬유, 무선 등 다양한 매체로 구성되어 있다. 매체를 사용하는 기기들도 다양한데, 컴퓨터와 컴퓨터 폰과 컴퓨터가 연결된 경우도 있다. 요즘에는 시계, 안경, 냉장고, 에어컨, 전구등의 일반 가전 기구들도 인터넷에 연결된다. 이들 네트워크들은 물리 계층(Physical link layer)에서 부터 그 상위 계층까지 많은 부분에 있어서 차이가 있을 수 있다.

다행인 것은 그런 다양성을 모두 고려하면서 개발할 필요는 없다는 점이다. 수년에 걸쳐서 IP와 TCP/UDP의 인터넷 스택으로 정리됐기 때문이다. 예를들어 블루투스는 물리계층과 프로토콜 계층을 정의하고 있으며 TCP/IP를 모델을 따르지 않지만, 많은 블루투스 디바이스들이 IP 스택을 가지고 있어서 동일한 인터넷 프로그래밍 기술을 사용할 수 있도록 하고 있다. 비슷하게 4G 무선 폰기술도 IP 스택을 이용하고 있다.

IP는 OSI 스택에서 3번째 계층에 해당하는 네트워크 프로토콜이며, TCP와 UCP는 4계층에 해당하는 프로토콜이다. 이들 프로토콜들은 유일한 프로토콜이 아니며, 아마도 최고의 프로토콜도 아닐 것이다. 물론 가장 성공적인 프로토콜이긴 하지만 다른 여러가지 대안 프로토콜들이 개발되고 있다. 예를들어 TCP와 UDP의 단점을 보완하기 위한 SCTP(Stream Control Transmission Protocol) 그리고 DTN(Delay-tolerant networking)같은 기술들이 논의되고 있다. 그럼에도 불구하고 IP와 TCP,UDP는 적어도 상당한 기간동안 네트워크의 주요 기술로 사용될 것으로 예상된다. Go는 TCP/IP 프로그램 개발을 위한 모든 것들을 지원한다.

TCP/IP 스택

TCP/IP는 DARPA 프로젝트에서 오랜기간동안의 연구를 거쳐서 고안된 네트워크 기술로, 유닉스 네트워크의 핵심 기술이다. TCP/IP는 Transmission Control Protocol / Internet Protocol의 줄임말이다. TCP는 3계층 프로토콜이고, IP는 2게층 프로토콜인 관계로 TCP over IP라고 부르기도 한다.하지만 그냥 편하게 TCPIP라고 부른다.

TCP/IP 스택은 OSI 7계층을 4계층으로 단순화 했다.

TCP는 연결지향 프로토콜이고, UDP는 connectionless 프로토콜이다.

IP data grams

IP 계층은 데이터를 전송 기능을 제공하며, connectionless, 비신뢰(unreliable)를 특징으로 한다. 이는 각각의 데이터그램이 서로 독립적으로 전송되는 것을 의미한다. 데이터를 원할히 전송하려면, 데이터간의 흐름을 조절해야 하는데, 이는 상위 계층 프로토콜에서 처리한다.

IP에 대한 자세한 내용은 IP Header문서를 참고하자.

인터넷 주소

IPv4 주소

서비스를 이용하기 위해서는 서비스를 찾아야 한다. 서비스를 찾기 위해서는 주소가 필요한데, 인터넷은 IP Address라는 주소 체계를 이용해서 서비스에 유일한 주소를 할 당한다. IP 주소는 2^32 크기로, 32bit unsigned integer 값을 이용한다. 이 주소체계를 IPv4라고 부른다. 2^32이라면 40억 정도의 크기가 되니, IPv4를 이용하면 최대 40억개의 컴퓨터가 참여하는 네트워크를 구성할 수 있을 것이다.

IPv4 주소는 32bit 크기의 숫자로, 네트워크 카드에 할당된다. 컴퓨터가 아니다. 컴퓨터가 2개의 네트워크 카드를 가지고 있다면 2개의 IP 주소가 필요하다. IP주소는 8bit(1byte)씩 4개로 나누어서 관리한다. 각 바이트는 "."을 이용해서 구분을 한다. 이렇게 해서 "127.0.0.1", "66.102.11.104" 같은 인터넷 주소 표기 방식이 만들어졌다.

IP주소는 Network와 Host의 두 영역으로 구성된다. 해당 네트워크에서 관리할 수 있는 Host를 설정하기 위한 장치다. 예를들어 소규모의 회사 네트워크라면, 수백개의 IP만으로 관리가 가능할테다. 대학이라면 수만개의 IP를 관리할 수 있어야 하고, 국가라면 수백만개 이상의 IP주소를 관리해야 할 것이다.

이러한 네트워크 규모에 따른 관리를 위해서 IP 주소를 몇 개의 클래스로 나눠서 관리하고 있다.

클래스의 IP 주소의 가장 앞에 있는 바이트의 값으로 구분한다. 0이면 A 클래스 10이면 B, 110이면 C 클래스 이런식이다.
  • A 클래스 : 24 bit크기를 Host에 할당할 수 있다. 1600만정도의 호스트를 관리할 수 있으니, 국가 수준의 조직에 할당할 수 있을 거다. 단 사용할 수 있는 네트워크의 크기가 7bit(128개)로 제한된다. 뭐.. 1600만개의 호스트를 관리할 수 있는 조직이 몇개나 되겠는가.
  • B 클래스 : 16bit 크기를 Host에 할당할 수 있다. 65천개의 호스트를 관리할 수 있으니, 대학이나 정부조직등에서 사용할 수 있겠다.
  • C 클래스 : 8bit를 Host에 할당할 수 있다. 소규모 회사에 할당할 수 있는 크기다.
  • D 클래스 : Multicast 주소에 할당한다. A,B,C 클래스와는 용도가 다르다.
이렇게 A, B, C 클래스로 큼직큼직 하게 할당하면, 할당 받은 조직은 네트워크를 다시 쪼개서 사용한다. B클래스 주소를 할당 받았다면 관리해야할 호스트가 65천개인데, 이걸 단일 시스템에서 관리할 수는 없을 테니 말이다. 예를들어 대학교라면, 대학으로 나누고 이걸 다시 학부로 나눠서 관리 할 거다.

이렇게 네트워크를 쪼개서 관리하는 것을 subnet(sub network) 구성이라고 한다. subnet 구성도 클래스와 마찬가지로 주소를 구성하는 bit 값을 이용한다. 자세한 내용은 subnet문서를 참고하자.

IPv6

인터넷 주소로 관리할 수 있는 호스트의 수는 자그마치 40억이 넘는다. 절대로 부족할리 없을 거라고 생각했건만 그 일이 실제로 일어났다. 무려 3년전일이다.

IP 주소가 부족할 경우 NAT 구성과 같은 우회할 수 있는 방법이 있긴 하지만, 근본적인 해결책은 아니다. 결국 더 많은 호스트를 관리할 수 있는 주소체계를 도입해야 하는데, 그렇게 해서 등장한게 IPv6다. IPv6는 128비트 크기의 인터넷 주소를 관리할 수 있다. 어느 정도 크기냐면.. 글쎄 그냥 지구의 모든 모래에 IP 주소를 할당하고도 남을 정도의 크기일 것이다. 인류가 지구를 벗어나지 않는 한 부족할 일은 없을 것이다.

IPv6에 대한 자세한 내용은 IPv6 소개문서를 참고하자.

IP 를 위한 데이터 타입

IP 타입

net 패키지에는 네트워크 프로그래밍을 위한 타입, 함수, 메서드들이 포함되어있다. IP는 byte 배열로 정의되어있다.
type IP []byte

네트워크 프로그램을 개발하다 보면, 분명 IP 데이터 타입을 다룰 일이 생길거다. 직접 바이트를 조작하지는 않을테고, IP 주소를 조작하는 몇 몇 함수들을 사용하게 될 거다. 예를들어 ParseIP(String)는 "점 스타일로 표기된" IPv4와 IPv6주소를 처리하는 함수다.
/* IP
 */

package main

import (
    "net"
    "os"
    "fmt"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    name := os.Args[1]

    addr := net.ParseIP(name)
    if addr == nil {
        fmt.Println("Invalid address")
    } else {
        fmt.Println("The address is ", addr.String())
    }
    os.Exit(0)
}

테스트 해보자.
# go run main.go 0:0:0:0:0:0:0:1
The address is  ::1

IPMask 타입

IPMask 타입이다. int, float, string 외에는 그냥 다 byte 혹은 byte 배열이라고 생각하면 되겠다.
type IPMask []byte

IPv4 주소로부터 mask 값을 가져오는 함수다.
func IPv4Mask(a, b, c, d byte) IPMask

DefaultMask 함수를 이용해서, IP로 부터 디폴트 마스크 값을 가져올 수 있다.
func (ip IP) DefaultMask() IPMask
Class Default Mask
A 255.0.0.0
B 255.255.0.0
C 255.255.255.0

예제 프로그램이다.
package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    dotAddr := os.Args[1]

    addr := net.ParseIP(dotAddr)
    if addr == nil {
        fmt.Println("Invalid address")
        os.Exit(1)
    }

    mask := addr.DefaultMask()
    network := addr.Mask(mask)
    ones, bits := mask.Size()
    fmt.Println("Address is ", addr.String(),
        "\nDefault mask length is ", bits,
        "\nLeading ones count is", ones,
        "\nMask is (hex) ", mask.String(),
        "\nNetwork is ", network.String())
    os.Exit(0)
}

127.0.0.1로 테스트 했다.
# go run main.go 127.0.0.1
Address is  127.0.0.1 
Default mask length is  32 
Leading ones count is 8 
Mask is (hex)  ff000000 
Network is  127.0.0.0
  • 주소는 127.0.0.1로 A클래스 주소다.
  • default mask의 크기는 32bit다.
  • A클래스이니 앞 8비트를 이용해서 마스크 연산을 한다.
  • 마스크는 ff000000(255.0.0.0)이다.
  • 네트워크는 127.0.0.0 이다.

IPAddr 타입

많은 함수들이 IP를 포함하는 IPAddr에 대한 포인터를 반환하다.
type IPAddr {
    IP IP
}

보통 ResolvIPAddr 함수에서 많이들 사용한다.
func ResolvIPAddr(net, addr string) (*IPAddr, os.Error)

DNS 이름(혹은 호스트 이름)으로 부터, IP 주소를 찾아내는 프로그램이다.
package main
    
import (
    "fmt"
    "net"
    "os"
)   

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    name := os.Args[1]
    
    addr, err := net.ResolveIPAddr("ip", name)
    if err != nil {
        fmt.Println("Resolution error", err.Error())
        os.Exit(1)
    }
    fmt.Println("Resolve address is ", addr.String())
    os.Exit(0)

}   

# go run main.go www.google.co.kr
Resolve address is  173.194.127.184

# go run main.go localhost
Resolve address is  127.0.0.1

Host look up

ResolveIPAddr은 DNS lookup을 수행하며, 단지 하나의 IP 주소만을 반환한다. 하지만 도메인이름이나 호스트 이름은 하나 이상의 IP 주소를 가질 수 있다. LookupHost 함수를 이용하면, IP 목록을 검색할 수 있다.
func LookupHost(name string) (cname string, addrs []string, err os.Error)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    name := os.Args[1]

    addrs, err := net.LookupHost(name)
    if err != nil { 
        fmt.Println("Resolution error", err.Error())
        os.Exit(1)  
    }               
    for _, s := range addrs {
        fmt.Println(s)
    }      
    os.Exit(0)
}     

www.google.co.kr를 다시 lookup 했다.
$ go run main.go www.google.co.kr
173.194.127.47
173.194.127.55
173.194.127.56
173.194.127.63
2404:6800:4005:800::100f

Services

서비스는 호스트 컴퓨터위에서 실행된다. 이렇게 컴퓨터 위에서 인터넷 서비스를 위해서 실행되는 프로그램을 "인터넷 서비스 프로그램"이라고 한다. 이들 인터넷 서비스 프로그램은 언제 있을지 모를 클라이언트의 요청을 처리해야 하기 때문에, "데몬 프로세스" 형태로 오랜 시간동안 활동을 한다.

인터넷 상에는 수많은 서비스들이 있으며, 호스트 컴퓨터는 하나 이상의 서비스들을 수행할 수도 있다. 예컨데, 웹 서비스와 FTP 서비스, SSH 서비스를 동시에 할 수 있다. 또한 동일한 서비스를 프로토콜을 달리해서 서비스할 수도 있는데, HTTP의 경우 TCP와 UDP로 동시에 서비스 할 수도 있다.

하나의 컴퓨터에서 여러 개의 서비스가 동시에 이루어질 경우, 서비스를 구분하기 위한 수단이 필요하다. 인터넷 서비스들은 포트(Port) 번호를 이용해서 서비스를 구분한다. HTTP 서비스는 80번, SSH는 22번, 메일서비스(SMTP)는 25번 이런 식이다.

인터넷에서 서비스를 찾는 것은 다음과 같이 정리 할 수 있다.
  1. IP 주소를 이용해서 서비스를 제공하는 호스트를 찾는다.
  2. 포트 번호를 이용해서 원하는 서비스 프로세스를 찾는다.
인터넷 상에는 수많은 서비스들이 있는데, 이들 서비스들 중 표준적인 서비스들에 대해서는 미리 포트번호를 할당해 두고 있다. 이 서비스 포트에 대한 정보는 /etc/services 에서 확인할 수 있다.

LookupPort 함수를 이용하면, 서비스에 할당된 표준 포트 번호를 확인할 수 있다.
func LookupPort(network, service string) (port int, err os.Error)

LookupPort 함수를 이용해서 서비스를 찾는 예제 프로그램
func main() {
    if len(os.Args) != 3 {
        fmt.Fprintf(os.Stderr,
            "Usage : %s network-type service\n",
            os.Args[0])
        os.Exit(1)
    }
    
    networkType := os.Args[1]
    service := os.Args[2]
    
    port, err := net.LookupPort(networkType, service)
    if err != nil {
        fmt.Println("Error : ", err.Error())
    }
    
    fmt.Println("Service Port", port)
    os.Exit(0)
}

ssh의 포트번호를 찾아보자.
# go run main.go tcp ssh 
Service Port 22

# go run main.go udp ssh 
Service Port 22

TCPAddr 타입

TCPAddr 타입은 IPPort를 포함한 구조체다.
type TCPAddr struct {
  IP    IP
  Port  int
}

ResolveTCPAddr함수가 TCPAddr 타입을 사용한다.
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
net는 tcp, tcp4, tcp6 중 하나를 사용하면 된다. "addr"은 호스트 이름 혹은 IP 주소로 ":"다음에 포트번호 혹은 서비스명을 지정하면 된다. "www.google.com:80", "127.0.0.1:ssh" 이런식이다.

예제 프로그램
func main() {
    tcpAddr, err := net.ResolveTCPAddr("tcp", "www.google.co.kr:ssh")
    if err != nil {                                         
        fmt.Println("Error : ", err.Error())                
        os.Exit(1)                                          
    }                                                       
    fmt.Println("IP ", tcpAddr.IP.String(),                 
        "Port ", tcpAddr.Port)                              
    os.Exit(0)
}

# go run main.go
IP  173.194.127.143 Port  22

TCP Socket

이렇게 해서 네트워크 상에서 호스트와 서비스를 찾을 수 있게 됐다. 만약 클라이언트 프로그램이라면 인터넷 주소와 포트번호를 이용해서 서버에 연결요청을 한 다음에, 데이터를 읽고 쓰는 작업을 수행할 것이다.

서버 프로그램은 호스트에서 포트에 bind 하면서, 클라이언트의 연결을 기다린다. 클라이언트와 연결되면, 클라이언트의 요청 데이터를 읽어서 처리하고 그 결과를 클라이언트에 전송한다.

Go의 net.TCPConn를 이용해서 서버와 클라이언트간 전이중 통신(full duplex communication)을 수행할 수 있다. 데이터 통신을 위한 가장 중요한 메서드들을 소개한다.
func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)   
Write는 데이터를 쓰기 위해서, Read는 데이터를 읽기 위해서 사용한다. 이들 함수는 서버와 클라이언트 모두 동일하게 사용한다.

TCP 클라이언트

TCP 클라이언트 프로그램은 TCP 서비스에 "dial"을 거는 것으로 연결을 만든다. 연결이 맺어지면 dial은 통신접점으로 사용할 수 있는 TCPConn를 반환한다. 서버와 클라이언트는 TCPConn을 이용해서 통신할 수 있다. 일반적으로 클라이언트는 TCPConn을 이용해서 서버에 데이터를 쓰고, 서버는 TCPConn에서 데이터를 읽어서 처리하고 응답하는 방식으로 작동한다. 클라이언트 프로그램은 "DialTCP" 함수를 이용해서 연결한다.
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)

간단한 TCP 클라이언트 프로그램을 하나 만들어 보려한다. 이 프로그램은 HTTP 클라이언트 프로그램으로 웹서버에 HTTP 요청을 전송한다. HTTP 요청은 다음과 같다.
HTTP / HTTTP/1.0\r\n\r\n

서버는 대략 다음과 같이 응답할 것이다.
HTTP/1.0 200 OK
ETag: "-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT
Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23

이 프로그램의 이름은 GetHeadInfo.go다.

import (
    "fmt"
    "io/ioutil"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println(os.Stderr, "Usage %s host:port", os.Args[0])
        os.Exit(0)
    }
    service := os.Args[1]

    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(err)

    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkError(err)

    result, err := ioutil.ReadAll(conn)
    checkError(err)

    fmt.Println(string(result))

    os.Exit(0)
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}

테스트 결과
# ./GetHeadInfo www.joinc.co.kr:80
HTTP/1.1 200 OK
Date: Sat, 30 Aug 2014 15:54:00 GMT
Server: Apache/2.2.14 (Ubuntu)
Vary: Accept-Encoding
Connection: close
Content-Type: text/html

이 프로그램은 모든 함수에 대해서 에러를 체크하고 있다. 네트워크 프로그램은 여러 계층에 걸쳐서 작동하기 때문에, 문제의 원인을 찾기가 쉽지 않다. 따라서 모든 함수에 대해서 철저하게 에러를 검사해야 만 한다. 네트워크 프로그램이라면 기본적으로 아래의 항목들은 체크를 해야 한다.
  1. 주소를 잘못 입력
  2. 원격 서버에 대한 연결 실패. 서버 프로그램이 떠있지 않을 수도 있고, 서버는 떠 있지만 네트워크 문제로 연결이 안될 수도 있다. 호스트의 하드웨어에 문제가 있을 수도 있다. 하드웨어 서버와 네트워크 모두 문제 없지만, 시스템에 너무 많은 요청이 몰려서, 연겔에 많은 시간이 걸릴 수도 있다.
  3. 연결은 성공했지만 데이터를 쓰는데 문제가 발생할 수 있다. 서버가 죽거나 네트워크 연결이 끊기거나 너무 많은 시간지연이 생기거나..
  4. 읽기 역시 비슷한 문제가 생길 수 있다.
서버로 부터 데이터를 읽는 부분을 보자. 이 경우 우리는 하나의 응답만을 읽었다. 아마도 이 응답은 파일의 마지막을 읽었을 대 종료될 것이다. 그러나 TCP는 여러 개의 패킷으로 구성되기 때문에, 파일의 끝인지 판독이 필요하다. "io/ioutil"패키지의 ReadAll함수는 완전한 응답을 반환한다.

Go 언어와 관련된 특징을 살펴보자. 첫번째 특징은 모든 함수는 에러를 포함한 두개의 값을 반환한다는 거다. 만약 에러가 없다면 nil을 반환할 것이다. C 언어라면 반환 값이 NULL, -1, 0 인지를 검사할 것이고, Java는 예외 처리를 할 것이다. C 언어의 경우 하나의 반환 값으로 에러를 처리해야 하기 때문에, 일관성을 유지하기가 쉽지 않다. 어떤 함수는 NULL 일 것이고, 다른 함수는 -1 이고, 또 다른 함수는 0일 것이다. Java의 예외처리는 코드를 읽기 어렵게 한다.

Daytime 서버

이제 네트워크 서버 프로그램을 만들어보자. 내가 만들 프로그램은 현재의 컴퓨터 시간을 알려주는 daytime 서비스 프로그램이다. daytime 서비스는 인터넷 표준 서비스로 13번 포트를 이용한다.

네트워크 서버 프로그램은 포트를 등록하는 것으로 네트워크에 연결된다. 네트워크에 연결된 다음에는 클라이언트의 연결을 "기다리(accept)"게 된다. 서버 프로그램은 accept 영역에서 대기하고 있다가, 클라이언트가 연결하면 클라이언트의 통신접점을 반환한다. Daytime 서비스는 클라이언트가 연결되면, 시스템의 타임을 클라이언트에 전송하는 간단한 일을 한다. 전송이 끝나면, 클라이언트와의 연결을 끊는다.

이와 관련된 함수들이다.
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

net는 tcp, tcp4, tcp6 중에 하나를 사용한다. laddr는 listen 하기 위한 네트워크 인터페이스 주소와 포트번호를 설정한다. listen 포트번호는 1024를 기준으로 두 개 영역으로 나뉜다. 1024 보다 작은 포트번호는 시스템 영역으로 "root" 권한을 가지고 있어야지만 사용할 수 있다. 일반계정으로 실행하길 원한다면 1024 보다 큰 번호를 선택해야 한다. 나는 일반권한으로 서버 프로그램을 실행할 거다. 해서 13번 대신 1200번 포트를 사용하기로 했다.

{{{!plain package main

import ( "fmt" "net" "os" "time" )

func main() { service := ":1200" // "0.0.0.0:1200" 과 같다. tcpAddr, err := net.ResolveTCPAddr("tcp4", service) checkError(err)

listener, err := net.ListenTCP("tcp", tcpAddr) checkError(err)

for { conn, err := listener.Accept() if err != nil { continue } daytime := time.Now().String() conn.Write([]byte(daytime)) conn.Close() } }

func checkError(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error()) os.Exit(1) } } }}}

서버를 실행하면, 클라이언트 연결을 기다린다. netstat로 확인할 수 있다.
$ netstat -nap | grep 1200
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp6       0      0 :::1200                 :::*                    LISTEN      4033/main       

telnet으로 테스트 해보자.
# telnet localhost 1200
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2014-09-01 00:53:36.682401081 +0900 KSTConnection closed by foreign host.
#

멀티 스레드 서버

"echo"는 간단한 인터넷 서비스다. 이 서비스는 클라이언트로 부터 수신한 데이터를 클라이언트로 그대로 전송한다.
package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    service := "0.0.0.0:1201"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        handleclient(conn)
        conn.Close()
    }
}

func handleclient(conn net.Conn) {
    var buf [512]byte
    for {
        n, err := conn.Read(buf[0:])
        if err != nil {
            return
        }
        _, err2 := conn.Write(buf[0:n])
        if err2 != nil {
            return
        }

    }
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}

이 프로그램은 잘 작동한다. 하지만 단일 스레드라는 문제점이 있다. 만약 클라이언트가 연결해서 echo 서비스를 이용하고 있는 와중에, 다른 클라이언트가 접속하려 하면, 이 클라이언트는 블럭된다. 다행히도 이 문제는 go-routine를 이용해서 쉽게 해결할 수 있다.
import (
    "fmt"
    "net"
    "os"
)

func main() {
    service := "0.0.0.0:1201"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        // go routine 실행
        go handleclient(conn)
    }
}

func handleclient(conn net.Conn) {
    defer conn.Close()
    var buf [512]byte
    for {
        n, err := conn.Read(buf[0:])
        if err != nil {
            return
        }
        _, err2 := conn.Write(buf[0:n])
        if err2 != nil {
            return
        }

    }
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}
이제 하나 이상의 연결을 처리 할 수 있다. 프로세스 정보를 확인해 보자.
# ps -eLf | grep main
yundream  3077  2397  3077  0    4 23:46 pts/13   00:00:00 ./main
yundream  3077  2397  3078  0    4 23:46 pts/13   00:00:00 ./main
yundream  3077  2397  3079  0    4 23:46 pts/13   00:00:00 ./main
yundream  3077  2397  3080  0    4 23:46 pts/13   00:00:00 ./main
4개의 스레드가 떠 있다. 테스트 삼아서 한 7개 정도의 클라이언트를 연결하더라도 스레드의 개수는 늘어나지 않는다. 3077 프로세스가 부모 쓰레드인데, 부모 스레드의 소켓 상태를 확인해봤다.
# cd /proc/3077/fd
# ls -al
lrwx------ 1 yundream yundream 64  9월  2 23:47 0 -> /dev/pts/13
lrwx------ 1 yundream yundream 64  9월  2 23:47 1 -> /dev/pts/13
lrwx------ 1 yundream yundream 64  9월  2 23:51 10 -> socket:[24209]
lrwx------ 1 yundream yundream 64  9월  2 23:47 2 -> /dev/pts/13
lrwx------ 1 yundream yundream 64  9월  2 23:47 3 -> socket:[24002]
lrwx------ 1 yundream yundream 64  9월  2 23:47 4 -> anon_inode:[eventpoll]
lrwx------ 1 yundream yundream 64  9월  2 23:47 5 -> socket:[24080]
lrwx------ 1 yundream yundream 64  9월  2 23:48 6 -> socket:[19403]
lrwx------ 1 yundream yundream 64  9월  2 23:49 7 -> socket:[19410]
lrwx------ 1 yundream yundream 64  9월  2 23:49 8 -> socket:[19423]
lrwx------ 1 yundream yundream 64  9월  2 23:51 9 -> socket:[25603]
anon_inode는 파일의 유형을 나타내기 위해서 사용하는데, eventpoll은 파일지정자가 epoll 형식으로 만들어졌음을 의미한다. event driven 방식으로 작동하고 있음을 알 수 있다. 자세한 내용은 좀 더 확인 해봐야 겠다.

TCP 연결 제어

타임 아웃

서버 프로그램은 클라이언트가 일정 시간 요청이 없다면, 타임아웃(Timeout)을 확인해서 연결을 닫아줘야 한다. 클라이언트도 마찬가지로 일정시간 서버의 응답이 없다면 연결을 끊어야 할 것이다. 타임아웃을 검사하기 위해서 사용하는 메서드들이다.
func SetReadDeadline(t time.Time) error
func SetWriteDeadline(t time.Time) error
func SetDeadline(t time.Time) error

클라이언트가 연결 한 후 10초 이내에 요청이 없으면 연결을 끊도록 handleclient 함수를 수정했다.
func handleclient(conn net.Conn) {
    defer conn.Close()
    var buf [512]byte
    fmt.Println("PID : ", os.Getpid())
    for {
        conn.SetReadDeadline(time.Now().Add(5 * time.Second))
        n, err := conn.Read(buf[0:])
        if err != nil {
            fmt.Println("Error : ", err.Error())
            return
        }   
        _, err2 := conn.Write(buf[0:n])
        if err2 != nil {
            return
        }

    }
}
타임아웃시 서버에서 발생하는 에러 메시지다.
$ go run main.go 
Error :  read tcp 127.0.0.1:40657: i/o timeout

UDP Datagram

UDP는 connectless 프로토콜로 연결 세션을 맺지 않는다. 따라서 UDP에서는 각 데이터를 서로에 대해서 독립적인 것으로 취급한다. 데이터를 쏘고 나서 잊어 버리는(fire and forget) 시스템으로, 상대방이 데이터를 수신했는지, 데이터를 올바르게 수신했는지, 순서에 맞게 수신했는지 등을 보장하지 않는다.

클라이언트는 그냥 상대편이 잘 받을 거라고 희망하고, 메시지를 전송한다. 서버는 수신한 메시에 대해서 클라이언트에게 응답을 보내는데, 마찬가지로 클라이언트가 잘 받을 거라고 희망하고 메시지를 전송한다.

네트워크 프로그래머 입장에서 체감할 수 있는 TCP와 UDP의 가장 큰 차이점은, 멀티스레드를 사용하지 않고도 동시에 여러 클라이언트의 메시지를 처리할 수 있다는 점일 거다. 세션이라는 게 없으니, 데이터가 오는 족족 처리하면 되는 거다. UDP 통신에 사용하는 주요한 메서드들은 다음과 같다.
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

UDP 서버 프로그램이다.
# cat udpserver.go
package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    service := ":1201"

    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkError(err) 
    
    conn, err := net.ListenUDP("udp", udpAddr)
    checkError(err)
    
    for {
        handleclient(conn)
    }
}   
    
func handleclient(conn *net.UDPConn) {
    var buf [512]byte
    _, addr, err := conn.ReadFromUDP(buf[0:])
    if err != nil {
        return
    }
    daytime := time.Now().String()
    conn.WriteToUDP([]byte(daytime), addr)
}   
    
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}       

테스트에 사용할 UDP 클라이언트 프로그램이다.
package main

import (
    "fmt"
    "net"
    "os"
)   

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage : %s host:port", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    
    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkError(err)
    
    conn, err := net.DialUDP("udp", nil, udpAddr)
    checkError(err)
    
    _, err = conn.Write([]byte("anything"))
    checkError(err)

    var buf [512]byte
    n, err := conn.Read(buf[0:])
    
    fmt.Println(string(buf[0:n]))
    os.Exit(0)
}   
    
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error : ", err.Error())
        os.Exit(0)
    }   
}       

The types Conn, PacketConn and Listener

TCP와 UDP의 API는 비슷하면서도 다르다. 각각 DialTCP와 DialUDP 메서드를 사용하며, TCPConn, UDPConn을 반환한다. 용도, 입력, 출력이 모두 비슷해서 인터페이스를 통일할 만하다. go는 Dial과 Conn이라는 인터페이를 제공한다. 이 인터페이스를 이용해서 TCP와 UDP메서드 모두를 사용할 수 있다.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage : %s host:port\n", os.Args[0])
        os.Exit(0)
    }
    service := os.Args[1]

    fmt.Println(service)
    conn, err := net.Dial("tcp", service)
    checkError(err)

    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkError(err)

    result, err := readFully(conn)
    checkError(err)

    fmt.Println(string(result))

    os.Exit(0)

}

func readFully(conn net.Conn) ([]byte, error) {
    defer conn.Close()

    result := bytes.NewBuffer(nil)
    var buf [512]byte
    for {
        n, err := conn.Read(buf[0:])
        result.Write(buf[0:n])
        if err != nil {
            if err == io.EOF {
                break
            }
            return nil, err
        }
    }
    return result.Bytes(), nil
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error())
        os.Exit(1)
    }
}

Raw sockets and the type IPConn

Socket은 고도로 추상화된 툴이다. 일반적인 TCP(UDP)/IP를 다루기에는 더 없이 편리하지만, 다른 프로토콜을 다룰 때는 어려움이 있다. Raw socket을 이용하면, IP 프로토콜을 조작하거나 TCP, UDP외에 다른 프로토콜을 이용할 수도 있다.

IP 계층위에는 TCP와 UDP만 있는게 아니다. protocol-numbers 사이트를 방문해 보자. 140개 이상의 프로토콜들을 볼 수 있을 거다. /etc/protocols에서도 확인할 수 있다.

TCP, UDP이외의 프로토콜을 사용하거나 혹은 자신만의 프로토콜을 개발해서 사용해야 한다면, raw 소켓을 사용하자.

Raw 소켓 예제 코드를 만들어보자. 만들 프로그램은 유명한 ping 프로그램의 간단 버전이다. Ping 프로그램은 ICMP 프로토콜을 사용한다. ICMP 프로토콜은 목적지 서버에 "ping" 메시지를 보내면, 목적지 서버는 이에 대한 응답 "pong" 메시지를 보낸다. 응답 여부와 응답에 걸리는 시간을 계산해서 서버와 네트워크의 상태를 테스트하는데 사용한다. ICMP에 대한 자세한 내용은 ICMP 프로그래밍문서를 참고하자. 이 문서에 보면 go 버전의 ping과 동일한 일을 하는 c 버전의 ping 프로그램을 확인할 수 있다. 두 개 코드를 비교해보는 것도 재미있을 거다.

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "host")
        os.Exit(1)
    }

    addr, err := net.ResolveIPAddr("ip", os.Args[1])
    if err != nil {
        fmt.Println("Resolution error", err.Error())
        os.Exit(1)
    }

    conn, err := net.DialIP("ip:icmp", nil, addr)
    checkError(err)

    var msg [512]byte
    msg[0] = 8  // echo
    msg[1] = 0  // code 0
    msg[2] = 0  // checksum, fix later
    msg[3] = 0  // checksum, fix later
    msg[4] = 0  // identifier[0]
    msg[5] = 13 //identifier[1]
    msg[6] = 0  // sequence[0]
    msg[7] = 37 // sequence[1]
    len := 8

    check := checkSum(msg[0:len])
    msg[2] = byte(check >> 8)
    msg[3] = byte(check & 255)

    fmt.Println("Write")
    _, err = conn.Write(msg[0:len])
    checkError(err)

    _, err = conn.Read(msg[0:])
    checkError(err)

    fmt.Println("Got response")
    if msg[5] == 13 {
        fmt.Println("identifier matches")
    }
    if msg[7] == 37 {
        fmt.Println("Sequence matches")
    }

    os.Exit(0)
}

func checkSum(msg []byte) uint16 {
    sum := 0

    // assume even for now
    for n := 1; n < len(msg)-1; n += 2 {
        sum += int(msg[n])*256 + int(msg[n+1])
    }
    sum = (sum >> 16) + (sum & 0xffff)
    sum += (sum >> 16)
    var answer uint16 = uint16(^sum)
    return answer
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s\n", err.Error())
        os.Exit(1)
    }
}