欢迎访问
我们一直在努力

TCP应用编程入门

目录

1、网络模型

1.1 OSI模型

OSI模型

1.2 http报文格式

http报文格式

关键字段:URL

1.3 tcp报文格式

tcp报文格式

关键字段:端口号

1.4 ip报文格式

IP层报文格式
关键字段:源IP地址、目的IP地址

1.5 以太帧

以太帧
关键字段:源MAC地址、目的MAC地址

1.6 问题

  1. lvs、slb的基于端口转发工作在哪一层呢?
  2. nginx的域名反向代理转发工作在哪一层呢?
  3. 家用普通路由器和无路由功能的交换机呢?

2、数据链路层

  1. 通过电信号当中的某一个字段或者光纤当中的某一个固定频率的波来传递控制报文,组合成电物理拓扑和光物理拓扑图;
  2. 某一个线路中断时,通过控制报文触发告警,让中断线路的两节点用户数据走其他线路来实现用户网络不中断的目的;
  3. 智能化的全自动交换的光网络(ASON);

3、应用场景

Http应用通常用于无状态一次性请求,请求之后连接就关闭的短连接,开发简单;
Tcp应用通常用于长连接,客户端和服务端反复发送接收数据,需要自己定义报文格式,开发稍复杂;

4、实现原理

文件路径都在/proc/sys/net目录下

1. TCP收发缓冲区

# TCP接收缓冲(用于TCP接收滑动窗口)的最小值、默认值、最大值
cat /proc/sys/net/ipv4/tcp_rmem
4096    87380   6291456

# TCP发送缓冲(用于TCP发送滑动窗口)的最小值、默认值、最大值
cat /proc/sys/net/ipv4/tcp_wmem
4096    16384   4194304

每个TCP套接字有一个发送和一个接收缓冲区,当某个应用进程调用write时,内核从该应用进程的缓冲区复制发送缓冲区;同样的调用read时,内核从接收缓冲区复制到应用进程的缓冲区。

2. 内核缓冲区

网卡接收到的数据存放到内核缓冲区内,然后内核缓冲区存放的数据根据TCP信息将数据移动到具体的某一个TCP连接上的接收缓冲区内,也就是接收滑动窗口内,整个完成从IP层到传输层的数据传递。

3. 滑动窗口

发送窗口
tcp报文格式
接收窗口
tcp报文格式

5、golang网络模型

  1. 常见网络模型比较
    五种模型比较
  2. golang用户态模型
    golang用户态模型

    • 绿色goroutine是接受TCP链接
    • 当完成握手accept返回conn对象后,使用一个单独的goroutine来阻塞读(紫色),使用一个单独的goroutine来阻塞写(红色)
    • 读到的数据通过解码后放入读channel,并由蓝色的goroutine来处理
    • 需要写数据时,蓝色的goroutine将数据写入写channel,从而触发红色的goroutine编码并写入conn

汇总:
Golang实利用了OS提供的非阻塞IO访问模式,并配合epoll/kqueue等IO事件监控机制;为了弥合OS的异步机制与Golang接口的差异,而在runtime上做的一层封装,这层封装就是netpoller。

最终的效果就是用户层阻塞,底层非阻塞

5、tcp应用样例

func HandleConn(conn net.Conn) {
    defer conn.Close()

    for {
        // 从连接里面循环读固定长度的数据,这里一般用封装好的io.ReadFull()接口
        bytesRead, err = conn.Read(buffer)
        totalBytesRead += bytesRead
        for totalBytesRead < toRead && err == nil {
            bytesRead, err = conn.Read(buffer[totalBytesRead:])
            totalBytesRead += bytesRead
        }

        // 向连接里循环写入指定长度的数据,实际conn.Write已经实现成循环去写,不需要应用层循环写入
        for totalBytesWritten < toWriteLen && writeError == nil {
            bytesWritten, writeError = conn.Write(buffer[totalBytesWritten:])
            totalBytesWritten += bytesWritten
        }
    }
}

func main() {
    listen, err := net.Listen("tcp", ":8888")
    if err != nil {
        fmt.Println("listen error: ", err)
        return
    }

    for {
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("accept error: ", err)
            break
        }

        // start a new goroutine to handle the new connection
        go HandleConn(conn)
    }
}

6、异常场景

1. 读取数据,对应接收缓冲区

场景 效果描述
缓冲区无数据 阻塞
缓冲区有部分数据 成功读取,返回读取多少个字节
缓冲区有足够数据 成功读取
client连接关闭,缓冲区有数据 成功读取,读完之后EOF
client连接关闭,缓冲区有数据 EOF

2. 写入数据,对应发送缓冲区

场景 效果描述
缓冲区足够 成功写入
缓冲区不够 成功写入,返回写了多少个字节
缓冲区满 阻塞
client连接关闭,缓冲区有数据 成功读取,读完之后EOF
client连接关闭,缓冲区有数据 EOF

3. 网络不可达或对方服务未启动

// 尝试连接一个未监听的端口
func main() {
    log.Println("begin dial...")
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    defer conn.Close()
    log.Println("dial ok")
}
# 如果本机8888端口未有服务程序监听,那么执行上面程序,Dial会很快返回错误
$go run client1.go
2015/11/16 14:37:41 begin dial...
2015/11/16 14:37:41 dial error: dial tcp :8888: getsockopt: connection refused

4. 服务端服务一直不accept

// 服务端
func main() {
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Println("error listen:", err)
        return
    }
    defer l.Close()
    log.Println("listen ok")

    var i int
    for {
        time.Sleep(time.Second * 10)
        if _, err := l.Accept(); err != nil {
            log.Println("accept error:", err)
            break
        }
        i++
        log.Printf("%d: accept a new connection\n", i)
    }
}
// 客户端
func establishConn(i int) net.Conn {
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        log.Printf("%d: dial error: %s", i, err)
        return nil
    }
    log.Println(i, ":connect to server ok")
    return conn
}

func main() {
    var sl []net.Conn
    for i := 1; i < 1000; i++ {
        conn := establishConn(i)
        if conn != nil {
            sl = append(sl, conn)
        }
    }

    time.Sleep(time.Second * 10000)
}
# 执行结果
# 可以看出Client初始时成功地一次性建立了128个连接,然后后续每阻塞近10s才能成功建立一条连接。也就是说在server端backlog满时(未及时accept),客户端将阻塞在Dial上,直到server端进行一次accept。
$go run server2.go
2015/11/16 21:55:41 listen ok
2015/11/16 21:55:51 1: accept a new connection
2015/11/16 21:56:01 2: accept a new connection
... ...

$go run client2.go
2015/11/16 21:55:44 1 :connect to server ok
2015/11/16 21:55:44 2 :connect to server ok
2015/11/16 21:55:44 3 :connect to server ok
... ...

2015/11/16 21:55:44 126 :connect to server ok
2015/11/16 21:55:44 127 :connect to server ok
2015/11/16 21:55:44 128 :connect to server ok

2015/11/16 21:55:52 129 :connect to server ok
2015/11/16 21:56:03 130 :connect to server ok
2015/11/16 21:56:14 131 :connect to server ok
... ...

5. 连接异常关闭

进程主动关闭、异常退出等最终系统会发送close消息给到服务端,服务端可以感知。对于设备断电、断网之类的,客户端无法发送close消息的这种场景,内核默认2小时才发送一次保活消息来监测连接是否断开,时间比较久,通常做法是应用这边定时发送心跳消息来自己判断。

7、参考资料

未经允许不得转载:威威牛 » TCP应用编程入门
分享到: 更多 (0)