Golang 网络传输

Jackey Golang 1,684 次浏览 , , , 没有评论

网络分层架构

典型协议

传输层:常见协议有TCP/UDP协议

应用层:常见的协议有HTTP协议,FTP协议

网络层:常见协议有IP协议,ICMP协议,IGMP协议

网络接口层:常见协议有ARP协议,RARP协议

TCP传输协议(Transmission Control Protocol)是一种面向链接的、可靠的、基于子接口的传输层通讯协议。

UDP用户数据包协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输协议,提供面向事务的简单不可靠信息传输服务。

HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最广泛的一种网络协议。

FTP文件传输协议(File Transfer Protocol)

IP协议是因特网互联协议(Internet Protocol)

ICMP协议是Internet控制报文协议(Internet Control Message Protocol),它是TCP/IP协议族的一个子协议,用于在IP主机、路由之间传递控制消息。

IGMP协议是Internet组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议,该协议运行在主机和组播路由器之间。

ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。

EARP是反向地址转换协议,通过MAC地址确定IP地址。

业内普遍的分层方式有两种:ISI七层模型和TCP/IP四层模型。

  1. 物理层:主要定义物理设备标准,如网线的接口类型、光前的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,叨叨目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
  2. 数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、I
  3. 网络层:在位于不同地理位置的网络中的两个主机系统之间提供链接和路径选择。Internet的发展使得从出界各站点访问信息的用户数大大增加,而网络层正式管理这种连接的层。
  4. 传输层:定义了一些传输数据的协议和端口号(www端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据)、UDP(用户数据包协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。主要是将从下层接收的数据进行分段和传输,达到目的地址后再进行重组。尝尝把这一层数据叫做段。
  5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要是在你的系统之间发起会话或者接收会话请求(设备之间需要相互认识可以是IP也可以是MAC或者主机名)
  6. 表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如:PC程序和另一台计算机进行通讯,其中一台计算机使用扩展二→十进制交换码(EBCDIC),而另一台则使用美国信息叫唤标准码(ASCII)来标识相同的字符。如有必要,表示层会通过使用一种通用格式来实现多种数据格式之间的转换。
  7. 应用层:是最靠近用户的OSI层,这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。

什么是Socket

Socket,英文含义是:插座、插孔。一般称之为套接字,用于描述IP地址和端口。可以实现不同程序见的数据通信。

Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open→write/read→关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的链接建立、数据传输等操作都是通过该Socket来实现的。

网络应用程序设计模式

  • C/S模式
    传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
  • B/S模式
    浏览器(Browser)/服务器(Server)模式,只需在一段部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。

优缺点:

对于C/S模式俩说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序有一个开发团队制作,所以他们之间所采用的协议相对灵活。一刻在标准协议的基础上根据需求裁剪及定制。例如:腾讯所采用的通信协议,即为ftp协议的修改剪裁版。

因此,传统的网路应用程序及较大型的网络应用程序都首选C/S模式进行开发。如知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理。从而提高观感。

C/S模式的缺点也较为突出。由于客户端和服务器都要有一个开发团队来完成开发,工作量将成倍提升,开发周期较长,另外从用户角度出发,需要将客户端安插到用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模型应用程序的重要原因。

B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小,只需开发服务器端即可,另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制,如早期的偷菜游戏。在各个平台上都可以完美运行。

B/S模式的缺点也较为明显。由于使用第三方浏览器,因此网络引用支持受限。另外美哦与客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量搜到限制。应用的感官大打折扣。第三必须与浏览器一样,采用标准HTTP协议进行通信,协议选择不灵活。

因此在开发过程中,模式的选择由上述各自的缺点决定。根据实际需求选择应用程序设计模式。

TCP的C/S架构

应用实例

server.go

func main() {
    // 指定服务器通讯协议,ip地址,端口
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("net.Listen err:", err)
        return
    }
    defer listener.Close()
    fmt.Println("服务器等待客户端建立链接...")
    // 阻塞监听客户端链接请求
    conn, err := listener.Accept()
    if err != nil {
        fmt.Println("listener.Accept err:", err)
        return
    }
    defer conn.Close()
    var buf = make([]byte, 4096)
    // 读取客户端发送的数据
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Println("conn.Read err:", err)
        return
    }
    conn.Write(buf)
    fmt.Println("服务器接收到数据:", string(buf[:n]))
}

