Golang ile eşzamanlı bir TCP (İletim Kontrol Protokolü) istemcisi ve sunucu örneği oluşturmak için aşağıdaki basit örneği kullanabilirsiniz. Eşzamanlı bir sunucuya sahip olmanın avantajı, sunucunuzun aynı anda birden fazla istemciye (bağlantı) hizmet verebilmesidir. TCP, tasarım gereği güvenilir bir iletişim protokolüdür. TCP paket dağıtım sorunlarını tespit ettiğinde, kayıp verilerin yeniden iletilmesini talep eder, sıra dışı verileri yeniden düzenler, diğer sorunların ortaya çıkmasını azaltmak için ağ tıkanıklığını en aza indirmeye yardımcı olur. IP ağı üzerinden iletişim kuran ana bilgisayarlarda çalışan uygulamalar arasında kullanılır. Bu yazının kapsamı dışında kalan diğer ağ protokolleri gibi kendi güvenlik açıklarına sahiptir. TCP, UDP (Kullanıcı Datagram Protokolü) protokolüne kıyasla daha güvenilir ancak daha yavaştır. Bu yazının kapsamı dışında olmasına rağmen, size UPD hakkında çok kısa bir bilgi vereyim. UDP hata kontrolü, düzeltme veya paket yeniden iletimi sağlamaz, bu nedenle güvenilmez ancak hızlıdır. Video konferans, canlı akış, gerçek zamanlı uygulamalar gibi çevrimiçi oyunlar için yaygın olarak kullanılır.


İstemci


package main

import (
"bufio"
"io"
"log"
"net"
"os"
"strings"
)

func main() {
con, err := net.Dial("tcp", "0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
defer con.Close()

clientReader := bufio.NewReader(os.Stdin)
serverReader := bufio.NewReader(con)

for {
// Waiting for the client request
clientRequest, err := clientReader.ReadString('\n')

switch err {
case nil:
clientRequest := strings.TrimSpace(clientRequest)
if _, err = con.Write([]byte(clientRequest + "\n")); err != nil {
log.Printf("failed to send the client request: %v\n", err)
}
case io.EOF:
log.Println("client closed the connection")
return
default:
log.Printf("client error: %v\n", err)
return
}

// Waiting for the server response
serverResponse, err := serverReader.ReadString('\n')

switch err {
case nil:
log.Println(strings.TrimSpace(serverResponse))
case io.EOF:
log.Println("server closed the connection")
return
default:
log.Printf("server error: %v\n", err)
return
}
}
}

Eşzamanlı Sunucu


package main

import (
"bufio"
"io"
"log"
"net"
"strings"
)

func main() {
listener, err := net.Listen("tcp", "0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
defer listener.Close()

for {
con, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}

// If you want, you can increment a counter here and inject to handleClientRequest below as client identifier
go handleClientRequest(con)
}
}

func handleClientRequest(con net.Conn) {
defer con.Close()

clientReader := bufio.NewReader(con)

for {
// Waiting for the client request
clientRequest, err := clientReader.ReadString('\n')

switch err {
case nil:
clientRequest := strings.TrimSpace(clientRequest)
if clientRequest == ":QUIT" {
log.Println("client requested server to close the connection so closing")
return
} else {
log.Println(clientRequest)
}
case io.EOF:
log.Println("client closed the connection by terminating the process")
return
default:
log.Printf("error: %v\n", err)
return
}

// Responding to the client request
if _, err = con.Write([]byte("GOT IT!\n")); err != nil {
log.Printf("failed to respond to client: %v\n", err)
}
}
}

Notlar


İstemci sunucuya bir istek gönderir. Sunucu GOT IT! ile yanıt veriyor. İstemci :QUIT yazıp enter tuşuna basarak bağlantıyı kesebilir. Sunucu isteği anlar ve istemci arasındaki ilgili bağlantıyı kapatacaktır. Sunucu ayrıca istemcinin bağlantıyı nasıl kesmek istediğini de anlar. Örneğin, Ctrl+C veya Ctrl+]'den sonra telnet> close sinyali gibi. Test amacıyla yukarıdaki istemci dosyamız yerine Telnet kullanmak istiyorsanız, bunu aşağıda gösterildiği gibi yapabilirsiniz. İstemci bağlantıyı kapattığında, bu sunucununda kapatılması anlamına gelmez. Açıkça kapatılmadığı sürece sunucu her zaman açıktır.


TCP sunucusu her zaman çalışır durumda olacak ve yeni bir TCP istemci bağlantısı bekleyecektir. Yeni bir istemci TCP sunucusuna her bağlandığında, yeni ve benzersiz bir TCP bağlantısı oluşturulur. Bunu, aşağıda gösterildiği gibi netstat komutunu çalıştırarak doğrulayabilirsiniz.


# When we have two clients connected to the server. One with Telnet and another with our Go script.
$ netstat -anp TCP | grep 9999
tcp4 0 0 127.0.0.1.9999 127.0.0.1.51877 ESTABLISHED # -> Client 2: established conn
tcp4 0 0 127.0.0.1.51877 127.0.0.1.9999 ESTABLISHED # -> Server: listening Client 2 conn
tcp4 0 0 127.0.0.1.9999 127.0.0.1.51872 ESTABLISHED # -> Client 1: established conn
tcp4 0 0 127.0.0.1.51872 127.0.0.1.9999 ESTABLISHED # -> Server: listening Client 1 conn
tcp46 0 0 *.9999 *.* LISTEN # -> Server listening

# After clients close connections.
$ netstat -anp TCP | grep 9999
tcp46 0 0 *.9999 *.* LISTEN

Dikkat edilmesi gereken bir nokta, bağlantılar tamamen kaybolmadan önce birkaç saniye boyunca ESTABLISHED durumundan TIME_WAIT durumuna dönecektir. TIME_WAIT sizin/istemcinizin bağlantıyı kapattığını, dolayısıyla bağlantının bir süre açık tutulmasını sağlar, böylece teslim edilmeyen tüm paketler bağlantıyı gerçekten kapatmadan önce işlenmek üzere bağlantıya teslim edilebilir. Başlangıçta bundan bahsettik.


Test


Bir istemci oturumuna girdikten sonra, ne olacağını görmek için bir şeyler yazın. Çıkış işlemlerini de test edebilirsiniz.


Sunucuyu çalıştırın


$ go run -race main.go

İstemciyi çalıştırın


$ go run -race main.go

Telnet


$ telnet 0.0.0.0 9999