Go 并發(fā)、Socket、HTTP 編程

并發(fā)編程

1、并行和并發(fā)

并行(parallel): 指同一時刻,有多條指令在多個處理器上執(zhí)行

并發(fā)(concurrency): 指同一個時刻只能有一條指令執(zhí)行,但多個進程指令被快速的輪換執(zhí)行,使得宏觀上具有多個 進程同時執(zhí)行的效果,但是微觀上并不是同時執(zhí)行的,只是把時間分成若干段,使多個進程快速交替的執(zhí)行。(時間片輪轉(zhuǎn))

Go 從語言層面就支持了并發(fā),同時,并發(fā)程序的內(nèi)存管理有時候是很復(fù)雜的,而 Go 語言提供了自動垃圾回收機制。

Go 語言為并發(fā)編程而內(nèi)置了上層 API 基于 CSP(communicating sequential processes,順序通信進程)模型。這就意味著顯示鎖都可以避免的,因為 Go 語言通過安全的通道發(fā)送和接受數(shù)據(jù)以實現(xiàn)同步,大大的簡化了并發(fā)程序的編寫。(CSP通信的方式實現(xiàn)同步而不是鎖)

一般情況下,一個普通的桌面計算機跑十幾二十個線程就有點負載過大了,但是同樣這臺機器卻可以輕松的讓成百上千甚至過萬個 goroutine 進行資源競爭。

2、goroutine

goroutine 是 Go 并發(fā)設(shè)計的核心。goroutine 說到底就是協(xié)程,但是它比線程更小,十幾個 goroutine 可能體現(xiàn)在底層就是五六個線程,Go 語言內(nèi)部幫你實現(xiàn)了這些 goroutine 之間的內(nèi)存共享。執(zhí)行 goroutine 只需極少的棧內(nèi)存(大概 4~5KB),當然會根據(jù)相應(yīng)的數(shù)據(jù)伸縮。也正因為如此,可同時運行成千上萬個并發(fā)任務(wù)。goroutine 比 thread 更易用、更高效、更輕便。

只需要在函數(shù)調(diào)用語句前添加 go 關(guān)鍵字,就可以創(chuàng)建并發(fā)執(zhí)行單元。開發(fā)人員無需了解任何執(zhí)行細節(jié),調(diào)度器會自動將其安排到合適的系統(tǒng)線程上執(zhí)行。

在并發(fā)編程里,我們通常想講一個過程切分成幾塊,然后讓每個 goroutine 各自負責(zé)一塊工作。當一個程序啟動時,其主函數(shù)即在一個單獨的 goroutine 中運行,我們叫它 main goroutine。新的 goroutine 會用 go 語句來創(chuàng)建。

runtime.Goexit(): 退出此協(xié)程
runtime.GOMAXPROCS(n):設(shè)置并行計算的 CPU 核數(shù)的最大值,并返回之前的值
runtime.Gosched(): 用于讓出 CPU 時間片,讓出當前 goroutine 的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運行,并在下次某個位置從該位置恢復(fù)執(zhí)行。這就像接力賽,A 拍了一會碰到代碼 runtime.Gosched() 就把接力棒交給 B 了,A 歇著 B 繼續(xù)跑。

注意:

  • 主協(xié)程退出了,其他子協(xié)程也要跟著退出
  • 有可能主協(xié)程退出了,但是子協(xié)程還沒來得及調(diào)用
package main

import (
    "fmt"
    "time"
    "runtime"
)

func test() {
    defer fmt.Println("aaa") // 終止協(xié)程,該語句仍然會被執(zhí)行
    
    // return // 終止此函數(shù)
    runtime.Goexit() // 終止坐在的協(xié)程
    
    fmt.Println("bbb")
}

func NewTask() {
    // do something
}

func main() {
    go newTask() // 新建一個協(xié)程,新建一個任務(wù)(只要看見一個 go 便創(chuàng)建一個協(xié)程)
    
    // do something
}

多任務(wù)很容易出現(xiàn)資源競爭,就需要channel 做協(xié)程同步

2、channel

本質(zhì)上就是一個管道

價值:

  • 使用通信來共享數(shù)據(jù)
  • 使用通信來同步

goroutine 運行在相同的地址空間,因此訪問共享內(nèi)存必須做好同步。gotoutine 奉行通過 CSP(通信) 來共享內(nèi)存,而不是共享內(nèi)存來通信。

引用類型 channel 是 CSP 模式的具體實現(xiàn),用于多個 goroutine 通訊。其內(nèi)部實現(xiàn)了同步,確保并發(fā)安全。

和 map 類似,channel 也一個對應(yīng) make 創(chuàng)建的底層數(shù)據(jù)結(jié)構(gòu)的引用。