client.go

func main() {
    // 指定服务端IP+port创建通讯套接字
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("net.Dial err:", err)
        return
    }
    defer conn.Close()
    // 主动写数据给服务端
    conn.Write([]byte("hello Jackey"))

    // 读取服务端发来的数据
    var buf = make([]byte, 4096)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Println("conn.Read err:", err)
        return
    }
    fmt.Println("客户端接收到数据:", string(buf[:n]))
}

并发处理

server.go

func HandlerConnect(conn net.Conn) {
    defer conn.Close()
    // 获取连接的客户端网络地址
    addr := conn.RemoteAddr()
    fmt.Println(addr, "客户端成功连接!")

    // 循环读取客户端发送数据
    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)
        if "exit\n" == string(buf[:n]) || "exit\r\n" == string(buf[:n]) {
            fmt.Println("服务器接收到客户端退出请求,服务器关闭")
            return
        }
        if n == 0 {
            fmt.Println("服务器检测到客户端已关闭,断开连接!!!")
            return
        }
        if err != nil {
            fmt.Println("conn.Read err:", err, n)
            return
        }
        fmt.Println("服务器读到数据:", string(buf[:n])) // 使用数据
        // 小写转大写,回发给客户端
        conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
    }
}

func main() {
    // 指定服务器通讯协议,ip地址,端口
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("net.Listen err:", err)
        return
    }
    defer listener.Close()
    fmt.Println("服务器等待客户端建立链接...")
    // 阻塞监听客户端链接请求
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("listener.Accept err:", err)
            return
        }
        // 具体完成服务端和客户端的通信
        go HandlerConnect(conn)
    }
}

client.go

func main() {
    // 指定服务端IP+port创建通讯套接字
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("net.Dial err:", err)
        return
    }
    defer conn.Close()
    // 获取用户的键盘输入(stdin),将输入数据发送给服务器
    go func() {
        str := make([]byte, 4096)
        for {
            n, err := os.Stdin.Read(str)
            if err != nil {
                fmt.Println("os.Stdin.Read err:", err)
                continue
            }
            // 写给服务器,读多少,写多少
            conn.Write(str[:n])
        }
    }()
    // 回显服务器回发的大写数据
    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)
        if n == 0 {
            fmt.Println("客户端监测到服务端已关闭,断开连接!!!")
            return
        }
        if err != nil {
            fmt.Println("conn.Read err:", err)
            return
        }
        fmt.Println("客户端读到服务器回发:", string(buf[:n]))
    }
}

TCP通讯过程

建立连接:三次握手

  1. 客户端发送一个带有SYN标志的TCP报文到服务器。这是上图中三次握手过程中的段1.客户端发出SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号需要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。
    另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001.
    mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大长度,就必须在IP层分片,为了避免这种情况,客户端声明自己最大段尺寸,建议服务端发来的段不要超过这个长度。
  2. 服务端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。表示对刚才客户端SYN的回应;同时发送SYN给客户端,询问客户端是否准备好进行数据通讯。
    服务端发出段2,也带有SYN位,同时置ACK位标识确认,确认序号是1001,标识”我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段“,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024.
  3. 客户端必须再次回应服务端一个ACK报文,这是报文段3.
    客户端发出段3,对服务器的连接请求进行应答,确认序号是8001.在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器请求和应答在一个段中发出。

因此一共有三个段用户建立连接,称为”三次握手“。在建立连接的同时,双方协商了一些信息,例如,双发发送序号的初始值、最大段尺寸等。

