Go語(yǔ)言中TCP/IP網(wǎng)絡(luò)編程的方法-創(chuàng)新互聯(lián)

這篇文章主要介紹“Go語(yǔ)言中TCP/IP網(wǎng)絡(luò)編程的方法”,在日常操作中,相信很多人在Go語(yǔ)言中TCP/IP網(wǎng)絡(luò)編程的方法問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Go語(yǔ)言中TCP/IP網(wǎng)絡(luò)編程的方法”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

成都創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比巴中網(wǎng)站開(kāi)發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式巴中網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋巴中地區(qū)。費(fèi)用合理售后完善,十載實(shí)體公司更值得信賴。

TCP/IP層發(fā)送數(shù)據(jù)的應(yīng)用場(chǎng)景


當(dāng)然很多情況下,不是大多數(shù)情況下,使用更高級(jí)別的網(wǎng)絡(luò)協(xié)議毫無(wú)疑問(wèn)會(huì)更好,因?yàn)榭梢允褂萌A麗的API, 它們隱藏了很多技術(shù)細(xì)節(jié)?,F(xiàn)在根據(jù)不同的需求,有很多選擇,比如消息隊(duì)列協(xié)議, gRPC, protobuf, FlatBuffers, RESTful網(wǎng)站API, websocket等等。

然而在一些特殊的場(chǎng)景下,特別是小型項(xiàng)目,選擇任何其他方式都會(huì)感覺(jué)太臃腫了,更不用說(shuō)你需要引入額外的依賴包了。

幸運(yùn)的是,使用標(biāo)準(zhǔn)庫(kù)的net包來(lái)創(chuàng)建簡(jiǎn)單的網(wǎng)絡(luò)通信不比你所見(jiàn)到的要困難。

因?yàn)镚o語(yǔ)言中有下面兩點(diǎn)簡(jiǎn)化。

簡(jiǎn)化1: 連接就是io流


net.Conn接口實(shí)現(xiàn)了io.Reader, io.Writer和io.Closer接口。 因此可以像對(duì)待其他io流一樣對(duì)待TCP連接。

你可能會(huì)認(rèn)為:"好,我能在TCP中發(fā)送字符串或字節(jié)分片,非常不錯(cuò),但是遇到復(fù)雜的數(shù)據(jù)結(jié)構(gòu)怎么辦? 例如我們遇到的是結(jié)構(gòu)體類型的數(shù)據(jù)?"

簡(jiǎn)化2: Go語(yǔ)言知道如何有效的解碼復(fù)雜的類型


當(dāng)說(shuō)到通過(guò)網(wǎng)絡(luò)發(fā)送編碼的結(jié)構(gòu)化數(shù)據(jù),首先想到的就是JSON。 不過(guò)先稍等一下 - Go語(yǔ)言的標(biāo)準(zhǔn)庫(kù)encoding/gob包提供了一種序列化和發(fā)序列話Go數(shù)據(jù)類型的方法,它無(wú)需給結(jié)構(gòu)體、Go語(yǔ)言不兼容的JSON添加字符串標(biāo)簽, 或者等待使用json.Unmarshal來(lái)費(fèi)勁的將文本解析為二進(jìn)制數(shù)據(jù)。

gob編碼解碼可以直接操作io流,這一點(diǎn)很完美的匹配第一條簡(jiǎn)化。

下面我們就通過(guò)這兩條簡(jiǎn)化規(guī)則一起實(shí)現(xiàn)一個(gè)簡(jiǎn)單的App。

這個(gè)簡(jiǎn)單APP的目標(biāo)


這個(gè)app應(yīng)該做兩件事情:

  • 發(fā)送和接收簡(jiǎn)單的字符串消息。

  • 通過(guò)gob發(fā)送和接收結(jié)構(gòu)體。

第一部分,發(fā)送簡(jiǎn)單字符串,將演示無(wú)需借助高級(jí)協(xié)議的情況下,通過(guò)TCP/IP網(wǎng)絡(luò)發(fā)送數(shù)據(jù)是多么簡(jiǎn)單。


第二部分,稍微深入一點(diǎn),通過(guò)網(wǎng)絡(luò)發(fā)送完整的結(jié)構(gòu)體,這些結(jié)構(gòu)體使用字符串、分片、映射、甚至包含到自身的遞歸指針。

辛虧有g(shù)ob包,要做到這些不費(fèi)吹灰之力。

客戶端                                        服務(wù)端

