NAT-PNP 와 UPNP 를 이용한 홀펀칭 - (2) 


이번글에서는 이더리움에서 extip와 UPNP를 어떻게 사용하는지 배워보도록 하자. 

외부에서 192.168.10.11:80 서버와 통신하려면  그 사설 IP를 입력해바짜 아무 의미가 없다. 따라서 그 서버와 연결시켜주는 라우터를 통하게 되는데, 라우터의 공인IP인 193.24.171.247 로 보내되, 포트를 8028로 한다면 그것을 라우터는 192.168.10.11:80으로 포트포워딩해주는 것이다. 

즉 8028 포트는 내부의 192.168.10.12:80 IP/PORT와 매핑되는데 이것을 수동으로 미리 해 놓는다면 외부에서 접속하는데 아무 문제가 없을 것이다. 이렇게 수동으로 미리 매핑되어 있으면 extip를 그냥 사용하면 되고, 미리 정해져 있지 않아서 이더리움 프로그램에서 직접 매핑 설정을 해주려면 UPNP가 필요한 것이다. 

따라서 앞으로 살펴볼 코드를 예상해 볼 수 있는 것이, extip 는 특별히 하는것이 없을 것이며, 그냥 정해져있는 외부 IP를 사용만 할 것이며, (따라서 매핑 코드가 필요 없다) UPNP 는 위의 그림에서 라우터의 공인IP를 찾아서 외부IP로 설정하고, 특정 포트를 자신과 연결해주는 작업을 하는 코드가 있을 것이다. 

extip


// ExtIP assumes that the local machine is reachable on the given
// external IP address, and that any required ports were mapped manually.
// Mapping operations will not return an error but won't actually do anything.
func ExtIP(ip net.IP) Interface {
if ip == nil {
panic("IP must not be nil")
}
return extIP(ip)
}

type extIP net.IP

func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }

// These do nothing.
func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil }
func (extIP) DeleteMapping(string, int, int) error { return nil }

역시나 AddMapping / DeleteMapping처럼 매핑관련 메소드는 아무것도 안하며, 공인(외부)IP를 리턴해주는 ExternalIP 메소드는 미리 라우터에서 설정을 하여 nat 옵션으로 넣어준 ip 값을 그대로 사용한다. 

다음으로는 어디에서 AddMapping와 ExternalIP가 사용하는지 살펴보자.

if srv.NAT != nil {
if !realaddr.IP.IsLoopback() {
go nat.Map(srv.NAT, srv.quit, "udp", realaddr.Port, realaddr.Port, "ethereum discovery")
}
if ext, err := srv.NAT.ExternalIP(); err == nil {
realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port}
}
}

위 코드는 Server.Start() 메소드 안에 있는데 NAT옵션이 있는 경우 

1. nat.Map 메소드를 이용해서 실제port (이더리움 기본 포트는 30303) 와 NAT 서비스의 port 를 동일한 숫자로 매핑시켜준다. 


func Map(m Interface, c chan struct{}, protocol string, extport, intport int, name string) {
log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m)
refresh := time.NewTimer(mapUpdateInterval)
defer func() {
refresh.Stop()
log.Debug("Deleting port mapping")
m.DeleteMapping(protocol, extport, intport)
}()
if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
log.Debug("Couldn't add port mapping", "err", err)
} else {
log.Info("Mapped network port")
}
for {
select {
case _, ok := <-c:
if !ok {
return
}
case <-refresh.C:
log.Trace("Refreshing port mapping")
if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
log.Debug("Couldn't add port mapping", "err", err)
}
refresh.Reset(mapUpdateInterval)
}
}
}

UPNP나 NAT-PMP의 AddMapping 메소드를 호출하여 실제 nat 서버와 통신하여 설정해준다.extip는 위에서 언급한 것 처럼 아무것도 안하며 UPNP에서 어떻게 작동하는지에 대한 코드는 UPNP목차에서 살펴본다.

2. 외부ip를 가져와서 net.UDPAddr 를 만들어 줘서 realaddr 로 사용한다. 


// node table
if !srv.NoDiscovery {
cfg := discover.Config{
PrivateKey: srv.PrivateKey,
AnnounceAddr: realaddr,
NodeDBPath: srv.NodeDatabase,
NetRestrict: srv.NetRestrict,
Bootnodes: srv.BootstrapNodes,
Unhandled: unhandled,
}
ntab, err := discover.ListenUDP(conn, cfg)
if err != nil {
return err
}
srv.ntab = ntab
}

해당 addr 는 노드디스커버리를 위해 AnnounceAddr로 설정되는데 이것은 다른 이더리움 사용자 노드들이 자신의 노드디스커버리테이블에 넣을 주변노드정보에 사용 될 것이다. 


// ListenUDP returns a new table that listens for UDP packets on laddr.
func ListenUDP(c conn, cfg Config) (*Table, error) {
tab, _, err := newUDP(c, cfg)
if err != nil {
return nil, err
}
log.Info("UDP listener up", "self", tab.self)
return tab, nil
}

func newUDP(c conn, cfg Config) (*Table, *udp, error) {
udp := &udp{
conn: c,
priv: cfg.PrivateKey,
netrestrict: cfg.NetRestrict,
closing: make(chan struct{}),
gotreply: make(chan reply),
addpending: make(chan *pending),
}
realaddr := c.LocalAddr().(*net.UDPAddr)
if cfg.AnnounceAddr != nil {
realaddr = cfg.AnnounceAddr
}
// TODO: separate TCP port
udp.ourEndpoint = makeEndpoint(realaddr, uint16(realaddr.Port))
tab, err := newTable(udp, PubkeyID(&cfg.PrivateKey.PublicKey), realaddr, cfg.NodeDBPath, cfg.Bootnodes)
if err != nil {
return nil, nil, err
}
udp.Table = tab

go udp.loop()
go udp.readLoop(cfg.Unhandled)
return udp.Table, udp, nil
}

AnnounceAddr는 realaddr로 대입되며, Endpoing로 세팅되고, 


func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string, bootnodes []*Node) (*Table, error) {
// If no node database was given, use an in-memory one
db, err := newNodeDB(nodeDBPath, Version, ourID)
if err != nil {
return nil, err
}
tab := &Table{
net: t,
db: db,
self: NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs),
refreshReq: make(chan chan struct{}),
initDone: make(chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
rand: mrand.New(mrand.NewSource(0)),
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
}
if err := tab.setFallbackNodes(bootnodes); err != nil {
return nil, err
}
for i := 0; i < cap(tab.bondslots); i++ {
tab.bondslots <- struct{}{}
}
for i := range tab.buckets {
tab.buckets[i] = &bucket{
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
}
}
tab.seedRand()
tab.loadSeedNodes(false)
// Start the background expiration goroutine after loading seeds so that the search for
// seed nodes also considers older nodes that would otherwise be removed by the
// expiration.
tab.db.ensureExpirer()
go tab.loop()
return tab, nil
}

  self:  NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)) 로 노드디스커버리테이블에 세팅된다. Kademlia DHT알고리즘에 따라 해당 테이블에 대한 정보는 전세계에 얽혀있는 이더리움 네트워크간에 공유된다. 

UPNP

다시 언급하지만 UPNP의 핵심은 NAT서비스의 외부IP를 가져오고,포트를 로컬IP/PORT로 매핑하는 것이다.

먼저 nat 공통 인터페이스를 구현한 UPnP객체를 만들어 보자. 


// UPnP returns a port mapper that uses UPnP. It will attempt to
// discover the address of your router using UDP broadcasts.
func UPnP() Interface {
return startautodisc("UPnP", discoverUPnP)
}

type autodisc struct {
what string // type of interface being autodiscovered
once sync.Once
doit func() Interface

mu sync.Mutex
found Interface
}

func startautodisc(what string, doit func() Interface) Interface {
// TODO: monitor network configuration and rerun doit when it changes.
return &autodisc{what: what, doit: doit}
}

UPnP 생성 함수를 호출하면 내부적으로 autodisc 객체가 만들어 지는데  UPnP라는 문자열 이름과, 진짜 생성 함수가 매개변수로 들어가서 doit 변수에 대입된다. 이 doit (discoverUPnP) 을 통해 nat 인터페이스 구현체가 만들어져서 found에 최종대입되며, found를 통해서 외부IP를 가져온다든지, 포트매핑을 한다든지  할 것이다.

import (
.....
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway1"
"github.com/huin/goupnp/dcps/internetgateway2"
)

먼저 이더리움에서는 huin/goupnp 라이브러리를 사용하고 있다.

type upnp struct {
dev *goupnp.RootDevice
service string
client upnpClient
}

type upnpClient interface {
GetExternalIPAddress() (string, error)
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
DeletePortMapping(string, uint16, string) error
GetNATRSIPStatus() (sip bool, nat bool, err error)
}

upnp 구조체에서 upnpClient 인터페이스를 보면 외부IP 주소를 가져오는 함수(GetExternalIPAddress) 와 포트를 매핑해주는 함수(AddPortMapping)가 있음을 알 수 있다.

upnpClient 를 통해서 값을 가져오고 매핑하는 코드를 보면 아래와 같다.

func (n *upnp) ExternalIP() (addr net.IP, err error) {
ipString, err := n.client.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, errors.New("bad IP in response")
}
return ip, nil
}

func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error {
ip, err := n.internalAddress()
if err != nil {
return nil
}
protocol = strings.ToUpper(protocol)
lifetimeS := uint32(lifetime / time.Second)
n.DeleteMapping(protocol, extport, intport)
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
}

이제 upnpClient를 세팅하고 upnp객체를 만드는 코드를 살펴보자.


// discoverUPnP searches for Internet Gateway Devices
// and returns the first one it can find on the local network.
func discoverUPnP() Interface {
found := make(chan *upnp, 2)
// IGDv1
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
switch sc.Service.ServiceType {
case internetgateway1.URN_WANIPConnection_1:
return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{ServiceClient: sc}}
case internetgateway1.URN_WANPPPConnection_1:
return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
}
return nil
})
// IGDv2
... 생략 ...

for i := 0; i < cap(found); i++ {
if c := <-found; c != nil {
return c
}
}
return nil
}

// finds devices matching the given target and calls matcher for all
// advertised services of each device. The first non-nil service found
// is sent into out. If no service matched, nil is sent.
func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) {
devs, err := goupnp.DiscoverDevices(target)
if err != nil {
out <- nil
return
}
found := false
for i := 0; i < len(devs) && !found; i++ {
if devs[i].Root == nil {
continue
}
devs[i].Root.Device.VisitServices(func(service *goupnp.Service) {
if found {
return
}
// check for a matching IGD service
sc := goupnp.ServiceClient{
SOAPClient: service.NewSOAPClient(),
RootDevice: devs[i].Root,
Location: devs[i].Location,
Service: service,
}
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
upnp := matcher(devs[i].Root, sc)
if upnp == nil {
return
}
// check whether port mapping is enabled
if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat {
return
}
out <- upnp
found = true
})
}
if !found {
out <- nil
}
}

꽤나 복잡해보이지만 목적은 단순하다. 아래의 구조체에 값을 채우는 것이다. 값을 채운후에 client를 통해 외부ip값도 가져올수 있고, 포트매핑도 할 수 있게 된다.

.type upnp struct {
   dev     *goupnp.RootDevice
   service string
   client  upnpClient
}

이렇게 만들어진 client upnpClient 를 통해서 외부ip값을 가져오고 매핑하는 코드를 보면 아래와 같다.