當我們復(fù)制一個 channel 或用于函數(shù)參數(shù)傳遞時,我們只是拷貝了一個 channel 的引用,因此調(diào)用者何時被調(diào)用者將引用同一個 channel 對象。和其他的引用類型一樣,channel 的零值也是 nil。

定義一個 channel 時,也需要定義發(fā)送到 channel 的值的類型。channel 可以使用內(nèi)置的 make() 函數(shù)來創(chuàng)建:

make (chan Type) // 無緩沖區(qū),等價于 make (chan Type, 0)
make (chan Type, capacity) // 有緩沖區(qū)

當 capacity = 0 時,channel 是無緩沖阻塞讀寫的,當 capacity > 0 時,channel 有緩沖、是非阻塞的,直到寫滿 capacity 個元素才阻塞寫入。

channel 操作符 <- 來接收和發(fā)送數(shù)據(jù),發(fā)送和接收數(shù)據(jù)語法:

channel <- value // 發(fā)送 value 到 channel
<-channel // 接收并將其丟棄
x := <-channel // 從 channel 中接受數(shù)據(jù),并賦值給 x
x, ok := <-channel // 同能同上,同時檢查通道是否已關(guān)閉或者是否為空

默認情況下,channel 接收和發(fā)送數(shù)據(jù)都是阻塞的,除非另一端已經(jīng)準備好,這樣就使得 goroutine 同步變的更加的簡單,而不需要顯式的 lock

var ch make(chan int) // 定義通道

func person1() {
    Printer("hello")
    ch <- 666 // 給管道寫數(shù)據(jù),發(fā)送
}

func person2() {
    <- ch // 從管道取數(shù)據(jù),接收,如果通道沒有數(shù)據(jù)他就會阻塞
    Printer("world")
}

3、無緩沖和有緩沖 channel

無緩沖的通道(unbuffered channel)是值在接受前沒有能力保存任何值的通道。

這種類型的通道要求發(fā)送 goroutine 和接收 goroutine 同時準備好,才能完成發(fā)送和接收操作。如果兩個 goroutine 沒有同時準備好,通道會導(dǎo)致先執(zhí)行發(fā)送和接收操作的 gotoutine 阻塞等待。

這種對通道進行發(fā)送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作的單獨存在。

使用無緩沖通道 goroutine 之間同步

在第 1 步,兩個 goroutine 都到達通道,但哪個都沒有開始執(zhí)行或者接收。
在第 2 步,左側(cè)的 goroutine 將它的手伸進了通道,這模擬了向通道發(fā)送數(shù)據(jù)的行為。這是,這個 goroutine 會在通道中被鎖住,直到交換完成。
在第 3 步,右側(cè)的 goroutine 將它的手放入通道,這模擬了從通道接收數(shù)據(jù)。這個 goroutine 一樣會在通道中被鎖住,直到交換完成。
在第 4 步和第 5 步,進行交換,并最終,在第 6 步,兩個 goroutine 將他們的手從通道里拿出來,這模擬了被鎖住的 goroutine 得到釋放。兩個 goroutine 現(xiàn)在都可以去做別的事情了。

無緩沖的 channel 創(chuàng)建格式:

make (chan Type) // 等價于 make(chan Type, 0)

0 表示容量,表示沒有緩存,不能存東西

如果沒有指定緩沖區(qū)容量,那么通道就是同步的,因此會阻塞到發(fā)送者準備好發(fā)送和接收著準備好接收。

有緩沖的通道在 goroutine 之間同步數(shù)據(jù)

有緩沖的通道和無緩沖的通道之間的一個很大的不同:無緩沖的通道保證進行發(fā)送和接收的 goroutine 會在同一時間進行數(shù)據(jù)交換:有緩沖的通道沒有這種保證。

在第 1 步,右側(cè)的 goroutine 正在從通道接收一個值。
在第 2 步,右側(cè)的這個 goroutine 獨立完成了接收值的動作,而左側(cè)的 goroutine 正在發(fā)送一個新值到通道里。
在第 3 步,左側(cè)的 goroutine 還在向通道發(fā)送新值,而右側(cè)的 goroutine 正在從通道接收另外一個值。這個步驟里的兩個操作既不是同步的,也不會互相阻塞。
最后,在第 4 步,所有的發(fā)送和接收都完成,而通道里還有幾個值,也有一些空間可以存更多的值。

有緩沖的 channel 創(chuàng)建格式:

make(chan Type, capacity)

如果給定了一個緩沖區(qū)容量,通道就是異步的。只要緩沖區(qū)有未使用空間用于發(fā)送數(shù)據(jù),或還包含可以接收的數(shù)據(jù),那么其通信就是無阻塞地進行。

