🐈

シンプルなTCP・UDPサーバを作ってみた

2021/06/24に公開

前提

学習のメモです。
TCP・UDP の仕組みについては言及しません。
具体的には、チェックサム、輻輳制御、フロー制御等については言及しません。
TCP はコネクション志向で、UDP はコネクションレスととは知っていたものの漠然としていたので、肌身で感じるために実装しました。

概要

ゆるふわな感じでサーバを作ってどのように通信しているか確認します。

nc(netcat)を使えばサーバとクライアントを用意できるので通信自体は簡単にできますが、tcp サーバも作ってみたかったので、サーバの方は雰囲気で Go で実際に実装してみました。

本題

Go で TCP サーバ及び UDP サーバをつくって通信の確認をします。

TCPサーバの実装

適当に Go で TCP サーバを作ります。

package main

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
)

var dead = 10 * time.Second

func main() {
	if err := run(); err != nil {
		fmt.Printf("error: %v\n", err)
	}
}

func run() error {
	termCh := make(chan os.Signal, 1)
	signal.Notify(termCh, syscall.SIGKILL, syscall.SIGINT)
	errCh := make(chan error, 1)

	addr := &net.TCPAddr{
		IP:   net.ParseIP("127.0.0.1"),
		Port: 8000,
	}
	ln, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return err
	}

	log.Println("Starting tcp server...")

	go func() {
		for {
			ctx := context.Background()
			conn, err := ln.Accept()
			if err != nil {
				errCh <- err
			}
			go func() {
				err = handle(ctx, conn)
				if !(errors.Is(err, os.ErrDeadlineExceeded) || errors.Is(err, io.EOF)) {
					errCh <- err
				}
				fmt.Println("connection is disconnected ")
			}()
		}
	}()

	select {
	case <-termCh:
		return errors.New("terminated by signal")
	case err = <-errCh:
		return err
	}
}

func handle(ctx context.Context, conn net.Conn) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	go func() {
		<-ctx.Done()
		conn.Close()
	}()

	err := conn.SetDeadline(time.Now().Add(dead))
	if err != nil {
		return err
	}
	defer conn.Close()

	r := bufio.NewReader(conn)
	for {
		t, err := r.ReadString('\n')
		if err != nil {
			return err
		}

		log.Printf("Receiving data: %v from %s", strings.TrimSuffix(t, "\n"), conn.RemoteAddr().String())
		log.Printf("Sending data..")
		conn.Write([]byte(fmt.Sprintf("received msg: %s\n", strings.TrimSuffix(t, "\n"))))
		log.Printf("Complete Sending data..")
	}
}

タイムアウトを 10 にして通信を確認してみます。

nc(netcat)コマンドでリクエストを送ってみます。

nc 127.0.0.1 8000

helloworld を送ってみます。

> hello
< received msg: hello
> world
< received msg: world

しっかりサーバから応答がきています。

そのままタイムアウトして、WireShark でもみてみます。

tcp

23~25 で 3way ハンドシェイクをしているのが確認できます。その後、よくわかりませんが、TCP window update してそこからデータのやり取りを始めています。

データの送受信の部分では、 [ACK] -> [PSH,ACK] の順で通信をし、[PSH,ACK] で TCP payload を送っているのが変わりました。ここらへんはフーンくらいで。

最後にコネクションを Close をしているのを確認できました。

UDPサーバの実装

package main

import (
	"errors"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

var mux sync.RWMutex
var buf = make([]byte, 5)

func run() error {
	termCh := make(chan os.Signal, 1)
	signal.Notify(termCh, syscall.SIGKILL, syscall.SIGINT)
	errCh := make(chan error, 1)

	udpAddr := &net.UDPAddr{
		IP:   net.ParseIP("127.0.0.1"),
		Port: 6000,
	}
	ln, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		return err
	}
	defer ln.Close()

	log.Println("Starting udp server...")

	go func() {
		for {
			err = handle(ln)
			if err != nil {
				errCh <- err
			}
		}
	}()

	select {
	case <-termCh:
		return errors.New("terminated by signal")
	case err = <-errCh:
		return err
	}
}

func handle(ln *net.UDPConn) error {
	mux.Lock()
	defer mux.Unlock()
	n, addr, err := ln.ReadFromUDP(buf)
	if err != nil {
		return err
	}

	log.Printf("Receiving data: %s from %s", string(buf[:n]), addr.String())
	log.Printf("Sending data..")
	ln.WriteTo([]byte(fmt.Sprintf("received msg: %v\n", string(buf))), addr)
	log.Printf("Complete Sending data..")
	return nil
}

バッファは 5byte しか許していません。なのでそれ以上が送られてきても切り捨て御免です。

また、バッファ(buf)を使いまわしていますが、メモリを逐一確保したくないくらいの意図で、特に強い意味はありません。

TCP との大きな違いは以下の部分です。

ln.WriteTo([]byte(fmt.Sprintf("received msg: %v\n", string(buf))), addr)

ここで n, addr, err := ln.ReadFromUDP(buf) からきたアドレス宛のソケットに対して書き込んでいます。コネクションレスなのでトランスポート層ではポート番号を保持しないので、アプリケーション側でよしなにやる必要があるのでしょうかね。

それでは、nc(netcat)コマンドで通信をしてみます。

nc -u 127.0.0.1 6000

UDP で通信するので -u オプションをつけています。

hello world と打ってみます。

hello world
received msg: hello

helloと返ってきました。きっちり 5byte 分だけ返ってきました。

WireShark で通信の確認をしてみます。

ws

クライアントがリクエストを投げてサーバが応答するだけです。TCP の概念である ACKFIN といった概念がなく、非常にシンプルですね。
ws

クライアントのリクエストを見てみると Data 的には 12byte 分(hello world)しっかり送っています。

終わりに

シンプルな TCP サーバと UDP サーバを実装して、WireShark で通信を確認しました。

UDP はシンプルな通信ですが、TCP は信頼のある通信のために色々と複雑なことをしていると実感できました。
クライアント側で、コネクションプーリングなどをして親切な通信をする大切さが 1 ミリだけ理解できました。

参考にしたもの

GitHubで編集を提案

Discussion