func (n *upnp) ExternalIP() (addr net.IP, err error) {
ipString, err := n.client.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, errors.New("bad IP in response")
}
return ip, nil
}

func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error {
ip, err := n.internalAddress()
if err != nil {
return nil
}
protocol = strings.ToUpper(protocol)
lifetimeS := uint32(lifetime / time.Second)
n.DeleteMapping(protocol, extport, intport)
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
}

 ipString, err := n.client.GetExternalIPAddress() 로 외부ip를 가져오며,
n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS) 로 포트매핑을 해주고 있다.



1. [이더리움에서 배우는 Go언어]   chan chan 이란?
2. [이더리움에서 배우는 Go언어]   1급함수활용과 채널
3. 
[이더리움에서 배우는 Go언어]   nat 옵션 이야기 - (1)

nat 옵션 이야기 - (1)

이번 시리즈에서는 네트워크 인프라의 내부에서 사설IP로 동작하여 공인IP가 없는 노드끼리 어떻게 통신 할 수 있는지에 대해서 알아 본다. 첫번째 글에서는 간략한 정의를 내려보며  이더리움에서 사용되는 옵션들을 살펴볼 것이며 두번째 글에서는 extip와 upnp를 구체적으로 살펴보려 한다. (참고로 nat-pnp는 upnp와 비슷하므로 생략)


홀펀칭

홀펀칭(막힌 구멍을 뚫는) 필요한 이유는 피씨들이 모두 자신의 개인 고정 IP를 사용하는게 아니기 때문이다. 많은 경우 NAT라는 장비를 통해서 앞쪽에 대리자/게이트웨이를 두고, 내부 IP/PORT를 따로 가지고 있기 때문에, 클라이언트들 끼리 직접 연결은 불가능하며 외부와 통신하려면 내부아이디와 외부로 노출되는 IP/PORT간에 매핑등이 되어 뚤려 있어야 하는데 이렇게 뚤어 주는 것을 홀펀칭이라고 한다. 게임등에서 많이 사용되며 이더리움 또한 외부의 무작위 노드들과 서로 양방향 통신을 해야 하기 때문에 반드시 필요하다. 


NAT-PMP 와 UPNP

NAT-PMP 및 UPnP(Universal Plug And Play)는 특히 인터넷 애플리케이션이 홈 라우터 및 게이트웨이를 구성하여 수동 포트 전달 구성을 우회할 수 있도록 하는 기술이다. 동일한 개념이라고 보면 되며 NAT 포트 매핑의 자동화하는 서로 다른 구현 방식일 뿐이다.일반적으로 UPnP 프로토콜은 Windows/BSD/Linux 시스템에서 사용되고 NAT-PMP는 Apple 시스템에서 사용된다. 이러한 방법을 활용하려면 NAT-PMP 또는 UPnP 지원 하드웨어가 있어야 한다. 요즘은 많은 경우 지원한다. 

IpTime 에서 설정 모습

NAT 이란?

NAT동작방식은 위에 간단히 설명한 것처럼, 내부에서 외부로 패킷을 보낼 때, NAT장치는 위부IP와 매핑된 내부를 기록하고 있다가, 해당 외부IP를 통해서 패킷이 날라오면 그것을 해당 내부IP로 보내준다. NAT에 매핑기록이 없으면 보통 드롭된다. 이 얘기는 내부에서 보내야 매핑정보가 남는다는 것인데, 외부에서 내부의 프로그램으로 보낼 땐 어떻게 하냐? 그건 수동으로 매핑정보를 입력해야 한다. 즉 외부52번은 내부 129번으로 매핑한다라는 기록이 있으면, 129번으로 포워딩 해주면 된다. 이후 설명될 NAT-PMP, UPNP는 이 설정이자동으로 할당되며, (인바운드매핑)을 요청하여 기록되게 할 수 있다. 

NAT-PMP 란? 

NAT 포트 매핑 프로토콜 (NAT-PMP)은 사용자의 노력없이 NAT (Network Address Translation) 설정 및 포트 전달 구성을 자동으로 설정하기위한 네트워크 프로토콜입니다. 이 프로토콜은 NAT 게이트웨이의 외부 IPv4 주소를 자동으로 결정하고 응용 프로그램이 통신 매개 변수를 피어와 통신 할 수있는 수단을 제공합니다. NAT-PMP는 2005 년에 많은 NAT 라우터에서 구현 된보다 일반적인 ISO 표준 [2] 인터넷 게이트웨이 장치 프로토콜 대신 Apple에 의해 도입되었습니다. 이 프로토콜은 RFC 6886의 IETF (Internet Engineering Task Force)에 의해 RFC (정보 요청 요청)로 게시되었습니다. NAT-PMP는 UDP (User Datagram Protocol)를 통해 실행되며 포트 번호 5351을 사용합니다. 포트 전달은 일반적으로 STUN 방법을 사용하여 수행 할 수없는 활동을 허용하지 않으므로 내장 된 인증 메커니즘이 없습니다. STUN에 비해 NAT-PMP의 이점은 STUN 서버가 필요없고 NAT-PMP 매핑에 알려진 만료 시간이있어 응용 프로그램이 비효율적 인 연결 유지 패킷을 보내지 않도록 할 수 있다는 것입니다. NAT-PMP는 포트 제어 프로토콜 (PCP)의 전신입니다. [3]      - 위키발췌- 

UPNP란? 

유니버설 플러그 앤 플레이 (UPnP)는 UPnP 포럼이 공표한 컴퓨터 네트워크 프로토콜의 집합이다. UPnP의 목표는 장치들을 부드럽게 연결하고 가정 네트워크와 회사 환경의 기능(데이터 공유, 통신, 엔터테인먼트)을 단순화하는 것이다. UPnP는 개방된 인터넷 기반의 통신 표준 기반의 UPnP 장치 제어 프로토콜을 정의하고 출판함으로써 이를 달성한다. UPnP라는 용어는 컴퓨터에 직접 장치를 유동적으로 부착하는 기술인 플러그 앤 플레이에서 비롯한 것이다.  UPNP의 기본 프로토콜이 (SSDP)이 UDP 베이스이다.

이러한 NAT-PNP,UPNP에는 보안문제가 있는데, 이것은 여기서 다루는 범위 밖이라 생략한다.

이더리움에서의 홀펀칭 소스 위치 [소스]

이더리움에서는 노드디스커버리 즉 주변에 접속할 대상을 물색하는데 UDP를 사용하며 이때 홀펀칭이 사용되어 이후 프로토콜에 의한 데이터교환에서는 TCP를 사용한다. 이더리움 코어에서는 p2p 폴더 아래의 nat 폴더에 관련된 기능들을 제공하는 소스가 있다. 참고로 TCP,UDP 모두 디폴트 포트로 30303을 사용한다. 30301은 단지 bootnode에 의해서만 기본 UDP 디스커버리 포트로 사용된다.

이더리움에서 NAT 옵션 설정 

--nat value   NAT port mapping mechanism (any|none|upnp|pmp|extip:<IP>) (default: "any")

extip:77.12.33.4: 주어진 IP로 고정 매핑 되었다고 설정함. port는 수동매핑되있어야하고~
any   : 자동으로 먼저 탐지된 메커니즘을 사용. (디폴트)
upnp : 유니버셜 플러그앤 플레이 프로토콜 사용 
pmp  : 자동으로 감지된 게이트웨이 어드레스를 이용하여 
NAT-PMP 를 사용. 
pmp:192.168.0.1 : 주어진 게이트웨어 어드레스를 이용하여  NAT-PMP를 사용. 
none 또는 off : 설정된게 없다는 의미로. 스스로가 외부접근가능한 IP면 이렇게 하면 된다. 참고로 nat옵션자체를 설정안한것과 같다. 내부적으론 NAT변수에 둘다 nil을 대입함. 

NATFlag = cli.StringFlag{
Name: "nat",
Usage: "NAT port mapping mechanism (any|none|upnp|pmp|extip:<IP>)",
Value: "any",
}

// setNAT creates a port mapper from command line flags.
func setNAT(ctx *cli.Context, cfg *p2p.Config) {
if ctx.GlobalIsSet(NATFlag.Name) {
natif, err := nat.Parse(ctx.GlobalString(NATFlag.Name))
if err != nil {
Fatalf("Option %s: %v", NATFlag.Name, err)
}
cfg.NAT = natif
}
}

GlobalIsSet 함수를 통해 해당 옵션이 커맨드라인에서 세팅되었는지 확인하여 있으면 파싱

func Parse(spec string) (Interface, error) {
var (
parts = strings.SplitN(spec, ":", 2)
mech = strings.ToLower(parts[0])
ip net.IP
)
if len(parts) > 1 {
ip = net.ParseIP(parts[1])
if ip == nil {
return nil, errors.New("invalid IP address")
}
}
switch mech {
case "", "none", "off":
return nil, nil
case "any", "auto", "on":
return Any(), nil
case "extip", "ip":
if ip == nil {
return nil, errors.New("missing IP address")
}
return ExtIP(ip), nil
case "upnp":
return UPnP(), nil
case "pmp", "natpmp", "nat-pmp":
return PMP(ip), nil
default:
return nil, fmt.Errorf("unknown mechanism %q", parts[0])
}
}

커맨드라인에서 읽은 옵션을 그것과 매칭되는 nat.Interface 상속타입을 만들어 cfg.NAT 에 설정 한다.아래에 nat.Interface 를 보면 로컬포트와 인터넷에 접속되는 포트와의 매핑을 추가하는 메소드 (시간이 지나면 자동으로 해제된다) 와 외부 인터넷과 접점인 게이트웨이의 IP를 알 수 있는 메소드가 있다. Golang은 덕타이핑이 기본이므로 해당 메소드를 구현하기만 하면 자바나 C++상속코딩 절차가 필요없이 다형적으로 사용 할 수 있게 된다. 

type Interface interface {

AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error
DeleteMapping(protocol string, extport, intport int) error

ExternalIP() (net.IP, error)
String() string
}


Any

func Any() Interface {
return startautodisc("UPnP or NAT-PMP", func() Interface {
found := make(chan Interface, 2)
go func() { found <- discoverUPnP() }()
go func() { found <- discoverPMP() }()
for i := 0; i < cap(found); i++ {
if c := <-found; c != nil {
return c
}
}
return nil
})
}

UPnP와 PMP 중 먼저 찾아지는것을 리턴한다. 2개의 버퍼를 가진 채널을 만들어서 for문을 capability (여기서는 2) 만큼 돌면서 먼저 보내지면서 nil 로 보내지지 않은 것을 리턴 해준다.

ExtIp


// ExtIP assumes that the local machine is reachable on the given
// external IP address, and that any required ports were mapped manually.
// Mapping operations will not return an error but won't actually do anything.
func ExtIP(ip net.IP) Interface {
if ip == nil {
panic("IP must not be nil")
}
return extIP(ip)
}

type extIP net.IP

func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }

// These do nothing.
func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil }
func (extIP) DeleteMapping(string, int, int) error { return nil }

주어지는 외부IP를 가지고 extIP객체를 만들어 리턴. 외부IP로 고정되어있기 때문에 매핑과 관련된 함수가 의미없다.

UPnP

func UPnP() Interface {
return startautodisc("UPnP", discoverUPnP)
}

노드 디스커버리를 dscoverUPnP 를 가지고 시작. 

PMP