待發(fā)送結(jié)構(gòu)體                                解碼后結(jié)構(gòu)體
testStruct結(jié)構(gòu)體                            testStruct結(jié)構(gòu)體
    |                                             ^
    V                                             |
gob編碼       ---------------------------->     gob解碼
    |                                             ^
    V                                             |  
   發(fā)送     ============網(wǎng)絡(luò)=================    接收


通過(guò)TCP發(fā)送字符串?dāng)?shù)據(jù)的基本要素


發(fā)送端上


發(fā)送字符串需要三個(gè)簡(jiǎn)單的步驟:

  • 打開(kāi)對(duì)應(yīng)接收進(jìn)程的連接。

  • 寫字符串。

  • 關(guān)閉連接。

net包提供了一對(duì)實(shí)現(xiàn)這個(gè)功能的方法。

  • ResolveTCPAddr(): 該函數(shù)返回TCP終端地址。

  • DialTCP(): 類似于TCP網(wǎng)絡(luò)的撥號(hào)。

這兩個(gè)方法都是在go源碼的src/net/tcpsock.go文件中定義的。

func ResolveTCPAddr(network, address string) (*TCPAddr, error) {
 switch network {
 case "tcp", "tcp4", "tcp6":
 case "": // a hint wildcard for Go 1.0 undocumented behavior
 network = "tcp"
 default:
 return nil, UnknownNetworkError(network)
 }
 addrs, err := DefaultResolver.internetAddrList(context.Background(), network, address)
 if err != nil {
 return nil, err
 }
 return addrs.forResolve(network, address).(*TCPAddr), nil
}

ResolveTCPAddr()接收兩個(gè)字符串參數(shù)。

  • network: 必須是TCP網(wǎng)絡(luò)名,比如tcp, tcp4, tcp6。

  • address: TCP地址字符串,如果它不是字面量的IP地址或者端口號(hào)不是字面量的端口號(hào), ResolveTCPAddr會(huì)將傳入的地址解決成TCP終端的地址。否則傳入一對(duì)字面量IP地址和端口數(shù)字作為地址。address參數(shù)可以使用host名稱,但是不推薦這樣做,因?yàn)樗疃鄷?huì)返回host名字的一個(gè)IP地址。

ResolveTCPAddr()接收的代表TCP地址的字符串(例如localhost:80, 127.0.0.1:80, 或[::1]:80, 都是代表本機(jī)的80端口), 返回(net.TCPAddr指針, nil)(如果字符串不能被解析成有效的TCP地址會(huì)返回(nil, error))。

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) {
 switch network {
 case "tcp", "tcp4", "tcp6":
 default:
 return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: raddr.opAddr(), Err: UnknownNetworkError(network)}
 }
 if raddr == nil {
 return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: nil, Err: errMissingAddress}
 }
 c, err := dialTCP(context.Background(), network, laddr, raddr)
 if err != nil {
 return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: raddr.opAddr(), Err: err}
 }
 return c, nil
}

DialTCP()函數(shù)接收三個(gè)參數(shù):

  • network: 這個(gè)參數(shù)和ResolveTCPAddr的network參數(shù)一樣,必須是TCP網(wǎng)絡(luò)名。

  • laddr: TCPAddr類型的指針, 代表本地TCP地址。

  • raddr: TCPAddr類型的指針,代表的是遠(yuǎn)程TCP地址。

它會(huì)連接撥號(hào)兩個(gè)TCP地址,并返回這個(gè)連接作為net.TCPConn對(duì)象返回(連接失敗返回error)。如果我們不需要對(duì)Dial設(shè)置有過(guò)多控制,那么我們就可以使用Dial()代替。

func Dial(network, address string) (Conn, error) {
 var d Dialer
 return d.Dial(network, address)
}

Dial()函數(shù)接收一個(gè)TCP地址,返回一個(gè)一般的net.Conn。 這已經(jīng)足夠我們的測(cè)試用例了。然而如果你需要只有在TCP連接上的可用功能,可以使用TCP變體(DialTCP, TCPConn, TCPAddr等等)。

成功撥號(hào)之后,我們就可以如上所述的那樣,將新的連接與其他的輸入輸出流同等對(duì)待了。我們甚至可以將連接包裝進(jìn)bufio.ReadWriter中,這樣可以使用各種ReadWriter方法,例如ReadString(), ReadBytes, WriteString等等。

