golang隨筆3

zhongweiLeex發表於2024-03-24

1. select 語句

1. 什麼是select:

select 是 Go 語言中用於實現多路複用的關鍵字。它可以監視多個通道檔案描述符,並在其中任何一個就緒時執行相應的操作。

2. select 的工作原理:

  • select 語句會阻塞,直到它監視的某個通道或檔案描述符就緒。
  • 當某個通道或檔案描述符就緒時,select 語句會隨機選擇一個就緒的通道或檔案描述符,並執行與該通道或檔案描述符關聯的case語句。
  • 如果沒有通道或檔案描述符就緒,select 語句會一直阻塞,直到有通道或檔案描述符就緒。

3. select 的語法:

Go

select {
case <-ch1:
    // 處理來自ch1的資料
case <-ch2:
    // 處理來自ch2的資料
default:
    // 沒有通道就緒時執行的操作
}

4. select 的應用場景:

  • 網路 I/O: 可以使用 select 來監視多個網路連線,並在其中任何一個連線可讀或可寫時執行相應的操作。
  • 定時器: 可以使用 select 來實現定時器功能。
  • 併發控制: 可以使用 select 來實現併發控制,例如互斥鎖和條件變數。

5. select 的注意事項:

  • select 語句中的 case 語句必須是順序執行的。
  • select 語句中的 default 語句是可選的,如果省略了 default 語句,則當沒有通道或檔案描述符就緒時,select 語句會一直阻塞。
  • select 語句可以與 goroutine 結合使用,以實現更復雜的併發操作。

以下是一些select的應用示例:

  • 網路 I/O:

Go

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

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()

    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            log.Fatal(err)
        }

        // 處理資料
        _, err = conn.Write(buf[:n])
        if err != nil {
            log.Fatal(err)
        }
    }
}
  • 定時器:

Go

func main() {
    timer := time.NewTimer(time.Second)

    select {
    case <-timer.C:
        // 定時器超時
    }
}

以下是一個使用 select 語句的簡單示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- "Hello"
	}()

	go func() {
		time.Sleep(3 * time.Second)
		ch2 <- "World"
	}()

	select {
	case msg1 := <-ch1:
		fmt.Println("Received:", msg1)
	case msg2 := <-ch2:
		fmt.Println("Received:", msg2)
	case <-time.After(1 * time.Second):
		fmt.Println("Timeout occurred")
	}
}

在這個示例中,我們建立了兩個通道 ch1ch2,然後分別在不同的 Go 協程中向它們傳送訊息。使用 select 語句來等待兩個通道的訊息,並在其中一個通道接收到訊息時列印出來。如果某個通道在1秒內都沒有訊息,則會觸發超時。‘

原子操作

概念: 在Go語言中,原子操作是一種特殊的操作,它們能夠確保在併發環境下對共享資料的讀取、修改和儲存是不可中斷的,從而避免了競態條件和資料競爭。

解釋: 原子操作是指在單個CPU指令中執行的操作,它們保證了對共享資料的操作是原子性的,即不會被中斷或干擾。這意味著其他goroutine無法同時訪問或修改同一資料,從而確保了資料的一致性和可靠性。

使用方法: 在Go語言中,可以使用sync/atomic包提供的原子操作函式來進行原子操作。這些函式能夠對基本資料型別(如整數和指標)執行原子讀取、寫入、增加、減少等操作。一些常見的原子操作函式包括AddXXXLoadXXXStoreXXX等,其中XXX代表不同的資料型別。

使用場景:

  1. 計數器和計時器: 當需要在併發環境下對計數器或計時器進行操作時,可以使用原子操作來確保操作的一致性。
  2. 狀態標誌: 在需要進行狀態判斷或更新時,可以使用原子操作來避免競態條件,保證狀態的正確性。

使用考慮:

  1. 效能最佳化: 原子操作通常比鎖定更輕量級,因此在高併發場景下可能更具效能優勢。
  2. 適用範圍: 原子操作適用於對單個變數的簡單操作,複雜的操作可能需要使用鎖或其他同步機制來實現。
package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

var counter int32 // 使用int32型別的計數器

func increment() {
	atomic.AddInt32(&counter, 1) // 原子增加計數器的值
	fmt.Println("Counter Incremented to:", atomic.LoadInt32(&counter)) // 原子讀取計數器的值
}