// PMP returns a port mapper that uses NAT-PMP. The provided gateway
// address should be the IP of your router. If the given gateway
// address is nil, PMP will attempt to auto-discover the router.
func PMP(gateway net.IP) Interface {
if gateway != nil {
return &pmp{gw: gateway, c: natpmp.NewClient(gateway)}
}
return startautodisc("NAT-PMP", discoverPMP)
}

노드 디스커버리를 dscoverPMP 를 가지고 시작. 

다음 글에서는 ExtIP 와 UPnP 에 대해서 정밀하게 관찰해보자.




1. [이더리움에서 배우는 Go언어]   chan chan 이란?
2. [이더리움에서 배우는 Go언어]   1급함수활용과 채널
3. 
[이더리움에서 배우는 Go언어]  nat 옵션 이야기 - (1)



 1급함수활용과 채널

지난 글에서는 채널에 채널을 보내는 chan chan 에 대해서 배워 보았는데요, 기억이 안난다면 이야기가 이렇습니다. 내가 (소비자) 먹을 준비됬으면 나의 주소와 함께 알려드릴께요. 당신은 (생산자) 만들면 내 주소로 피자를 보내주세요.
즉 생산자 주도적이 아니라, 소비자 주도적이다. 

이번 글에서도 채널과 관련된 이야기를 해 보겠습니다. 이번에는 채널에 함수를 보내는 방식입니다. 이야기는 이렇습니다.
내가 피자만드는 방식(함수)를 알려줄께요. 그 방식대로 피자를 만들어서 보내주세요. 주소는 피자만드는 방식(함수) 끝에 적혀져있답니다.

먼가 감이 잡히시나요? 안잡혀도 괜찮습니다. 이제부터 글보다 익숙한 코드로 배워 볼 것이니까요. 혹시나 채널과 고루틴 자체를 모르신다면 위에 링크 글이나 구글에서 검색해서 알아보시길 바랍니다. 약간의 이해는 필요합니다.

먼저 이더리움에서 어떻게 사용되었는지 코드를 보고 진도를 나가겠습니다.(쌍괄식?)

type peerOpFunc func(map[discover.NodeID]*Peer) .... srv.peerOp = make(chan peerOpFunc) .... // PeerCount returns the number of connected peers.
func (srv *Server) PeerCount() int {
var count int
select {
case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }:
<-srv.peerOpDone
case <-srv.quit:
}
return count
}

간단히 소스를 설명하자면 먼저 NodeID와 Peer포인터를 쌍으로 갖는 맵을 매개변수로 하는 함수를 하나의 타입으로 선언했으며(peerOpFunc), 그것에 대한 채널(srv.peerOp)을 만들었습니다. 그리고 Peer의 갯수를 세는 함수에서 해당 채널에 직접 구현된 함수 func(ps map[discover.NodeID]*Peer) { count = len(ps) }:를 넘기고 있습니다. 

case op := <-srv.peerOp:
op(peers)
srv.peerOpDone <- struct{}{}

넘겨진 함수는 run이라는 함수내에서 받아서 위와 같이 처리(보내진 함수를 호출) 합니다. 이런 핑퐁관계는 파이썬,자바스크립트나 최근 코틀린에서도 지원하고 있는 코루틴과 비슷하며, 함수내에서 무한loop를 돌면서 다양한 이벤트를 기다렸다가 해당되는 이벤트에 맞게 처리되는 모습은 일견 액터패턴도 유추시킵니다. 액터패턴은 하나의 큐에서 넘어오는 일련의 명령(+데이터)들을 기다렸다가 처리한다 치면 위의 코드에서는 select를 통해서 펼쳐놓은 모습입니다. 어쨋거나 둘 다 직접 락을 거는 Mutex 식의 사용을 지양하고 쓰레드들의 동기 처리를 보다 분명하게 에러가 없게 처리하기 위한 방식인거죠. 

자 그럼 왜 이렇게 함수를 넘겨서 처리 하는 걸까요? 이제 부터 살펴보겠습니다. 


const (
OP_ADD = 1 << iota
OP_SUB
OP_MUL
)

type Calculator struct {
acc float64
}


func (c *Calculator) Do(op int, v float64) float64 {
switch op {
case OP_ADD:
c.acc += v
case OP_SUB:
c.acc -= v
case OP_MUL:
c.acc *= v
default:
panic("unhandled operation")
}
return c.acc
}

func main() {
var c Calculator
fmt.Println(c.Do(OP_ADD, 100)) // 100
fmt.Println(c.Do(OP_SUB, 50)) // 50
fmt.Println(c.Do(OP_MUL, 2)) // 100
}

위에 간단한 계산기의 코드가 있습니다.
메소드 Do에는 더하기,빼기,곱하기가 모두 정의 되어 있구요. 너무 많은 것을 하는것 처럼 보입니다.
추가적으로 제곱하기,평균내기 등이 들어간다면 더욱 더 하나의 함수가 복잡해 질 거 같습니다. 

개인적으로 코딩 할 때 간단히 2가지 법칙을 염두해 두는데요. ( SOLID,DRY법칙처럼 내가 만들었습니다 ㅎ)
1. 간단 책임의 원칙 :  함수(객체)는 최대한 심플하게 하자. 책임을 적게 지자.
2. 책임 전가의 원칙:  책임은 최대한 외부에 전가시키자. 주입받도록 하자.

근데 솔직히 자신이 모두 다 짜고,개발하며,한번 만든거 이상의 추가 업데이트가 있을 가능성이 적다? 하면 앞으로 나올 내용 처럼 생쇼 안부리고 직관적이게 코딩하는게 더 좋습니다. 마술을 부리거나 지나친 패턴사용으로 오버엔지니어링 되는게 더 문제겠지요.

Do는 무엇을 받아서 실행만 시키게 하면 안될 까요? 즉 책임을 적게 가지고 가자는 겁니다. 
Go에서의 방식은 아래와 같은데요. 


type Calculator struct {
acc float64
}

type opfunc func(float64, float64) float64

func (c *Calculator) Do(op opfunc, v float64) float64 {
c.acc = op(c.acc, v)
return c.acc
}

func Add(a, b float64) float64 { return a + b }
func Sub(a, b float64) float64 { return a - b }
func Mul(a, b float64) float64 { return a * b }

func main() {
var c Calculator
fmt.Println(c.Do(Add, 5)) // 5
fmt.Println(c.Do(Sub, 3)) // 2
fmt.Println(c.Do(Mul, 8)) // 16
}

이렇게 어떤 역할을 하는 함수 자체를 Do의 매개변수로 넣는 방식입니다.  즉 매개변수 2개를 받아서 float64타입을 리턴하는  함수타입이면 모두 ㅇㅋ 입니다.  이제 Do는 그냥 실행만 하면 땡이네요. 굉장히 간단해 졌으며, 추가적인 계산방식이 생기면 외부에서 만들어서 주입해주면 됩니다. 좀 더 유연해 졌다고 볼 수 있습니다. 


func Sqrt(n, _ float64) float64 {
return math.Sqrt(n)
}

func main() {
var c Calculator
c.Do(Add, 16)
c.Do(Sqrt, 0) // operand ignored
}

제곱(sqrt) 로직 추가시에는  쓸때없는 매개변수를 하나 더 넣어 줘야하는 상황이 됩니다. 이런 경우는 


func Add(n float64) func(float64) float64 {
return func(acc float64) float64 {
return acc + n
}
}

func Sqrt() func(float64) float64 {
return func(n float64) float64 {
return math.Sqrt(n)
}
}

func (c *Calculator) Do(op func(float64) float64) float64 {
c.acc = op(c.acc)
return c.acc
}
func main() {
var c Calculator
c.Do(Add(2))
c.Do(Sqrt()) // 1.41421356237
}

이런식으로 커링(부분함수)식으로 처리하면 됩니다. 공통 타입의 함수타입을 만들기 위해서 겉에 하나의 함수를 더 감싸는거죠. 겉 함수에서는 리턴 될 내부 함수에서 사용 될 매개변수를 미리 넘겨주게 됩니다. 
자 이제 1급함수의 역할을 2가지로 맛보기 했는데요 (매개변수로써의 함수, 리턴값으로써의 함수) 


이제 1급함수를 채널에서 사용해 보겠습니다.
먼저 채널을 어떤 경우에 사용하는지 잠시 살펴보겠습니다. 


type Mux struct {
mu sync.Mutex
conns map[net.Addr]net.Conn
}

func (m *Mux) Add(conn net.Conn) {
m.mu.Lock()
defer m.mu.Unlock()
m.conns[conn.RemoteAddr()] = conn
}

func (m *Mux) Remove(addr net.Addr) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.conns, addr)
}
}

conns 맵이 여러 쓰레드에서 한번에 사용되면, 접근 경쟁에 의한 버그가 발생할 위험이 생기겠지요?
따라서 소스에서 처럼 Mutex로 진입시 락을 걸어서 혼자 만 사용하겠다고 알려줘야 합니다.
이렇게 직접 락을 걸면서 코딩을 하면 동시성 프로그래밍에서 알기 힘든 버그를 만들수 있다고 구루들이 경고하잖아요.
그래서 다양한 동시성패턴들이 생겨났는데, Go언어에서는 자신(코루틴) 이 건드리고, 남(다른 코루틴)에게 패스하고 그 패스를 받은 사람이 또 건드리고, 패스하고 이런 전략을 사용합니다.직접 메모리를 공유하지말고 서로 패스를  통해서 공유하자는 겁니다. (무엇인가 액션을 던진다고 생각하면 액터패턴과도 비슷합니다. 간단히 말해 그걸 넣고 받는게 큐면 액터가 되는 것이고, 채널이면 고루틴이 되는 것이죠.)

아래 소스를 보겠습니다.


type Mux struct {
add chan net.Conn
remove chan net.Addr
sendMsg chan string
}

func (m *Mux) Add(conn net.Conn) {
m.add <- conn
}

func (m *Mux) Remove(addr net.Addr) {
m.remove <- addr
}

func (m *Mux) SendMsg(msg string) error {
m.sendMsg <- msg
return nil
}

func (m *Mux) loop() {
conns := make(map[net.Addr]net.Conn)
for {
select {
case conn := <-m.add:
m.conns[conn.RemoteAddr()] = conn
case addr := <-m.remove:
delete(m.conns, addr)
case msg := <-m.sendMsg:
for _, conn := range m.conns {
io.WriteString(conn, msg)
}
}
}
}

Add,Remove,SendMsg 각각의 채널을 만들고, 해당 함수에서는 해당 로직을 수행하길 바란다고 이벤트(+데이터) 를 패스하기만 합니다. 그러면 패스를 받은 loop메소드에서는 (이게 액터역할을 하지요) 어떤 후보 이벤트가 발생할 때까지 select 에서 대기하고 있다가 해당 이벤트가 발생하면 그것을 처리해 줍니다.

m.add <- conn

이것은 m.add채널에 conn이라는 이벤트(+데이터)를 패스하는 것이고

case conn := <-m.add:

이것은 m.add채널에서 conn이라는 이벤트(+데이터)를 받는 것이겠지요.

근데 처음에 Calculator 코드를 보면 Do안에서 모든것을 다 처리해서 Do는 그냥 로직을 외부에서 주입받는 방식으로 바꾼거 기억하시나요? 기억안나면 위로 올라가서 다시 확인하세요.
여기도 마찬가지로 loop함수에서 모든것을 다 하는게 싫습니다. loop함수도 그냥 주입받은 로직을 처리하고 싶어지는데요
이때 채널에 함수를 패스하게 됩니다. 아래 처럼 말이죠.

