并發(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ā)送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作的單獨存在。

在第 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ā)送和接收著準備好接收。

有緩沖的通道和無緩沖的通道之間的一個很大的不同:無緩沖的通道保證進行發(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

網(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 服務(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ò)文件路徑
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)容
}