func main() {
	for i := 0; i < 10; i++ {
		go increment()
	}
	time.Sleep(time.Second) // 等待goroutine完成
	fmt.Println("Final Counter Value:", atomic.LoadInt32(&counter)) // 原子讀取最終的計數器值
}

方法 解釋
func LoadInt32(addr *int32) (val int32)func LoadInt64(addr *int64) (val int64)func LoadUint32(addr *uint32) (val uint32)func LoadUint64(addr *uint64) (val uint64)func LoadUintptr(addr *uintptr) (val uintptr)func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) 讀取操作
func StoreInt32(addr *int32, val int32)func StoreInt64(addr *int64, val int64)func StoreUint32(addr *uint32, val uint32)func StoreUint64(addr *uint64, val uint64)func StoreUintptr(addr *uintptr, val uintptr)func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) 寫入操作
func AddInt32(addr *int32, delta int32) (new int32)func AddInt64(addr *int64, delta int64) (new int64)func AddUint32(addr *uint32, delta uint32) (new uint32)func AddUint64(addr *uint64, delta uint64) (new uint64)func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) 修改操作
func SwapInt32(addr *int32, new int32) (old int32)func SwapInt64(addr *int64, new int64) (old int64)func SwapUint32(addr *uint32, new uint32) (old uint32)func SwapUint64(addr *uint64, new uint64) (old uint64)func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) 交換操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) 比較並交換操作
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Counter interface {
	Inc()
	Load() int64
}

// 普通版
type CommonCounter struct {
	counter int64
}

func (c CommonCounter) Inc() {
	c.counter++
}

func (c CommonCounter) Load() int64 {
	return c.counter
}

// 互斥鎖版
type MutexCounter struct {
	counter int64
	lock    sync.Mutex
}

func (m *MutexCounter) Inc() {
	m.lock.Lock()
	defer m.lock.Unlock()
	m.counter++
}

func (m *MutexCounter) Load() int64 {
	m.lock.Lock()
	defer m.lock.Unlock()
	return m.counter
}

// 原子操作版
type AtomicCounter struct {
	counter int64
}

func (a *AtomicCounter) Inc() {
	atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
	return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			c.Inc()
			wg.Done()
		}()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(c.Load(), end.Sub(start))
}

func main() {
	c1 := CommonCounter{} // 非併發安全
	test(c1)
	c2 := MutexCounter{} // 使用互斥鎖實現併發安全
	test(&c2)
	c3 := AtomicCounter{} // 併發安全且比互斥鎖效率更高
	test(&c3)
}

context

作用

在 Go 語言中,上下文(Context)是一個標準庫中的型別,用於在函式之間傳遞請求作用域的資料、取消訊號和超時。

上下文主要用於以下幾個方面:

  1. 請求作用域的資料傳遞: 在一個請求處理過程中,可能會涉及多個函式之間的呼叫,這些函式需要共享一些請求相關的資訊,比如請求的 ID、使用者身份驗證資訊等。透過上下文,可以將這些資訊傳遞給所有相關的函式,而不必每個函式都顯式地傳遞這些引數。

  2. 取消訊號: 上下文可以用於在請求處理過程中傳遞取消訊號,以便在某些情況下中止正在進行的操作。這在處理超時請求或者收到中斷訊號時特別有用。

  3. 超時控制: 上下文還可以用於設定操作的超時時間。如果某個操作在超時時間內未完成,可以透過上下文取消訊號來中止該操作。

在 Go 中,上下文通常是透過 context.Context 型別來表示的,標準庫提供了一系列與上下文相關的函式和方法,例如 context.WithCancel()context.WithTimeout()context.WithDeadline() 等,用於建立具有取消和超時功能的上下文。

總之,上下文提供了一種有效的方式來管理請求作用域的資料、處理取消訊號和控制操作的超時,從而提高了程式的可靠性和可控性。

示例

package main

import (
	"context"
	"fmt"
	"time"
)

// worker 接收到上下文的 停止訊號 則停止工作
func worker(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s 收到取消訊號 , 停止工作 \n", name)
			return
		default:
			fmt.Printf("%s 正在工作 ... \n", name)
			time.Sleep(1 * time.Second)
		}
	}
}

