您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)Go語言中TCP/IP網(wǎng)絡(luò)編程的示例分析,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
TCP/IP層發(fā)送數(shù)據(jù)的應(yīng)用場景
當(dāng)然很多情況下,不是大多數(shù)情況下,使用更高級別的網(wǎng)絡(luò)協(xié)議毫無疑問會更好,因為可以使用華麗的API, 它們隱藏了很多技術(shù)細(xì)節(jié)?,F(xiàn)在根據(jù)不同的需求,有很多選擇,比如消息隊列協(xié)議, gRPC, protobuf, FlatBuffers, RESTful網(wǎng)站API, websocket等等。
然而在一些特殊的場景下,特別是小型項目,選擇任何其他方式都會感覺太臃腫了,更不用說你需要引入額外的依賴包了。
幸運的是,使用標(biāo)準(zhǔn)庫的net包來創(chuàng)建簡單的網(wǎng)絡(luò)通信不比你所見到的要困難。
因為Go語言中有下面兩點簡化。
簡化1: 連接就是io流
net.Conn接口實現(xiàn)了io.Reader, io.Writer和io.Closer接口。 因此可以像對待其他io流一樣對待TCP連接。
你可能會認(rèn)為:"好,我能在TCP中發(fā)送字符串或字節(jié)分片,非常不錯,但是遇到復(fù)雜的數(shù)據(jù)結(jié)構(gòu)怎么辦? 例如我們遇到的是結(jié)構(gòu)體類型的數(shù)據(jù)?"
簡化2: Go語言知道如何有效的解碼復(fù)雜的類型
當(dāng)說到通過網(wǎng)絡(luò)發(fā)送編碼的結(jié)構(gòu)化數(shù)據(jù),首先想到的就是JSON。 不過先稍等一下 - Go語言的標(biāo)準(zhǔn)庫encoding/gob包提供了一種序列化和發(fā)序列話Go數(shù)據(jù)類型的方法,它無需給結(jié)構(gòu)體、Go語言不兼容的JSON添加字符串標(biāo)簽, 或者等待使用json.Unmarshal來費勁的將文本解析為二進(jìn)制數(shù)據(jù)。
gob編碼解碼可以直接操作io流,這一點很完美的匹配第一條簡化。
下面我們就通過這兩條簡化規(guī)則一起實現(xiàn)一個簡單的App。
這個簡單APP的目標(biāo)
這個app應(yīng)該做兩件事情:
發(fā)送和接收簡單的字符串消息。
通過gob發(fā)送和接收結(jié)構(gòu)體。
第一部分,發(fā)送簡單字符串,將演示無需借助高級協(xié)議的情況下,通過TCP/IP網(wǎng)絡(luò)發(fā)送數(shù)據(jù)是多么簡單。
第二部分,稍微深入一點,通過網(wǎng)絡(luò)發(fā)送完整的結(jié)構(gòu)體,這些結(jié)構(gòu)體使用字符串、分片、映射、甚至包含到自身的遞歸指針。
辛虧有g(shù)ob包,要做到這些不費吹灰之力。
客戶端 服務(wù)端
待發(fā)送結(jié)構(gòu)體 解碼后結(jié)構(gòu)體
testStruct結(jié)構(gòu)體 testStruct結(jié)構(gòu)體
| ^
V |
gob編碼 ----------------------------> gob解碼
| ^
V |
發(fā)送 ============網(wǎng)絡(luò)================= 接收
通過TCP發(fā)送字符串?dāng)?shù)據(jù)的基本要素
發(fā)送端上
發(fā)送字符串需要三個簡單的步驟:
打開對應(yīng)接收進(jìn)程的連接。
寫字符串。
關(guān)閉連接。
net包提供了一對實現(xiàn)這個功能的方法。
ResolveTCPAddr(): 該函數(shù)返回TCP終端地址。
DialTCP(): 類似于TCP網(wǎng)絡(luò)的撥號。
這兩個方法都是在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()接收兩個字符串參數(shù)。
network: 必須是TCP網(wǎng)絡(luò)名,比如tcp, tcp4, tcp6。
address: TCP地址字符串,如果它不是字面量的IP地址或者端口號不是字面量的端口號, ResolveTCPAddr會將傳入的地址解決成TCP終端的地址。否則傳入一對字面量IP地址和端口數(shù)字作為地址。address參數(shù)可以使用host名稱,但是不推薦這樣做,因為它最多會返回host名字的一個IP地址。
ResolveTCPAddr()接收的代表TCP地址的字符串(例如localhost:80, 127.0.0.1:80, 或[::1]:80, 都是代表本機的80端口), 返回(net.TCPAddr指針, nil)(如果字符串不能被解析成有效的TCP地址會返回(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ù)接收三個參數(shù):
network: 這個參數(shù)和ResolveTCPAddr的network參數(shù)一樣,必須是TCP網(wǎng)絡(luò)名。
laddr: TCPAddr類型的指針, 代表本地TCP地址。
raddr: TCPAddr類型的指針,代表的是遠(yuǎn)程TCP地址。
它會連接撥號兩個TCP地址,并返回這個連接作為net.TCPConn對象返回(連接失敗返回error)。如果我們不需要對Dial設(shè)置有過多控制,那么我們就可以使用Dial()代替。
func Dial(network, address string) (Conn, error) { var d Dialer return d.Dial(network, address) }
Dial()函數(shù)接收一個TCP地址,返回一個一般的net.Conn。 這已經(jīng)足夠我們的測試用例了。然而如果你需要只有在TCP連接上的可用功能,可以使用TCP變體(DialTCP, TCPConn, TCPAddr等等)。
成功撥號之后,我們就可以如上所述的那樣,將新的連接與其他的輸入輸出流同等對待了。我們甚至可以將連接包裝進(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對象包裝到bufio.ReadWriter中 return bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), nil }
記住緩沖Writer在寫之后需要調(diào)用Flush()方法, 這樣所有的數(shù)據(jù)才會刷到底層網(wǎng)絡(luò)連接中。
最后,每個連接對象都有一個Close()方法來終止通信。
微調(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: 撥號等待連接結(jié)束的最大時間數(shù)。如果同時設(shè)置了Deadline, 可以更早失敗。默認(rèn)沒有超時。 當(dāng)使用TCP并使用多個IP地址撥號主機名,超時會在它們之間劃分。使用或不使用超時,操作系統(tǒng)都可以強迫更早超時。例如,TCP超時一般在3分鐘左右。
Deadline: 是撥號即將失敗的絕對時間點。如果設(shè)置了Timeout, 可能會更早失敗。0值表示沒有截止期限, 或者依賴操作系統(tǒng)或使用Timeout選項。
LocalAddr: 是撥號一個地址時使用的本地地址。這個地址必須是要撥號的network地址完全兼容的類型。如果為nil, 會自動選擇一個本地地址。
DualStack: 這個屬性可以啟用RFC 6555兼容的"歡樂眼球(Happy Eyeballs) "撥號,當(dāng)network是tcp時,address參數(shù)中的host可以被解析被IPv4和IPv6地址。這樣就允許客戶端容忍(tolerate)一個地址家族的網(wǎng)絡(luò)規(guī)定稍微打破一下。
FallbackDelay: 當(dāng)DualStack啟用的時候, 指定在產(chǎn)生回退連接之前需要等待的時間。如果設(shè)置為0, 默認(rèn)使用延時300ms。
KeepAlive: 為活動網(wǎng)絡(luò)連接指定保持活動的時間。如果設(shè)置為0,沒有啟用keep-alive。不支持keep-alive的網(wǎng)絡(luò)協(xié)議會忽略掉這個字段。
Resolver: 可選項,指定使用的可替代resolver。
Cancel: 可選通道,它的閉包表示撥號應(yīng)該被取消。不是所有的撥號類型都支持撥號取消。 已廢棄,可使用DialContext代替。
有兩個可用選項可以微調(diào)。
因此Dialer接口提供了可以微調(diào)的兩方面選項:
DeadLine和Timeout選項: 用于不成功撥號的超時設(shè)置。
KeepAlive選項: 管理連接的使用壽命(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)的讀寫最后期限。等價于同時調(diào)用SetReadDeadline()和SetWriteDeadline()。
SetReadDeadline(): 設(shè)置將來的讀調(diào)用和當(dāng)前阻塞的讀調(diào)用的超時最后期限。
SetWriteDeadline(): 設(shè)置將來寫調(diào)用以及當(dāng)前阻塞的寫調(diào)用的超時最后期限。
Conn接口也有deadline設(shè)置; 有對整個連接的(SetDeadLine()),也有特定讀寫調(diào)用的(SetReadDeadLine()和SetWriteDeadLine())。
注意deadline是(wallclock)時間固定點。和timeout不同,它們新活動之后不會重置。因此連接上的每個活動必須設(shè)置新的deadline。
下面的樣本代碼沒有使用deadline, 因為它足夠簡單,我們可以很容易看到什么時候會被卡住。Ctrl-C時我們手動觸發(fā)deadline的工具。
接收端上
接收端步驟如下:
對本地端口打開監(jiān)聽。
當(dāng)請求到來時,產(chǎn)生(spawn)goroutine來處理請求。
在goroutine中,讀取數(shù)據(jù)。也可以選擇性的發(fā)送響應(yīng)。
關(guān)閉連接。
監(jiān)聽需要指定本地監(jiān)聽的端口號。一般來說,監(jiān)聽?wèi)?yīng)用程序(也叫server)宣布監(jiān)聽的端口號,如果提供標(biāo)準(zhǔn)服務(wù), 那么使用這個服務(wù)對應(yīng)的相關(guān)端口。例如,web服務(wù)通常監(jiān)聽80來伺服HTTP, 443端口伺服HTTPS請求。 SSH守護(hù)默認(rèn)監(jiān)聽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包實現(xiàn)服務(wù)端的核心部分是:
net.Listen()在給定的本地網(wǎng)絡(luò)地址上來創(chuàng)建新的監(jiān)聽器。如果只傳端口號給它,例如":61000", 那么監(jiān)聽器會監(jiān)聽所有可用的網(wǎng)絡(luò)接口。 這相當(dāng)方便,因為計算機通常至少提供兩個活動接口,回環(huán)接口和最少一個真實網(wǎng)卡。 這個函數(shù)成功的話返回Listener。
Listener接口有一個Accept()方法用來等待請求進(jìn)來。然后它接受請求,并給調(diào)用者返回新的連接。Accept()一般來說都是在循環(huán)中調(diào)用,能夠同時服務(wù)多個連接。每個連接可以由一個單獨的goroutine處理,正如下面代碼所示的。
代碼部分
與其讓代碼來回推送一些字節(jié),我更想要它演示一些更有用的東西。 我想讓它能給服務(wù)器發(fā)送帶有不同數(shù)據(jù)載體的不同命令。服務(wù)器應(yīng)該能標(biāo)識每個命令和解碼命令數(shù)據(jù)。
我們代碼中客戶端會發(fā)送兩種類型的命令: "STRING"和"GOB"。它們都以換行符終止。
"STRING"命令包含一行字符串?dāng)?shù)據(jù),可以通過bufio中的簡單讀寫操作來處理。
"GOB"命令由結(jié)構(gòu)體組成,這個結(jié)構(gòu)體包含一些字段,包含一個分片和映射,甚至指向自己的指針。 正如你所見,當(dāng)運行這個代碼時,gob包能通過我們的網(wǎng)絡(luò)連接移動這些數(shù)據(jù)沒有什么稀奇(fuss).
我們這里基本上都是一些即席協(xié)議(ad-hoc protocol: 特設(shè)的、特定目的的、即席的、專案的), 客戶端和服務(wù)端都遵循它,命令行后面是換行,然后是數(shù)據(jù)。對于每個命令來說,服務(wù)端必須知道數(shù)據(jù)的確切格式,知道如何處理它。
要達(dá)到這個目的,服務(wù)端代碼采取兩步方式實現(xiàn)。
第一步: 當(dāng)Listen()函數(shù)接收到新連接,它會產(chǎn)生一個新的goroutine來調(diào)用handleMessage()。 這個函數(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 }
打開TCP地址的連接。它返回一個帶有超時的TCP連接,并將其包裝進(jìn)緩沖的ReadWriter。撥號遠(yuǎn)程進(jìn)程。注意本地端口是實時(on the fly)分配的。如果必須指定本地端口號,請使用DialTCP()方法。
進(jìn)入連接
這節(jié)有點涉及到對進(jìn)入數(shù)據(jù)的準(zhǔn)備環(huán)節(jié)處理。根據(jù)我們前面介紹的ad-hoc協(xié)議,命令名+換行符+數(shù)據(jù)+換行符。自然數(shù)據(jù)是和具體命令相關(guān)的。要處理這樣的情況,我們創(chuàng)建了一個Endpoint對象,它具有下面的屬性:
它允許注冊一個或多個處理器函數(shù),每個函數(shù)可以處理一個特殊的命令。
它根據(jù)命令名將具體命令調(diào)度到相關(guān)的處理器函數(shù)。
首先我們聲明一個HandleFunc類型,該類型為接收一個bufio.ReadWriter指針值的函數(shù)類型, 也就是后面我們要為每種不同命令注冊的處理器函數(shù)。它接收的參數(shù)是使用ReadWriter接口包裝的net.Conn連接。
type HandleFunc func(*bufio.ReadWriter)
然后我們聲明一個Endpoint結(jié)構(gòu)體類型,它有三個屬性:
listener: net.Listen()返回的Listener對象。
handler: 用于保存已注冊的處理器函數(shù)的映射。
m: 一個互斥鎖,用于解決map的多goroutine不安全的問題。
type Endpoint struct { listener net.Listener handler map[string]HandleFunc m sync.RWMutex // Maps不是線程安全的,因此需要互斥鎖來控制訪問。 } 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讀取連接到第一個換行符。 基于這個字符串,它會調(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)用注冊的用于該命令的處理器。 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) } // 修剪請求字符串中的多余回車和空格- ReadString不會去掉任何換行。 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ù)。它只對handler映射進(jìn)行了初始化。為了簡化問題,假設(shè)我們的終端監(jiān)聽的端口好是固定的。
Endpoint類型聲明了幾個方法:
AddHandleFunc(): 使用互斥鎖為handler屬性安全添加處理特定類型命令的處理器函數(shù)。
Listen(): 對終端端口的所有接口啟動監(jiān)聽。 在調(diào)用Listen之前,至少要通過AddHandleFunc()注冊一個handler函數(shù)。
HandleMessages(): 將連接用bufio包裝起來,然后分兩步讀取,首先讀取命令加換行,我們得到命令名字。 然后通過handler獲取注冊的該命令對應(yīng)的處理器函數(shù), 然后調(diào)度這個函數(shù)來執(zhí)行數(shù)據(jù)讀取和解析。
.注意:上面如何使用動態(tài)函數(shù)的。 根據(jù)命令名查找具體函數(shù),然后這個具體函數(shù)賦值給handleCommand, 其實這個變量類型為HandleFunc類型, 即前面聲明的處理器函數(shù)類型。
Endpoint的Listen方法調(diào)用之前需要先至少注冊一個處理器函數(shù)。因此我們下面定義兩個類型的處理器函數(shù): handleStrings和handleGob。
handleStrings()函數(shù)接收和處理我們即時協(xié)議中只發(fā)送字符串?dāng)?shù)據(jù)的處理器函數(shù)。handleGob()函數(shù)是接收并處理發(fā)送的gob數(shù)據(jù)的復(fù)雜結(jié)構(gòu)體。handleGob稍微復(fù)雜一點,除了讀取數(shù)據(jù)外,我們海需要解碼數(shù)據(jù)。
我們可以看到連續(xù)兩次使用rw.ReadString('n'), 讀取字符串,遇到換行停止, 將讀到的內(nèi)容保存到字符串中。注意這個字符串是包含末尾換行的。
另外對于普通字符串?dāng)?shù)據(jù)來說,我們直接用bufio包裝連接后的ReadString來讀取。而對于復(fù)雜的gob結(jié)構(gòu)體來說,我們使用gob來解碼數(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請求。
服務(wù)端開始監(jiān)聽請求并觸發(fā)恰當(dāng)?shù)奶幚砥鳌?br/>
// 當(dāng)應(yīng)用程序使用-connect=ip地址的時候被調(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請求。發(fā)送請求名并發(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請求。 創(chuàng)建一個encoder直接將它轉(zhuǎn)換為rw.Send的請求名。發(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)用程序時指定connect標(biāo)志的時候執(zhí)行,這點后面的代碼可以看到。
下面是服務(wù)端程序server。服務(wù)端監(jiān)聽進(jìn)來的請求并根據(jù)請求命令名將它們調(diào)度給注冊的具體相關(guān)處理器。
func server() error { endpoint := NewEndpoint() // 添加處理器函數(shù) endpoint.AddHandleFunc("STRING", handleStrings) endpoint.AddHandleFunc("GOB", handleGOB) // 開始監(jiān)聽 return endpoint.Listen() }
main函數(shù)
下面的main函數(shù)既可以啟動客戶端也可以啟動服務(wù)端, 依賴于是否設(shè)置connect標(biāo)志。 如果沒有這個標(biāo)志,則以服務(wù)器啟動進(jìn)程, 監(jiān)聽進(jìn)來的請求。如果有標(biāo)志, 啟動為客戶端,并連接到這個標(biāo)志指定的主機。
可以使用localhost或127.0.0.1在同一機器上運行這兩個進(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) }
如何獲取并運行代碼
第一步: 獲取代碼。 注意-d標(biāo)志自動安裝二進(jìn)制到$GOPATH/bin目錄。
go get -d github.com/appliedgo/networking
第二步: cd到源代碼目錄。
cd $GOPATH/src/github.com/appliedgo/networking
第三步: 運行服務(wù)端。
go run networking.go
第四步: 打開另外一個shell, 同樣進(jìn)入到源碼目錄(第二步), 然后運行客戶端。
go run networking.go -connect localhost
Tips
如果你想稍微修改下源代碼,下面是一些建議:
在不同機器運行客戶端和服務(wù)端(同一個局域網(wǎng)中).
用更多的映射和指針來增強(beef up)complexData, 看看gob如何應(yīng)對它(cope with it)。
同時啟動多個客戶端,看看服務(wù)端是否能處理它們。
2017-02-09: map不是線程安全的,因此如果在不同的goroutine中使用同一個map, 應(yīng)該使用互斥鎖來控制map的訪問。
而上面的代碼,map在goroutine啟動之前已經(jīng)添加好了, 因此你可以安全的修改代碼,在handleMessages goroutine已經(jīng)運行的時候調(diào)用AddHandleFunc()。
關(guān)于“Go語言中TCP/IP網(wǎng)絡(luò)編程的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學(xué)到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。