1. 網(wǎng)絡(luò)概述
-
協(xié)議
從應(yīng)用的角度出發(fā),協(xié)議可理解為“規(guī)則”,是數(shù)據(jù)傳輸和數(shù)據(jù)的解釋的規(guī)則。假設(shè),A、B雙方欲傳輸文件,規(guī)定:
第一次,傳輸文件名,接收方接收到文件名,應(yīng)答OK給傳輸方;
第二次,發(fā)送文件的尺寸,接收方接收到該數(shù)據(jù)再次應(yīng)答一個(gè)OK;
第三次,傳輸文件內(nèi)容。同樣,接收方接收數(shù)據(jù)完成后應(yīng)答OK表示文件內(nèi)容接收成功。
由此,無論A、B之間傳遞何種文件,都是通過三次數(shù)據(jù)傳輸來完成。A、B之間形成了一個(gè)最簡(jiǎn)單的數(shù)據(jù)傳輸規(guī)則。雙方都按此規(guī)則發(fā)送、接收數(shù)據(jù)。A、B之間達(dá)成的這個(gè)相互遵守的規(guī)則即為協(xié)議。
這種僅在A、B之間被遵守的協(xié)議稱之為原始協(xié)議。
當(dāng)此協(xié)議被更多的人采用,不斷的增加、改進(jìn)、維護(hù)、完善。最終形成一個(gè)穩(wěn)定的、完整的文件傳輸協(xié)議,被廣泛應(yīng)用于各種文件傳輸過程中。該協(xié)議就成為一個(gè)標(biāo)準(zhǔn)協(xié)議。最早的ftp協(xié)議就是由此衍生而來。
-
典型協(xié)議
傳輸層 常見協(xié)議有TCP/UDP協(xié)議。
應(yīng)用層 常見的協(xié)議有HTTP協(xié)議,F(xiàn)TP協(xié)議。
網(wǎng)絡(luò)層 常見協(xié)議有IP協(xié)議、ICMP協(xié)議、IGMP協(xié)議。
網(wǎng)絡(luò)接口層 常見協(xié)議有ARP協(xié)議、RARP協(xié)議。
TCP傳輸控制協(xié)議(Transmission Control Protocol)是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。
UDP用戶數(shù)據(jù)報(bào)協(xié)議(User Datagram Protocol)是OSI參考模型中一種無連接的傳輸層協(xié)議,提供面向事務(wù)的簡(jiǎn)單不可靠信息傳送服務(wù)。
HTTP超文本傳輸協(xié)議(Hyper Text Transfer Protocol)是互聯(lián)網(wǎng)上應(yīng)用最為廣泛的一種網(wǎng)絡(luò)協(xié)議。
FTP文件傳輸協(xié)議(File Transfer Protocol)。
IP協(xié)議是因特網(wǎng)互聯(lián)協(xié)議(Internet Protocol)。
ICMP協(xié)議是Internet控制報(bào)文協(xié)議(Internet Control Message Protocol)它是TCP/IP協(xié)議族的一個(gè)子協(xié)議,用于在IP主機(jī)、路由器之間傳遞控制消息。
IGMP協(xié)議是 Internet 組管理協(xié)議(Internet Group Management Protocol),是因特網(wǎng)協(xié)議家族中的一個(gè)組播協(xié)議。該協(xié)議運(yùn)行在主機(jī)和組播路由器之間。
ARP協(xié)議是正向地址解析協(xié)議(Address Resolution Protocol),通過已知的IP,尋找對(duì)應(yīng)主機(jī)的MAC地址。
RARP是反向地址轉(zhuǎn)換協(xié)議,通過MAC地址確定IP地址。
2. 分層模型
-
網(wǎng)絡(luò)分層架構(gòu)
為了減少協(xié)議設(shè)計(jì)的復(fù)雜性,大多數(shù)網(wǎng)絡(luò)模型均采用分層的方式來組織。每一層都有自己的功能,就像建筑物一樣,每一層都靠下一層支持。每一層利用下一層提供的服務(wù)來為上一層提供服務(wù),本層服務(wù)的實(shí)現(xiàn)細(xì)節(jié)對(duì)上層屏蔽。