func main() {
	// 建立一個父級上下文
	parentCtx := context.Background()
	// 建立一個 3秒鐘自動取消的上下文
	ctx, cancelFunc := context.WithTimeout(parentCtx, 3*time.Second)

	defer cancelFunc()

	// 開啟兩個goroutine
	go worker(ctx, "worker1")
	go worker(ctx, "worker2")

	// 等待時間  手動取消上下文
	time.Sleep(2 * time.Second)
	// 手動呼叫 cancel() 向上下文中 傳送取消訊號
	cancelFunc() // 手動取消上下文

	time.Sleep(2 * time.Second)
	fmt.Println("主程式退出")

}

常用方法

下面是 context 包中常用的方法及其含義:

方法 含義
context.Background() 返回一個空的 Context,通常用作根 Context
context.TODO() 返回一個非空的 Context,表示不確定的 Context
context.WithCancel() 返回父上下文的副本,並建立一個新的 CancelFunc。當呼叫該函式時,會取消該 Context
context.WithDeadline() 返回父上下文的副本,並設定截止時間,超過截止時間後,該 Context 會自動被取消。
context.WithTimeout() 返回父上下文的副本,並設定超時時間,超過超時時間後,該 Context 會自動被取消。
context.WithValue() 返回父上下文的副本,並設定一個鍵值對,可以在 Context 中儲存和傳遞請求作用域資料。
ctx.Done() 返回一個 chan,當 Context 被取消或者超時時,該 chan 會被關閉。
ctx.Err() 返回取消原因,如果 Context 沒有被取消,則返回 nil
ctx.Value(key interface{}) 返回與鍵關聯的值,如果 Context 中不存在該鍵,則返回 nil

這些方法可以幫助我們有效地使用 Context,實現併發控制、超時處理和請求作用域資料的傳遞。

errorGroup

errgroup 包是 Go 語言標準庫 golang.org/x/sync/errgroup 提供的一個有用工具,用於在併發任務中管理多個 goroutine,並能夠捕獲這些 goroutine 中的錯誤。

該包中最重要的元件是 errgroup.Group 結構體,它提供了管理和等待 goroutine 完成的功能,並能夠在任何一個 goroutine 出現錯誤時取消所有任務。

下面是 errgroup.Group 結構體的主要方法:

方法 描述
func (g *Group) Go(f func() error) 啟動一個新的 goroutine 來執行給定的函式,函式的返回值為 error 型別。如果函式返回非 nil 的錯誤,則所有 goroutine 都會被取消。
func (g *Group) Wait() error 等待所有 goroutine 完成,並返回第一個出現的錯誤,如果沒有出現錯誤則返回 nil。
func (g Group) WithContext(ctx context.Context) (Group, context.Context) 返回一個新的 Group 和繼承自指定上下文的上下文,這樣新建立的 goroutine 將繼承該上下文。

下面是一個示例程式碼,演示瞭如何使用 errgroup.Group 來管理併發任務,並捕獲其中的錯誤:

package main

import (
	"context"
	"fmt"
	"golang.org/x/sync/errgroup"
)

func main() {
	// 建立一個帶有上下文的 Group
	g, ctx := errgroup.WithContext(context.Background())

	// 啟動多個 goroutine 來執行任務
	for i := 0; i < 3; i++ {
		id := i
		g.Go(func() error {
			// 模擬執行任務,這裡將第二個任務模擬為失敗的情況
			if id == 1 {
				return fmt.Errorf("goroutine %d 執行失敗", id)
			}
			fmt.Printf("goroutine %d 執行成功\n", id)
			return nil
		})
	}

	// 等待所有任務完成
	if err := g.Wait(); err != nil {
		fmt.Println("出現錯誤:", err)
	} else {
		fmt.Println("所有任務執行成功")
	}
}

在這個示例中,

建立了一個帶有上下文的 errgroup.Group,然後啟動了三個 goroutine 來執行任務。其中第二個任務被模擬為失敗。

最後,我們呼叫 Wait() 方法來等待所有任務完成,並檢查是否有任何錯誤發生。

網路程式設計

osi七層模型

OSI(開放系統互聯)模型是一個用於理解和描述網路通訊的框架,它將網路通訊劃分為七個層次,每個層次負責不同的功能。下面是 OSI 七層模型的詳細解釋:

下面是使用表格形式輸出的 OSI 七層模型的詳細解釋:

層級 名稱 功能
7 應用層(Application Layer) 提供應用程式間的通訊和資料交換。 提供了各種應用程式和網路服務,如電子郵件、檔案傳輸、網頁瀏覽等。
6 表示層(Presentation Layer) 資料的格式化、加密和壓縮。 使不同系統之間的資料能夠互相理解和交換。
5 會話層(Session Layer) 建立、管理和終止會話(或連線)。 提供了在兩個應用程式之間建立通訊會話所需的協議和機制。
4 傳輸層(Transport Layer) 端到端的資料傳輸,提供資料可靠性、流量控制和擁塞控制。 提供了資料可靠性、流量控制和擁塞控制等功能,通常透過 TCP(傳輸控制協議)或 UDP(使用者資料包協議)實現。
3 網路層(Network Layer) 選擇最佳路徑傳輸資料,實現邏輯地址分配和路由選擇。 實現了邏輯地址(如 IP 地址)的分配和路由選擇,以便在源和目的地之間進行通訊。
2 資料鏈路層(Data Link Layer) 直接相連節點之間的資料鏈路,處理資料幀的傳輸、同步和錯誤檢測。 負責資料的幀同步、流量控制、錯誤檢測和糾正等。
1 物理層(Physical Layer) 傳輸原始位元流,處理傳輸介質的物理特性,如電壓、速率、聯結器型別等。 處理與傳輸介質(如電纜、光纖)相關的物理特性,如電壓、訊號速率、聯結器型別等。

網路模型中的HTTP協議在通訊過程中會新增各個部首,這些部首通常包括請求頭和響應頭。下面是HTTP通訊過程中的主要步驟及部首的詳細解釋:

步驟 描述 舉例
1.建立連線 客戶端向伺服器傳送連線請求。 TCP握手
2.傳送請求 客戶端向伺服器傳送HTTP請求,包括請求行、請求頭和請求體。 GET /index.html HTTP/1.1
3.接收請求 伺服器接收到HTTP請求,準備處理請求,並返回HTTP響應。
4.傳送響應 伺服器向客戶端傳送HTTP響應,包括狀態行、響應頭和響應體。 HTTP/1.1 200 OK
5.接收響應 客戶端接收到HTTP響應,處理響應資料。
6.關閉連線 雙方根據需要決定是否關閉連線。

下面是HTTP請求和響應中常見的部首:

  • 請求行:包括請求方法、請求URL和HTTP協議版本。
  • 請求頭:包括客戶端向伺服器傳遞的附加資訊,如User-Agent、Accept、Content-Type等。
  • 響應行:包括HTTP協議版本、狀態碼和狀態訊息。
  • 響應頭:包括伺服器向客戶端傳遞的附加資訊,如Server、Content-Type、Content-Length等。

HTTP請求和響應的部首在通訊過程中起著非常重要的作用,它們包含了關於請求和響應的各種元資訊,幫助客戶端和伺服器進行正確的處理和解析。

HTTP資料傳輸圖解

socket程式設計

Socket程式設計是一種在網路通訊中常用的程式設計方式,它允許不同計算機上的程式之間進行資料交換和通訊。Socket程式設計通常涉及兩種角色:伺服器和客戶端。伺服器等待客戶端的連線請求,而客戶端則傳送請求並與伺服器建立連線,然後進行資料交換。

下面是Socket程式設計的主要步驟和涉及的關鍵概念:

  1. 建立Socket:在伺服器端和客戶端分別建立一個Socket物件,用於與對方建立連線和進行資料傳輸。

  2. 繫結地址和埠:在伺服器端,將Socket物件繫結到一個IP地址和埠上,以便客戶端能夠透過該地址和埠連線到伺服器。在客戶端,通常不需要繫結地址,而是直接指定伺服器的地址和埠。

  3. 監聽連線請求:在伺服器端,呼叫listen()方法開始監聽客戶端的連線請求。

  4. 接受連線:在伺服器端,呼叫accept()方法接受客戶端的連線請求,並返回一個新的Socket物件,用於與該客戶端進行通訊。

  5. 建立連線:在客戶端,呼叫connect()方法向伺服器發起連線請求,並建立連線。

  6. 資料交換:建立連線後,伺服器和客戶端之間可以透過Socket物件進行資料交換。通常使用send()recv()方法傳送和接收資料。

  7. 關閉連線:通訊結束後,伺服器和客戶端分別呼叫close()方法關閉Socket連線。