标志TCP三次握手建立完成:

  1. server:Accept()返回
  2. client:Dial()返回

数据传输过程

  1. 客户端发出段4,包含从序号1001开始的20个字节数据。
  2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送序号8001开始的10个字节数据。
  3. 客户端发出段6,对服务器发来的序号为8001-8010的数据标识确认收到,请求发送序号为8011开始的数据。

在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会存在TCP层的发送缓冲区,发出数据包给对方后,只有收到对方应答的ACK段才知道该数据包确实发给了对方,可以从发发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者对视了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

断开连接:四次挥手

  1. 客户端发送一个FIN报文段7,报文中会指定一个序列号1021,此时客户端处于FIN_WAIT1状态。
    即发出连接释放报文段,并停止再次发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  2. 服务端收到FIN后,会发送ACK报文段8,切把客户端的序列号值+1最为ACK报文的序列号值1022,表名已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态。
    即服务端收到连接释放报文段后即发出确认报文段,服务端进入CLOSE_WAIT(关闭等待)状态,此时TCP处于半关闭状态,客户端到服务端的链接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  3. 如果服务端也想断开连接了,和客户端的第一次挥手一样,发给FIN报文段9,且指定一个序号8011.此时服务端处于LAST_ACK的状态。
    即服务端没有要向客户端发出的数据,服务端发出连接释放报文段,服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  4. 客户端收到FIN之后,一样发送一个ACK报文段10作为应答,且把服务端的序号值+1作为自己ACK报文的序号值8012,此时客户端处于TIME_WAIT状态。需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态,服务端收到ACK报文之后,就关闭连接了,处于CLOSED状态。
    即客户端收到服务端的链接释放报文段后,对此发出确认报文段,客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户单执行主动关闭并进入TIME_WAIT是正常的,服务端通畅执行被动关闭,不会进入TIME_WAIT状态。

TCP状态转换

TCP状态对应的通讯过程:

TCP状态转换图重点解析(可使用命令:netstat -an查看):

主动发起连接请求端:CLOSED→完成三次握手→ESTABLISEHED(数据通讯状态)[Dial()函数返回]

被动发起连接请求端:CLOSED→LISTEN[调用Accept()函数]→完成三次握手→ESTABLISEHED(数据通讯状态)[Accept()函数返回]

注:数据传递期间处于ESTABLISEHED状态

主动关闭连接请求端:ESTABLISEHED→FIN_WAIT_2(半关闭)→TIME_WAIT→2MSL等待→确认最后一个ACK被对端接收→CLOSE

注:半关闭、TIME_WAIT、2MSL只会出现在主动关闭连接请求端

被动关闭连接请求端:ESTABLISEHED→CLOSE

什么是UDP?

UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,IETF RFC 768是UDP的正式规范。

UDP协议与TCP协议一样用于处理数据包,在OSI模型中,两者都位于传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP用来支持那些需要在计算机之间传输数据的网络应用。

UDP服务器

由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP喝Port,然后监听该地址,等待客户端与之建立连接,即可通信。

创建监听地址:

func ResolveUDPAddr(network, address string) (*UDPAddr, error)

创建监听连接:

func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)

接收udp数据

func (c *UDPConn) ReadFormUDP(b []byte) (int, *UDPAddr, error)

写出数据到UDP:

func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

应用实例

server.go