4、channel 關(guān)閉

  • channel 不像文件一樣需要經(jīng)常去關(guān)閉,只有當你確實沒有任何發(fā)送數(shù)據(jù)了,或者你想顯示的結(jié)束 range 循環(huán)之類的,才去關(guān)閉 channel;
  • 關(guān)閉 channel 后,無法向 channel 再發(fā)送數(shù)據(jù)(引發(fā) panic 錯誤后導(dǎo)致接收立即返回零值);
  • 關(guān)閉 channel 后,可以繼續(xù)向 channel 接收數(shù)據(jù);
  • 對于 nil channel,無論收發(fā)都會被阻塞
ch := make(chan int) // 創(chuàng)建一個無緩存的 channel

// 不需要寫數(shù)據(jù)的時候,關(guān)閉 channel
cose(ch)

// 如果 ok 為 true,說明管道沒有關(guān)閉
num, ok := <-ch

5、訪問 channel 內(nèi)容

  • 迭代
  • range
for num := range ch {
    fmt.Println("num =", num)
}

6、單向的 channel

默認情況下,通道是雙向的,也就是,既可以往里面發(fā)送數(shù)據(jù)也可以從里面接收數(shù)據(jù)。

但是,我們經(jīng)常見一個通道作為參數(shù)進行傳遞而值希望對方是單向使用的,要么只讓它發(fā)送數(shù)據(jù),要么只讓它接收數(shù)據(jù),這時候我們可以指定通道的方向。

單向 channel 變量的聲明非常簡單,如下:

var ch1 chan int // ch1 是一個正常的 channel,不是單向的
var ch2 chan<- float64 // ch2 是單向 channel,只用于寫 float64 數(shù)據(jù)
var ch3 <-chan int // ch3 是單向 channel,只用于讀取 int 數(shù)據(jù)
  • chan<- 表示數(shù)據(jù)進入管道,要把數(shù)據(jù)寫進管道,對于調(diào)用者就是輸出
  • <-chan 表示數(shù)據(jù)從管道出來,對于調(diào)用者就是得到管道的數(shù)據(jù),當然就是輸入

可以將 channel 隱式轉(zhuǎn)換為單向隊列,只收或只發(fā),不能將單向 channel 轉(zhuǎn)換為普通 channel:

c := make(chan int, 3)

var send chan<- int = c // send-only
var revc <-chan int = c // receive-ony

send <- 1
<- recv

生產(chǎn)者消費者應(yīng)用:

package main 

import "fmt"

// 此通道只能寫,不能讀
func producer(in  chan<- int) {
    for i := 0; i < 10; i++ {
        out <- i * I
    }
}

// 此 channel 只能讀,不能寫
func consumer(out <-chan int) {
    for num := range out {
        fmt>Println("num = ", num)
    }
}

func main() {
    // 創(chuàng)建一個雙向通道
    ch := make(chan int)
    
    // 生產(chǎn)者,生產(chǎn)數(shù)字,寫入 channel
    // 新開一個協(xié)程
    go producer(ch) // channel 傳參,引用傳遞
    
    // 消費者,從 channel 讀取內(nèi)容,打印
    consumer(ch)
}

7、Timer / Ticker 定時器

Timer 是一個定時器,代表未來的一個單一事件,你可以告訴 timer 你要等待多長時間,它提供一個 channel,在將來的那個時間那個 channel 提供了一個時間值。

// 創(chuàng)建定時器,2 秒后就會往 time 通道寫內(nèi)容(當前時間)
timer1 := timer.NewTimer(time.Second * 2)
fmt.Println("當前時間: ", time.Now())

// 2s 后,往 timer.C 寫數(shù)據(jù),有數(shù)據(jù)后,就可以讀取
t := <-timer1.C
fmt.Printf("t= %V\n", t)

延遲:

// 定時2s,阻塞2s,2s后產(chǎn)生事件,往channel寫內(nèi)容
<-time.After(time.Second * 2)

停止和重置:

timer.Stop() // 停止定時器
timer.Reset(1 * time.Second) // 重新設(shè)置為 1 秒

Ticker 是一個定時觸發(fā)的計時器,它會以一個間隔(interval)往 channel 發(fā)送一個事件(當前時間),而 channel 的接收者可以以固定的時間間隔從 channel 中讀取事件

ticker := time.NewTicker(1 : time.Second)

i := 0

for {
    <-ticker.C
    I++
    fmt.Println("i = ", i)
    
    if i == 5 {
        ticker.Stop()
        break
    }
}_

8、Select

Go 里面提供了一個關(guān)鍵字 select,通過 select 可以監(jiān)聽 channel 上的數(shù)據(jù)流動。

select 的用法和 switch 語言非常類似,由 select 開始一個新的選擇塊,每個選擇條件由 case 語句來描述。

與 switch 語句可以選擇任何可使用相等比較的條件相比,select 有比較多的限制,其中最大的一條限制就是每個 case 語句必須是一個 IO 操作,大致的結(jié)構(gòu)如下:

select {
case <-chan1:
// 如果 chan1 成功讀到數(shù)據(jù),則進行該 case 處理語句
case chan2 <- 1:
// 如果成功向 chan 2 寫入數(shù)據(jù),則進行該 case 處理語句
default:
// 如果上面都沒有成功,則進入 default 處理流程
}

在一個 select 語句中,Go 語言會按照順序從頭到尾評估每一個發(fā)送和接收的語句。

如果其中的任意一語句可以繼續(xù)執(zhí)行(即沒有被阻塞),那么就從哪些可以執(zhí)行的語句中任意選擇一條來使用。

超時的實現(xiàn):

import "time"

func main() {
    ch := make(chan int)
    quit := make(chan bool)
    
    // 新開一個協(xié)程
    go func() {
        for {
           select {
                case num := <-ch:
                    fmt.Println("num = ", num)
                    case <-time.After(3 * time.Second)
                        fmt.Println("超時")
                        quit <- true
           } 
        }()
    }
    
    for i := 0; i < 5; i++ {
        ch <- I
        time.Sleeep(time.Second)
    }
    
    <-quit
    fmt.Println("程序結(jié)束")
}

網(wǎng)絡(luò)概述、Socket 編程

1、網(wǎng)絡(luò)協(xié)議

協(xié)議可以理解為規(guī)則,是數(shù)據(jù)傳輸和數(shù)據(jù)的解釋的規(guī)則。

為了減少協(xié)議復(fù)雜性,大多數(shù)網(wǎng)絡(luò)模型采用分層來組織。每一層都有自己的功能,每一層利用下一層提供的服務(wù)來為上一層提供服務(wù),本層服務(wù)的實現(xiàn)細節(jié)對上層屏蔽。

越下面的層,越靠近硬件;越上面的層,越靠近用戶。至于每一層叫什么名字,其實并不重要(除了辨識會問,哈哈)。但要具體指導(dǎo)每一層的作用。

OSI 七層協(xié)議:

  • 物理層:主要是無力設(shè)備標準,如網(wǎng)線接口、光線接口。主要作用就是傳輸比特流,這一層的數(shù)據(jù)叫做比特。
  • 數(shù)據(jù)鏈路層:定義如何讓格式化數(shù)據(jù)數(shù)據(jù)以幀為單位進行傳輸,以及如何讓控制對無力介質(zhì)的訪問。錯誤檢測和糾正,保證數(shù)據(jù)的可靠傳輸。
  • 網(wǎng)絡(luò)層:位于不同位置兩個主機系統(tǒng)提供連接和路徑選擇。(IP\ICMP\IGMP)
  • 傳輸層:定義了一些傳輸數(shù)據(jù)的協(xié)議和端口號(WWW端口80等)。跟端口相關(guān)(TCP\UDP)
  • 會話層:建立和維持會話(Session)
  • 表示層:主要是提供格式化的表示和轉(zhuǎn)換數(shù)據(jù)的服務(wù)。數(shù)據(jù)的壓縮、加密、解密等都是在該層完成。
  • 應(yīng)用層:(FTP\Telnet\NFS)

幾個常見協(xié)議:

  • ARP: 通過 IP 地址找 MAC 地址
  • RARP: 通過 MAC 地址找 IP 地址
  • IP: 因特網(wǎng)互聯(lián)協(xié)議
  • TCP: Transmission Control Protocol, 是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議
  • UDP: User Datagram Protocol, 是一種無連接的傳輸層協(xié)議,提供面向事務(wù)的簡單不可靠信息傳送服務(wù)。
  • HTTP: 超文本傳輸協(xié)議
  • ICMP: Internet Control Message Protocol, 因特網(wǎng)控制報文協(xié)議,用于在 IP主機、路由器之間的控制協(xié)議。
  • IGMP: Internet Group Message Protocol,提供互聯(lián)網(wǎng)多點傳送的功能,即將一個 ip 包拷貝到多個 host
TCP / IP 協(xié)議

網(wǎng)絡(luò)通信條件:

  • 網(wǎng)卡:MAC 地址(不需要用戶處理):通過 IP 找 MAC
  • 邏輯地址:IP 地址(需要用戶指定),為了確定哪個電腦接收
  • 端口:確定哪個程序接收
    • 同一個程序/進程,只能綁定一個端口
    • 不同系統(tǒng),同一端口對應(yīng)的程序可能不一樣
封包和解包流程

2、Socket 編程

Socket 套接字,起源于 Unix,而 Unix 基本哲學(xué)之一就是“一切皆文件“,都可以用”打開 open -> 讀寫 write/read -> 關(guān)閉 close”。網(wǎng)絡(luò)的 Socket 數(shù)據(jù)涮出是一種特殊的 IO,Socket 也是一種文件描述符。Socket 也具有一個類似于打開文件的函數(shù)調(diào)用:Socket(),該函數(shù)返回一個整型的 Socket 描述符,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^該 Socket 實現(xiàn)的。