Socket程式設計可以基於不同的協議進行,如TCP(傳輸控制協議)和UDP(使用者資料包協議)。TCP提供可靠的、面向連線的資料傳輸,適用於需要確保資料完整性和順序性的場景;而UDP提供不可靠的、無連線的資料傳輸,適用於實時性要求較高、資料量較小的場景。

總的來說,Socket程式設計是一種靈活且功能強大的網路程式設計方式,可用於實現各種網路應用,如Web伺服器、聊天程式、檔案傳輸等。

flowchart TB subgraph "伺服器" 建立Socket --> 繫結地址和埠 --> 監聽連線請求 --> 接受連線 --> 資料交換 --> 關閉連線 end subgraph "客戶端" 建立Socket --> 指定伺服器地址和埠 --> 建立連線 --> 資料交換 --> 關閉連線 end

Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket後面,對使用者來說只需要呼叫Socket規定的相關函式,讓Socket去組織符合指定的協議資料然後進行通訊。

socket圖解

概念

網路程式設計是指透過計算機網路進行資料交換和通訊的程式開發。在Golang中,網路程式設計主要透過標準庫中的net包來實現。這個包提供了建立網路應用所需的各種功能和介面,包括操作TCP、UDP、Unix域套接字等。

下面是Golang中網路程式設計的一般步驟:

  1. 建立連線:在客戶端,首先建立一個連線,通常是TCP或UDP連線。在伺服器端,需要監聽特定的網路地址和埠,等待客戶端的連線請求。

  2. 資料傳輸:一旦連線建立,資料的傳輸就可以開始了。可以透過連線的讀取和寫入操作進行資料的傳送和接收。

  3. 處理連線:伺服器端需要處理多個客戶端的連線請求,可以透過併發處理或使用事件驅動模型來實現。

  4. 關閉連線:在資料傳輸結束後,需要關閉連線,釋放資源。

在Golang中,可以使用net包中的net.Listen()來監聽連線請求,使用net.Dial()來建立連線。一旦連線建立,可以使用net.Conn介面來進行資料的讀寫操作。

另外,Golang的net/http包提供了一個高階的HTTP伺服器和客戶端的實現,簡化了HTTP協議的處理過程,使得開發HTTP應用更加便捷。

主要方法

下面是net/http包中常用的一些方法:

方法名 說明
http.ListenAndServe 啟動一個HTTP伺服器並監聽指定地址和埠
http.ListenAndServeTLS 啟動一個HTTPS伺服器並監聽指定地址和埠
http.HandleFunc 註冊一個HTTP請求處理函式
http.Handle 註冊一個HTTP請求處理器
http.FileServer 返回一個處理靜態檔案的HTTP處理器
http.ServeFile 返回一個處理單個檔案的HTTP處理器
http.NotFound 返回一個處理404錯誤的HTTP處理器
http.Redirect 返回一個HTTP處理器,用於重定向請求
http.StripPrefix 返回一個新的處理器,用於去掉URL路徑的字首
http.Error 返回一個HTTP處理器,用於傳送指定狀態碼的錯誤資訊
http.Client 建立一個新的HTTP客戶端
http.NewRequest 建立一個新的HTTP請求
http.Get 傳送一個GET請求
http.Post 傳送一個POST請求
http.Head 傳送一個HEAD請求
http.Do 傳送一個自定義方法的HTTP請求
http.Serve 在指定的監聽器上啟動HTTP伺服器

net包方法