func Open(addr string) (*bufio.ReadWriter, error) {
 conn, err := net.Dial("tcp", addr)
 if err != nil {
 return nil, errors.Wrap(err, "Dialing "+addr+" failed")
 }
 // 將net.Conn對(duì)象包裝到bufio.ReadWriter中
 return bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), nil
}

記住緩沖Writer在寫之后需要調(diào)用Flush()方法, 這樣所有的數(shù)據(jù)才會(huì)刷到底層網(wǎng)絡(luò)連接中。
最后,每個(gè)連接對(duì)象都有一個(gè)Close()方法來(lái)終止通信。

微調(diào)(fine tuning)


Dialer結(jié)構(gòu)體定義如下:

type Dialer struct {
 Timeout time.Duration
 Deadline time.Time
 LocalAddr Addr
 DualStack bool
 FallbackDelay time.Duration
 KeepAlive time.Duration
 Resolver *Resolver
 Cancel <-chan struct{}
}
  • Timeout: 撥號(hào)等待連接結(jié)束的較大時(shí)間數(shù)。如果同時(shí)設(shè)置了Deadline, 可以更早失敗。默認(rèn)沒(méi)有超時(shí)。 當(dāng)使用TCP并使用多個(gè)IP地址撥號(hào)主機(jī)名,超時(shí)會(huì)在它們之間劃分。使用或不使用超時(shí),操作系統(tǒng)都可以強(qiáng)迫更早超時(shí)。例如,TCP超時(shí)一般在3分鐘左右。

  • Deadline: 是撥號(hào)即將失敗的絕對(duì)時(shí)間點(diǎn)。如果設(shè)置了Timeout, 可能會(huì)更早失敗。0值表示沒(méi)有截止期限, 或者依賴操作系統(tǒng)或使用Timeout選項(xiàng)。

  • LocalAddr: 是撥號(hào)一個(gè)地址時(shí)使用的本地地址。這個(gè)地址必須是要撥號(hào)的network地址完全兼容的類型。如果為nil, 會(huì)自動(dòng)選擇一個(gè)本地地址。

  • DualStack: 這個(gè)屬性可以啟用RFC 6555兼容的"歡樂(lè)眼球(Happy Eyeballs) "撥號(hào),當(dāng)network是tcp時(shí),address參數(shù)中的host可以被解析被IPv4和IPv6地址。這樣就允許客戶端容忍(tolerate)一個(gè)地址家族的網(wǎng)絡(luò)規(guī)定稍微打破一下。

  • FallbackDelay: 當(dāng)DualStack啟用的時(shí)候, 指定在產(chǎn)生回退連接之前需要等待的時(shí)間。如果設(shè)置為0, 默認(rèn)使用延時(shí)300ms。

  • KeepAlive: 為活動(dòng)網(wǎng)絡(luò)連接指定保持活動(dòng)的時(shí)間。如果設(shè)置為0,沒(méi)有啟用keep-alive。不支持keep-alive的網(wǎng)絡(luò)協(xié)議會(huì)忽略掉這個(gè)字段。

  • Resolver: 可選項(xiàng),指定使用的可替代resolver。

  • Cancel: 可選通道,它的閉包表示撥號(hào)應(yīng)該被取消。不是所有的撥號(hào)類型都支持撥號(hào)取消。 已廢棄,可使用DialContext代替。

有兩個(gè)可用選項(xiàng)可以微調(diào)。

因此Dialer接口提供了可以微調(diào)的兩方面選項(xiàng):

  • DeadLine和Timeout選項(xiàng): 用于不成功撥號(hào)的超時(shí)設(shè)置。

  • KeepAlive選項(xiàng): 管理連接的使用壽命(life span)。

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
}

net.Conn接口是面向流的一般的網(wǎng)絡(luò)連接。它具有下面這些接口方法:

  • Read(): 從連接上讀取數(shù)據(jù)。

  • Write(): 向連接上寫入數(shù)據(jù)。

  • Close(): 關(guān)閉連接。

  • LocalAddr(): 返回本地網(wǎng)絡(luò)地址。

  • RemoteAddr(): 返回遠(yuǎn)程網(wǎng)絡(luò)地址。

  • SetDeadline(): 設(shè)置連接相關(guān)的讀寫最后期限。等價(jià)于同時(shí)調(diào)用SetReadDeadline()和SetWriteDeadline()。

  • SetReadDeadline(): 設(shè)置將來(lái)的讀調(diào)用和當(dāng)前阻塞的讀調(diào)用的超時(shí)最后期限。

  • SetWriteDeadline(): 設(shè)置將來(lái)寫調(diào)用以及當(dāng)前阻塞的寫調(diào)用的超時(shí)最后期限。

