很多招聘需求上都会要求熟悉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的奇技淫巧和网络方面的一些解决思路,非常精彩。很多人不断强调数据结构和算法这些内功,不屑于专门学习工具的使用,但好的工具在学习和工作中能带来巨大的帮助,能造出好用的工具更是了不起。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。