常見的 Socket 類型有兩種:流式 Socket(SOCK_STREAM)和數(shù)據(jù)報式 Socket(SOCK_DGRAM)。流式是一種面向連接的 Socket,針對于面向連接的 TCP 服務(wù)應(yīng)用;數(shù)據(jù)報式 Socket 是一種無連接的 Socket,對應(yīng)于無連接的 UDP 服務(wù)應(yīng)用。

C/S模型:

  • 客戶端(Client): 主動請求服務(wù)
  • 服務(wù)器(Server):被動提供服務(wù)

B/S模型:

  • 瀏覽器(Browser):html
  • 服務(wù)器(Server)

TCP 的 C/S 架構(gòu):

TCP 的 C/S 架構(gòu)

TCP 服務(wù)器

package main

import (
    "fmt"
    "net"
)

func main() {
    // 監(jiān)聽
    ln, err := net.Listen("tcp", ":127.0.0.1:8080")
    if err != nil {
        fmt.Println("err = ", err)
        return
    }
    
    defer listener.Close()
    
    // 阻塞等待用戶連接
    for {
        conn, err := listener.Accept()
        if err != nil {
           fmt.Println("err = ", err)
           continue
        }
        
        // 接收用戶的請求
        buf := make([]byte, 1024)// 定義 1024 大小的緩沖區(qū)
        n, err1 := conn.Read(buf)
        if err1 != nil {
            fmt.Println("errr1 = ", err1)
            continue
        }
        
        fmt.Println("buf = ", string(buf[:n]))
    }
    
    defer conn.Close() // 關(guān)閉當前用戶鏈接
}

TCP 客戶端

package main

import (
    "fmt"
    "net"
)

func main() {
    // 主動連接服務(wù)器
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("err = ", err)
        return
    }
    
    defer conn.Close()
    
    // 發(fā)送數(shù)據(jù)
    conn.Write([]byte("are u ok?"))
}

3、Socket 并發(fā)

使用 goroutine 處理多個用戶的 Socket 連接

// 處理用戶請求
func HanleConn(conn net.Conn) {
    // 函數(shù)調(diào)用完畢,自動關(guān)閉 conn
    defer conn.Close()

    // 獲取客戶端的網(wǎng)絡(luò)地址信息
     addr := conn.RemoteAddr().String()
     // print addr
    
     buf := make([]byte, 2048)
     for {
         // 讀取用戶數(shù)據(jù)
         n, err := conn.Read(buf)
         if err != nil {
            // print err
            return 
        }
        
        // print n
        
        // 輸入 exit 退出連接
        if "exit" == string(buf[:n]) {
            // print addr
            return
        }
        
        // 把數(shù)據(jù)轉(zhuǎn)換為答謝,再發(fā)給用戶
        conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
     }
     
}

func main() {
    // 監(jiān)聽
    listen, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        // print err
        return
    }
    
    defer listener.Close()
    
    // 接收多個用戶的請求
    for {
        conn, err := listener.Accept()
        if err != nil {
            // print err
            return
        }
        
        // 處理用戶請求,每來一個請求新建一個協(xié)程
        go HandleConn(conn)
    }
}

客戶端多任務(wù)模式,即可以接受輸入,也可以接受服務(wù)器 Socket 回復(fù)


func main() {
    // 主動連接服務(wù)器
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        // print err
        return
    }

    // main 調(diào)用完畢,關(guān)閉連接
    defer conn.Close()
    
    go func {
        // 從鍵盤輸入內(nèi)容,給服務(wù)器發(fā)送內(nèi)容
        str := nake([]byte, 1024)
        for {
            n, err := os.Stdin.Read(str) // 從鍵盤讀取內(nèi)容,放在 str
            if err != nil {
                // print err
                return
            }
            
            // 把輸入的內(nèi)容發(fā)送給服務(wù)器
            conn.Write(str[:n])
        }
    }()

    // 接收服務(wù)器回復(fù)的數(shù)據(jù)
    // 切片緩沖
    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf) // 接受的服務(wù)器的請求
        if err != nil {
            // print err
            return
        }
        
        // print string(buf[:n])
    }
}

4、使用 Socket 實現(xiàn)文件傳輸

獲取文件屬性

package main

import (
    "fmt"
    "os"
)

func main() {
    list := os.Args
    if len(list) != 2 {
        // print useage: xxx file
        return
    }
    
    fileName := list[1]
    
    info, rr := os.Stat(fileName) // 獲取文件屬性
    if err != nil {
        // print err
        return
    }
    
    // print info.Name(), info.Size()
}

文件發(fā)送端