越下面的層,越靠近硬件;越上面的層,越靠近用戶。
業(yè)內(nèi)普遍的分層方式有兩種。OSI七層模型 和TCP/IP四層模型。可以通過背誦兩個(gè)口訣來快速記憶:
OSI七層模型:物、數(shù)、網(wǎng)、傳、會(huì)、表、應(yīng)
TCP/IP四層模型:鏈、網(wǎng)、傳、應(yīng)
1 ) 物理層:主要定義物理設(shè)備標(biāo)準(zhǔn),如網(wǎng)線的接口類型、光纖的接口類型、各種傳輸介質(zhì)的傳輸速率等。它的主要作用是傳輸比特流(就是由1、0轉(zhuǎn)化為電流強(qiáng)弱來進(jìn)行傳輸,到達(dá)目的地后再轉(zhuǎn)化為1、0,也就是我們常說的數(shù)模轉(zhuǎn)換與模數(shù)轉(zhuǎn)換)。這一層的數(shù)據(jù)叫做比特。
2 ) 數(shù)據(jù)鏈路層:定義了如何讓格式化數(shù)據(jù)以幀為單位進(jìn)行傳輸,以及如何讓控制對(duì)物理介質(zhì)的訪問。這一層通常還提供錯(cuò)誤檢測(cè)和糾正,以確保數(shù)據(jù)的可靠傳輸。如:串口通信中使用到的115200、8、N、1
3 ) 網(wǎng)絡(luò)層:在位于不同地理位置的網(wǎng)絡(luò)中的兩個(gè)主機(jī)系統(tǒng)之間提供連接和路徑選擇。Internet的發(fā)展使得從世界各站點(diǎn)訪問信息的用戶數(shù)大大增加,而網(wǎng)絡(luò)層正是管理這種連接的層。
4 ) 傳輸層:定義了一些傳輸數(shù)據(jù)的協(xié)議和端口號(hào)(WWW端口80等),如:TCP(傳輸控制協(xié)議,傳輸效率低,可靠性強(qiáng),用于傳輸可靠性要求高,數(shù)據(jù)量大的數(shù)據(jù)),UDP(用戶數(shù)據(jù)報(bào)協(xié)議,與TCP特性恰恰相反,用于傳輸可靠性要求不高,數(shù)據(jù)量小的數(shù)據(jù),如QQ聊天數(shù)據(jù)就是通過這種方式傳輸?shù)模?主要是將從下層接收的數(shù)據(jù)進(jìn)行分段和傳輸,到達(dá)目的地址后再進(jìn)行重組。常常把這一層數(shù)據(jù)叫做段。
5 ) 會(huì)話層:通過傳輸層(端口號(hào):傳輸端口與接收端口)建立數(shù)據(jù)傳輸?shù)耐?。主要在你的系統(tǒng)之間發(fā)起會(huì)話或者接受會(huì)話請(qǐng)求(設(shè)備之間需要互相認(rèn)識(shí)可以是IP也可以是MAC或者是主機(jī)名)。
6 ) 表示層:可確保一個(gè)系統(tǒng)的應(yīng)用層所發(fā)送的信息可以被另一個(gè)系統(tǒng)的應(yīng)用層讀取。例如,PC程序與另一臺(tái)計(jì)算機(jī)進(jìn)行通信,其中一臺(tái)計(jì)算機(jī)使用擴(kuò)展二一十進(jìn)制交換碼(EBCDIC),而另一臺(tái)則使用美國(guó)信息交換標(biāo)準(zhǔn)碼(ASCII)來表示相同的字符。如有必要,表示層會(huì)通過使用一種通格式來實(shí)現(xiàn)多種數(shù)據(jù)格式之間的轉(zhuǎn)換。
7 ) 應(yīng)用層:是最靠近用戶的OSI層。這一層為用戶的應(yīng)用程序(例如電子郵件、文件傳輸和終端仿真)提供網(wǎng)絡(luò)服務(wù)。
-
層與協(xié)議
每一層都是為了完成一種功能,為了實(shí)現(xiàn)這些功能,就需要大家都遵守共同的規(guī)則。大家都遵守這規(guī)則,就叫做“協(xié)議”(protocol)。
網(wǎng)絡(luò)的每一層,都定義了很多協(xié)議。這些協(xié)議的總稱,叫“TCP/IP協(xié)議”。TCP/IP協(xié)議是一個(gè)大家族,不僅僅只有TCP和IP協(xié)議,它還包括其它的協(xié)議,如下圖:

-
各層功能

鏈路層
以太網(wǎng)規(guī)定,連入網(wǎng)絡(luò)的所有設(shè)備,都必須具有“網(wǎng)卡”接口。數(shù)據(jù)包必須是從一塊網(wǎng)卡,傳送到另一塊網(wǎng)卡。通過網(wǎng)卡能夠使不同的計(jì)算機(jī)之間連接,從而完成數(shù)據(jù)通信等功能。網(wǎng)卡的地址——MAC 地址,就是數(shù)據(jù)包的物理發(fā)送地址和物理接收地址。
網(wǎng)絡(luò)層
網(wǎng)絡(luò)層的作用是引進(jìn)一套新的地址,使得我們能夠區(qū)分不同的計(jì)算機(jī)是否屬于同一個(gè)子網(wǎng)絡(luò)。這套地址就叫做“網(wǎng)絡(luò)地址”,就是我們平時(shí)所說的IP地址。這個(gè)IP地址好比我們的手機(jī)號(hào)碼,通過手機(jī)號(hào)碼可以得到用戶所在的歸屬地。
網(wǎng)絡(luò)地址幫助我們確定計(jì)算機(jī)所在的子網(wǎng)絡(luò),MAC 地址則將數(shù)據(jù)包送到該子網(wǎng)絡(luò)中的目標(biāo)網(wǎng)卡。網(wǎng)絡(luò)層協(xié)議包含的主要信息是源IP和目的IP。
于是,“網(wǎng)絡(luò)層”出現(xiàn)以后,每臺(tái)計(jì)算機(jī)有了兩種地址,一種是 MAC 地址,另一種是網(wǎng)絡(luò)地址。兩種地址之間沒有任何聯(lián)系,MAC 地址是綁定在網(wǎng)卡上的,網(wǎng)絡(luò)地址則是管理員分配的,它們只是隨機(jī)組合在一起。
網(wǎng)絡(luò)地址幫助我們確定計(jì)算機(jī)所在的子網(wǎng)絡(luò),MAC 地址則將數(shù)據(jù)包送到該子網(wǎng)絡(luò)中的目標(biāo)網(wǎng)卡。因此,從邏輯上可以推斷,必定是先處理網(wǎng)絡(luò)地址,然后再處理 MAC 地址。
傳輸層
當(dāng)我們一邊聊QQ,一邊聊微信,當(dāng)一個(gè)數(shù)據(jù)包從互聯(lián)網(wǎng)上發(fā)來的時(shí)候,我們?cè)趺粗溃莵碜訯Q的內(nèi)容,還是來自微信的內(nèi)容?
也就是說,我們還需要一個(gè)參數(shù),表示這個(gè)數(shù)據(jù)包到底供哪個(gè)程序(進(jìn)程)使用。這個(gè)參數(shù)就叫做“端口”(port),它其實(shí)是每一個(gè)使用網(wǎng)卡的程序的編號(hào)。每個(gè)數(shù)據(jù)包都發(fā)到主機(jī)的特定端口,所以不同的程序就能取到自己所需要的數(shù)據(jù)。
端口特點(diǎn):
對(duì)于同一個(gè)端口,在不同系統(tǒng)中對(duì)應(yīng)著不同的進(jìn)程
對(duì)于同一個(gè)系統(tǒng),一個(gè)端口只能被一個(gè)進(jìn)程擁有
應(yīng)用層
應(yīng)用程序收到“傳輸層”的數(shù)據(jù),接下來就要進(jìn)行解讀。由于互聯(lián)網(wǎng)是開放架構(gòu),數(shù)據(jù)來源五花八門,必須事先規(guī)定好格式,否則根本無法解讀。“應(yīng)用層”的作用,就是規(guī)定應(yīng)用程序的數(shù)據(jù)格式。
-
通信過程
兩臺(tái)計(jì)算機(jī)通過TCP/IP協(xié)議通訊的過程如下所示:

3. Socket編程
-
什么是Socket
Socket,英文含義是【插座、插孔】,一般稱之為套接字,用于描述IP地址和端口??梢詫?shí)現(xiàn)不同程序間的數(shù)據(jù)通信。
Socket起源于Unix,而Unix基本哲學(xué)之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關(guān)閉close”模式來操作。Socket就是該模式的一個(gè)實(shí)現(xiàn),網(wǎng)絡(luò)的Socket數(shù)據(jù)傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個(gè)類似于打開文件的函數(shù)調(diào)用:Socket(),該函數(shù)返回一個(gè)整型的Socket描述符,隨后的連接建立、數(shù)據(jù)傳輸?shù)炔僮鞫际峭ㄟ^該Socket實(shí)現(xiàn)的。
套接字的內(nèi)核實(shí)現(xiàn)較為復(fù)雜,不宜在學(xué)習(xí)初期深入學(xué)習(xí),了解到如下結(jié)構(gòu)足矣。

在TCP/IP協(xié)議中,“IP地址+TCP或UDP端口號(hào)”唯一標(biāo)識(shí)網(wǎng)絡(luò)通訊中的一個(gè)進(jìn)程?!癐P地址+端口號(hào)”就對(duì)應(yīng)一個(gè)socket。欲建立連接的兩個(gè)進(jìn)程各自有一個(gè)socket來標(biāo)識(shí),那么這兩個(gè)socket組成的socket pair就唯一標(biāo)識(shí)一個(gè)連接。因此可以用Socket來描述網(wǎng)絡(luò)連接的一對(duì)一關(guān)系。
常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數(shù)據(jù)報(bào)式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對(duì)于面向連接的TCP服務(wù)應(yīng)用;數(shù)據(jù)報(bào)式Socket是一種無連接的Socket,對(duì)應(yīng)于無連接的UDP服務(wù)應(yīng)用。
4. 網(wǎng)絡(luò)應(yīng)用程序設(shè)計(jì)模式
-
C/S模式
傳統(tǒng)的網(wǎng)絡(luò)應(yīng)用設(shè)計(jì)模式,客戶機(jī)(client)/服務(wù)器(server)模式。需要在通訊兩端各自部署客戶機(jī)和服務(wù)器來完成數(shù)據(jù)通信。
-
B/S模式
瀏覽器(Browser)/服務(wù)器(Server)模式。只需在一端部署服務(wù)器,而另外一端使用每臺(tái)PC都默認(rèn)配置的瀏覽器即可完成數(shù)據(jù)的傳輸。
-
優(yōu)缺點(diǎn)
對(duì)于C/S模式來說,其優(yōu)點(diǎn)明顯??蛻舳宋挥谀繕?biāo)主機(jī)上可以保證性能,將數(shù)據(jù)緩存至客戶端本地,從而提高數(shù)據(jù)傳輸效率。且,一般來說客戶端和服務(wù)器程序由一個(gè)開發(fā)團(tuán)隊(duì)創(chuàng)作,所以他們之間所采用的協(xié)議相對(duì)靈活。可以在標(biāo)準(zhǔn)協(xié)議的基礎(chǔ)上根據(jù)需求裁剪及定制。例如,騰訊所采用的通信協(xié)議,即為ftp協(xié)議的修改剪裁版
因此,傳統(tǒng)的網(wǎng)絡(luò)應(yīng)用程序及較大型的網(wǎng)絡(luò)應(yīng)用程序都首選C/S模式進(jìn)行開發(fā)。如,知名的網(wǎng)絡(luò)游戲魔獸世界。3D畫面,數(shù)據(jù)量龐大,使用C/S模式可以提前在本地進(jìn)行大量數(shù)據(jù)的緩存處理,從而提高觀感。
C/S模式的缺點(diǎn)也較突出。由于客戶端和服務(wù)器都需要有一個(gè)開發(fā)團(tuán)隊(duì)來完成開發(fā)。工作量將成倍提升,開發(fā)周期較長(zhǎng)。另外,從用戶角度出發(fā),需要將客戶端安插至用戶主機(jī)上,對(duì)用戶主機(jī)的安全性構(gòu)成威脅。這也是很多用戶不愿使用C/S模式應(yīng)用程序的重要原因。
B/S模式相比C/S模式而言,由于它沒有獨(dú)立的客戶端,使用標(biāo)準(zhǔn)瀏覽器作為客戶端,其工作開發(fā)量較小。只需開發(fā)服務(wù)器端即可。另外由于其采用瀏覽器顯示數(shù)據(jù),因此移植性非常好,不受平臺(tái)限制。如早期的偷菜游戲,在各個(gè)平臺(tái)上都可以完美運(yùn)行。
B/S模式的缺點(diǎn)也較明顯。由于使用第三方瀏覽器,因此網(wǎng)絡(luò)應(yīng)用支持受限。另外,沒有客戶端放到對(duì)方主機(jī)上,緩存數(shù)據(jù)不盡如人意,從而傳輸數(shù)據(jù)量受到限制。應(yīng)用的觀感大打折扣。第三,必須與瀏覽器一樣,采用標(biāo)準(zhǔn)http協(xié)議進(jìn)行通信,協(xié)議選擇不靈活。
因此在開發(fā)過程中,模式的選擇由上述各自的特點(diǎn)決定。根據(jù)實(shí)際需求選擇應(yīng)用程序設(shè)計(jì)模式。
-
簡(jiǎn)單的C/S模型通信