func (m *Mux) Add(conn net.Conn) {
m.ops <- func(m map[net.Addr]net.Conn) {
m[conn.RemoteAddr()] = conn
}
}

자 이제 왜 이더리움에서 저런식의 코드를 짯는지 이해가 되시나요? (올라가지 마시라고 아래 다시 적어봤습니다) 

// PeerCount returns the number of connected peers.
func (srv *Server) PeerCount() int {
var count int
select {
case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }:
<-srv.peerOpDone
case <-srv.quit:
}
return count
}


func (srv *Server) run(dialstate dialer) {

    running:
for {
...
select {
    case <-srv.quit:
     break running
    case n := <-srv.addstatic:
    ...
     case n := <-srv.removestatic:
     ...
     case op := <-srv.peerOp:
     // This channel is used by Peers and PeerCount.
     op(peers)
    srv.peerOpDone <- struct{}{}
    ...
    }

}

위의 run 함수에서 모든것을 처리하기 싫어서 입니다. 로직은 외부에서 주입받고 싶은것이죠.
소스에 대한 설명을 하자면 PeerCount 함수에서 피어의 갯수를 세는 로직을 만들어서 다른 쓰레드에게  srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }: 이렇게 보내주고 기다리면, 다른 쓰레드에서는 op(peers)처럼 실행만 한뒤에 다 했다고 peerOpDone <- struct{}{} 이렇게 익명구조체 생성하여 채널로 보내서 알려줍니다. 그러면 PeerCount에서는 <-srv.peerOpDone 이렇게 기다리다가 종료하고 있습니다 . 

근데 안타깝게도 실제 이더리움소스에서 모든것을 주입받는 것은 아닙니다. run안에서는 그냥 이벤트만 호출받고 그에 해당하는 로직을 처리하거나 위임 처리 한 후에 또 다른 채널에 패스를 하기도 합니다. 실전은 항상 단순하진 않지요.

마지막으로 해당 run전체 함수를 보여드리면서 글을 끝마치겠습니다.


func (srv *Server) run(dialstate dialer) {
defer srv.loopWG.Done()
var (
peers = make(map[discover.NodeID]*Peer)
inboundCount = 0
trusted = make(map[discover.NodeID]bool, len(srv.TrustedNodes))
taskdone = make(chan task, maxActiveDialTasks)
runningTasks []task
queuedTasks []task // tasks that can't run yet
)
// Put trusted nodes into a map to speed up checks.
// Trusted peers are loaded on startup and cannot be
// modified while the server is running.
for _, n := range srv.TrustedNodes {
trusted[n.ID] = true
}

// removes t from runningTasks
delTask := func(t task) {
for i := range runningTasks {
if runningTasks[i] == t {
runningTasks = append(runningTasks[:i], runningTasks[i+1:]...)
break
}
}
}
// starts until max number of active tasks is satisfied
startTasks := func(ts []task) (rest []task) {
i := 0
for ; len(runningTasks) < maxActiveDialTasks && i < len(ts); i++ {
t := ts[i]
srv.log.Trace("New dial task", "task", t)
go func() { t.Do(srv); taskdone <- t }()
runningTasks = append(runningTasks, t)
}
return ts[i:]
}
scheduleTasks := func() {
// Start from queue first.
queuedTasks = append(queuedTasks[:0], startTasks(queuedTasks)...)
// Query dialer for new tasks and start as many as possible now.
if len(runningTasks) < maxActiveDialTasks {
nt := dialstate.newTasks(len(runningTasks)+len(queuedTasks), peers, time.Now())
queuedTasks = append(queuedTasks, startTasks(nt)...)
}
}

running:
for {
scheduleTasks()

select {
case <-srv.quit:
// The server was stopped. Run the cleanup logic.
break running
case n := <-srv.addstatic:
// This channel is used by AddPeer to add to the
// ephemeral static peer list. Add it to the dialer,
// it will keep the node connected.
srv.log.Trace("Adding static node", "node", n)
dialstate.addStatic(n)
case n := <-srv.removestatic:
// This channel is used by RemovePeer to send a
// disconnect request to a peer and begin the
// stop keeping the node connected
srv.log.Trace("Removing static node", "node", n)
dialstate.removeStatic(n)
if p, ok := peers[n.ID]; ok {
p.Disconnect(DiscRequested)
}
case op := <-srv.peerOp:
// This channel is used by Peers and PeerCount.
op(peers)
srv.peerOpDone <- struct{}{}
case t := <-taskdone:
// A task got done. Tell dialstate about it so it
// can update its state and remove it from the active
// tasks list.
srv.log.Trace("Dial task done", "task", t)
dialstate.taskDone(t, time.Now())
delTask(t)
case c := <-srv.posthandshake:
// A connection has passed the encryption handshake so
// the remote identity is known (but hasn't been verified yet).
if trusted[c.id] {
// Ensure that the trusted flag is set before checking against MaxPeers.
c.flags |= trustedConn
}
// TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them.
select {
case c.cont <- srv.encHandshakeChecks(peers, inboundCount, c):
case <-srv.quit:
break running
}
case c := <-srv.addpeer:
// At this point the connection is past the protocol handshake.
// Its capabilities are known and the remote identity is verified.
err := srv.protoHandshakeChecks(peers, inboundCount, c)
if err == nil {
// The handshakes are done and it passed all checks.
p := newPeer(c, srv.Protocols)
// If message events are enabled, pass the peerFeed
// to the peer
if srv.EnableMsgEvents {
p.events = &srv.peerFeed
}
name := truncateName(c.name)
srv.log.Debug("Adding p2p peer", "name", name, "addr", c.fd.RemoteAddr(), "peers", len(peers)+1)
go srv.runPeer(p)
peers[c.id] = p
if p.Inbound() {
inboundCount++
}
}
// The dialer logic relies on the assumption that
// dial tasks complete after the peer has been added or
// discarded. Unblock the task last.
select {
case c.cont <- err:
case <-srv.quit:
break running
}
case pd := <-srv.delpeer:
// A peer disconnected.
d := common.PrettyDuration(mclock.Now() - pd.created)
pd.log.Debug("Removing p2p peer", "duration", d, "peers", len(peers)-1, "req", pd.requested, "err", pd.err)
delete(peers, pd.ID())
if pd.Inbound() {
inboundCount--
}
}
}

srv.log.Trace("P2P networking is spinning down")

// Terminate discovery. If there is a running lookup it will terminate soon.
if srv.ntab != nil {
srv.ntab.Close()
}
if srv.DiscV5 != nil {
srv.DiscV5.Close()
}
// Disconnect all peers.
for _, p := range peers {
p.Disconnect(DiscQuitting)
}
// Wait for peers to shut down. Pending connections and tasks are
// not handled here and will terminate soon-ish because srv.quit
// is closed.
for len(peers) > 0 {
p := <-srv.delpeer
p.log.Trace("<-delpeer (spindown)", "remainingTasks", len(runningTasks))
delete(peers, p.ID())
}
}

레퍼런스:
https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions
https://github.com/ethereum/go-ethereum/blob/master/p2p/server.go


chan chan 이란?

블록체인을 비롯해서 서버쪽 개발 언어로 go 언어가 엄청나게 부상하고 있는데요. 이 글에서는 go 언어의 가장 큰 특징 중 하나인 고루틴/채널에 대하여 간단히 설명하며, 좀 더 고급기술인 채널 위에 채널을 얹는 문법을 이더리움 코드를 통해서 살펴봅니다.

golang은 C,C++ 보다 실전적으로 고성능이라고 생각하는데, 그 근거가 바로 고루틴과 채널입니다. 물론 C++도 쓰레드가 있긴 하지만 코딩 할 때, 아~ 이 부분은 좀 시간 걸리겠는데 하더라도 쓰레드로 빼긴 부담스럽게 느껴지는게 사실이며, 많은 C++ 개발자들이 그러한 습관에 코딩을 하고 있습니다. 하지만 GO언어는 언어자체에서 경량쓰레드를 지원하므로, 너무나도 자연스럽게 쓰레드화하고, 채널을 통해서 그것의 결과에 해당하는 값을 받아 볼 수 있게 하고 있습니다.CPU를 너무나 적극적으로 사용하고 있는 이 과정이 너무 쉽기 때문에 golang 으로 짜여진 오픈소스를 보면 고루틴,채널이 범벅되어 있는 것을 많이 볼 수 있습니다. (아무리 쉽다고 해도 고루틴,채널 범벅이면 시리얼한것보다는 복잡해 보이는것은 사실입니다) 혹시 고루틴과 채널에 대해서 처음 듣는 것이라면 제가 예전에 번역한 고 언어에서의 동시성 모델(CSP) 를 먼저 읽고 이 포스트를 보시면 좋을 거 같습니다.

(혹시 고성능이 무조건 최고다라고 생각하시는 분은 없었으면 합니다. 0.0000001초가 걸리던 0.00001초가 걸리던, 일반적인 현실에서는 0.01초만 걸려도 아무 상관없는 솔루션이 대부분이기 때문입니다. 성능보다는 다른 지표로 언어를 선택해야하는 경우가 더 많습니다.그래서 파이썬이 잘나가는거겠지요.^^) 

고루틴

경량쓰레드인 고 루틴을 만드는것은 너무나 간단하다. 로직을 짜다가 이것은 조금 시간이 걸리겠다 싶으면 그냥 고루틴으로 만들 수 있다. 


func main()
{
go doSomthing() // GO 루틴 시작

}

func doSomthing()
{
.... 무엇인가 한다 ...
}


이렇게 일반 함수 앞에 go 라고 붙히기만 하면 경량 쓰레드로 동작한다.

채널

저렇게 고루틴을 만들고 나면, 고루틴에서 어떤 작업을 했을 때, 그 결과를 받고 싶을 수가 있다. 보통 다른 언어로는 동시성을 보장하는 큐를 만들어서 매개변수로 보내서 처리 할 텐데, (혹은 이벤트 동기화) 고 언어에서는 더욱 간편한 것이 있다. 이걸 채널이라고 부르는데 이거 덕분에 고언어에서 쓰레드를 사용하는 것은 더욱 쉬워 지며, 안정적이게 된다. Mutex 를 사용하지 않고 액터패턴처럼 구현되기 때문인데 아래에 예를 살펴보자.


func main()
{
ch := make(chan int) // 통신용 채널 생성

go sendingGoRoutine(ch) // GO 루틴 시작
go receivingGoRoutine(ch) // GO 루틴 시작
..
}
// 송신 코루틴
func sendingGoRoutine(ch chan int)
{
... 어떤 작업을 함 ...

ch <- 45 // 결과 값을 보냄.
}
// 수신 코루틴
func receivingGoRoutine(ch chan int)
{
// 송신 채널로부터 값을 올때까지 기다리다가 오면, v 에 입력
v := <- ch
fmt.Println("Received value ", v)
}

ch := make(chan int) // 통신용 채널 생성

채널은 위와 같이 만들며 int 타입을 전송할 수 있는 채널을 만든 다는 것이다.
생산자가 ch 에 어떤 값을 쓰면, 소비자는 ch 을 기다리다가 값이 들어오면 처리한다.
위의 예에서 생산자는

func sendingGoRoutine(ch chan int)
{
   ...
  ch <- 45
}
이것이 되며, ch <- 45 , 즉 채널에 45를 송신해 주고 있다.
소비자는
func receivingGoRoutine(ch chan int)
{
   // 채널로부터 값을 받으면 v 에 입력
   v := <- ch
   fmt.Println("Received value ", v)
}
이것이 되며, v := <-ch , 즉 채널로부터 값을 수신 받기를 기다리다가 값이 들어오면 처리한다.