package main

import (
    "fmt"
    "os"
    "io"
    "os"
)

func SendFile(path string, conn net.Conn) {
    // 以只讀方式打開文件
    f, err := os.Open(path)
    if err != nil {
        // print err
        return
    }
    
    defer f.Close()
    
    // 讀文件內(nèi)容,讀多少發(fā)送多少
    buf := make([]byte, 1024 * 4)
    for {
        n, err := f.Read(buf)
        if err != nil {
            if err == io.EOF {
                // 文件發(fā)送完成
            } else {
                print err
            }
            return
        }
        // 發(fā)送內(nèi)容
        conn.Write(buf[:n]) // 給服務(wù)器發(fā)送內(nèi)容
    }
}

func main() {
    // 提示輸入文件
    // print
    
    var path string
    fmt.Scan(&path)
    
    fileName := list[1]
    
    info, rr := os.Stat(fileName) // 獲取文件屬性
    if err != nil {
        // print err
        return
    }
    
    // 主動連接服務(wù)器
    conn, err1 := net.Dial("tcp", "127.0.0.1:8000")
    if err1 != nil {
        // print err
        return
    }
    
    defer conn.Close()
    
    var n int
    
    // 給發(fā)送方,先發(fā)送文件名
    _, err = conn.Write([]byte(info.Name()))
    if err != nil {
        // print err
        return
    }
    
    // 接收對方回復(fù),如果回復(fù)ok,說明對方準備好,可以發(fā)送文件
    var n int 
    buf := make([]byte, 1024)
    
    n, err = conn.Read(buf)
    if err != nil {
        // print err
        return
    }
    
    if "ok" == buf[:n] {
        // 發(fā)送文件內(nèi)容
        SendFile(path, conn)
    }
    
}

文件接收端

package main

import (
    "fmt"
    "os"
    "io"
    "os"
)

// 接收文件內(nèi)容
func RecvFile(fileName string, conn net.Conn) {
    // 新建文件
    f, err := os.Create(fileName)
    if err != nil {
        // print err
        return
    }
    
    buf := make([]byte, 1024 * 4)
    
    // 接收多少,寫多少
    for {
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                // 文件接收完畢
            } else {
                // print err
            }
            return
        }
        
        if n == 0 {
            // 文件接收完畢
            return
        }
        
        
        // 往文件寫入內(nèi)容
        f.Write(buf[:n])
    }
}

func main() {
    // 監(jiān)聽
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        // print err
        return
    }
    
    defer listenner.Close()
    
    // 阻塞等待用戶連接
    conn, err1 := listenner.Accept()
    if err1 != nil {
        // print err1
        return
    }
    
    buf := make([]byte, 1024)
    var n int
    n, err2 = conn.Read(buf) // 讀取對方發(fā)送的文件名
    if err2 != nil {
        // print errr2
        return
    }
    
    defer conn.Close()
    
    // fileName 保存一下
    fileNmae := stirng(buf[:n])
    
    // 回復(fù) ok
    conn.Write([]byte("ok"))
    
    // 接收文件內(nèi)容
    Recv(fileName, conn)
}

5、使用 Socket 實現(xiàn)并發(fā)聊天室服務(wù)器

可以使用 NetCat 工具調(diào)試 Socket 連接

設(shè)計思路

  • map: 保存在線用戶
type Client struct {
    c chan string
    Name string
    Addr string
}

var onlineMap[string] Client

主協(xié)程:

  • 處理用戶連接
  • 將用戶加入 map
  • 告訴所有在線用戶,誰上線了
message <- 某個用戶上線了

go 新開一個協(xié)程:

for {
    msg := <-message // 如果有內(nèi)容
    // 遍歷 map,看有多少個成員
    for _, cli := range onlineMap {
        cli.C <- msg
    }
}

go 專門發(fā)送信息:

for msg := range cli.C {
    write(msg)
}

go 專門接收用戶的請求,把用戶發(fā)過來的數(shù)據(jù)轉(zhuǎn)發(fā)。用戶發(fā)送過來的數(shù)據(jù)是 buf

mssage <- buf

對方下線,把當前用戶從 map 中移除

具體實現(xiàn)

main.go

package main

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

type Client struct {
    C chan string // 用于發(fā)送數(shù)據(jù)的管道
    Name string // 用戶名
    Addr string // 網(wǎng)絡(luò)地址
}

// 保存在線用戶 cliAddr ===> Client
var onlineMap map[string]Client
// 消息
var massage = make(chan string)

// 新開一個協(xié)程,轉(zhuǎn)發(fā)消息,只要有消息來了,就遍歷 map,給 map 的每個成員發(fā)送消息
func Manager() {
    for {
        // 給 map 分配空間
        onlineMap = make(map[string]Client)
        msg := <-message // 沒有消息前,這里會阻塞
        
        // 遍歷 map, 給 map 每個成員發(fā)送消息
        for _, cli range onlineMap {
            cli.C <- msg
        }
    }
}