Conn接口也有deadline設(shè)置; 有對(duì)整個(gè)連接的(SetDeadLine()),也有特定讀寫調(diào)用的(SetReadDeadLine()和SetWriteDeadLine())。

注意deadline是(wallclock)時(shí)間固定點(diǎn)。和timeout不同,它們新活動(dòng)之后不會(huì)重置。因此連接上的每個(gè)活動(dòng)必須設(shè)置新的deadline。

下面的樣本代碼沒(méi)有使用deadline, 因?yàn)樗銐蚝?jiǎn)單,我們可以很容易看到什么時(shí)候會(huì)被卡住。Ctrl-C時(shí)我們手動(dòng)觸發(fā)deadline的工具。

接收端上


接收端步驟如下:

  • 對(duì)本地端口打開(kāi)監(jiān)聽(tīng)。

  • 當(dāng)請(qǐng)求到來(lái)時(shí),產(chǎn)生(spawn)goroutine來(lái)處理請(qǐng)求。

  • 在goroutine中,讀取數(shù)據(jù)。也可以選擇性的發(fā)送響應(yīng)。

  • 關(guān)閉連接。

監(jiān)聽(tīng)需要指定本地監(jiān)聽(tīng)的端口號(hào)。一般來(lái)說(shuō),監(jiān)聽(tīng)?wèi)?yīng)用程序(也叫server)宣布監(jiān)聽(tīng)的端口號(hào),如果提供標(biāo)準(zhǔn)服務(wù), 那么使用這個(gè)服務(wù)對(duì)應(yīng)的相關(guān)端口。例如,web服務(wù)通常監(jiān)聽(tīng)80來(lái)伺服HTTP, 443端口伺服HTTPS請(qǐng)求。 SSH守護(hù)默認(rèn)監(jiān)聽(tīng)22端口, WHOIS服務(wù)使用端口43。

type Listener interface {
 // Accept waits for and returns the next connection to the listener.
 Accept() (Conn, error)

 // Close closes the listener.
 // Any blocked Accept operations will be unblocked and return errors.
 Close() error

 // Addr returns the listener's network address.
 Addr() Addr
}
func Listen(network, address string) (Listener, error) {
 addrs, err := DefaultResolver.resolveAddrList(context.Background(), "listen", network, address, nil)
 if err != nil {
 return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: nil, Err: err}
 }
 var l Listener
 switch la := addrs.first(isIPv4).(type) {
 case *TCPAddr:
 l, err = ListenTCP(network, la)
 case *UnixAddr:
 l, err = ListenUnix(network, la)
 default:
 return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: address}}
 }
 if err != nil {
 return nil, err // l is non-nil interface containing nil pointer
 }
 return l, nil
}

net包實(shí)現(xiàn)服務(wù)端的核心部分是:

net.Listen()在給定的本地網(wǎng)絡(luò)地址上來(lái)創(chuàng)建新的監(jiān)聽(tīng)器。如果只傳端口號(hào)給它,例如":61000", 那么監(jiān)聽(tīng)器會(huì)監(jiān)聽(tīng)所有可用的網(wǎng)絡(luò)接口。 這相當(dāng)方便,因?yàn)橛?jì)算機(jī)通常至少提供兩個(gè)活動(dòng)接口,回環(huán)接口和最少一個(gè)真實(shí)網(wǎng)卡。 這個(gè)函數(shù)成功的話返回Listener。

Listener接口有一個(gè)Accept()方法用來(lái)等待請(qǐng)求進(jìn)來(lái)。然后它接受請(qǐng)求,并給調(diào)用者返回新的連接。Accept()一般來(lái)說(shuō)都是在循環(huán)中調(diào)用,能夠同時(shí)服務(wù)多個(gè)連接。每個(gè)連接可以由一個(gè)單獨(dú)的goroutine處理,正如下面代碼所示的。

代碼部分


與其讓代碼來(lái)回推送一些字節(jié),我更想要它演示一些更有用的東西。 我想讓它能給服務(wù)器發(fā)送帶有不同數(shù)據(jù)載體的不同命令。服務(wù)器應(yīng)該能標(biāo)識(shí)每個(gè)命令和解碼命令數(shù)據(jù)。