chan chan 이란?

requestChan := make(chan chan string)

자 채널에 대해서 공부해 봤는데. chan chan 이렇게 두개를 연속으로 사용 할 수도 있다.무엇 일까?
채널에 값을 보내는게 아니라, 채널에 채널(문자열을 통신하는)을 보내고 있다. 즉
chan 이 "보내면 받을께" 라면 
chan chan 은 "내가 준비됬다는것을 알려줄께 기다리다, 내가 보낸 채널을 통해 보내" 이다. 양방향 처리를 할 수 있다.
아래 그림을 보자. 고루틴 C (소비자 역할)는 고루틴 D (생산자 역할) 에게 "내가 준비 됬다는 것을 chan chan채널을 통해서 알려주면, 채널을 통해 내가 보낸 채널을 통해 니가 나한테 생산한것을 건네줘" 라고 하는 것을 도식화 한 것이다.

Screenshot

1. 고루틴 C 와 고루틴 D은 chan chan 채널(requestChan := make(chan chan string)을 함께 가지고 있다.

Screenshot

2. 고루틴 C (소비자) 는 처리 할 준비가 되었다고, 알려주며 이 채널 (여기선 response 채널)을 통해서 보내라고 고루틴 D에게  말해준다.

Screenshot

3. 고루틴 D (생산자) 는  고루틴 C에게 받은  채널 (여기선 response 채널)을 통해서 고루틴 C에게 문자열을 보낸다.

package main

import "fmt"
import "time"

func main() {

requestChan := make(chan chan string)

go goroutineC(requestChan)
go goroutineD(requestChan)

time.Sleep(time.Second)

}

func goroutineC(requestChan chan chan string) {

responseChan := make(chan string)

requestChan <- responseChan

response := <-responseChan

fmt.Printf("Response: %v\n", response)

}

func goroutineD(requestChan chan chan string) {

responseChan := <-requestChan

responseChan <- "wassup!"
}

소스를 통해 명확히 이해 할 수 있을 것이다. 키포인트는
requestChan <- responseChan  채널에게 값을 전송하는게 아니라, 채널을 전송하는 지점이다. 

이더리움 오픈소스에서 chan chan 은 어떻게 사용 되었나? (소스)

이더리움에서 p2p 시작 부분을 살펴보면, (kademlia 알고리즘에 따라) 다른 노드를 찾기 위한 요청을 하기 전에 자신의 버켓을 다시 채우는(리프레쉬)과정이 있는데, 여기서 리프레쉬를  고루틴(쓰레드)화 해서 처리하고, 리프레쉬가 끝나면 끝났다고 알려주길 바라는 부분이 있다. 이때 chan chan을 통해서 처리하는데, 소스를 통해 확인해 보자.

refreshReq: make(chan chan struct{}),

 refreshReq 라는 chan chan을 만듭니다. 변수 이름이 말해 주듯이 리프레쉬를 요청하는 채널인데, 위의 예제와 마찬가지로 이 채널에 response에 해당하는 채널을 건네주고, 대기 하는 로직 일 수도 있겠다.

func (tab *Table) refresh() <-chan struct{} {
    done := make(chan struct{})
    select {
    case tab.refreshReq <- done: <-- 이 부분
    case <-tab.closed:
        close(done)
    }
    return done
}

response 는 아니고, done 이라는 채널을 만들어서 건네주고 있다. 리프레쉬를 한 다음에 완료되면 done 채널을 통해 보내라는 것 같다. 그럼 누가 받을 까? 리프레쉬를 진짜 실행하는 놈이 받겠지. 그리고 리프레쉬 하는 놈이 done에 어떤 것을 보내면 그것을 처리 하기 위해 위의 return done 을 받은 녀석은 어디선가 대기하고 있을 것이다.


// loop schedules refresh, revalidate runs and coordinates shutdown.
func (tab *Table) loop() {
    ...
    for {
        select {
  
        case req := <-tab.refreshReq: <--- 이 부분
            waiting = append(waiting, req)
            if refreshDone == nil {
                refreshDone = make(chan struct{})
                go tab.doRefresh(refreshDone)
            }
       ...

loop 함수에서 req := <-tab.refreshReq:  이렇게 받고 있다. 받은 것을 waiting 배열에 넣어서 보관하며
진짜 리프레쉬를 하는 go tab.doRefresh(refreshDone)  함수를 호출하고 있다.

    case req := <-tab.refreshReq:
            waiting = append(waiting, req)
            if refreshDone == nil {
                refreshDone = make(chan struct{})
                go tab.doRefresh(refreshDone)
            }
        case <-refreshDone: <--- 이 부분
            for _, ch := range waiting {
                close(ch) <--- 이 부분
            }
            waiting, refreshDone = nil, nil

go tab.doRefresh(refreshDone) 부분을 보면, refreshDone 이라는 채널을 매개변수로 넣어주는데, 예상하듯이 리프레쉬가 완료되면 이 채널로 완료 보고를 하라는 것이고,  case <-refreshDone:  <--- 이 부분 여기서 완료 보고를 기다리고 있다.
그 아래 close(ch)가 있는데, 여기서 진짜 처음 소스에 있는 done := make(chan struct{})이 done에 대한 완료 보고를 하고 있다. 즉 모든 처리가 끝났다고 알려 준다. 그럼 done 을 보고 받는 마지막 코드를 보자.

func (tab *Table) lookup(targetID NodeID, refreshIfEmpty bool) []*Node {
....
    for {
        tab.mutex.Lock()
        // generate initial result set
        result = tab.closest(target, bucketSize)
        tab.mutex.Unlock()
        if len(result.entries) > 0 || !refreshIfEmpty {
            break
        }
        // The result set is empty, all nodes were dropped, refresh.
        // We actually wait for the refresh to complete here. The very
        // first query will hit this case and run the bootstrapping
        // logic.
        <-tab.refresh() <--- 여기 대기
        refreshIfEmpty = false
    } .. look up 처리 ..

위의 코드를 보면 tab.refresh() 를 호출하고 리프레쉬가 완료되길 기다리고 있다. 즉 리프레쉬가 완료 된 후에야 lookup을 처리하겠다는 것이다. 

소스를 보면 아시다시피, 고 언어에서 고루틴/채널은 정말 너무 사소하게 사용 된다. 그러다보니, 채널의 채널이 생기고 그 그 채널을 처리 하기 위해 또 고루틴을 만들고 그것을 위한 채널을 또 만들고...멀티코어 를 적극적으로 사용하는 모습이 기본이며, 그래도 고 언어이기 때문에 그나마 복잡도가 많이 줄어 든거 같긴 하다. :-) 



레퍼런스:
http://tleyden.github.io/blog/2013/11/23/understanding-chan-chans-in-go/
https://github.com/ethereum/go-ethereum/blob/master/p2p/discover/table.go



임백준님이 번역하신 "7가지 동시성 모델" 책에는 순차 프로세스 통신 (CSP) 이라는 내용이 있는데  Golang 에서 구현한 모델을 클로저언어로 래핑한 라이브러리를 이용해서 설명하고 있다. 역시 책에 나오는 내용 "미래는 불변이다", "미래는 분산이다" 라는 구절이 있다. 분산을 잘하기 위한 도우미로 "메세지 전달" 이 매우 중요한데, "액터" 나 "CSP" 처럼 메세지 전달을 기반으로 삼는 테크닉이 점점 더 중요한 역할을 하리라 예측하고 있다. 이번 번역 글 (중간 중간 동시성에 대한 개인적인 견해가 많이 들어가 있다) 에서는 Golang에서의 CSP 에 대해서 살펴본다. 학술적인 내용이 아니며 아주 기초적인 내용을 짧게 담고 있는데, 액터에 대해서 알고 있는 분이라면 통신하는 객체 자체(액터) 보다 "통신" 그 자체에 방점을 찍는게 CSP 라고 이해하고 읽어보면 편 할 것이다.



Concurrency in Golang

[번역] http://www.minaandrawos.com/2015/12/06/concurrency-in-golang/


어제, Quora 에서 고 언어에서의 동시성 모델에 대한 답변을 달아주었는데, 좀 더 말 할게 있겠다는 생각이 들어. 고 언어에서의 동시성은 고 언어를 빛나게 하는 가장 강력한 것 중 하나거든. 많은 사람들이 이것에 관해 간단한것 부터 시작해서 복잡한 내용에 이르기까지 말해 왔었는데, 이번에는 내 생각을 말 할 차례가 온 거 같아.

고 언어에서의 동시성은 단지 문법적인 요소라고 생각 하기에는 좀 더 깊이 있게 생각해 볼 만한 것들이 있어. 고 언어의 파워를 잘 활용하기 위해서는 일단 어떻게 고 언어가 동시성을 다루는지에 대해 잘 이해하고 있어야 하지. 고 언어는 CSP (Communicating Sequential Processes : 순차적 프로세스들의 통신) 라 불리는 동시성 모델을 사용하는데,이것은 컴공에서 동시성 시스템들 사이에서 일어나는 상호작용을 묘사하는 아주 기본적인 모델이야. 근데 이 글이 과학논문은 아니기 때문에 아주 구체적인 내용에 대해서는 일단 건너 뛰려해

고 언어에서의 동시성을 설명 할 때 주로 사용되는 문구는 다음과 같지.

Do not communicate by sharing memory; instead, share memory by communicating.

공유 메모리를 이용하여 커뮤니케이션 하지말고, 커뮤니케이션에 의해 메모리를 나누자.

뭐 좋은 소리 같긴 하다 ㅎㅎ  근데 이게 의미하는게 무엇일까? 커뮤니케이션에 의해 메모리를 나누자-라니..  내 머리속에서 개념들이 휘몰아 치다가 한순간에 정리 됬어. 알버트 아인슈타인은 이런 말을 했었거든 "만약 당신이 초딩도 알수 있게 끔 간단히 설명 할 수 없다면, 당신은 진정 그것을 이해한다고 볼 수 없습니다" 나는 이제 정말 간단히 설명할 수 있을 거 같아. 

(역주 :  쓰레드들이 공유 될 것들을 동시간에 접근하는게 아니라, 서로 전달하는 방식임. 여기서 함정(?)이 Go 언어에서 다른 언어처럼 공유변수를 서로 접근하는 방식을 사용 안하는건 아니며 서로 공유변수를 접근하는 방식을 사용하라고 sync 관련 키워드도 열라 많음. 자바나 스칼라도 STM,CSP,액터를 기본 동시성 객체와 함께 사용 할 수 있듯이~~차이점이라면 Go 언어는 언어 자체에서 CSP 를 지원한다는 점. 참고로 클로저언어는 STM 을 내장한다) 

진짜 내가 이해했는지 증명해보지. 자! 시작해 보자고~

공유 메모리에 의해 communicate 하지 말라!

주로 프로그래밍 언어에서는 코드를 동시적으로 실행시키는 것에 대해 생각 할 때,  먼저 여러개의 쓰레드들을 떠올릴 거야. 그것들이 복잡하게 구현되면서 병렬적 수행을 하는 것 말이지. 그리고 쓰레드들이 서로 나누어 가질 데이터 구조/변수/메모리/등등이 무엇인지 파악할 것이고, 이것들을 사이 좋게 나누어 갖게 하기 위해 동시성 객체(뮤텍스등)을 이용할 테지. 그 결과 2개의 쓰레드가 동시간에 한 군데에 쓰는 작업을 할 수 없을 테고 ~ 뭐 그냥 알아서 잘되겠지 하고 놔두거나, 아예 인지도 못할지도 모르지. 이런것이 전형적인 어떻게 다른 쓰레드들끼리 "communicate" 하는가에 관한 것일 꺼야. 아시다시피 이런 행위는 결국 레이스 컨디션, 메모리 매니지먼트, 알수 없는 예외, 데드락 등을 일으키는거지. 예외 없을꺼야. 대규모 동시성 프로그래밍을 C++ 나 Java 등으로 해본 사람들이라면 잘 알것이야.

대신해서, communicating 에 의해 메모리를 나누어라.

어떤 아이디어로 고 언어는 이걸 수행 할까? 공유 메모리 변수에 대해 락을 거는 대신해서 고 언어는 하나의 쓰레드에서 다른 하나(실제로는 우리가 알고있는 그 쓰레드는 아니지만 지금은 그렇게 생각하자고) 로 변수에 저장되어진 값을 communicate (or send) 하게 만들어줘. 기본 행동은 데이터를 보내는 쓰레드와 데이터를 받는(도착할때까지 기다리는)쓰레드야. 쓰레드들의 그 "기다림"은 쓰레드들 사이에 교환이 일어날 때 더 적합한 싱크를 하게 만들지.

좀 더 선명하게 말하자면, 안정적인 상태라는 것은 보내는 쓰레드와  받는 쓰레드가 전송이 완료 될 때까지 아무것도 안한다는데에 있어. 레이스 컨디션이라든지 비슷한 문제가 발생할 기회가 없어지게 한다는 거지. 즉 하나의 쓰레드가 어떤것을 완료하기 전에 다른 쓰레드가 행동 할 상황을 만들지 않는거야. 단순하지

고 언어는 이런 순차적 통신 행위를 할 수 있게 하는 여러 기능들을 지원하는데  중요한 것은 라이브러리 차원이 아니라 언어 차원  라는 거야. 엄청 간단하지. "buffered channel" 이라는 것을 지원하는데 이것에 의해 전송이 완료 될 때까지 쓰데드들 간에 어떤 락이나 sync 를 맞출 필요가 없어. 

대신 두개의 쓰레드들 사이의 미리 정해진 멈버를 조작 할 때는 싱크로니제이션/락킹을 할 수 는 있겠다. 간련된 뮤텍스 예제는 여기를 참고해: sync.Mutex  (역주1: 다시 언급하지만 이러한 문제가 있기 때문에 golang 도 동시성에 여전히 위험한 언어라고 생각된다. 아예 불변이 디폴트인 클로저나 얼랭이 이런 면에서는 괜찮긴 한데.. 여러모로..선택을 주저하게 만들고,..따라서 동시적 행위가 비교적 단순한 것은 golang ,좀 복잡하고 유기적이면 스칼라/아카를 선택하게 만들고 있다. 참고로 파이썬은 이상하게 멀티쓰레드,멀티프로세스를 사용하는데 문제가 많더라고.몇일후 혹은 한달후에 갑자기 작동을 안한다던지 하는..2.7 에서 3.6으로 바꾸니깐 그런 현상이 사라지기도 하고..따라서 파이썬에서 Golang으로 갈아탔다.)


(역주2: 불변은 쓰레드세이프하다? CPU 여러개를 활용하는 동시성이 뜨면서  불변이 강조되며, 불변=동시성=함수형프로그래밍이 라는 단어가 항상 함께 나열되다보니, 그럼 불변이면 쓰레드 세이프 한거야? 라고 생각 할 수 있는데 광의로 보면 그렇지 않습니다. 쓰레드세이프한 솔루션이 되지는 않습니다. 주의해야합니다.) 

고 언어를 이용한 동시성 코딩 

도대체 어떻게 한다는 것일까? 이제 코드를 보면 좀 더 명쾌해 질거야.

Go에서 "goroutine"은 위의 설명 한 스레드 개념을 제공하는데, 실제로는 스레드가 아니며 기본적으로 동일한 주소 공간에서 다른 goroutine과 동시에 실행할 수있는 기능이라고 볼 수 있지. 경량화된 쓰레드라고도 해. 그들은 O.S 쓰레드 사이에 다중화(multiplexed)되어 있는데 하나의 블록이 있다면 다른 것들은 계속 진행 될 수 있지. 모든 동기화 및 메모리 관리는 기본적으로 Go 언어에 의해 수행되며, 그들이 진짜 스레드가 아닌 이유는 항상 병행적으로 행동되지 않기 때문이야. 하지만 멀티플렉싱 및 동기화로 인해 동시적으로 동작이 발생 한다고 볼 수 있어. 

쓰레드와 비슷한  goroutine을 시작하려면 아무 함수에나 "go"라는 키워드를 사용하면 되. 간단하지~

go myfunction()

Go 채널은 Go에서 동시성을 실현하는 또 다른 핵심 개념이며 이 글의 주제이기도 하지. CSP 에서 강조하는 그 "통신" 역할을 담당하니깐. 즉 goroutine간에 메모리를 전달하는 데 사용되며 채널을 만들려면 "make" 를 사용하면되.

myChannel := make(chan int64)

goroutines이 대기하기 전에 더 많은 값을 대기열에 넣을 수 있도록 버퍼링 된 채널도 만들 수 있어.

myBufferedChannel := make(chan int64,4)

위의 두 예제에서는 채널 변수가 이 전에 생성되지 않았다고 가정했어. 그래서 ": ="을 사용하여 추론된 유형을 가진 변수를 만들었는데. ( "="는 값을 할당 할 뿐이며 변수가 이전에 선언되지 않은 경우 컴파일 오류가 발생한다.)

채널을 사용하려면 "<-"표기법을 사용하는데, 값을 (여기서 숫자 54) 보내는 goroutine은 다음과 같이 채널에 값을 할당하지.

mychannel <- 54

값을 받는 goroutine은 채널에서 그것을 추출하여 다음과 같은 새로운 변수에 할당해.

myVar := <- mychannel

이제 Golang에서 동시성을 보여주는 전체 예제를 살펴 보자고.

package main

import (
"fmt"
"time"
)
// 메인
func main()
{
ch := make(chan int) // 통신용 채널 생성
done := make(chan bool)

go sendingGoRoutine(ch) // GO 루틴 시작
go receivingGoRoutine(ch,done) // GO 루틴 시작
// 프로그램이 종료되는 것을 막음.
<- done
}
// 송신 코루틴
func sendingGoRoutine(ch chan int)
{
// 5초후에 메세지 달라는 코루틴 생성
t := time.NewTimer(time.Second*5)
// 5초후에 아무 메세지나 받음.
<- t.C

fmt.Println("Sending a value on a channel")
ch <- 45
}
// 수신 코루틴
func receivingGoRoutine(ch chan int, done chan bool)
{
// 채널로부터 값을 받으면 v 에 입력
v := <- ch
fmt.Println("Received value ", v)
done <- true
}

결과는 다음과 같어.

Sending a value on a channel
Received value 45

코루틴들끼리 채널을 이용해서 공통 관심 사항을 전달(커뮤니케이션)을 통해서 처리하는 것을 잘 기억하라고. 공통 관심 사항에 대해 메모리를 나누어 가지면서 서로 접근을 통해서 처리하는게 아니야


[요약번역] https://fabianlee.org/2017/05/21/golang-running-a-go-binary-as-a-systemd-service-on-ubuntu-16-04/

1. SleepService  예제 만들기 

  1. package main
  2. import (
  3. "time"
  4. "log"
  5. "flag"
  6. "math/rand"
  7. "os"
  8. "os/signal"
  9. //"syscall"
  10. )
  11. func main() {
  12. // load command line arguments
  13. name := flag.String("name","world","name to print")
  14. flag.Parse()
  15. log.Printf("Starting sleepservice for %s",*name)
  16. // setup signal catching
  17. sigs := make(chan os.Signal, 1)
  18. // catch all signals since not explicitly listing
  19. signal.Notify(sigs)
  20. //signal.Notify(sigs,syscall.SIGQUIT)
  21. // method invoked upon seeing signal
  22. go func() {
  23. s := <-sigs
  24. log.Printf("RECEIVED SIGNAL: %s",s)
  25. AppCleanup()
  26. os.Exit(1)
  27. }()
  28. // infinite print loop
  29. for {
  30. log.Printf("hello %s",*name)
  31. // wait random number of milliseconds
  32. Nsecs := rand.Intn(3000)
  33. log.Printf("About to sleep %dms before looping again",Nsecs)
  34. time.Sleep(time.Millisecond * time.Duration(Nsecs))
  35. }
  36. }
  37. func AppCleanup() {
  38. log.Println("CLEANUP APP BEFORE EXIT!!!")
  39. }

$ ./sleepservice

이렇게 포그라운드에서 실행 시키면 SSH 터미널을 닫거나 하면 프로그램도 같이 죽어버린다.

2. systemd 서비스로 SleepService 만들기 

systemd 를 이용해서 서비스로 만들기 위해서는 “/lib/systemd/system/sleepservice.service” 아래에
다음과 같은 것을 만든다. 

[Unit]
Description=Sleep service
ConditionPathExists=/home/ubuntu/work/src/sleepservice/sleepservice
After=network.target
 
[Service]
Type=simple
User=sleepservice
Group=sleepservice
LimitNOFILE=1024

Restart=on-failure
RestartSec=10
startLimitIntervalSec=60

WorkingDirectory=/home/ubuntu/work/src/sleepservice
ExecStart=/home/ubuntu/work/src/sleepservice/sleepservice --name=foo

# make sure log directory exists and owned by syslog
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /var/log/sleepservice
ExecStartPre=/bin/chown syslog:adm /var/log/sleepservice
ExecStartPre=/bin/chmod 755 /var/log/sleepservice
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=sleepservice
 
[Install]
WantedBy=multi-user.target

위의 파일에서 사용된 패스들은 여러분의 경로에 맞게 수정 하면 될 것 이다.  
물론 sleepservice 라는 서비스 이름 자체도 말이다.

해당 프로그램 전용 사용자를 만들고 , 깃헙에서 만들어 둔것이 있으면 제대로 된 위치로 옮기고, 755 권한을 준다.

$ cd /tmp
$ sudo useradd sleepservice -s /sbin/nologin -M
$ wget https://raw.githubusercontent.com/fabianlee/blogcode/master/golang/sleepservice/systemd/sleepservice.service
$ sudo mv sleepservice.service /lib/systemd/system/.
$ sudo chmod 755 /lib/systemd/system/sleepservice.service

 systemctl 를 이용해서 sleepservice.service 를  enable 시키고 시작시키고, journalctl 을 통해 제대로 시작되는지 확인한다.

$ sudo systemctl enable sleepservice.service

$ sudo systemctl start sleepservice

$ sudo journalctl -f -u sleepservice

May 21 16:20:43 xenial1 sleepservice[4037]: 2017/05/21 16:20:43 hello foo
May 21 16:20:43 xenial1 sleepservice[4037]: 2017/05/21 16:20:43 About to sleep 1526ms before looping again
May 21 16:20:45 xenial1 sleepservice[4037]: 2017/05/21 16:20:45 hello foo
May 21 16:20:45 xenial1 sleepservice[4037]: 2017/05/21 16:20:45 About to sleep 196ms before looping again


번외) 

기존의 etc/init.d 를 이용해서 할 수도 있다. 

1) /etc/init.d 아래 스크립트 작성 (Golang 프로그램을 시작시키거나 멈추는 run 스크립트를 부팅시 호출 해준다.

#! /bin/sh
# /etc/init.d/myservice


USERNAME=who??
COMMAND_MYSERVICE_SCRIPT="/home/$USERNAME/myservice/run"


case "$1" in
  start)
    echo "Starting myservice .."
    sudo -u $USERNAME $COMMAND_MYSERVICE_SCRIPTstart
  
    echo "Done!!"
    ;;
  stop)
    echo "Stopping myservice .."
    sudo -u $USERNAME $COMMAND_MYSERVICE_SCRIPTstop
    echo "Done!!"
    ;;
  *)
    echo "Usage: /etc/init.d/myservice {start|stop}"
    exit 1
    ;;
