8

很多招聘需求上都会要求熟悉TCP/IP协议、socket编程之类的,可见这一块是对于web编程是非常重要的。作为一个野生程序员对这块没什么概念,于是便找来一些书籍想来补补。很多关于协议的大部头书都是非常枯燥的,我特意挑了比较友好的《图解TCP/IP》和《图解HTTP》,但看了一遍仍是云里雾里,找不到掌握了知识后的那种自信。所以得换一种思路来学习————通过敲代码来学习,通过抓包工具来分析网络,抓包神器首推wireshark。本文是自己学习TCP过程的记录和总结。

1、使用TCP socket实现服务端和客户端,模拟http请求

写一个简单的server和client,模拟最简单的http请求,即client发送get请求,server返回hello。这里是用golang写的,最近在学习golang。

完成之后可以使用postman充当client测试你的server能不能正常返回响应,或者使用完备的http模块测试你的client。

client向指定端口发送连接请求,连接后发送一个request并收到response断开连接并退出。server可以和不同的客户端建立多个TCP连接,每来了一个新连接就开一个goruntine去处理。

TCP是全双工的,所谓全双工就是读写有两个通道,互不影响,我当时还纳闷在conn上又读又写不会出毛病吗-_-

TCP是流式传输,所以要在for中不断的去读取数据,直到断开。注意没有断开连接的时候是读不到EOF的,代码使用了bufio包中的scanner这个API来逐行读取数据,以\n为结束标志。但数据并不都是以\n结尾的,如果读不到结尾,read就会一直阻塞,所以我们需要通过header中的length判断数据的大小。

我这里偷懒了,只读了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:

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
}

2、通过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的奇技淫巧和网络方面的一些解决思路,非常精彩。很多人不断强调数据结构和算法这些内功,不屑于专门学习工具的使用,但好的工具在学习和工作中能带来巨大的帮助,能造出好用的工具更是了不起。


无风
670 声望63 粉丝