func main() {
    // 组织一个UDP地址结构,指定服务器IP+port
    srvAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003")
    if err != nil {
        fmt.Println("net.ResolveUDPAddr err:", err)
        return
    }
    fmt.Println("udp 服务器地址结构创建完成")
    // 创建用户通信的socket
    udpConn, err := net.ListenUDP("udp", srvAddr)
    if err != nil {
        fmt.Println("net.ListenUDP err:", err)
        return
    }
    defer udpConn.Close()
    fmt.Println("udp 服务器通信socket创建完成")

    // 读取客户端发送的数据
    buf := make([]byte, 4096)
    // 返回三个值,分别是 读取到的字节数,客户端地址,error
    n, cltAddr, err := udpConn.ReadFromUDP(buf)
    if err != nil {
        fmt.Println("udpConn.ReadFromUDP err:", err)
        return
    }
    // 模拟处理数据
    fmt.Printf("服务器读到 %v 的数据:%s\n", cltAddr, string(buf[:n]))

    // 写数据给客户端
    dateTime := time.Now().String()
    _, err = udpConn.WriteToUDP([]byte(dateTime), cltAddr)
    if err != nil {
        fmt.Println("udpConn.WriteToUDP err:", err)
        return
    }
}

client.go

func main() {
    // 指定服务端IP+port创建通讯套接字
    conn, err := net.Dial("udp", "127.0.0.1:8003")
    if err != nil {
        fmt.Println("net.Dial err:", err)
        return
    }
    defer conn.Close()
    // 主动写数据给服务端
    conn.Write([]byte("hello Jackey"))

    // 读取服务端发来的数据
    var buf = make([]byte, 4096)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Println("conn.Read err:", err)
        return
    }
    fmt.Println("客户端接收到数据:", string(buf[:n]))
}

并发处理

server.go

func main() {
    // 组织一个UDP地址结构,指定服务器IP+port
    srvAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003")
    if err != nil {
        fmt.Println("net.ResolveUDPAddr err:", err)
        return
    }
    fmt.Println("udp 服务器地址结构创建完成")
    // 创建用户通信的socket
    udpConn, err := net.ListenUDP("udp", srvAddr)
    if err != nil {
        fmt.Println("net.ListenUDP err:", err)
        return
    }
    defer udpConn.Close()
    fmt.Println("udp 服务器通信socket创建完成")

    // 读取客户端发送的数据
    buf := make([]byte, 4096)
    for {
        // 返回三个值,分别是 读取到的字节数,客户端地址,error
        n, cltAddr, err := udpConn.ReadFromUDP(buf)
        if err != nil {
            fmt.Println("udpConn.ReadFromUDP err:", err)
            return
        }
        // 模拟处理数据
        fmt.Printf("服务器读到 %v 的数据:%s\n", cltAddr, string(buf[:n]))

        go func() {
            // 写数据给客户端
            dateTime := time.Now().String()
            _, err = udpConn.WriteToUDP([]byte(dateTime), cltAddr)
            if err != nil {
                fmt.Println("udpConn.WriteToUDP err:", err)
                return
            }
        }()
    }
}

client.go

func main() {
    // 指定服务端IP+port创建通讯套接字
    conn, err := net.Dial("udp", "127.0.0.1:8003")
    if err != nil {
        fmt.Println("net.Dial err:", err)
        return
    }
    defer conn.Close()
    // 获取用户的键盘输入(stdin),将输入数据发送给服务器
    go func() {
        str := make([]byte, 4096)
        for {
            n, err := os.Stdin.Read(str)
            if err != nil {
                fmt.Println("os.Stdin.Read err:", err)
                continue
            }
            // 写给服务器,读多少,写多少
            conn.Write(str[:n])
        }
    }()
    // 回显服务器回发的大写数据
    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)
        if n == 0 {
            fmt.Println("客户端监测到服务端已关闭,断开连接!!!")
            return
        }
        if err != nil {
            fmt.Println("conn.Read err:", err)
            return
        }
        fmt.Println("客户端读到服务器回发:", string(buf[:n]))
    }
}

TCP与UDP的差异

TCPUDP
面向连接面向无连接
要求系统资源较多要求系统资源较少
TCP程序结构较复杂UDP程序结构较简单
使用流式使用数据包式
保证数据准确性不保证数据准确性
保证数据顺序不保证数据顺序
通讯速度较慢通讯速度较快

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Go