方法 描述
Dial 建立到指定網路地址的連線
Listen 在指定的網路地址上監聽連線請求
ResolveIPAddr 解析 IP 地址
ResolveTCPAddr 解析 TCP 地址
ResolveUDPAddr 解析 UDP 地址
ResolveUnixAddr 解析 Unix 地址
SplitHostPort 將地址字串分割為主機和埠
JoinHostPort 將主機和埠組合成一個地址字串
LookupIP 查詢主機的 IP 地址
LookupPort 查詢服務的埠號
LookupAddr 查詢 IP 地址的域名
LookupCNAME 查詢域名的規範名
LookupHost 查詢域名對應的主機名
LookupMX 查詢域名的郵件交換伺服器記錄
LookupNS 查詢域名的 DNS 伺服器記錄
LookupTXT 查詢域名的文字記錄
LookupSRV 查詢域名的服務記錄
LookupAddrContext 帶有上下文的查詢 IP 地址
LookupCNAMEContext 帶有上下文的查詢域名的規範名
LookupHostContext 帶有上下文的查詢域名對應的主機名
LookupMXContext 帶有上下文的查詢域名的郵件交換伺服器記錄
LookupNSContext 帶有上下文的查詢域名的 DNS 伺服器記錄
LookupTXTContext 帶有上下文的查詢域名的文字記錄
LookupSRVContext 帶有上下文的查詢域名的服務記錄
InterfaceAddrs 返回本地系統介面的地址列表
InterfaceByIndex 根據索引返回系統介面
Interfaces 返回本地系統介面列表
ParseCIDR 解析 CIDR 地址字串
CIDRMask 建立指定位數的 CIDR 掩碼
IPv4Mask 建立指定位數的 IPv4 掩碼
IPMask 建立指定位數的 IP 掩碼
IPv4 將 4 個位元組的切片轉換為 IPv4 地址
ParseIP 解析 IP 地址字串
IP.Equal 比較兩個 IP 地址是否相等
IP.IsGlobalUnicast 檢查 IP 地址是否是全域性單播地址
IP.IsInterfaceLocal 檢查 IP 地址是否是介面本地地址
IP.IsLinkLocalMulticast 檢查 IP 地址是否是鏈路本地多播地址
IP.IsLinkLocalUnicast 檢查 IP 地址是否是鏈路本地單播地址
IP.IsLoopback 檢查 IP 地址是否是環回地址
IP.IsMulticast 檢查 IP 地址是否是多播地址
IP.IsUnspecified 檢查 IP 地址是否是未指定地址
IP.String 返回 IP 地址的字串表示
IP.To4 將 IPv4 地址轉換為 4 個位元組的切片
IP.To16 將 IPv4 或 IPv6 地址轉換為 16 個位元組的切片
IPv6 將 16 個位元組的切片轉換為 IPv6 地址
TCPAddr TCP 地址結構
TCPListener TCP 監聽器
TCPConn TCP 連線
UDPAddr UDP 地址結構
UDPConn UDP 連線
UnixAddr Unix 地址結構
UnixConn Unix 連線
FileConn 建立一個已經存在的檔案描述符的連線
Pipe 建立一個已經存在的管道的連線

示例

/* ------------------ server ---------------------- */
package main

import (
	"fmt"
	"net"
)

func main() {
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error listening:", err.Error())
		return
	}
	// 處理connection close error handler
	defer func(listener net.Listener) {
		err := listener.Close()
		if err != nil {
			fmt.Println("connetion close error occour:", err)
		}
	}(listener)
	fmt.Println("Server is listening on port 8080")

	// 在迴圈中 接受 連結資料
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting:", err.Error())
			return
		}

		//  處理連結
		go handleRequest(conn)
	}
}

func handleRequest(conn net.Conn) {
	defer func(conn net.Conn) {
		err := conn.Close()
		if err != nil {
			fmt.Println("close connection:", err)
		}
	}(conn)

	buffer := make([]byte, 1024)
	for {
		reqLen, err := conn.Read(buffer)

		// 異常處理
		if err != nil {
			fmt.Println("error reading:", err)
			return
		}

		fmt.Printf("Received message from client: %s \n", string(buffer[:reqLen]))
		// 傳送響應資料到客戶端
		_, err = conn.Write([]byte("Message received"))

		if err != nil {
			fmt.Println("Error writing:", err.Error())
			return
		}
	}
}

/* ------------------ client ---------------------- */

package main

import (
	"fmt"
	"net"
	"strconv"
	"time"
)