Server端:
Listen函數(shù):
func Listen(network, address string) (Listener, error)
//network:選用的協(xié)議:TCP、UDP, 如:“tcp”或 “udp” 注意:只支持小寫字母
//address:IP地址+端口號(hào), 如:“127.0.0.1:8000”或 “:8000”
Listener 接口:
type Listener interface {
Accept() (Conn, error)
Close() error
Addr() Addr
}
Conn 接口:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
TCP服務(wù)器示例代碼:
package main
import (
"net"
"fmt"
)
func main() {
// 創(chuàng)建用于監(jiān)聽的 socket
listener, err := net.Listen("tcp", "127.0.0.1:7020")
if err != nil {
fmt.Println("Listen err:", err)
return
}
fmt.Println("監(jiān)聽套接字,創(chuàng)建成功。。。")
// 服務(wù)器結(jié)束前關(guān)閉 listener
defer listener.Close()
// 創(chuàng)建用戶數(shù)據(jù)通信的socket
conn, err := listener.Accept() // 阻塞等待...
if err != nil {
fmt.Println("Accept err:", err)
return
}
defer conn.Close()
fmt.Println("通信套接字,創(chuàng)建成功。。。")
// 創(chuàng)建一個(gè)用保存數(shù)據(jù)的緩沖區(qū)
buf := make([]byte, 4096)
for {
// 獲取客戶端發(fā)送的數(shù)據(jù)內(nèi)容
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Read err:", err)
return
}
// 處理 客戶端 的數(shù)據(jù)
fmt.Println("讀到客戶端發(fā)送:", string(buf[:n]))
// 回寫數(shù)據(jù)給客戶端
_, err = conn.Write([]byte("This is Server\n"))
if err != nil {
fmt.Println("Write err:", err)
return
}
}
}
如圖,在整個(gè)通信過程中,服務(wù)器端有兩個(gè)socket參與進(jìn)來,但用于通信的只有 conn 這個(gè)socket。它是由 listener創(chuàng)建的。隸屬于服務(wù)器端。