我們代碼中客戶端會(huì)發(fā)送兩種類型的命令: "STRING"和"GOB"。它們都以換行符終止。

"STRING"命令包含一行字符串?dāng)?shù)據(jù),可以通過(guò)bufio中的簡(jiǎn)單讀寫操作來(lái)處理。

"GOB"命令由結(jié)構(gòu)體組成,這個(gè)結(jié)構(gòu)體包含一些字段,包含一個(gè)分片和映射,甚至指向自己的指針。 正如你所見(jiàn),當(dāng)運(yùn)行這個(gè)代碼時(shí),gob包能通過(guò)我們的網(wǎng)絡(luò)連接移動(dòng)這些數(shù)據(jù)沒(méi)有什么稀奇(fuss).

我們這里基本上都是一些即席協(xié)議(ad-hoc protocol: 特設(shè)的、特定目的的、即席的、專案的), 客戶端和服務(wù)端都遵循它,命令行后面是換行,然后是數(shù)據(jù)。對(duì)于每個(gè)命令來(lái)說(shuō),服務(wù)端必須知道數(shù)據(jù)的確切格式,知道如何處理它。

要達(dá)到這個(gè)目的,服務(wù)端代碼采取兩步方式實(shí)現(xiàn)。

  • 第一步: 當(dāng)Listen()函數(shù)接收到新連接,它會(huì)產(chǎn)生一個(gè)新的goroutine來(lái)調(diào)用handleMessage()。 這個(gè)函數(shù)從連接中讀取命令名, 從映射中查詢合適的處理器函數(shù),然后調(diào)用它。

  • 第二步: 選擇的處理器函數(shù)讀取并處理命令行的數(shù)據(jù)。

package main

import (
 "bufio"
 "encoding/gob"
 "flag"
 "github.com/pkg/errors"
 "io"
 "log"
 "net"
 "strconv"
 "strings"
 "sync"
)

type complexData struct {
 N int
 S string
 M map[string]int
 P []byte
 C *complexData
}

const (
 Port = ":61000"
)

Outcoing connections(發(fā)射連接)


使用發(fā)射連接是一種快照。net.Conn滿足io.Reader和io.Writer接口,因此我們可以將TCP連接和其他任何的Reader和Writer一樣看待。

func Open(addr string) (*bufio.ReadWriter, error) {
 log.Println("Dial " + addr)
 conn, err := net.Dial("tcp", addr)

 if err != nil {
  return nil, errors.Wrap(err, "Dialing " + addr + " failed")
 }

 return bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), nil
}

打開(kāi)TCP地址的連接。它返回一個(gè)帶有超時(shí)的TCP連接,并將其包裝進(jìn)緩沖的ReadWriter。撥號(hào)遠(yuǎn)程進(jìn)程。注意本地端口是實(shí)時(shí)(on the fly)分配的。如果必須指定本地端口號(hào),請(qǐng)使用DialTCP()方法。

進(jìn)入連接


這節(jié)有點(diǎn)涉及到對(duì)進(jìn)入數(shù)據(jù)的準(zhǔn)備環(huán)節(jié)處理。根據(jù)我們前面介紹的ad-hoc協(xié)議,命令名+換行符+數(shù)據(jù)+換行符。自然數(shù)據(jù)是和具體命令相關(guān)的。要處理這樣的情況,我們創(chuàng)建了一個(gè)Endpoint對(duì)象,它具有下面的屬性:

  • 它允許注冊(cè)一個(gè)或多個(gè)處理器函數(shù),每個(gè)函數(shù)可以處理一個(gè)特殊的命令。

  • 它根據(jù)命令名將具體命令調(diào)度到相關(guān)的處理器函數(shù)。

首先我們聲明一個(gè)HandleFunc類型,該類型為接收一個(gè)bufio.ReadWriter指針值的函數(shù)類型, 也就是后面我們要為每種不同命令注冊(cè)的處理器函數(shù)。它接收的參數(shù)是使用ReadWriter接口包裝的net.Conn連接。

type HandleFunc func(*bufio.ReadWriter)

然后我們聲明一個(gè)Endpoint結(jié)構(gòu)體類型,它有三個(gè)屬性:

  • listener: net.Listen()返回的Listener對(duì)象。

  • handler: 用于保存已注冊(cè)的處理器函數(shù)的映射。

  • m: 一個(gè)互斥鎖,用于解決map的多goroutine不安全的問(wèn)題。