// 給當前客戶端發(fā)送信息
func WriteMsgToClient(cli Client, conn net.Conn) {
    for msg := range cli.C {
        conn.Write([]byte(msg + "\n"))
    }
}

// 生成 msg
func MakeMsg(cli Client, msg string) (buf string) {
    buf = "[" + cli.Addr + "]" + cli.Name + msg

}

// 處理用戶連接
func HandleConn(conn net.Conn) {
    // 獲取客戶端的網(wǎng)絡(luò)地址
    cliAddr := conn.RemoteAddr().String()
    
    // 創(chuàng)建一個結(jié)構(gòu)體, 默認,用戶名和網(wǎng)絡(luò)地址一樣
    cli := Client{make(chan string), cliAddr, cliAddr}
    
    // 把結(jié)構(gòu)體添加到 map
    onlineMap[cliAddr] = cli
    
    // 新開一個協(xié)程,專門給當前客戶端發(fā)送信息
    go WriteMsgToClient(cli, conn)
    
    // 提示,我是誰
    cli.C <- MakeMsg(cli, "I am here")
    
    // 廣播某個人在線
    message <- "[" + cli.Addr + "]" + cli.Name + ": login"
    message <- MakeMsg(cli, "login")
    
    isQuit := make(chan bool) // 對方是否主動退出
    hasData := make(chan bool) // 對方是否有數(shù)據(jù)發(fā)送
    
    // 新建一個協(xié)程,接收用戶發(fā)送過來的數(shù)據(jù)
    go func() {
        for {
            buf := make([]byte, 2048)
            
            n, err := conn.Read(buf)
            if err != nil {
                // print err
                continue
            }
            
            // 對方斷開或者出問題
            if n == 0 {
                isQuit <- true
                // print err          
                return
            }
            
            // 轉(zhuǎn)發(fā)此內(nèi)容
            msg := string(buf[:n])
            
            if len(msg) == 3 && msg == "who" {
                // who 消息,查詢在線用戶
                conn.Write([]byte("user list:\n"))
                for _, tmp := range onlineMap {
                    msg = tmp.Affr + ":" + tmp.Name + "\n"
                    conn.Write([]byte(msg))
                }
            } else if len(msg) >= 8 && msg[:6] == "rename" {
                // rename 消息
                name = strings.Split(msg, "|")[1]
                cli.Name = name
                onlineMap[cliAddr] = cli
                conn.Write([]byte("rename ok \n"))
            } else {
                // 普通消息,直接轉(zhuǎn)發(fā)
                message <- MakeMsg(cli, msg)
            }  
            
            // 表示有數(shù)據(jù)
            hasData <- true
        }
    }()
    
    for {
        // 通過 select 檢測 channel 的流動
        select {
            case <- isQuit:
                // 當前用戶從 map 移除
                delete(onlineMap, cliAddr)
                // 廣播誰下線了
                message <- MakeMsg(cli, "login out")
                return   
            case <- hasData:
                // 有數(shù)據(jù),不處理
            case <-time.After(60 * time.Second):
                // 60s 之后超時處理
                delete(onlineMap, cliAddr)
                // 廣播一下誰下線了
                message <- MakeMsg(cli, "time out leave out") 
                return
        }
    }
}

func main() {
    // 監(jiān)聽
    listener, err := net.Listen("tcp", ":8000")
    if err != nil {
        // print err
        return
    }
    
    defer listener.Close()
    
    // 新開一個協(xié)程,轉(zhuǎn)發(fā)消息,只要有消息來了,就遍歷 map,給 map 的每個成員發(fā)送消息
    go Manager()
    
    // 主協(xié)程,循環(huán)阻塞等待用戶連接
    for {
        conn, err := listener.Accept()
        if err != nil {
            // print err
            continue
        }
        
        go HandleConn(conn) // 處理用戶連接
    }
}
HTTP 編程

1、Web 的工作方式

Web 服務(wù)器的工作原理可以簡單地歸納為:

  • 客戶端通過 TCP/IP 協(xié)議建立到服務(wù)器的 TCP 連接
  • 客戶端向服務(wù)器發(fā)送 HTTP 協(xié)議請求包,請求服務(wù)器里的資源文檔
  • 服務(wù)器向客戶端發(fā)送 HTTP 協(xié)議應(yīng)答包,如果請求的資源包含有動態(tài)語言的內(nèi)容,那么服務(wù)器會調(diào)用動態(tài)語言的解釋引擎負責(zé)處理 “動態(tài)內(nèi)容”,并將處理得到的數(shù)據(jù)返回給客戶端
  • 客戶端與服務(wù)器斷開。由客戶端解釋 HTML 文檔,在客戶端屏幕上渲染圖形結(jié)果