esac

exit 0

권한설정

* sudo chmod 755 myservice

* update-rc.d 로 설정   ( sudo update-rc.d myservice defaults) 





Golang 에서의 Map, Filter 등


Python 으로 코딩하다가 Golang 으로 바꿔보면 가장 크게 불편한점은 List Comprehension 의 부재라고 느꼈다.
나도 Newbie라 Golang 에서는 어떻게 리스트 조작을 할까 궁금해서 자료들을 찾아서 정리 해보았다. 


기본 

가장 기본적인것은 Python 이나 Scala등에서 제공하는 synthetic sugar 마법을 사용하지 않고, Golang 답게 직접 해당 함수를 만들어서 호출하는 것이다. 아래 예를 보자.

func Map(vs []int, f func(int) int) []int {
vsm := make([]int, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}

func add5(n int) int {
return n + 5
}
func main(){
var strs = []int{1,2,3}

fmt.Println(Map(strs, add5))
}

Map 이라는 함수를 만들었다. 첫번째 인자로 배열이 들어가고, 두번째 인자로 그 배열을 조작할 함수가 매개변수로 들어갔다.사용법은 간단한 위의 코드가 설명해 줄 것이다.

 Filter 같은 경우는 아래와 같이 코딩 하면 될 것이다.

func Filter(vs []string, f func(string) bool) []string {
vsf := make([]string, 0)
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}


func main(){
var strs = []string{"peach", "apple", "pear", "plum"}

fmt.Println(Filter(strs, func(v string) bool {
return strings.Contains(v, "e")
}))
}

모든 것이 직관적이고 심플하다.!!  (아쉬운 점은 제네릭하지 않다는 것인데, 사실 공용 라이브러리 개발자가 아닌 이상 경험상 제네릭은 그닥 필요 없다.)

다른 예제들은 다음 링크를 참고하자. https://gobyexample.com/collection-functions


고급 (go-funk) 

자신의 필요성에 맞게 빠륵 개발 할 경우에는 제네릭이 거의 필요 없지만, 공용으로 사용될 라이브러리 차원이라면 좀 말이 달라진다. golnag 의 경우는 리플렉션,인터페이스를 통해서 제네릭을 흉내내는 것으로 보이는데, go-funk 라이브러리가 그 것이다. 자바스크립트이 유명한 유틸리티 라이브러리인 lodash와 비슷한 구석이 있지만 차별되는 로드맵을 가지고 있다고 한다. 

사용법을 간단히 살펴보면 (go get github.com/thoas/go-funk 을 통해 패키지 설치한다)

r := funk.Map([]int{1, 2, 3, 4}, func(x int) int {
return x * 2
}) // []int{2, 4, 6, 8}

r := funk.Map([]string{"1", "2"}, func(x string) string {
return "ID-" + x
})
// [ID1 ID2]

와 같이 다양한 타입으로 사용 할 수 있다.

참고로 Map 의 내부는 이렇다.


// Map manipulates an iteratee and transforms it to another type.
func Map(arr interface{}, mapFunc interface{}) interface{} {
if !IsIteratee(arr) {
panic("First parameter must be an iteratee")
}

if !IsFunction(mapFunc) {
panic("Second argument must be function")
}

var (
funcValue = reflect.ValueOf(mapFunc)
arrValue = reflect.ValueOf(arr)
arrType = arrValue.Type()
)

kind := arrType.Kind()

if kind == reflect.Slice || kind == reflect.Array {
return mapSlice(arrValue, funcValue)
}

if kind == reflect.Map {
return mapMap(arrValue, funcValue)
}

panic(fmt.Sprintf("Type %s is not supported by Map", arrType.String()))
}

먼저 순회가능한 타입과 함수가 매개변수로 각각 들어왔는지 확인 한후에, 순회가능 타입이 배열인지, 맵타입인지를 리플렉션으로 확인해서 mapSlice 혹은 mapMap 을 호출해 주고있다.



func mapSlice(arrValue reflect.Value, funcValue reflect.Value) interface{} {
funcType := funcValue.Type()

if funcType.NumIn() != 1 || funcType.NumOut() == 0 || funcType.NumOut() > 2 {
panic("Map function with an array must have one parameter and must return one or two parameters")
}

arrElemType := arrValue.Type().Elem()

// Checking whether element type is convertible to function's first argument's type.
if !arrElemType.ConvertibleTo(funcType.In(0)) {
panic("Map function's argument is not compatible with type of array.")
}

if funcType.NumOut() == 1 {
// Get slice type corresponding to function's return value's type.
resultSliceType := reflect.SliceOf(funcType.Out(0))

// MakeSlice takes a slice kind type, and makes a slice.
resultSlice := reflect.MakeSlice(resultSliceType, 0, 0)

for i := 0; i < arrValue.Len(); i++ {
result := funcValue.Call([]reflect.Value{arrValue.Index(i)})[0]

resultSlice = reflect.Append(resultSlice, result)
}

return resultSlice.Interface()
}

if funcType.NumOut() == 2 {
// value of the map will be the input type
collectionType := reflect.MapOf(funcType.Out(0), funcType.Out(1))

// create a map from scratch
collection := reflect.MakeMap(collectionType)

for i := 0; i < arrValue.Len(); i++ {
results := funcValue.Call([]reflect.Value{arrValue.Index(i)})

collection.SetMapIndex(results[0], results[1])
}

return collection.Interface()
}

return nil
}

배열의 타입과 배열을 조작할 맵함수의 파라미터 타입을 비교하여 같을 경우에 한해서,
배열을 순회하며 맵함수를 적용시켜서 

result := funcValue.Call([]reflect.Value{arrValue.Index(i)})[0]

새 배열에 추가해서 리턴한다.

resultSlice = reflect.Append(resultSlice, result)

레퍼런스:

https://gobyexample.com/collection-functions
go-funk



현재 파이썬으로 짜여져 있는 IoT 데이터중계/분석 프로그램에서 일부분을 C 로 짜려고 하다가 C,C++ 보다는 Go 가 모든 면에서 좋다고 판단하여 관련 자료를 찾다가 이하 글을 번역하였습니다.

마이크로서비스를 넘어 서버리스 아키텍처가 유행하는 요즘,  보다 작은 모듈단위로 강력한 힘을 가질 수 있는 golang 은 구글의 막강한 지원을 등에 업고 큰 힘을 발휘하고 있는거 같습니다. 실제 언어 순위를 매기는 각종 지표에서도 Go 는 파죽지세로 위로 솟구쳐 올라가고 있습니다. 개인적으로는 쓸데없이 복잡하다고 느끼는 소위  객체지향 언어들에 대한 염증(사실 OOP디자인을 사용하는것도 때와 시기가 있는데..무조건 적용하려고 하면.. 부작용이 생기겠지요) 과 함께 스크립트 언어들이 떴는데, 그 스크립트 언어에 없는 강력함을 갖춘 언어가 Go 라서 그런거 같습니다. 즉 심플함 + 강력함 + 구글의지원 (미래보장) 





다른언어로 부터의 Go 함수 호출 

글쓴이는 Packt Publishing의 Learning Go Programming 의 저자이다.)