Client 端:
Dial函數(shù):
func Dial(network, address string) (Conn, error)
//network:選用的協(xié)議:TCP、UDP,如:“tcp”或 “udp”
//address:服務(wù)器IP地址+端口號(hào), 如:“121.36.108.11:8000”或 “www.itcast.cn:8000”
Conn 接口:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
TCP客戶端示例代碼:
package main
import (
"net"
"fmt"
"time"
)
func main() {
// 創(chuàng)建用于通信socket
conn, err := net.Dial("tcp", "127.0.0.1:7020") // IP地址/prot端口號(hào)--服務(wù)器的
if err != nil {
fmt.Println("Dial err:", err)
return
}
// 關(guān)閉連接
defer conn.Close()
for {
// 發(fā)送數(shù)據(jù) write
_, err = conn.Write([]byte("hello socket"))
if err != nil {
fmt.Println("Write err:", err)
return
}
buf := make([]byte, 4096)
// 接收數(shù)據(jù) read
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Read err:", err)
return
}
fmt.Println("讀到 服務(wù)器 回發(fā):", string(buf[:n]))
time.Sleep(time.Second)
}
}
-
并發(fā)C/S模型通信
并發(fā)Server
現(xiàn)在已經(jīng)完成了客戶端與服務(wù)端的通信,但是服務(wù)端只能接收一個(gè)用戶發(fā)送過來的數(shù)據(jù),怎樣接收多個(gè)客戶端發(fā)送過來的數(shù)據(jù),實(shí)現(xiàn)一個(gè)高效的并發(fā)服務(wù)器呢?
Accept()函數(shù)的作用是等待客戶端的鏈接,如果客戶端沒有鏈接,該方法會(huì)阻塞。如果有客戶端鏈接,那么該方法返回一個(gè)Socket負(fù)責(zé)與客戶端進(jìn)行通信。所以,每來一個(gè)客戶端,該方法就應(yīng)該返回一個(gè)Socket與其通信,因此,可以使用一個(gè)死循環(huán),將Accept()調(diào)用過程包裹起來。
需要注意的是,實(shí)現(xiàn)并發(fā)處理多個(gè)客戶端數(shù)據(jù)的服務(wù)器,就需要針對(duì)每一個(gè)客戶端連接,單獨(dú)產(chǎn)生一個(gè)Socket,并創(chuàng)建一個(gè)單獨(dú)的goroutine與之完成通信。
//監(jiān)聽
listener, err := net.Listen("tcp", "127.0.0.1:8001") // tcp 不能使用大寫
if err != nil {
fmt.Println("err = ", err)
return
}
defer listener.Close()
//接收多個(gè)用戶
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("err = ", err)
return
}
//處理用戶請(qǐng)求, 新建一個(gè)協(xié)程
go HandleConn(conn)
}
將客戶端的數(shù)據(jù)處理工作封裝到HandleConn方法中,需將Accept()返回的Socket傳遞給該方法,變量conn的類型為:net.Conn。可以使用conn.RemoteAddr()來獲取成功與服務(wù)器建立連接的客戶端IP地址和端口號(hào):
//Conn 接口:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
//獲取客戶端的網(wǎng)絡(luò)地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, " conncet sucessful")
客戶端可能持續(xù)不斷的發(fā)送數(shù)據(jù),因此接收數(shù)據(jù)的過程可以放在for循環(huán)中,服務(wù)端也持續(xù)不斷的向客戶端返回處理后的數(shù)據(jù)。
添加一個(gè)限定,如果客戶端發(fā)送一個(gè)“exit”字符串,表示客戶端通知服務(wù)器不再向服務(wù)端發(fā)送數(shù)據(jù),此時(shí)應(yīng)該結(jié)束HandleConn方法,同時(shí)關(guān)閉與該客戶端關(guān)聯(lián)的Socket。
buf := make([]byte, 2048) //創(chuàng)建一個(gè)切片,存儲(chǔ)客戶端發(fā)送的數(shù)據(jù)
for {
//讀取用戶數(shù)據(jù)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
if "exit" == string(buf[:n-2]) { //自己寫的客戶端測(cè)試, 發(fā)送時(shí),多了2個(gè)字符, "\r\n"
fmt.Println(addr, " exit")
return
}
//服務(wù)器處理數(shù)據(jù):把客戶端數(shù)據(jù)轉(zhuǎn)大寫,再寫回給client
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
在上面的代碼中,Read()方法獲取客戶端發(fā)送過來的數(shù)據(jù),填充到切片buf中,返回的是實(shí)際填充的數(shù)據(jù)的長(zhǎng)度,所以將客戶端發(fā)送過來的數(shù)據(jù)進(jìn)行打印,打印的是實(shí)際接收到的數(shù)據(jù)。
fmt.Printf("[%s]: %s\n", addr, string(buf[:n])).同時(shí)也可以將客戶端的網(wǎng)絡(luò)地址信息打印出來。
在判斷客戶端數(shù)據(jù)是否為“exit”字符串時(shí),要注意,客戶端會(huì)自動(dòng)的多發(fā)送2個(gè)字符:“\r\n”(這在windows系統(tǒng)下代表回車、換行)。
Server使用Write方法將數(shù)據(jù)寫回給客戶端,參數(shù)類型是 []byte,需使用strings包下的ToUpper函數(shù)來完成大小寫轉(zhuǎn)換。轉(zhuǎn)換的對(duì)象即為string(buf[:n])。
綜上,HandleConn方法完整定義如下:
//處理用戶請(qǐng)求
func HandleConn(conn net.Conn) {
//函數(shù)調(diào)用完畢,自動(dòng)關(guān)閉conn
defer conn.Close()
//獲取客戶端的網(wǎng)絡(luò)地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, " conncet sucessful")
buf := make([]byte, 2048)
for {
//讀取用戶數(shù)據(jù)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
fmt.Println("len = ", len(string(buf[:n])))
//if "exit" == string(buf[:n-1]) { // nc測(cè)試,發(fā)送時(shí),只有 \n
if "exit" == string(buf[:n-2]) { // 自己寫的客戶端測(cè)試, 發(fā)送時(shí),多了2個(gè)字符, "\r\n"
fmt.Println(addr, " exit")
return
}
//把數(shù)據(jù)轉(zhuǎn)換為大寫,再給用戶發(fā)送
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
并發(fā)Client
客戶端不僅需要持續(xù)的向服務(wù)端發(fā)送數(shù)據(jù),同時(shí)也要接收從服務(wù)端返回的數(shù)據(jù)。因此可將發(fā)送和接收放到不同的協(xié)程中。
主協(xié)程循環(huán)接收服務(wù)器回發(fā)的數(shù)據(jù)(該數(shù)據(jù)應(yīng)已轉(zhuǎn)換為大寫),并打印至屏幕;子協(xié)程循環(huán)從鍵盤讀取用戶輸入數(shù)據(jù),寫給服務(wù)器。讀取鍵盤輸入可使用 os.Stdin.Read(str)。定義切片str,將讀到的數(shù)據(jù)保存至str中。
這樣,客戶端也實(shí)現(xiàn)了多任務(wù)。
客戶端代碼實(shí)現(xiàn):
package main
import (
"net"
"fmt"
"os"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("Dial err:", err)
return
}
defer conn.Close()
// 讀取用戶的鍵盤輸入。
go func() {
buf := make([]byte, 4096)
for {
// 獲取鍵盤輸入。 fmt.Scan --》 結(jié)束標(biāo)記 \n 和 空格
n, err := os.Stdin.Read(buf) // buf[:n]
if err != nil {
fmt.Println("os.Stdin.Read err:", err)
return
}
// 直接將讀到鍵盤輸入數(shù)據(jù),寫到 socket 中,發(fā)送給服務(wù)器
conn.Write(buf[:n])
}
}()
// 在 主go程中, 獲取服務(wù)器回發(fā)數(shù)據(jù)。
buf2 := make([]byte, 4096)
for {
// 借助 socket 從服務(wù)器讀取 數(shù)據(jù)。
n, err := conn.Read(buf2)
if n == 0 {
fmt.Println("客戶端檢查到服務(wù)器,關(guān)閉連接, 本端也退出")
return
}
if err != nil {
fmt.Println("os.Stdin.Read err:", err)
return
}
fmt.Println("客戶端讀到:", string(buf2[:n]))
}
}
-
TCP通信過程
下圖是一次TCP通訊的時(shí)序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次握手。

在這個(gè)例子中,首先客戶端主動(dòng)發(fā)起連接、發(fā)送請(qǐng)求,然后服務(wù)器端響應(yīng)請(qǐng)求,然后客戶端主動(dòng)關(guān)閉連接。兩條豎線表示通訊的兩端,從上到下表示時(shí)間的先后順序。注意,數(shù)據(jù)從一端傳到網(wǎng)絡(luò)的另一端也需要時(shí)間,所以圖中的箭頭都是斜的。
三次握手
所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個(gè)TCP連接時(shí),需要客戶端和服務(wù)端總共發(fā)送3個(gè)包以確認(rèn)連接的建立。好比兩個(gè)人在打電話:
Client:“喂,你聽得到嗎?”
Server:“我聽得到,你聽得到我嗎?”
Client:“我能聽到你,今天balabala…”
建立連接(三次握手)的過程:
-
客戶端發(fā)送一個(gè)帶SYN標(biāo)志的TCP報(bào)文到服務(wù)器。這是上圖中三次握手過程中的段1。客戶端發(fā)出SYN位表示連接請(qǐng)求。序號(hào)是1000,這個(gè)序號(hào)在網(wǎng)絡(luò)通訊中用作臨時(shí)的地址,每發(fā)一個(gè)數(shù)據(jù)字節(jié),這個(gè)序號(hào)要加1,這樣在接收端可以根據(jù)序號(hào)排出數(shù)據(jù)包的正確順序,也可以發(fā)現(xiàn)丟包的情況。
另外,規(guī)定SYN位和FIN位也要占一個(gè)序號(hào),這次雖然沒發(fā)數(shù)據(jù),但是由于發(fā)了SYN位,因此下次再發(fā)送應(yīng)該用序號(hào)1001。
mss表示最大段尺寸,如果一個(gè)段太大,封裝成幀后超過了鏈路層的最大長(zhǎng)度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務(wù)器端發(fā)來的段不要超過這個(gè)長(zhǎng)度。
-
服務(wù)器端回應(yīng)客戶端,是三次握手中的第2個(gè)報(bào)文段,同時(shí)帶ACK標(biāo)志和SYN標(biāo)志。表示對(duì)剛才客戶端SYN的回應(yīng);同時(shí)又發(fā)送SYN給客戶端,詢問客戶端是否準(zhǔn)備好進(jìn)行數(shù)據(jù)通訊。
服務(wù)器發(fā)出段2,也帶有SYN位,同時(shí)置ACK位表示確認(rèn),確認(rèn)序號(hào)是1001,表示“我接收到序號(hào)1000及其以前所有的段,請(qǐng)你下次發(fā)送序號(hào)為1001的段”,也就是應(yīng)答了客戶端的連接請(qǐng)求,同時(shí)也給客戶端發(fā)出一個(gè)連接請(qǐng)求,同時(shí)聲明最大尺寸為1024。
客戶必須再次回應(yīng)服務(wù)器端一個(gè)ACK報(bào)文,這是報(bào)文段3。
客戶端發(fā)出段3,對(duì)服務(wù)器的連接請(qǐng)求進(jìn)行應(yīng)答,確認(rèn)序號(hào)是8001。在這個(gè)過程中,客戶端和服務(wù)器分別給對(duì)方發(fā)了連接請(qǐng)求,也應(yīng)答了對(duì)方的連接請(qǐng)求,其中服務(wù)器的請(qǐng)求和應(yīng)答在一個(gè)段中發(fā)出。
因此一共有三個(gè)段用于建立連接,稱為“三方握手”。在建立連接的同時(shí),雙方協(xié)商了一些信息,例如,雙方發(fā)送序號(hào)的初始值、最大段尺寸等。
數(shù)據(jù)傳輸?shù)倪^程:
客戶端發(fā)出段4,包含從序號(hào)1001開始的20個(gè)字節(jié)數(shù)據(jù)。
服務(wù)器發(fā)出段5,確認(rèn)序號(hào)為1021,對(duì)序號(hào)為1001-1020的數(shù)據(jù)表示確認(rèn)收到,同時(shí)請(qǐng)求發(fā)送序號(hào)1021開始的數(shù)據(jù),服務(wù)器在應(yīng)答的同時(shí)也向客戶端發(fā)送從序號(hào)8001開始的10個(gè)字節(jié)數(shù)據(jù)。
客戶端發(fā)出段6,對(duì)服務(wù)器發(fā)來的序號(hào)為8001-8010的數(shù)據(jù)表示確認(rèn)收到,請(qǐng)求發(fā)送序號(hào)8011開始的數(shù)據(jù)。
在數(shù)據(jù)傳輸過程中,ACK和確認(rèn)序號(hào)是非常重要的,應(yīng)用程序交給TCP協(xié)議發(fā)送的數(shù)據(jù)會(huì)暫存在TCP層的發(fā)送緩沖區(qū)中,發(fā)出數(shù)據(jù)包給對(duì)方之后,只有收到對(duì)方應(yīng)答的ACK段才知道該數(shù)據(jù)包確實(shí)發(fā)到了對(duì)方,可以從發(fā)送緩沖區(qū)中釋放掉了,如果因?yàn)榫W(wǎng)絡(luò)故障丟失了數(shù)據(jù)包或者丟失了對(duì)方發(fā)回的ACK段,經(jīng)過等待超時(shí)后TCP協(xié)議自動(dòng)將發(fā)送緩沖區(qū)中的數(shù)據(jù)包重發(fā)。
四次揮手
所謂四次揮手(Four-Way-Wavehand)即終止TCP連接,就是指斷開一個(gè)TCP連接時(shí),需要客戶端和服務(wù)端總共發(fā)送4個(gè)包以確認(rèn)連接的斷開。在socket編程中,這一過程由客戶端或服務(wù)器任一方執(zhí)行close來觸發(fā)。好比兩個(gè)人打完電話要掛斷:
Client:“我要說的事情都說完了,我沒事了。掛啦?”
Server:“等下,我還有一個(gè)事兒。Balabala…”
Server:“好了,我沒事兒了。掛了啊?!?/p>
Client:“ok!拜拜”
關(guān)閉連接(四次握手)的過程:
由于TCP連接是全雙工的,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個(gè)FIN來終止這個(gè)方向的連接。收到一個(gè) FIN只意味著這一方向上沒有數(shù)據(jù)流動(dòng),一個(gè)TCP連接在收到一個(gè)FIN后仍能發(fā)送數(shù)據(jù)。首先進(jìn)行關(guān)閉的一方將執(zhí)行主動(dòng)關(guān)閉,而另一方執(zhí)行被動(dòng)關(guān)閉。
客戶端發(fā)出段7,F(xiàn)IN位表示關(guān)閉連接的請(qǐng)求。
服務(wù)器發(fā)出段8,應(yīng)答客戶端的關(guān)閉連接請(qǐng)求。
服務(wù)器發(fā)出段9,其中也包含F(xiàn)IN位,向客戶端發(fā)送關(guān)閉連接請(qǐng)求。
客戶端發(fā)出段10,應(yīng)答服務(wù)器的關(guān)閉連接請(qǐng)求。
建立連接的過程是三次握手,而關(guān)閉連接通常需要4個(gè)段,服務(wù)器的應(yīng)答和關(guān)閉連接請(qǐng)求通常不合并在一個(gè)段中,因?yàn)橛羞B接半關(guān)閉的情況,這種情況下客戶端關(guān)閉連接之后就不能再發(fā)送數(shù)據(jù)給服務(wù)器了,但是服務(wù)器還可以發(fā)送數(shù)據(jù)給客戶端,直到服務(wù)器也關(guān)閉連接為止。
TCP狀態(tài)轉(zhuǎn)換
TCP狀態(tài)圖很多人都知道,它對(duì)排除和定位網(wǎng)絡(luò)或系統(tǒng)故障時(shí)大有幫助。如果能熟練掌握這張圖,了解圖中的每一個(gè)狀態(tài),能大大提高我們對(duì)于TCP的理解和認(rèn)識(shí)。下面對(duì)這張圖的11種狀態(tài)詳細(xì)解析一下,以便加強(qiáng)記憶!不過在這之前,一定要熟練掌握TCP建立連接的三次握手過程,以及關(guān)閉連接的四次揮手過程。

