這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
這篇文章主要使用Go語言實現一個簡單的TCP伺服器和用戶端。
伺服器和用戶端之間的協議是 ECHO, 這個RFC 862定義的一個簡單協議。
為什麼說這個協議很簡單呢, 這是因為伺服器只需把收到的用戶端的請求資料發給這個用戶端即可,其它什麼功能都不做。
首先聲明, 我絕對是一個Golang的初學者,十四、五年的編程時間我主要使用Java來做開發,這篇文章主要記錄我學習go網路編程的體驗。如果你認為這篇文章有錯誤或者不好的寫法,請在回複中添加你的意見。
簡單介紹
儘管OSI(開放系統互聯)協議從未被完整地實現過,但它仍對分布式系統的討論和設計產生了十分重要的影響。它的結構大致為所示:
當OSI標準模型正在為實現細節鬧得不可開交時,DARPA互連網技術項目卻在忙著構建TCP/IP協議。它們取得了極大的成功,並引領了Internet(首字母大寫),因為這是個更簡單的階層:
儘管現在到處都是TCP/IP協議,但它並不是唯一存在的。還有些協議佔有重要的地位,比如:
- Firewire
- USB
- Bluetooth
- WiFi
多年的發展,使得IP和TCP/UDP協議基本上就等價於網路通訊協定棧。例如, 藍芽定義了物理層和協議層,但在其上任然是IP協議棧,可以在許多藍牙裝置之間使用互連網編程技術。同樣, 開發4G無線手機技術,如LTE(Long Term Evolution)也將使用IP協議棧。
在OIS或TCP/IP協議棧層與層之間的通訊,是通過將資料包從一個層發送到下一個層,最終穿過整個網路的。每一層都有必須保持其自身層的管理資訊。從上層接收到的資料包在向下傳遞時,會添加頭資訊。在接收端,這些頭資訊會在向上傳遞時移除。
例如,TFTP(普通檔案傳輸通訊協定)將檔案從一台電腦移動到另一台上。它使用IP協議上的UDP協議,該協議可通過乙太網路發送。看起來就像這樣
最終在乙太網路上傳輸的資料,就是圖中最底層的那個資料。
為了兩個電腦進行通訊,就必須建立一個路徑,使他們能夠在一個會話中發送至少一條訊息。有兩個主要的模型:
- 連線導向模型, 如TCP
- 無串連模型, 如UDP, IP
服務運行在主機。通常它們的生命期很長,同時被設計成等待請求和響應請求。當前有各種類型的服務,通過各種方法向客戶提供服務。互連網的世界基於TCP和UDP這兩種通訊方法提供許多這些服務,雖然也有其他通訊協定如SCTP伺機取代。許多其他類型的服務,例如點對點, 遠程序呼叫, 通訊代理, 和許多其他也建立在TCP和UDP之上。
服務存活於主機內。IP地址可以定位主機。但在每台電腦上可能會提供多種服務,需要一個簡單的方法對它們加以區分。TCP,UDP,SCTP或者其他協議使用連接埠號碼來加以區分。這裡使用一個1到65,535的不帶正負號的整數,每個服務將這些連接埠號碼中的一個或多個相關聯。
有很多“標準”的連接埠。Telnet服務通常使用連接埠號碼23的TCP協議。DNS使用連接埠號碼53的TCP或UDP協議。FTP使用連接埠21和20的命令,進行資料轉送。HTTP通常使用連接埠80,但經常使用,連接埠8000,8080和8088,協議為TCP。X Window系統往往需要連接埠6000-6007,TCP和UDP協議。
在Unix系統中, /etc/services檔案列出了常用的連接埠。Go語言有一個函數可以擷取該檔案。
1 |
func LookupPort(network, service string) (port int, err os.Error) |
Go提供IP, IP掩碼, TCPAddr, UDPAddr, 網卡,主機查詢這些對象的操作函數。
當你知道如何通過網路和連接埠ID尋找一個服務時,然後呢?如果你是一個用戶端,你需要一個API,讓您串連到服務,然後將訊息發送到該服務,並從服務讀取回複。
如果你是一個伺服器,你需要能夠綁定到一個連接埠,並監聽它。當有訊息到來,你需要能夠讀取它並回複用戶端。
net.TCPConn是允許在TCP用戶端和TCP伺服器之間的全雙工系統通訊的Go類型。兩種主要方法是
12 |
func (c *TCPConn) Write(b []byte) (n int, err os.Error)func (c *TCPConn) Read(b []byte) (n int, err os.Error) |
TCPConn被用戶端和伺服器用來讀寫訊息。
ECHO伺服器
在一個伺服器上註冊並監聽一個連接埠。然後它阻塞在一個"accept"操作,並等待用戶端串連。當一個用戶端串連, accept調用返回一個串連(connection)對象。ECHO服務非常簡單,只是用戶端, 關閉該串連的請求資料寫回到用戶端,就像回聲一樣,直到某一方關閉串連。
12 |
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)func (l *TCPListener) Accept() (c Conn, err os.Error) |
net參數可以設定為字串"tcp", "tcp4"或者"tcp6"中的一個。如果你想監聽所有網路介面,IP地址應設定為0。 如果你只是想監聽一個特定網路介面,IP地址可以設定為該網路介面的地址。如果連接埠設定為0,作業系統會為你選擇一個連接埠。否則,你可以選擇你自己的。需要注意的是,在Unix系統上,除非你是監控系統,否則不能監聽低於1024的連接埠,小於128的連接埠是由IETF標準化。該樣本程式選擇連接埠1200沒有特別的原因。TCP地址如下":1200" - 所有網路介面, 連接埠1200。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 |
package mainimport ("flag""fmt""io""net""os")var host = flag.String("host", "", "host")var port = flag.String("port", "3333", "port")func main() {flag.Parse()var l net.Listenervar err errorl, err = net.Listen("tcp", *host+":"+*port)if err != nil {fmt.Println("Error listening:", err)os.Exit(1)}defer l.Close()fmt.Println("Listening on " + *host + ":" + *port)for {conn, err := l.Accept()if err != nil {fmt.Println("Error accepting: ", err)os.Exit(1)}//logs an incoming messagefmt.Printf("Received message %s -> %s \n", conn.RemoteAddr(), conn.LocalAddr())// Handle connections in a new goroutine.go handleRequest(conn)}}func handleRequest(conn net.Conn) {defer conn.Close()for {io.Copy(conn, conn)}} |
執行go run echoserver.go啟動伺服器。
ECHO用戶端
一旦用戶端已經建立TCP服務, 就可以"撥號"了. 如果成功,該調用返回一個用於通訊的TCPConn。用戶端和伺服器通過它交換訊息。通常情況下,用戶端使用TCPConn寫入請求到伺服器, 並從TCPConn的讀取響應。持續如此,直到任一(或兩者)的兩側關閉串連。用戶端使用該函數建立一個TCP串連。
1 |
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error) |
其中laddr是本地地址,通常設定為nil。 raddr是一個服務的遠程地址, net是一個字串,可以根據你的需要設定為"tcp4", "tcp6"或"tcp"中的一個。
在介紹實現時,我們需要介紹同步機制, 因為用戶端發送和接收是在兩個goroutine中。 main函數中如果不加上同步機制, 用戶端還沒有發送接收就執行完了。
我們實現了兩種同步方式。 當然還有其它方式, 如time.Sleep(60*1000)或者等待從命令列輸入,不過看起來有點傻。
Go格言
Share memory by communicating, don't communicate by sharing memory
使用Channel等待goroutine完成
比如老套的方式通過channel實現同步。 讀和寫完成後分別往channel中寫入"done"。 main讀取channel中的值,當兩個done都讀取到後就知道讀寫已經完成。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556 |
package mainimport ("flag""fmt""net""os""strconv")var host = flag.String("host", "localhost", "host")var port = flag.String("port", "3333", "port")func main() {flag.Parse()conn, err := net.Dial("tcp", *host+":"+*port)if err != nil {fmt.Println("Error connecting:", err)os.Exit(1)}defer conn.Close()fmt.Println("Connecting to " + *host + ":" + *port)done := make(chan string)go handleWrite(conn, done)go handleRead(conn, done)fmt.Println(<-done)fmt.Println(<-done)}func handleWrite(conn net.Conn, done chan string) {for i := 10; i > 0; i-- {_, e := conn.Write([]byte("hello " + strconv.Itoa(i) + "\r\n"))if e != nil {fmt.Println("Error to send message because of ", e.Error())break}}done <- "Sent"}func handleRead(conn net.Conn, done chan string) {buf := make([]byte, 1024)reqLen, err := conn.Read(buf)if err != nil {fmt.Println("Error to read message because of ", err)return}fmt.Println(string(buf[:reqLen-1]))done <- "Read"} |
net.Dial建立串連, handleWrite發送十個請求, handleRead 接收伺服器的響應。一旦完成,往channel中寫done。
執行go run echoclient.go啟動伺服器。
使用WaitGroup等待goroutine完成
上面的方式雖好,但是不夠靈活。我們需要明確知道有多少個done。 如果增加若干個goroutine,修改起來比較麻煩。
所以還是使用sync包的WaitGroup比較靈活。 它類似Java中的CountDownLatch。
A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.
將上面的例子修改如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 |
package mainimport ("bufio""flag""fmt""net""os""strconv""sync")var host = flag.String("host", "localhost", "host")var port = flag.String("port", "3333", "port")func main() {flag.Parse()conn, err := net.Dial("tcp", *host+":"+*port)if err != nil {fmt.Println("Error connecting:", err)os.Exit(1)}defer conn.Close()fmt.Println("Connecting to " + *host + ":" + *port)var wg sync.WaitGroupwg.Add(2)go handleWrite(conn, &wg)go handleRead(conn, &wg)wg.Wait()}func handleWrite(conn net.Conn, wg *sync.WaitGroup) {defer wg.Done()for i := 10; i > 0; i-- {_, e := conn.Write([]byte("hello " + strconv.Itoa(i) + "\r\n"))if e != nil {fmt.Println("Error to send message because of ", e.Error())break}}}func handleRead(conn net.Conn, wg *sync.WaitGroup) {defer wg.Done()reader := bufio.NewReader(conn)for i := 1; i <= 10; i++ {line, err := reader.ReadString(byte('\n'))if err != nil {fmt.Print("Error to read message because of ", err)return}fmt.Print(line)}} |
wg.Add(2)設定等待兩個goroutines, 然後調用wg.Wait()等待goroutines完成。 當goroutine完成時, 調用wg.Done()。 使用起來相當簡潔。
參考
- http://tools.ietf.org/html/rfc862
- http://loige.com/simple-echo-server-written-in-go-dockerized/
- http://nathanleclaire.com/blog/2014/02/15/how-to-wait-for-all-goroutines-to-finish-executing-before-continuing/
- http://jan.newmarch.name/go/zh/
- https://talks.golang.org/2012/concurrency.slide
- http://jimmyfrasche.github.io/go-reflection-codex/
- https://sites.google.com/site/gopatterns/
- https://github.com/golang-samples
- http://golang-examples.tumblr.com/
- https://code.google.com/p/go-wiki/wiki/Articles
- https://github.com/mindreframer/golang-stuff