버전 1.5부터 Go 컴파일러는 -buildmode 플래그를 통해 여러 빌드 모드에 대한 지원을 도입 했습니다. Go 실행 모드라고 알려진 이러한 빌드 모드는 go tool를 확장하여 Go 패키지를 Go 아카이브, Go 공유 라이브러리, C 아카이브, C 공유 라이브러리 및 (introduced in 1.8) Go 동적 플러그인을 비롯한 여러 형식으로 컴파일 합니다.

이 게시물은 Go 패키지를 C 공유 라이브러리로 컴파일하는 것에 관한 내용입니다. 이 빌드 모드에서 컴파일러는 표준 공유 객체 바이너리 파일 (.so 역주: 윈도우즈에서 dll)을 출력하여 Go 함수를 C 스타일 API로 노출하게 됩니다. 여기에서는 C, Python, Node 및 Java에서 호출 할 수 있는 Go 라이브러리를 작성하는 방법에 대해 설명 할 예정입니다.

모든 소스는 올려두었습니다:  GitHub.

Go 코드

다른 언어에서 사용 할 수 있는 멋진 Go 라이브러리를 작성했다고 가정 보겠습니다. 
 코드를 공유 라이브러리로 컴파일하기 전에 수행해야 할 4 가지 요구 사항이 있는데요.

  • 패키지는 반드시 main 패키지 여야 합니다. 컴파일러는 패키지와 모든 종속성을 단일 공유 객체 바이너리로 빌드합니다.
  • 소스는 pseudo-package  "C"를 import 해야 합니다.
  • // export 주석을 사용하여 다른 언어에서 액세스 할 수 있도록 하려는 함수에 주석을 답니다.
  • 빈 main 함수를 선언해야합니다.

다음 Go 소스는 Add, Cosine, Sort 및 Log 함수를 export 할 것인데요  나름 괜찮다고 보지만 여러분을 만족시킬 만큼 멋진 패키지는 아닐 수도 있겠지만 다양한 함수 시그니처는 타입 매핑 관련 사항을 탐색하는 데 도움이 될 것입니다


package main
import "C"
import (
 "fmt"
 "math"
 "sort"
 "sync"
)
var count int
var mtx sync.Mutex

//export Add
func Add(a, b int) int { return a + b }

//export Cosine
func Cosine(x float64) float64 { return math.Cos(x) }

//export Sort
func Sort(vals []int) { sort.Ints(vals) }

//export Log
func Log(msg string) int {
  mtx.Lock()
  defer mtx.Unlock()
  fmt.Println(msg)
  count++
  return count
}

func main() {}

패키지는 -buildmode = c-shared 빌드 플래그를 사용해 컴파일되어 공유 객체 바이너리로 만들어 집니다.

go build -o awesome.so -buildmode=c-shared awesome.go

완료되면 컴파일러는 awesome.h 이라는 C 헤더 파일 및 awesome.so라는 공유 객체 파일을 출력합니다.

-rw-rw-r —    1362 Feb 11 07:59 awesome.h
-rw-rw-r — 1997880 Feb 11 07:59 awesome.so

.so 파일은 약 2MB 크기로 작은 라이브러리에 비해 상대적으로 큽니다. 이것은 Go 실행 엔진의 장점과 종속 패키지가 하나의 공유 객체 바이너리로 묶여 있기 때문이에요. (하나의 정적 실행 파일을 컴파일하는 것과 비슷 함).

헤더파일


헤더 파일은 Go 호환 타입에 매핑 된 C 타입을 정의합니다 (여기서는 설명하지 않음).

