---
id: 8f79af5c6af89b748112aa8b77b348ab
author: prravda
title: Go Uygulamasına Gömülü NATS Sunucusu Yapılandırması ve Süreç İçi İletişim Mimarisi
description: Bu belge, nats-server/v2 kütüphanesini kullanarak NATS sunucusunu bir Go sürecinin içinde gömülü olarak çalıştırmaya yönelik yapılandırma yöntemlerini, InProcessConn tabanlı bellek içi iletişimi ve tüketici tanımlı arayüz tasarımını ele almaktadır.
language: tr
date: 2025-08-04T01:05:22.150350794Z
path: /blog/posts/how-embedded-nats-communicate-with-go-application-z36089af0
category: network
---

# Başlarken

## NATS Hakkında

> Yazılım uygulamaları ve servisleri verilerini birbiriyle alışverişe ihtiyaç duyar. NATS, bu tür veri alışverişini mesajlar biçiminde parçalara ayırarak mümkün kılan bir altyapıdır. Buna "**message oriented middleware**" (mesaj odaklı ara katman yazılımı) diyoruz.
>
> NATS ile uygulama geliştiricileri:
>
> - Dağıtık ve ölçeklenebilir istemci-sunucu uygulamalarını zahmetsizce geliştirebilir.
> - Verileri gerçek zamanlı olarak genel bir biçimde saklayabilir ve dağıtabilir. Bu, çeşitli ortamlar, diller, bulut sağlayıcıları ve yerinde (on-premises) sistemler genelinde esnek bir şekilde gerçekleştirilebilir.
>
> [What is NATS, NATS docs](https://docs.nats.io/nats-concepts/what-is-nats)

- NATS, Go ile yapılandırılmış bir mesaj brokeridır.

## Gömülü NATS (Embedded NATS)

> Uygulamanız Go ile yazılmışsa ve kullanım senaryonuza ve dağıtım senaryolarınıza uygunsa, NATS sunucusunu doğrudan uygulamanızın içine gömebilirsiniz.

[Embedding NATS, NATS docs](https://docs.nats.io/running-a-nats-service/clients#embedding-nats)

>

- Ayrıca NATS'a özgü bir özellik vardır: Go ile yapılandırılmış uygulamalar için embedded mode desteklenmektedir.
- Yani, mesaj brokerlarının genel yaklaşımı olan ayrı bir broker sunucusunun çalıştırılması ve uygulamanın istemcisi aracılığıyla bu sunucuyla iletişim kurulması yapısının aksine, broker'ın kendisi Go ile geliştirilmiş uygulamaya gömülebilir (embed) anlamına gelir.

## Gömülü NATS'ın faydaları ve kullanım alanları

- Konuyu iyi anlatan bir [Youtube videosu](https://youtu.be/cdTrl8UfcBo?si=KYcXOQpiyLUft6AN) bulunduğundan, açıklamayı videonun bağlantısıyla yetinerek geçiyorum.
- Ayrı bir mesaj brokeri sunucusu dağıtmasanız bile modular monolith applictaion geliştirerek separate of concern hedefine ulaşabilir ve NATS'ı gömülü olarak uygulamanıza yerleştirmenin avantajlarından yararlanabilirsiniz. Buna ek olarak single binary deployment da mümkün hale gelir.
- platform with no network (wasm) ortamlarının yanı sıra offline-first application uygulamalarında da yararlı biçimde kullanılabilir.

# Resmi dokümandaki örnek

```go
package main

import (
    "fmt"
    "time"

    "github.com/nats-io/nats-server/v2/server"
    "github.com/nats-io/nats.go"
)

func main() {
    opts := &server.Options{}

    // Seçeneklerle yeni sunucuyu başlat
    ns, err := server.NewServer(opts)

    if err != nil {
        panic(err)
    }

    // Sunucuyu goroutine aracılığıyla başlat
    go ns.Start()

    // Sunucunun bağlantılar için hazır olmasını bekle
    if !ns.ReadyForConnections(4 * time.Second) {
        panic("not ready for connection")
    }

    // Sunucuya bağlan
    nc, err := nats.Connect(ns.ClientURL())

    if err != nil {
        panic(err)
    }

    subject := "my-subject"

    // Konuya abone ol
    nc.Subscribe(subject, func(msg *nats.Msg) {
        // Mesaj verisini yazdır
        data := string(msg.Data)
        fmt.Println(data)

        // Sunucuyu kapat (isteğe bağlı)
        ns.Shutdown()
    })

    // Konuya veri yayınla
    nc.Publish(subject, []byte("Hello embedded NATS!"))

    // Sunucunun kapanmasını bekle
    ns.WaitForShutdown()
}
```

- Bu, NATS resmi dokümanında yer alan [Embedded NATS örneği](https://dev.to/karanpratapsingh/embedding-nats-in-go-19o) olmakla birlikte, bu örnek koduyla ilerlendiğinde iletişim embedding mode ile gerçekleşmez.

```bash
Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:34:20
                                                                     in 0.017s (0)
...
tcp4       0      0  127.0.0.1.4222         127.0.0.1.63769        TIME_WAIT
```

- `watch 'netstat -an | grep 127.0.0.1'` komutuyla [localhost](http://localhost) (127.0.0.1) üzerinden giden ağ trafiğini izlerken `go run .` ile ilgili go dosyası çalıştırıldığında, NATS'ın varsayılan portu olan `4222` üzerinden başlayan yeni ağ isteklerinin eklendiğini görebilirsiniz.

# Embedding modu için doğru yapılandırmalar

- Amaçlandığı gibi embedded mode ile iletişim kurmak için aşağıdaki iki seçeneğe ihtiyaç vardır:
  - Client: `InProcessServer` seçeneği eklenmelidir.
  - Server: `Server.Options` içinde `DontListen` adlı bayrak (flag) `true` olarak belirtilmelidir.
- Bu kısımlar resmi olarak dokümante edilmemiştir; bu özelliğin başlangıcı ise şu [PR](https://github.com/nats-io/nats-server/pull/2360) üzerinden takip edilebilir.

  > Bu PR üç şey ekliyor:
  >
  > 1. `Server` üzerinde, TCP soketleri kullanmadan NATS sunucusuna bağlantı sağlayan bir `net.Pipe` oluşturan `InProcessConn()` fonksiyonu
  > 2. NATS sunucusuna olağan TCP dinleyicisinde dinleme yapmamasını söyleyen `DontListen` seçeneği
  > 3. `AcceptLoop` başlatılmadan hemen önce kapatılan ve `readyForConnections` fonksiyonunun beklediği `startupComplete` kanalı
  >
  > Bunun temel motivasyonu, tek süreçli (monolith) veya çok süreçli (polylith) modda çalışabilen bir uygulamamız olmasıdır. Basitlik için her iki mod için de NATS kullanmak istiyoruz; ancak monolith modunun, soket bağlantısı açmanın ya anlamsız olduğu (mobil) ya da tamamen mümkün olmadığı (WASM) çeşitli platformları desteklemesi gerekiyor. Bu değişiklikler, NATS'ı tamamen süreç içinde kullanmamıza olanak tanıyacak.
  >
  > İlgili [nats-io/nats.go#774](https://github.com/nats-io/nats.go/pull/774) PR'ı, istemci tarafına destek ekliyor.
  >
  > Bu projeye yaptığım ilk PR olduğundan, herhangi bir yerde bariz bir şeyi kaçırmışsam şimdiden özür dilerim.
  >
  > /cc @nats-io/core

# Embedded mode için çalışan örnek

```go
package main

import (
	"fmt"
	"time"

	"github.com/nats-io/nats-server/v2/server"
	"github.com/nats-io/nats.go"
)

func main() {
	opts := &server.Options{
		// Gömülü NATS sunucusunu yapılandırmak için
		// DonListen değerini true olarak ayarla
		DontListen: true,
	}

	// Seçeneklerle yeni sunucuyu başlat
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

	// Sunucuyu goroutine aracılığıyla başlat
	go ns.Start()

	// Sunucunun bağlantılar için hazır olmasını bekle
	if !ns.ReadyForConnections(10 * time.Second) {
		panic("not ready for connection")
	}

	// Sunucuya süreç içi (in-process) bağlantı ile bağlan
	nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Konuya abone ol
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Mesaj verisini yazdır
		data := string(msg.Data)
		fmt.Println(data)

		// Sunucuyu kapat (isteğe bağlı)
		ns.Shutdown()
	})

	// Konuya veri yayınla
	nc.Publish(subject, []byte("Hello embedded NATS!"))

	// Sunucunun kapanmasını bekle
	ns.WaitForShutdown()
}

```

```bash
Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:37:50
                                                                     in 0.023s (0)
...no additional logs

```

- Artık amaçlandığı gibi ek bir network hop oluşmadığını görebilirsiniz.

# Perde arkası

## TL;DR

![diagram1](/assets/images/embedded-nats/seq1.svg)

- Bu kod `main.go` içinde çalıştırıldığında, dahili olarak hangi fonksiyonların nasıl çalıştığını gösteren sequence diagram'dır ve özü aşağıdaki gibidir.
  - `DontListen: true` ile sunucu, `AcceptLoop` adlı client listening phase aşamasını atlar.
  - Client'ın Connect seçeneklerinden `InProcessServer` etkinleştirilirse, bellek içi (in-memory) bir bağlantı oluşturulur, `net.Pipe` ile bir pipe yaratılır ve pipe'ın diğer ucu `net.Conn` tipinde client'a döndürülür.
  - Client ve server bu bağlantı üzerinden in-process iletişim gerçekleştirir.

## Server

### AcceptLoop

```go
// nats-server/server/server.go

// Wait for clients.
if !opts.DontListen {
	s.AcceptLoop(clientListenReady)
}
```

- Öncelikle `DontListen` true ise, `AcceptLoop` adlı client listening phase aşaması atlanır.

```go
// nats-server/server/server.go

// AcceptLoop is exported for easier testing.
func (s *Server) AcceptLoop(clr chan struct{}) {
	// If we were to exit before the listener is setup properly,
	// make sure we close the channel.
	defer func() {
		if clr != nil {
			close(clr)
		}
	}()

	if s.isShuttingDown() {
		return
	}

	// Snapshot server options.
	opts := s.getOpts()

	// Setup state that can enable shutdown
	s.mu.Lock()
	hp := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))
	l, e := natsListen("tcp", hp)
	s.listenerErr = e
	if e != nil {
		s.mu.Unlock()
		s.Fatalf("Error listening on port: %s, %q", hp, e)
		return
	}
	s.Noticef("Listening for client connections on %s",
		net.JoinHostPort(opts.Host, strconv.Itoa(l.Addr().(*net.TCPAddr).Port)))

	// Alert of TLS enabled.
	if opts.TLSConfig != nil {
		s.Noticef("TLS required for client connections")
		if opts.TLSHandshakeFirst && opts.TLSHandshakeFirstFallback == 0 {
			s.Warnf("Clients that are not using \"TLS Handshake First\" option will fail to connect")
		}
	}

	// If server was started with RANDOM_PORT (-1), opts.Port would be equal
	// to 0 at the beginning this function. So we need to get the actual port
	if opts.Port == 0 {
		// Write resolved port back to options.
		opts.Port = l.Addr().(*net.TCPAddr).Port
	}

	// Now that port has been set (if it was set to RANDOM), set the
	// server's info Host/Port with either values from Options or
	// ClientAdvertise.
	if err := s.setInfoHostPort(); err != nil {
		s.Fatalf("Error setting server INFO with ClientAdvertise value of %s, err=%v", opts.ClientAdvertise, err)
		l.Close()
		s.mu.Unlock()
		return
	}
	// Keep track of client connect URLs. We may need them later.
	s.clientConnectURLs = s.getClientConnectURLs()
	s.listener = l

	go s.acceptConnections(l, "Client", func(conn net.Conn) { s.createClient(conn) },
		func(_ error) bool {
			if s.isLameDuckMode() {
				// Signal that we are not accepting new clients
				s.ldmCh <- true
				// Now wait for the Shutdown...
				<-s.quitCh
				return true
			}
			return false
		})
	s.mu.Unlock()

	// Let the caller know that we are ready
	close(clr)
	clr = nil
}
```

- Referans olarak, AcceptLoop fonksiyonu şu adımları gerçekleştirir. `TLS` veya `hostPort` gibi network iletişimiyle ilgili kısımlar, süreç içi iletişim yapıldığında gerekli olmadığından atlanabilir.

## Client

### InProcessServer

```go

// nats-go/nats.go

// Connect will attempt to connect to the NATS system.
// The url can contain username/password semantics. e.g. nats://derek:pass@localhost:4222
// Comma separated arrays are also supported, e.g. urlA, urlB.
// Options start with the defaults but can be overridden.
// To connect to a NATS Server's websocket port, use the `ws` or `wss` scheme, such as
// `ws://localhost:8080`. Note that websocket schemes cannot be mixed with others (nats/tls).
func Connect(url string, options ...Option) (*Conn, error) {
	opts := GetDefaultOptions()
	opts.Servers = processUrlString(url)
	for _, opt := range options {
		if opt != nil {
			if err := opt(&opts); err != nil {
				return nil, err
			}
		}
	}
	return opts.Connect()
}
```

```go
// nats-go/nats.go

// Options can be used to create a customized connection.
type Options struct {
	// Url represents a single NATS server url to which the client
	// will be connecting. If the Servers option is also set, it
	// then becomes the first server in the Servers array.
	Url string

	// InProcessServer represents a NATS server running within the
	// same process. If this is set then we will attempt to connect
	// to the server directly rather than using external TCP conns.
	InProcessServer InProcessConnProvider

	//...
}
```

```go
// nats-go/nats.go

type InProcessConnProvider interface {
	InProcessConn() (net.Conn, error)
}
```

- NATS server ile NATS client arasındaki bağlantıyı kuran `Connect` fonksiyonu, client URL'si ve connect Option'larını alır; bu Option'ları bir araya getiren Options struct'ında ise `InProcesConnProvider` interface tipinde `InProcessServer` adlı bir field bulunur.

```go
// main.go of example code

// Initialize new server with options
ns, err := server.NewServer(opts)

//...

// Connect to server via in-process connection
nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
```

- NATS client tarafında Connect çağrılırken, `InProcessServer` field'ına `nats.InProcessServer(ns)` verildiğinde

```go
// nats-go/nats.go

// InProcessServer is an Option that will try to establish a direction to a NATS server
// running within the process instead of dialing via TCP.
func InProcessServer(server InProcessConnProvider) Option {
	return func(o *Options) error {
		o.InProcessServer = server
		return nil
	}
}
```

- Option'daki InProcessServer, gömülü NATS sunucusuyla değiştirilir ve

```go
// nats-go/nats.go

// createConn will connect to the server and wrap the appropriate
// bufio structures. It will do the right thing when an existing
// connection is in place.
func (nc *Conn) createConn() (err error) {
	if nc.Opts.Timeout < 0 {
		return ErrBadTimeout
	}
	if _, cur := nc.currentServer(); cur == nil {
		return ErrNoServers
	}

	// If we have a reference to an in-process server then establish a
	// connection using that.
	if nc.Opts.InProcessServer != nil {
		conn, err := nc.Opts.InProcessServer.InProcessConn()
		if err != nil {
			return fmt.Errorf("failed to get in-process connection: %w", err)
		}
		nc.conn = conn
		nc.bindToNewConn()
		return nil
	}

	//...
}
```

- Bu interface, bağlantıyı oluşturan `createConn` fonksiyonunda, `InProcessServer` option'ı nil olmadığında (geçerli olduğunda) option içindeki InProcessServer'ın `InProcesConn` fonksiyonunu çalıştırarak

```go
// nats-server/server/server.go

// InProcessConn returns an in-process connection to the server,
// avoiding the need to use a TCP listener for local connectivity
// within the same process. This can be used regardless of the
// state of the DontListen option.
func (s *Server) InProcessConn() (net.Conn, error) {
	pl, pr := net.Pipe()
	if !s.startGoRoutine(func() {
		s.createClientInProcess(pl)
		s.grWG.Done()
	}) {
		pl.Close()
		pr.Close()
		return nil, fmt.Errorf("failed to create connection")
	}
	return pr, nil
}
```

- server'da uygulanan `InProcessConn` fonksiyonunu çağırır.
- Bu fonksiyon, nats'ın Go client'ı olan `nats.go` içinde nc (nats connection) üzerindeki `InProcessServer` nil değilse çağrılır, connection (`net.Conn`) oluşturur ve bunu server'ın connection'ına bind eder.

# Go'da tüketici tanımlı arayüz (consumer driven interface)

> Bir tip, bir arayüzü yalnızca metodlarını uygulayarak gerçekleştirir. Açık bir niyet bildirimi, "implements" anahtar kelimesi yoktur. Örtük arayüzler, arayüzün tanımını uygulamasından ayırır; böylece bu arayüz, önceden bir düzenleme yapılmadan herhangi bir pakette yer alabilir.

[Interfaces are implemented implicitly, A Tour of Go](https://go.dev/tour/methods/10)

> Bir tip yalnızca bir arayüzü uygulamak için var oluyorsa ve bu arayüzün ötesinde hiçbir exported metodu olmayacaksa, tipin kendisini dışa aktarmaya gerek yoktur.

[Generality, Effective Go](https://go.dev/doc/effective_go#generality)

- Bu interface tasarımı, Go'da sıkça bahsedilen consumer defined interface ve structural typing (duck typing) kavramlarını iyi yansıttığından, bu konuyu da birlikte tanıtmak istiyorum.

```go
// nats-go/nats.go

// Options can be used to create a customized connection.
type Options struct {
	// Url represents a single NATS server url to which the client
	// will be connecting. If the Servers option is also set, it
	// then becomes the first server in the Servers array.
	Url string

	// InProcessServer represents a NATS server running within the
	// same process. If this is set then we will attempt to connect
	// to the server directly rather than using external TCP conns.
	InProcessServer InProcessConnProvider

	//...
}
```

```go
// nats-go/nats.go

type InProcessConnProvider interface {
	InProcessConn() (net.Conn, error)
}
```

- Koda geri dönelim. nats.go client'ında `InProcessServer` option struct field'ı, yalnızca `InProcessConn` işlevini gerçekleştiren `InProcessConnProvider` interface'i olarak tanımlanmıştır.

```go
// nats-server/server/server.go

// InProcessConn returns an in-process connection to the server,
// avoiding the need to use a TCP listener for local connectivity
// within the same process. This can be used regardless of the
// state of the DontListen option.
func (s *Server) InProcessConn() (net.Conn, error) {
	pl, pr := net.Pipe()
	if !s.startGoRoutine(func() {
		s.createClientInProcess(pl)
		s.grWG.Done()
	}) {
		pl.Close()
		pr.Close()
		return nil, fmt.Errorf("failed to create connection")
	}
	return pr, nil
}
```

- Ancak buna atanan tip, nats-server'daki `Server` olup, InProcessConn dışında birçok başka işlevi de yerine getirmektedir.
- Çünkü bu durumda client'ın ilgilendiği tek şey `InProcessConn` adlı bir interface'in sağlanıp sağlanmadığıdır; diğerleri pek önemli değildir.
- Bu nedenle nats.go client'ı, yalnızca `InProcessConn() (net.Conn, error)` işlevini tanımlayan `InProcessConnProvider` adlı consumer defined interface'i oluşturup kullanmaktadır.

# Sonuç

- NATS'ın embedded mode'unu, çalışma biçimini ve NATS kodu üzerinden incelenebilen Go'nun consumer defined interface yapısını kısaca ele aldık.
- Bu bilgilerin, NATS'ı yukarıdaki amaçlarla kullanan kişilere yardımcı olmasını umuyor, yazıyı burada bitirmek istiyorum.
