不少招聘需求上都會要求熟悉TCP/IP協議、socket編程之類的,可見這一塊是對於web編程是很是重要的。做爲一個野生程序員對這塊沒什麼概念,因而便找來一些書籍想來補補。不少關於協議的大部頭書都是很是枯燥的,我特地挑了比較友好的《圖解TCP/IP》和《圖解HTTP》,但看了一遍還是雲裏霧裏,找不到掌握了知識後的那種自信。因此得換一種思路來學習————經過敲代碼來學習,經過抓包工具來分析網絡,抓包神器首推wireshark。本文是本身學習TCP過程的記錄和總結。程序員
寫一個簡單的server和client,模擬最簡單的http請求,即client發送get請求,server返回hello。這裏是用golang寫的,最近在學習golang。golang
完成以後可使用postman充當client測試你的server能不能正常返回響應,或者使用完備的http模塊測試你的client。web
client向指定端口發送鏈接請求,鏈接後發送一個request並收到response斷開鏈接並退出。server能夠和不一樣的客戶端創建多個TCP鏈接,每來了一個新鏈接就開一個goruntine去處理。算法
TCP是全雙工的,所謂全雙工就是讀寫有兩個通道,互不影響,我當時還納悶在conn上又讀又寫不會出毛病嗎-_-編程
TCP是流式傳輸,因此要在for中不斷的去讀取數據,直到斷開。注意沒有斷開鏈接的時候是讀不到EOF的,代碼使用了bufio包中的scanner這個API來逐行讀取數據,以\n爲結束標誌。但數據並不都是以\n結尾的,若是讀不到結尾,read就會一直阻塞,因此咱們須要經過header中的length判斷數據的大小。segmentfault
我這裏偷懶了,只讀了header,讀到header下面的空行就返回了。加了個超時,客戶端5s不理我就斷線,若是有數據過來就保持鏈接。網絡
server:數據結構
package main import ( "bufio" "bytes" "fmt" "io" "net" "time" ) const rn = "\r\n" func main() { l, err := net.Listen("tcp", ":8888") if err != nil { panic(err) } fmt.Println("listen to 8888") for { conn, err := l.Accept() if err != nil { fmt.Println("conn err:", err) } go handleConn(conn) } } func handleConn(conn net.Conn) { defer conn.Close() defer fmt.Println("關閉") fmt.Println("新鏈接:", conn.RemoteAddr()) t := time.Now().Unix() // 超時 go func(t *int64) { for { if time.Now().Unix() - *t >= 5 { fmt.Println("超時") conn.Close() return } time.Sleep(100 * time.Millisecond) } }(&t) for { data, err := readTcp(conn) if err != nil { if err == io.EOF { continue } else { fmt.Println("read err:", err) break } } if (data > 0) { writeTcp(conn) t = time.Now().Unix() } else { break } } } func readTcp(conn net.Conn) (int, error) { var buf bytes.Buffer var err error rd := bufio.NewScanner(conn) total := 0 for rd.Scan() { var n int n, err = buf.Write(rd.Bytes()) if err != nil { panic(err) } buf.Write([]byte(rn)) total += n fmt.Println("讀到字節:", n) if n == 0 { break } } err = rd.Err() fmt.Println("總字節數:", total) fmt.Println("內容:", rn, buf.String()) return total, err } func writeTcp(conn net.Conn) { wt := bufio.NewWriter(conn) wt.WriteString("HTTP/1.1 200 OK" + rn) wt.WriteString("Date: " + time.Now().String() + rn) wt.WriteString("Content-Length: 5" + rn) wt.WriteString("Content-Type: text/plain" + rn) wt.WriteString(rn) wt.WriteString("hello") err := wt.Flush() if err != nil { fmt.Println("Flush err: ", err) } fmt.Println("寫入完畢", conn.RemoteAddr()) }
client:socket
package main import ( "bufio" "bytes" "fmt" "net" "time" ) const rn = "\r\n" func main() { conn, err := net.Dial("tcp", ":8888") defer conn.Close() defer fmt.Println("斷開") if err != nil { panic(err) } sendReq(conn) for { total, err := readResp(conn) if err != nil { panic(err) } if total > 0 { break } } } func sendReq(conn net.Conn) { wt := bufio.NewWriter(conn) wt.WriteString("GET / HTTP/1.1" + rn) wt.WriteString("Date: " + time.Now().String() + rn) wt.WriteString(rn) err := wt.Flush() if err != nil { fmt.Println("Flush err: ", err) } fmt.Println("寫入完畢", conn.RemoteAddr()) } func readResp(conn net.Conn) (int, error) { var buf bytes.Buffer var err error rd := bufio.NewScanner(conn) total := 0 for rd.Scan() { var n int n, err = buf.Write(rd.Bytes()) if err != nil { panic(err) } buf.Write([]byte(rn)) if err != nil { panic(err) } total += n fmt.Println("讀到字節:", n) if n == 0 { break } } if err = rd.Err(); err != nil { fmt.Println("read err:", err) } if (total > 0) { fmt.Println("resp:", rn, buf.String()) } return total, err }
server和client作出來了,下面來使用wireshark抓包來看看TCP連接的真容。固然你也能夠現成的http模塊來收發抓包,不過仍是建議本身寫一個最簡單的。由於現成的模塊裏面不少細節被隱藏,好比我開始用postman發一個請求可是會創建兩個鏈接,疑似是先發了個HEAD請求。
打開wireshark,默認設置就好了。選擇一個網卡,輸入過濾條件開始抓包,由於咱們是localhost,因此選擇loopback。數據結構和算法
抓包開始後,啓動以前的server監聽8888端口,再啓動client發送請求,因而便抓到了一次新鮮的TCP請求。
從圖中咱們能夠清晰的看到三次握手(1-3)和四次揮手(9-12),還有seq和ack的變化,基於TCP的HTTP請求和響應,還有什麼window update(TCP的窗口控制,告訴客戶端我這邊很空虛,趕忙發射數據)。
這個時候再結合大部頭的協議書籍,理解起來印象更深。還有各類抓包姿式,更多複雜場景,留給你們本身去調教了。
我在抓一次文件上傳的過程當中,看到有個包length達到了16000,一個TCP包最大的數據載荷能達到多少呢?請聽下文分解。
最後給你們推薦兩本書《wiresharks網絡分析就是這麼簡單》和《wireshark網絡分析的藝術》,這兩本爲一個系列,做者用通俗易懂的語言,介紹wireshark的奇技淫巧和網絡方面的一些解決思路,很是精彩。不少人不斷強調數據結構和算法這些內功,不屑於專門學習工具的使用,但好的工具在學習和工做中能帶來巨大的幫助,能造出好用的工具更是了不得。