type Endpoint struct {
 listener net.Listener
 handler map[string]HandleFunc
 m sync.RWMutex  // Maps不是線程安全的,因此需要互斥鎖來(lái)控制訪問(wèn)。
}

func NewEndpoint() *Endpoint {
 return &Endpoint{
  handler: map[string]HandleFunc{},
 }
}

func (e *Endpoint) AddHandleFunc(name string, f HandleFunc) {
 e.m.Lock()
 e.handler[name] = f
 e.m.Unlock()
}

func (e *Endpoint) Listen() error {
 var err error
 e.listener, err = net.Listen("tcp", Port)
 if err != nil {
  return errors.Wrap(err, "Unable to listen on "+e.listener.Addr().String()+"\n")
 }
 log.Println("Listen on", e.listener.Addr().String())
 for {
  log.Println("Accept a connection request.")
  conn, err := e.listener.Accept()
  if err != nil {
   log.Println("Failed accepting a connection request:", err)
   continue
  }
  log.Println("Handle incoming messages.")
  go e.handleMessages(conn)
 }
}

// handleMessages讀取連接到第一個(gè)換行符。 基于這個(gè)字符串,它會(huì)調(diào)用恰當(dāng)?shù)腍andleFunc。

func (e *Endpoint) handleMessages(conn net.Conn) {
 // 將連接包裝到緩沖reader以便于讀取
 rw := bufio.NewReadWrite(bufio.NewReader(conn), bufio.NewWriter(conn))
 defer conn.Close()

 // 從連接讀取直到遇到EOF. 期望下一次輸入是命令名。調(diào)用注冊(cè)的用于該命令的處理器。

 for {
  log.Print("Receive command '")
  cmd, err := rw.ReadString('\n')
  switch {
  case err == io.EOF:
   log.Println("Reached EOF - close this connection.\n ---")
   return
  case err != nil:
   log.Println("\nError reading command. Got: '" + cmd + "'\n", err)
  }

  // 修剪請(qǐng)求字符串中的多余回車和空格- ReadString不會(huì)去掉任何換行。

  cmd = strings.Trim(cmd, "\n ")
  log.Println(cmd + "'")

  // 從handler映射中獲取恰當(dāng)?shù)奶幚砥骱瘮?shù), 并調(diào)用它。

  e.m.Lock()
  handleCommand, ok := e.handler[cmd]
  e.m.Unlock()

  if !ok {
   log.Println("Command '" + cmd + "' is not registered.")
   return
  }

  handleCommand(rw)
 }
}

NewEndpoint()函數(shù)是Endpoint的工廠函數(shù)。它只對(duì)handler映射進(jìn)行了初始化。為了簡(jiǎn)化問(wèn)題,假設(shè)我們的終端監(jiān)聽(tīng)的端口好是固定的。

Endpoint類型聲明了幾個(gè)方法:

  • AddHandleFunc(): 使用互斥鎖為handler屬性安全添加處理特定類型命令的處理器函數(shù)。

  • Listen(): 對(duì)終端端口的所有接口啟動(dòng)監(jiān)聽(tīng)。 在調(diào)用Listen之前,至少要通過(guò)AddHandleFunc()注冊(cè)一個(gè)handler函數(shù)。

  • HandleMessages(): 將連接用bufio包裝起來(lái),然后分兩步讀取,首先讀取命令加換行,我們得到命令名字。 然后通過(guò)handler獲取注冊(cè)的該命令對(duì)應(yīng)的處理器函數(shù), 然后調(diào)度這個(gè)函數(shù)來(lái)執(zhí)行數(shù)據(jù)讀取和解析。

.注意:上面如何使用動(dòng)態(tài)函數(shù)的。 根據(jù)命令名查找具體函數(shù),然后這個(gè)具體函數(shù)賦值給handleCommand, 其實(shí)這個(gè)變量類型為HandleFunc類型, 即前面聲明的處理器函數(shù)類型。


Endpoint的Listen方法調(diào)用之前需要先至少注冊(cè)一個(gè)處理器函數(shù)。因此我們下面定義兩個(gè)類型的處理器函數(shù): handleStrings和handleGob。

handleStrings()函數(shù)接收和處理我們即時(shí)協(xié)議中只發(fā)送字符串?dāng)?shù)據(jù)的處理器函數(shù)。handleGob()函數(shù)是接收并處理發(fā)送的gob數(shù)據(jù)的復(fù)雜結(jié)構(gòu)體。handleGob稍微復(fù)雜一點(diǎn),除了讀取數(shù)據(jù)外,我們海需要解碼數(shù)據(jù)。