2、HTTP 協(xié)議

HyperText Transfer Protocol, 超文本傳輸協(xié)議,詳細規(guī)定了瀏覽器和萬維網(wǎng)之間互相通信的規(guī)則,通過因特網(wǎng)傳送萬維網(wǎng)文檔的數(shù)據(jù)傳送協(xié)議。

HTTP 協(xié)議通常承載于 TCP 協(xié)議之上,有事也承載于 TLS 或 SSL 協(xié)議層之上,這個時候,就成了我們常說的 HTTPS

URL(Unique Resource Location) ,用來表示網(wǎng)絡(luò)資源,可以理解為網(wǎng)絡(luò)文件路徑

格式:http://host:port/path

3、常見請求方式

GET: 獲取資源,不適合上傳數(shù)據(jù),參數(shù)顯示在瀏覽器地址欄,保密性差

POST:用于提交數(shù)據(jù),長度沒有限制,數(shù)據(jù)放在請求正文中

4、請求和響應(yīng)報文格式

請求格式:

#GET/ HTTP/1.1 // 請求行
Host: 127.0.0.1:8000 // 請求頭 
Connectin: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0(Windows NT 12.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) 6
Accept: text/html. application/xhtml+xml, application/xml; q=0.9, image/webp, */*; q=0.8
Accept-Encoding:gzip, defalte, sdch, br
Accept-Language:zh-CN, zh; q=0.8
 // 空行
# // 包體

常見請求頭:

User-Agent: 請求瀏覽器類型
Accept: 可是識別響應(yīng)內(nèi)容列表
Accept-Language: 可接收自然語言
Accept-Encoding: 可接收的編碼壓縮格式
Accept-Charset: 可接收的應(yīng)答的字符集
Host: 請求的主機名,允許多個域名同處一個 IP 地址,即虛擬主機
connection: 連接方式:close / keepalive
Cookie: 存儲于客戶端拓展字段,向同一域名的服務(wù)器發(fā)送屬于該域名的 cookie

響應(yīng)報文:

#HTTP/1.1 200 OK // 狀態(tài)頭
Date: SUb, 28 Jan 2018 06:11:59 GMT // 狀態(tài)行
Content-Length: 12
Content-Type: text/plain; charset=utf-8

hello world

常見狀態(tài)碼:

- 200 OK :客戶端請求成功
- 400 Bad Request:請求報文有語法錯誤
- 401 Unauthorized:未授權(quán)訪問
- 403 Forbidden: 服務(wù)器拒絕服務(wù)
- 404 Nof Found: 資源不存在
- 500 Internal Server Error: 服務(wù)器內(nèi)部錯誤
- 503 Server Unavailable: 服務(wù)器臨時不能處理客戶端請求

http 服務(wù)器響應(yīng)示例:

package main

import (
    "fmt"
    "net/http"
)

// 服務(wù)器編寫的業(yè)務(wù)邏輯處理程序
func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}

func main() {
    http.HandleFunc("/go", myHandler)
    
    // 在指定的地址進行監(jiān)聽,開啟一個 HTTP
    http.LisenAndServe("127.0.0.1:8000", nil)
}

5、HTTP 服務(wù)器編程

直接用 HTTP 內(nèi)置的包實現(xiàn) HTTP 服務(wù)器編程:

package main

import (
    "fmt"
    "net/http"
)

// w, 給客戶端回復(fù)數(shù)據(jù)
// r, 讀取客戶端發(fā)送的數(shù)據(jù)
func HandConn(w http.ResponseWriter, r *Request) {
    // r.URL 請求鏈接
    // r.Method 請求的方式
    // r.Body 請求體
    // r.RomoteAddr 請求的 IP 地址
    w.Write([]byte("hello go")) // 給客戶端回復(fù)數(shù)據(jù)
}

func main() {
    // 注冊處理函數(shù),用戶聯(lián)機額,自動調(diào)用指定的處理函數(shù)
    http.HandleFunc("/", HandConn)
    
    // 監(jiān)聽綁定
    http:ListenAndServe(":8000", nil)
}

6、HTTP 客戶端編程

package main

import (
    "fmt"
    "net/http"
)

func main() {
    resp, err := http.Get("http://www.baidu.com")
    if err != nil {
        // print err
        return
    } 
    
    defer resp.Body.Close()
    
    // print resp.Status, resp.StatusCode, resp.Header, resp.Body
    
    // body 需要 io 的方式讀取
    var tmp string
    buf := make([]byte, 4 * 1024)
    for {
        n, err := resp.Body.Read(buf)
        if n == 0 {
            // print err
            break
        }
        
        tmp += string
    }
    
    // print tmp 網(wǎng)頁內(nèi)容
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容