經過wireshark抓包來學習TCP HTTP網絡協議

不少招聘需求上都會要求熟悉TCP/IP協議、socket編程之類的,可見這一塊是對於web編程是很是重要的。做爲一個野生程序員對這塊沒什麼概念,因而便找來一些書籍想來補補。不少關於協議的大部頭書都是很是枯燥的,我特地挑了比較友好的《圖解TCP/IP》和《圖解HTTP》,但看了一遍還是雲裏霧裏,找不到掌握了知識後的那種自信。因此得換一種思路來學習————經過敲代碼來學習,經過抓包工具來分析網絡,抓包神器首推wireshark。本文是本身學習TCP過程的記錄和總結。程序員

一、使用TCP socket實現服務端和客戶端,模擬http請求

寫一個簡單的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
}

二、經過wireshark監聽對應端口抓包分析

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的奇技淫巧和網絡方面的一些解決思路,很是精彩。不少人不斷強調數據結構和算法這些內功,不屑於專門學習工具的使用,但好的工具在學習和工做中能帶來巨大的幫助,能造出好用的工具更是了不得。

相關文章
相關標籤/搜索