我們可以看到連續(xù)兩次使用rw.ReadString('n'), 讀取字符串,遇到換行停止, 將讀到的內(nèi)容保存到字符串中。注意這個(gè)字符串是包含末尾換行的。

另外對(duì)于普通字符串?dāng)?shù)據(jù)來(lái)說(shuō),我們直接用bufio包裝連接后的ReadString來(lái)讀取。而對(duì)于復(fù)雜的gob結(jié)構(gòu)體來(lái)說(shuō),我們使用gob來(lái)解碼數(shù)據(jù)。

func handleStrings(rw *bufio.ReadWriter) {
 log.Print("Receive STRING message:")
 s, err := rw.ReadString('\n')
 if err != nil {
  log.Println("Cannot read from connection.\n", err)
 }

 s = strings.Trim(s, "\n ")
 log.Println(s)

 -, err = rw.WriteString("Thank you.\n")
 if err != nil {
  log.Println("Cannot write to connection.\n", err)
 }

 err = rw.Flush()
 if err != nil {
  log.Println("Flush failed.", err)
 }
}

func handleGob(rw *bufio.ReadWriter) {
 log.Print("Receive GOB data:")
 var data complexData
  
 dec := gob.NewDecoder(rw)
 err := dec.Decode(&data)

 if err != nil {
  log.Println("Error decoding GOB data:", err)
  return
 }

 log.Printf("Outer complexData struct: \n%#v\n", data)
 log.Printf("Inner complexData struct: \n%#v\n", data.C)
}

客戶端和服務(wù)端函數(shù)


一切就緒,我們可以準(zhǔn)備我們的客戶端和服務(wù)端函數(shù)了。

客戶端函數(shù)連接到服務(wù)器并發(fā)送STRING和GOB請(qǐng)求。


服務(wù)端開(kāi)始監(jiān)聽(tīng)請(qǐng)求并觸發(fā)恰當(dāng)?shù)奶幚砥鳌?/p>

// 當(dāng)應(yīng)用程序使用-connect=ip地址的時(shí)候被調(diào)用

func client(ip string) error {
 testStruct := complexData{
  N: 23,
  S: "string data",
  M: map[string]int{"one": 1, "two": 2, "three": 3},
  P: []byte("abc"),
  C: &complexData{
   N: 256,
   S: "Recursive structs? Piece of cake!",
   M: Map[string]int{"01": "10": 2, "11": 3},
  },
 }

 rw, err := Open(ip + Port)
 if err != nil {
  return errors.Wrap(err, "Client: Failed to open connection to " + ip + Port)
 }

 log.Println("Send the string request.")

 n, err := rw.WriteString("STRING\n")
 if err != nil {
  return errors.Wrap(err, "Could not send the STRING request (" + strconv.Itoa(n) + " bytes written)")
 }

 // 發(fā)送STRING請(qǐng)求。發(fā)送請(qǐng)求名并發(fā)送數(shù)據(jù)。

 log.Println("Send the string request.")

 n, err = rw.WriteString("Additional data.\n")
 if err != nil {
  return errors.Wrap(err, "Could not send additional STRING data (" + strconv.Itoa(n) + " bytes written)")
 }

 log.Println("Flush the buffer.")
 err = rw.Flush()
 if err != nil {
  return errors.Wrap(err, "Flush failed.")
 }

 // 讀取響應(yīng)

 log.Println("Read the reply.")

 response, err := rw.ReadString('\n')
 if err != nil {
  return errors.Wrap(err, "Client: Failed to read the reply: '" + response + "'")
 }

 log.Println("STRING request: got a response:", response)
 
 // 發(fā)送GOB請(qǐng)求。 創(chuàng)建一個(gè)encoder直接將它轉(zhuǎn)換為rw.Send的請(qǐng)求名。發(fā)送GOB

 log.Println("Send a struct as GOB:")
 log.Printf("Outer complexData struct: \n%#v\n", testStruct)
 log.Printf("Inner complexData struct: \n%#v\n", testStruct.C)
 enc := gob.NewDecoder(rw)
 n, err = rw.WriteString("GOB\n")
 if err != nil {
  return errors.Wrap(err, "Could not write GOB data (" + strconv.Itoa(n) + " bytes written)")
 }

 err = enc.Encode(testStruct)
 if err != nil {
  return errors.Wrap(err, "Encode failed for struct: %#v", testStruct)
 }

 err = rw.Flush()
 if err != nil {
  return errors.Wrap(err, "Flush failed.")
 }
 return nil
}