CLOSED:表示初始狀態(tài)。
LISTEN:該狀態(tài)表示服務(wù)器端的某個(gè)SOCKET處于監(jiān)聽狀態(tài),可以接受連接。
SYN_SENT:這個(gè)狀態(tài)與SYN_RCVD遙相呼應(yīng),當(dāng)客戶端SOCKET執(zhí)行CONNECT連接時(shí),它首先發(fā)送SYN報(bào)文,隨即進(jìn)入到了SYN_SENT狀態(tài),并等待服務(wù)端的發(fā)送三次握手中的第2個(gè)報(bào)文。SYN_SENT狀態(tài)表示客戶端已發(fā)送SYN報(bào)文。
SYN_RCVD: 該狀態(tài)表示接收到SYN報(bào)文,在正常情況下,這個(gè)狀態(tài)是服務(wù)器端的SOCKET在建立TCP連接時(shí)的三次握手會(huì)話過程中的一個(gè)中間狀態(tài),很短暫。此種狀態(tài)時(shí),當(dāng)收到客戶端的ACK報(bào)文后,會(huì)進(jìn)入到ESTABLISHED狀態(tài)。
ESTABLISHED:表示連接已經(jīng)建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2狀態(tài)的真正含義都是表示等待對(duì)方的FIN報(bào)文。區(qū)別是:
FIN_WAIT_1狀態(tài)是當(dāng)socket在ESTABLISHED狀態(tài)時(shí),想主動(dòng)關(guān)閉連接,向?qū)Ψ桨l(fā)送了FIN報(bào)文,此時(shí)該socket進(jìn)入到FIN_WAIT_1狀態(tài)。
FIN_WAIT_2狀態(tài)是當(dāng)對(duì)方回應(yīng)ACK后,該socket進(jìn)入到FIN_WAIT_2狀態(tài),正常情況下,對(duì)方應(yīng)馬上回應(yīng)ACK報(bào)文,所以FIN_WAIT_1狀態(tài)一般較難見到,而FIN_WAIT_2狀態(tài)可用netstat看到。
FIN_WAIT_2:主動(dòng)關(guān)閉鏈接的一方,發(fā)出FIN收到ACK以后進(jìn)入該狀態(tài)。稱之為半連接或半關(guān)閉狀態(tài)。該狀態(tài)下的socket只能接收數(shù)據(jù),不能發(fā)。
TIME_WAIT: 表示收到了對(duì)方的FIN報(bào)文,并發(fā)送出了ACK報(bào)文,等2MSL后即可回到CLOSED可用狀態(tài)。如果FIN_WAIT_1狀態(tài)下,收到對(duì)方同時(shí)帶 FIN標(biāo)志和ACK標(biāo)志的報(bào)文時(shí),可以直接進(jìn)入到TIME_WAIT狀態(tài),而無須經(jīng)過FIN_WAIT_2狀態(tài)。
CLOSING:這種狀態(tài)較特殊,屬于一種較罕見的狀態(tài)。正常情況下,當(dāng)你發(fā)送FIN報(bào)文后,按理來說是應(yīng)該先收到(或同時(shí)收到)對(duì)方的 ACK報(bào)文,再收到對(duì)方的FIN報(bào)文。但是CLOSING狀態(tài)表示你發(fā)送FIN報(bào)文后,并沒有收到對(duì)方的ACK報(bào)文,反而卻也收到了對(duì)方的FIN報(bào)文。什么情況下會(huì)出現(xiàn)此種情況呢?如果雙方幾乎在同時(shí)close一個(gè)SOCKET的話,那么就出現(xiàn)了雙方同時(shí)發(fā)送FIN報(bào)文的情況,也即會(huì)出現(xiàn)CLOSING狀態(tài),表示雙方都正在關(guān)閉SOCKET連接。
CLOSE_WAIT:此種狀態(tài)表示在等待關(guān)閉。當(dāng)對(duì)方關(guān)閉一個(gè)SOCKET后發(fā)送FIN報(bào)文給自己,系統(tǒng)會(huì)回應(yīng)一個(gè)ACK報(bào)文給對(duì)方,此時(shí)則進(jìn)入到CLOSE_WAIT狀態(tài)。接下來呢,察看是否還有數(shù)據(jù)發(fā)送給對(duì)方,如果沒有可以 close這個(gè)SOCKET,發(fā)送FIN報(bào)文給對(duì)方,即關(guān)閉連接。所以在CLOSE_WAIT狀態(tài)下,需要關(guān)閉連接。
LAST_ACK: 該狀態(tài)是被動(dòng)關(guān)閉一方在發(fā)送FIN報(bào)文后,最后等待對(duì)方的ACK報(bào)文。當(dāng)收到ACK報(bào)文后,即可以進(jìn)入到CLOSED可用狀態(tài)。
2MSL (Maximum Segment Lifetime) 和與之對(duì)應(yīng)的TIME_WAIT狀態(tài),可以讓4次握手關(guān)閉流程更加可靠。4次握手的最后一個(gè)ACK是是由主動(dòng)關(guān)閉方發(fā)送出去的,若這個(gè)ACK丟失,被動(dòng)關(guān)閉方會(huì)再次發(fā)一個(gè)FIN過來。若主動(dòng)關(guān)閉方能夠保持一個(gè)2MSL的TIME_WAIT狀態(tài),則有更大的機(jī)會(huì)讓丟失的ACK被再次發(fā)送出去。注意,TIME_WAIT狀態(tài)一定出現(xiàn)在主動(dòng)關(guān)閉這一方。