/* Created by “go tool cgo” — DO NOT EDIT. */
...
typedef long long GoInt64;
typedef GoInt64 GoInt;
...
typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
...
extern GoInt Add(GoInt p0, GoInt p1);
extern GoFloat64 Cosine(GoFloat64 p0);
...

공유 오브젝트 파일 


컴파일러에 의해 생성된 다른 파일은 64 비트 ELF 공유 오브젝트 바이너리 파일입니다. file command를 사용하여 정보를 확인할 수 있습니다.

$> file awesome.so
awesome.so: ELF 64-bit LSB shared object, x86–64, version 1 (SYSV), dynamically linked, BuildID[sha1]=1fcf29a2779a335371f17219fffbdc47b2ed378a, not stripped

nm 및 grep 명령을 사용하여 Go 함수가 exported 되었는지 확인할 수 있습니다.

$> nm awesome.so | grep -e "T Add" -e "T Cosine" -e "T Sort" -e "T Log"
00000000000d0db0 T Add
00000000000d0e30 T Cosine
00000000000d0f30 T Log
00000000000d0eb0 T Sort

다음으로는 다른 언어에서 Go 로 만들어서 exported 한 함수를 호출하는 방법에 대해 보여주는 몇 가지 예제를 살펴 보겠습니다.

C 에서 호출하기 


공유 객체 라이브러리를 사용하여 C에서 Go 함수를 호출하는 두 가지 방법이 있습니다. 먼저 컴파일 중에 코드를 정적으로 공유 라이브러리에 바인딩 할 수 있지만 런타임에 동적으로 링크합니다. 또는 Go 함수 심볼을 런타임에 동적으로 로드하고 바인딩 할 수 있습니다.

동적 링크

이 접근법에서는 헤더 파일을 사용하여 공유 객체 파일에서 내 보낸 타입 및 함수를 정적으로 참조합니다. 코드는 아래와 같이 간단하고 깔끔 합니다 (일부 출력문은 생략).


#include <stdio.h>
#include "awesome.h"
int main() {
    GoInt a = 12;
    GoInt b = 99;
    printf("awesome.Add(12,99) = %d\n", Add(a, b));
    printf("awesome.Cosine(1) = %f\n", (float)(Cosine(1.0)));
    GoInt data[6] = {77, 12, 5, 99, 28, 23};
    GoSlice nums = {data, 6, 6};
    Sort(nums);
    for (int i = 0; i < 6; i++){
        printf("%d,", ((GoInt *)nums.data)[i]);
    }
    GoString msg = {"Hello from C!", 13};
    Log(msg);
}

다음 코드는 공유 객체 라이브러리를 지정하여 컴파일됩니다.

$> gcc -o client client1.c ./awesome.so

실행되면, 바이너리는 awesome.so 라이브러리에 링크되어 아래 출력을 생성합니다.

$> ./client
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(77,12,5,99,28,23): 5,12,23,28,77,99,
Hello from C!

동적 로드

이 방법에서 C 코드는 동적 링크 로더 라이브러리 (libdl.so)를 사용하여 내 보낸 심볼을 동적으로 로드하고 바인딩합니다. dlopen과 같이 dhfcn.h에 정의 된 함수를 사용하여 라이브러리 파일을 열고 dlsym을 사용하여 심볼을 찾거나 dlerror를 사용하여 오류를 검색하거나 dlclose를 사용하여 공유 라이브러리 파일을 닫습니다.

아래에서 강조한 바와 같이 C 버전은 더 길지만 이전과 똑같습니다

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
typedef long long go_int;
typedef double go_float64;
typedef struct{void *arr; go_int len; go_int cap} go_slice;
typedef struct{const char *p; go_int len;} go_str;
int main(int argc, char **argv) {
  void *handle;
  char *error;
  handle = dlopen ("./awesome.so", RTLD_LAZY);
  if (!handle) {
    fputs (dlerror(), stderr);
    exit(1);
  }
  go_int (*add)(go_int, go_int) = dlsym(handle, "Add");
  if ((error = dlerror()) != NULL)  {
      fputs(error, stderr);
      exit(1);
  }
  go_int sum = (*add)(12, 99);
  printf("awesome.Add(12, 99) = %d\n", sum);

 go_float64 (*cosine)(go_float64) = dlsym(handle, "Cosine");
 go_float64 cos = (*cosine)(1.0);

 void (*sort)(go_slice) = dlsym(handle, "Sort");
 go_int data[5] = {44,23,7,66,2};
 go_slice nums = {data, 5, 5};
 sort(nums);

 go_int (*log)(go_str) = dlsym(handle, "Log");
 go_str msg = {"Hello from C!", 13};
 log(msg);

 dlclose(handle);
}

이 버전에서 코드는 자체 정의 된 C 타입인 go_int, go_float, go_slice 및 go_str을 사용합니다 (설명을 위해 awesome.h가 사용될 수 있음). 함수 dlsym은 함수 심볼을 로드하고 각각의 함수 포인터에 할당합니다. 다음으로, 코드를 dl 라이브러리 (awesome.so가 아닌)와 링크하여 컴파일 할 수 있습니다.

$> gcc -o client client2.c -ldl

코드가 실행되면 C 바이너리가 로드되어 공유 라이브러리 awesome.so에 연결되어 다음과 같은 결과가 출력됩니다.

$> ./client
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(44,23,7,66,2): 2,7,23,44,66,
Hello from C!

Python 에서 호출하기 

파이썬에서는 좀 더 쉬워집니다. ctypes 외부 함수 라이브러리가 export 된 Go 함수를  호출하는 데 사용됩니다.  (일부 출력문은 생략 됨).

from ctypes import *
lib = cdll.LoadLibrary("./awesome.so")
lib.Add.argtypes = [c_longlong, c_longlong]
print "awesome.Add(12,99) = %d" % lib.Add(12,99)
lib.Cosine.argtypes = [c_double]
lib.Cosine.restype = c_double 
cos = lib.Cosine(1)
print "awesome.Cosine(1) = %f" % cos
class GoSlice(Structure):
    _fields_ = [("data", POINTER(c_void_p)), 
                ("len", c_longlong), ("cap", c_longlong)]
nums = GoSlice((c_void_p * 5)(74, 4, 122, 9, 12), 5, 5)
lib.Sort.argtypes = [GoSlice]
lib.Sort.restype = None
lib.Sort(nums)
class GoString(Structure):
    _fields_ = [("p", c_char_p), ("n", c_longlong)]
lib.Log.argtypes = [GoString]
msg = GoString(b"Hello Python!", 13)
lib.Log(msg)

lib 변수는 공유 객체 파일에서 로드된 심볼을 나타냅니다. Python 클래스인 GoString과 GoSlice는 각각의 C struct 타입에 매핑됩니다. 파이썬 코드가 실행되면 공유 객체에서 Go 함수를 호출하여 다음 출력을 생성합니다.

$> python client.py
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(74,4,122,9,12) = [ 4 9 12 74 122 ]
Hello Python!

Node 에서 호출하기 

Node는 node-ffi (및 기타 종속 라이브러리)라는 외부 함수 라이브러리를 사용하여 다음 코드 조각에 강조 표시된대로 export 된 Go 함수를 동적으로 로드하고 호출합니다.

var ref = require("ref");
var ffi = require("ffi");
var Struct = require("ref-struct")
var ArrayType = require("ref-array")
var LongArray = ArrayType(ref.types.longlong);
var GoSlice = Struct({
  data: LongArray,
  len:  "longlong",
  cap: "longlong"
});
var GoString = Struct({
  p: "string",
  n: "longlong"
});
var awesome = ffi.Library("./awesome.so", {
  Add: ["longlong", ["longlong", "longlong"]],
  Cosine: ["double", ["double"]],
  Sort: ["void", [GoSlice]],
  Log: ["longlong", [GoString]]
});
console.log("awesome.Add(12, 99) = ", awesome.Add(12, 99));
console.log("awesome.Cosine(1) = ", awesome.Cosine(1));
nums = LongArray([12,54,0,423,9]);
var slice = new GoSlice();
slice["data"] = nums;
slice["len"] = 5;
slice["cap"] = 5;
awesome.Sort(slice);
str = new GoString();
str["p"] = "Hello Node!";
str["n"] = 11;
awesome.Log(str);

ffi 객체는 공유 라이브러리에서 로드 된 심볼을 관리합니다. Node Sturct 개체는 GoSlice 및 GoString을 만들어 각각의 C 구조체에 매핑하는 데 사용됩니다. 코드를 실행하면 아래와 같이 export 된 Go 함수가 호출 됩니다.

awesome.Add(12, 99) =  111
awesome.Cosine(1) = 0.5403023058681398
awesome.Sort([12,54,9,423,9] = [ 0, 9, 12, 54, 423 ]
Hello Node!

Java 에서 호출하기

Java에서 export 된 Go 함수를 호출하려면 Java Native Access 프로젝트 또는 JNA를 사용하여 다음 코드에 표시된 것처럼 프로그래밍 하십시오 (일부 명령문은 생략되거나 축약 됨).


import com.sun.jna.*;
public class Client {
  public interface Awesome extends Library {
    public class GoSlice extends Structure {
      ...
      public Pointer data;
      public long len;
      public long cap;
    }
  
    public class GoString extends Structure {
      ...
      public String p;
      public long n;
    }
    public long Add(long a, long b);
    public double Cosine(double val);
    public void Sort(GoSlice.ByValue vals);
    public long Log(GoString.ByValue str);
  }
  static public void main(String argv[]) {
    Awesome awesome = (Awesome) Native.loadLibrary(
      "./awesome.so", Awesome.class);
    System.out.printf(... awesome.Add(12, 99));
    System.out.printf(... awesome.Cosine(1.0));
    long[] nums = new long[]{53,11,5,2,88};
    Memory arr = new Memory(... Native.getNativeSize(Long.TYPE));
    Awesome.GoSlice.ByValue slice = new Awesome.GoSlice.ByValue();
    slice.data = arr;
    slice.len = nums.length;
    slice.cap = nums.length;
    awesome.Sort(slice);
    Awesome.GoString.ByValue str = new Awesome.GoString.ByValue();
    str.p = "Hello Java!";
    str.n = str.p.length();
    System.out.printf(... awesome.Log(str));
  }
}

Java 인터페이스 Awesome은 awesome.so 공유 라이브러리 파일에서 로드된 심볼을 나타냅니다. GoSlice 및 GoString 클래스는 각각의 C struct 표현에 매핑됩니다. 코드가 컴파일되고 실행되면 아래와 같이 export 된 Go 함수가 호출됩니다.

$> javac -cp jna.jar Client.java
$> java -cp .:jna.jar Client
awesome.Add(12, 99) = 111
awesome.Cosine(1.0) = 0.5403023058681398
awesome.Sort(53,11,5,2,88) = [2 5 11 53 88 ]
Hello Java!

결론

이 게시물은 다른 언어에서 사용할 수 있는 Go 라이브러리를 작성하는 방법을 보여줍니다. Go 패키지를 C 스타일 공유 라이브러리로 컴파일함으로써 Go 프로그래머는 공유 객체 바이너리의 In-Process Integration을 사용하여 C, Python, Ruby, Node, Java 등과 함께 프로젝트를 쉽게 작업 할 수 있습니다. 따라서 혹시 다음에 Go 를 이용하여 괜찮은 API를 만들게 되면  Go가 아닌 다른 개발자와 공유하는 것을 잊지 마십시오. :-) 



원문링크: https://medium.com/learning-the-go-programming-language/calling-go-functions-from-other-languages-4c7d8bcc69bf


+ Recent posts