客戶端函數(shù)在執(zhí)行應(yīng)用程序時(shí)指定connect標(biāo)志的時(shí)候執(zhí)行,這點(diǎn)后面的代碼可以看到。

下面是服務(wù)端程序server。服務(wù)端監(jiān)聽(tīng)進(jìn)來(lái)的請(qǐng)求并根據(jù)請(qǐng)求命令名將它們調(diào)度給注冊(cè)的具體相關(guān)處理器。

func server() error {
 endpoint := NewEndpoint()
 // 添加處理器函數(shù)
 endpoint.AddHandleFunc("STRING", handleStrings)
 endpoint.AddHandleFunc("GOB", handleGOB)
 // 開(kāi)始監(jiān)聽(tīng)
 return endpoint.Listen()
}

main函數(shù)


下面的main函數(shù)既可以啟動(dòng)客戶端也可以啟動(dòng)服務(wù)端, 依賴于是否設(shè)置connect標(biāo)志。 如果沒(méi)有這個(gè)標(biāo)志,則以服務(wù)器啟動(dòng)進(jìn)程, 監(jiān)聽(tīng)進(jìn)來(lái)的請(qǐng)求。如果有標(biāo)志, 啟動(dòng)為客戶端,并連接到這個(gè)標(biāo)志指定的主機(jī)。

可以使用localhost或127.0.0.1在同一機(jī)器上運(yùn)行這兩個(gè)進(jìn)程。

func main() {
 connect := flag.String("connect", "", "IP address of process to join. If empty, go into the listen mode.")
 flag.Parse()
 // 如果設(shè)置了connect標(biāo)志,進(jìn)入客戶端模式
 if *connect != '' {
  err := client(*connect)
  if err != nil {
   log.Println("Error:", errors.WithStack(err))
  }
  log.Println("Client done.")
  return
 }
 // 否則進(jìn)入服務(wù)端模式
 err := server()
 if err != nil {
  log.Println("Error:", errors.WithStack(err))
 }
 log.Println("Server done.")
}

// 設(shè)置日志記錄的字段標(biāo)志
func init() {
 log.SetFlags(log.Lshortfile)
}

如何獲取并運(yùn)行代碼


第一步: 獲取代碼。 注意-d標(biāo)志自動(dòng)安裝二進(jìn)制到$GOPATH/bin目錄。

go get -d github.com/appliedgo/networking

第二步: cd到源代碼目錄。


cd $GOPATH/src/github.com/appliedgo/networking

第三步: 運(yùn)行服務(wù)端。


go run networking.go

第四步: 打開(kāi)另外一個(gè)shell, 同樣進(jìn)入到源碼目錄(第二步), 然后運(yùn)行客戶端。


go run networking.go -connect localhost

Tips


如果你想稍微修改下源代碼,下面是一些建議:

  • 在不同機(jī)器運(yùn)行客戶端和服務(wù)端(同一個(gè)局域網(wǎng)中).

  • 用更多的映射和指針來(lái)增強(qiáng)(beef up)complexData, 看看gob如何應(yīng)對(duì)它(cope with it)。

  • 同時(shí)啟動(dòng)多個(gè)客戶端,看看服務(wù)端是否能處理它們。

2017-02-09: map不是線程安全的,因此如果在不同的goroutine中使用同一個(gè)map, 應(yīng)該使用互斥鎖來(lái)控制map的訪問(wèn)。
而上面的代碼,map在goroutine啟動(dòng)之前已經(jīng)添加好了, 因此你可以安全的修改代碼,在handleMessages goroutine已經(jīng)運(yùn)行的時(shí)候調(diào)用AddHandleFunc()。


到此,關(guān)于“Go語(yǔ)言中TCP/IP網(wǎng)絡(luò)編程的方法”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

網(wǎng)站題目:Go語(yǔ)言中TCP/IP網(wǎng)絡(luò)編程的方法-創(chuàng)新互聯(lián)
分享URL:http://www.muchs.cn/article0/pidoo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站維護(hù)定制開(kāi)發(fā)品牌網(wǎng)站設(shè)計(jì)App設(shè)計(jì)、定制網(wǎng)站電子商務(wù)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

網(wǎng)站托管運(yùn)營(yíng)