func main() {
	// 連線伺服器
	conn, err := net.Dial("tcp", "localhost:8080")

	if err != nil {
		fmt.Println("Error connecting:", err.Error())
		return
	}
	defer func(conn net.Conn) {
		err := conn.Close()
		if err != nil {
			fmt.Println("connection close error:", err)
		}
	}(conn)
	i := 1
	for {
		message := "Hello , server! " + strconv.Itoa(i)
		// 如果寫入發生錯誤 , 則向連線中 傳送錯誤資訊
		_, err = conn.Write([]byte(message))
		if err != nil {
			fmt.Println("Error sending...:", err.Error())
			return
		}

		// 處理服務端響應
		handleResponse(err, conn)

		time.Sleep(3 * time.Second)
		i++
	}

}

// handleResponse 處理接受的異常資料
func handleResponse(err error, conn net.Conn) {
	// 讀取伺服器響應
	buffer := make([]byte, 1024)
	respLen, err := conn.Read(buffer)
	// 錯誤處理
	if err != nil {
		fmt.Println("Error receiving:", err.Error())
	}
	fmt.Printf("Received response from server : %s \n", string(buffer[:respLen]))
}

TCP 粘包問題

TCP粘包問題通常發生在資料傳送方連續傳送多個小資料包,而接收方在一次讀取操作中無法區分這些資料包的邊界,導致多個資料包被看作一個大的資料包,或者一個大的資料包被分割成多個小的資料包,造成粘包現象。

🔴原因

  1. TCP協議的工作機制:TCP協議是面向流的,它不保證資料包的邊界,而是將資料流切分成適當大小的資料塊進行傳輸。這意味著傳送方傳送的資料包並不會按照應用層傳送的資料包進行劃分,而是根據TCP緩衝區的大小和網路狀況等因素來劃分。

  2. 應用程式的讀取方式:接收方的應用程式可能會使用不恰當的方式來讀取資料,比如一次讀取的位元組數不足以容納一個完整的資料包,或者沒有正確處理粘包現象。

解決方法

  1. 使用訊息邊界:在資料包中新增訊息邊界標識,比如新增特殊的分隔符或者固定長度的頭部資訊來表示每個資料包的結束。接收方根據這些邊界來識別每個資料包,從而解決粘包問題。

  2. 使用訊息長度:在資料包的頭部新增訊息長度資訊,表示後續資料的長度。接收方根據訊息長度來讀取資料,確保每次讀取的資料長度正確,從而避免粘包問題。

  3. 封包)採用定長資料包:固定每個資料包的長度,不管資料內容的實際長度如何,都傳送固定長度的資料包。接收方根據固定長度來讀取資料,從而避免粘包問題。

  4. 使用緩衝區:接收方可以使用緩衝區來快取接收到的資料,等待完整的資料包再進行處理,從而避免因為資料包不完整而導致的粘包問題。

  5. 應用層協議設計:在設計應用層協議時,考慮到TCP協議的特性,合理規劃訊息格式和訊息邊界,從而減少粘包問題的發生。

📧封包解決原理

封包是解決TCP粘包問題的一種常見方法,其原理是在傳送資料之前,將資料按照一定的規則封裝成固定格式的資料包,然後傳送給接收方。接收方在接收到資料包後,根據固定的格式解析資料包,從而正確地識別和處理每個資料包,避免粘包問題的發生。

封包的原理主要包括以下幾個步驟:

  1. 資料封裝:傳送方將要傳送的資料按照一定的格式封裝成資料包。這個格式通常包括資料的頭部資訊和資料內容兩部分。頭部資訊可以包括資料長度、訊息型別、校驗碼等資訊,用於描述資料的屬性和特徵,以便接收方正確解析資料包。

  2. 資料傳送:傳送方將封裝好的資料包透過TCP連線傳送給接收方。由於資料包已經按照固定格式進行封裝,因此即使在網路傳輸過程中發生資料拆分或合併,接收方仍然可以透過解析資料包頭部資訊來正確識別每個資料包的邊界和內容。

  3. 資料解析:接收方收到資料包後,根據固定的格式解析資料包,提取出資料的頭部資訊和內容。透過頭部資訊中的資料長度等資訊,接收方可以準確地判斷每個資料包的邊界和長度,從而正確地處理每個資料包,避免粘包問題的發生。

總的來說,封包的原理就是在資料傳輸的兩端都採用相同的封裝和解析規則,透過對資料進行封裝和解析,保證資料在傳輸過程中的完整性和準確性,從而解決TCP粘包問題。