Go网络编程

概述

网络协议

从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。假设,A、B双方欲传输文件。规定:golang

  • 第一次,传输文件名,接收方接收到文件名,应答OK给传输方;
  • 第二次,发送文件的尺寸,接收方接收到该数据再次应答一个OK;
  • 第三次,传输文件内容。一样,接收方接收数据完成后应答OK表示文件内容接收成功。

由此,不管A、B之间传递何种文件,都是经过三次数据传输来完成。A、B之间造成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B之间达成的这个相互遵照的规则即为协议。面试

这种仅在A、B之间被遵照的协议称之为原始协议。编程

当此协议被更多的人采用,不断的增长、改进、维护、完善。最终造成一个稳定的、完整的文件传输协议,被普遍应用于各类文件传输过程当中。该协议就成为一个标准协议。最先的ftp协议就是由此衍生而来。设计模式

典型协议

应用层: 常见的协议有HTTP协议,FTP协议。浏览器

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

网络层: 常见协议有IP协议、ICMP协议、IGMP协议。安全

网络接口层: 常见协议有ARP协议、RARP协议。服务器

各个协议用途简述:微信

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

TCP传输控制协议(Transmission Control Protocol)是一种面向链接的、可靠的、基于字节流的传输层通讯协议。

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

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

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

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

RARP是反向地址转换协议,经过MAC地址肯定IP地址。

分层模型

网络分层架构

为了减小协议设计的复杂性,大多数网络模型均采用分层的方式来组织。每一层都有本身的功能,就像建筑物同样,每一层都靠下一层支持。每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节对上层屏蔽。

造车比喻(协议分层)

减小复杂度,解耦

越下面的层,越靠近硬件;越上面的层,越靠近用户。至于每一层叫什么名字,对应编程而言不重要,但面试的时候,面试官可能会问每一层的名字。

业内广泛的分层方式有两种。OSI七层模型 和TCP/IP四层模型。能够经过背诵两个口诀来快速记忆:

OSI七层模型: 应、表、会、传、网、数、物

TCP/IP四层模型: 应、传、网、链

物理层: 主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各类传输介质的传输速率等。它的主要做用是传输比特流(就是由一、0转化为电流强弱来进行传输,到达目的地后再转化为一、0,也就是咱们常说的数模转换与模数转换)。这一层的数据叫作比特。

数据链路层: 定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层一般还提供错误检测和纠正,以确保数据的可靠传输。如:串口通讯中使用到的115200、八、N、1

网络层: 在位于不一样地理位置的网络中的两个主机系统之间提供链接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增长,而网络层正是管理这种链接的层。

传输层: 定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性偏偏相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是经过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。经常把这一层数据叫作段。

会话层: 经过传输层(端口号:传输端口与接收端口)创建数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间须要互相认识能够是IP也能够是MAC或者是主机名)。

应用层: 是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。

层与协议

每一层都是为了完成一种功能,为了实现这些功能,就须要你们都遵照共同的规则。你们都遵照这规则,就叫作“协议”(protocol)。

网络的每一层,都定义了不少协议。这些协议的总称,叫“TCP/IP协议”。TCP/IP协议是一个你们族,不只仅只有TCP和IP协议,它还包括其它的协议,以下图:

协议功能

链路层

以太网规定,连入网络的全部设备,都必须具备“网卡”接口。数据包必须是从一块网卡,传送到另外一块网卡。经过网卡可以使不一样的计算机之间链接,从而完成数据通讯等功能。网卡的地址——MAC 地址,就是数据包的物理发送地址和物理接收地址。

网卡对应到协议里面就是与链路层ARP协议相关的

每一个网卡有本身惟一的Mac地址

ARP能够帮助借助IP获取Mac地址

RARP能够借助Mac地址获取IP。

网络层

网络层的做用是引进一套新的地址,使得咱们可以区分不一样的计算机是否属于同一个子网络。这套地址就叫作“网络地址”,这是咱们平时所说的IP地址。这个IP地址比如咱们的手机号码,经过手机号码能够获得用户所在的归属地。

网络地址帮助咱们肯定计算机所在的子网络,MAC 地址则将数据包送到该子网络中的目标网卡。网络层协议包含的主要信息是源IP和目的IP。

因而,“网络层”出现之后,每台计算机有了两种地址,一种是 MAC 地址,另外一种是网络地址两种地址之间没有任何联系,MAC 地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合在一块儿。

网络地址帮助咱们肯定计算机所在的子网络,MAC 地址则将数据包送到该子网络中的目标网卡。所以,从逻辑上能够推断,一定是先处理网络地址,而后再处理 MAC 地址。

IP地址本质:2进制数。----点分十进制IP地址(string)

传输层

当咱们一边聊QQ,一边聊微信,当一个数据包从互联网上发来的时候,咱们怎么知道,它是来自QQ的内容,仍是来自微信的内容?

也就是说,咱们还须要一个参数,表示这个数据包到底供哪一个程序(进程)使用。这个参数就叫作“端口”(port),它实际上是每个使用网卡的程序的编号。每一个数据包都发到主机的特定端口,因此不一样的程序就能取到本身所须要的数据。

端口就是在传输层指定的。

port -- 在一台主机上惟一标识一个进程

端口特色:

  • 对于同一个端口,在不一样系统中对应着不一样的进程
  • 对于同一个系统,一个端口只能被一个进程拥有

经常使用协议:TCP、UDP

应用层

应用程序收到“传输层”的数据,接下来就要进行解读。因为互联网是开放架构,数据来源五花八门,必须事先规定好格式,不然根本没法解读。“应用层”的做用,就是规定应用程序的数据格式。

FTP、HTTP、或自定义协议
对数据进行封装、解封装

通讯过程

数据通讯过程

封装: 应用层 ----------------- 传输层 ---------------- 网络层 ----------- 链路层

解封装: 链路层 ------------- 网路层 ------------- 传输层 ------------ 应用层

Socket编程

什么是Socket

Socket,英文含义是【插座、插孔】,通常称之为套接字,用于描述IP地址和端口。能够实现不一样程序间的数据通讯。

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

套接字的内核实现较为复杂,不宜在学习初期深刻学习,了解到以下结构足矣。

在TCP/IP协议中,“IP地址+TCP或UDP端口号”惟一标识网络通信中的一个进程。“IP地址+端口号”就对应一个socket。欲创建链接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就惟一标识一个链接。所以能够用Socket来描述网络链接的一对一关系。

经常使用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向链接的Socket,针对于面向链接的TCP服务应用;数据报式Socket是一种无链接的Socket,对应于无链接的UDP服务应用。

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架构

服务器首先启动一个net.Listen(),这个net.Listen()从名字上看好像是启动一个监听,实际上这是因为套接字socket最先期设计的缘由,在Go语言设计的时候仍是沿用了Unix当初设计的思想,直接把函数名拿过来了,这个函数初学的同窗都会有一个误解,认为它是监听,实际上它不是,这个listen()函数不是真正的监听客户端,要监听的话监听什么?要监听客户端和个人链接,可是这个Listen不是监听客户端,而是我设置服务器监听的资源(IP、端口),Accept()才是真正监听的,那言外之意,监听嘛,我等着你对我进行访问吧,那就是说,你没访问我以前是否是应该一直处于等待状态,一下子咱们写程序看一下,是在Listen()的时候等着仍是在Accept的时候等着,因此Accept是表示接受的意思,当它Accpet调用起来之后,它就等着客户端和我创建链接,比方说,图示上已经说了,它会阻塞等待用户创建链接,那言外之意,我没有用户创建链接以前它就一直阻塞在那里等待着,实际上监听是在Accept的时候才发起的,固然Accept不是无源之水,它必要Listen设置好了链接方式(tcp仍是udp)、IP地址以及端口之后才能阻塞去监听,当有一个客户端和服务器发起请求以后,我调Accept()函数完成了,那就说明我服务器和客户端之间的链接创建好了,接来下干什么呢?进行数据传输,我创建好链接的目的就是为了进行数据传递,咱们这里假定那通常实际上也是这样,客户端主动找服务器创建链接,链接创建好了,客户端先发送数据给服务器,服务器被动的接受客户端发来的请求,被动接受客户端请求数据,接受到了请求之后,服务器进行相应的分析处理,处理完之后把你要请求的数据回写回去,服务端Read()是读取客户端发送过来的请求,Write()是我把你的请求处理完以后再给你写回去,当这些都作完了,说明咱们跟客户端的一次通讯就完成了,那这个时候咱们就能够关闭链接。固然若是你还想后续继续通讯的话,这个close()关闭就要延迟。

客户端这个流程很简单,由于服务器先要站出来在那儿等着客户端和我创建链接,因此说,服务器就得先启动,客户端至关因而我得等你服务器启动起来之后你都准备好了,我在给你发送访问请求,客户端发送访问请求,也是调用一个函数,叫作net.Dail()函数,这个Dail()函数会对阻塞的Accept()发送一个请求,若是服务器准备好,Accept()返回的时候,Dail也返回,我们就说客户端和服务器创建好了链接,客户端先发送数据,因此客户端先是一个写操做,发送完数据,服务器那边读到客户端请求进行处理,处理完以后写回来,客户端再Read()读取服务器写回来的数据,读完之后客户端也能够作简单处理,比方说我读到了之后,打印显示,完成了写,完成了读,一次跟网络端的通讯也就完成了,客户端能够关闭链接,大体的流程就是这样。

前面说过,socket通讯,既然要通讯,至少得是一对,如上图所示,Accpet()和Dail()成功后都会返回一个socket。

其实Listen()的时候也会建立一个socket,可是这个socket不是用于通讯的,只是建立用于通讯的socket,绑定IP地址和端口设置监听的

简单的C/S模型通讯

Server端

Listen函数:

network: 选用的协议:TCP、UDP 如: "tcp"或"udp"

address:IP地址+端口号 如: "127.0.0.1:8000"或":8000"

Listener接口:

Conn接口:

参看 https://studygolang.com/pkgdoc 中文帮助文档中的demo:

TCP服务器端代码:

package main

import (
    "fmt"
    "net"
)

func main() {
    //指定服务器 通讯协议,IP地址,port. 建立一个用户监听的socket
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("服务器设置监听失败,缘由是:%v\n", err)
        return
    }
    defer listener.Close()

    fmt.Println("服务器等待客户端创建链接...")
    //阻塞监听客户端链接请求,成功创建链接,返回用于通讯的socket
    conn, err := listener.Accept()
    if err != nil {
        fmt.Printf("服务器监听失败,缘由是:%v\n", err)
    }
    defer conn.Close()

    fmt.Println("服务器与客户端成功创建链接!!!")
    //读取客户端发送的数据
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("Conn Read()错误,缘由是:%v\n", err)
    }

    //处理数据 -- 打印
    fmt.Println("服务器读到数据:", string(buf[:n]))
}

运行代码

利用nc模式客户端请求

再次查看运行的终端,能够发现已经成功创建了链接

咱们用nc链接后,还能够发送数据

再次查看运行的终端

如图,在整个通讯过程当中,服务器端有两个socket参与进来,但用于通讯的只有 conn 这个socket。它是由 listener建立的。隶属于服务器端。

Client端

Dial函数:

network: 选用的协议:TCP、UDP 如: "tcp"或"udp"

address:IP地址+端口号 如: "127.0.0.1:8000"或":8000"

Conn接口:

客户端代码:

package main

import (
    "fmt"
    "net"
)

func main() {

    //指定服务器IP+port建立 通讯套接字
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial err:%v\n", err)
        return
    }
    defer conn.Close()

    //主动写数据给服务器
    _, err = conn.Write([]byte("Are you ready?"))
    if err != nil {
        fmt.Printf("conn.Write err:%v\n", err)
        return
    }

    buf := make([]byte, 1024)
    //接受服务器回发的数据
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("conn.Read err:%v\n", err)
        return
    }

    fmt.Printf("服务器回发的数据为:%v\n", string(buf[:n]))
}

因为咱们想要服务器端回写内容,因此须要修改一下以前的服务端代码

更新服务器端代码:

package main

import (
    "fmt"
    "net"
)

func main() {
    //指定服务器 通讯协议,IP地址,port. 建立一个用户监听的socket
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("服务器设置监听失败,缘由是:%v\n", err)
        return
    }
    defer listener.Close()

    fmt.Println("服务器等待客户端创建链接...")
    //阻塞监听客户端链接请求,成功创建链接,返回用于通讯的socket
    conn, err := listener.Accept()
    if err != nil {
        fmt.Printf("服务器监听失败,缘由是:%v\n", err)
    }
    defer conn.Close()

    fmt.Println("服务器与客户端成功创建链接!!!")
    //读取客户端发送的数据
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("Conn Read()错误,缘由是:%v\n", err)
    }

    //处理数据 -- 回写给客户端
    if string(buf[:n]) == "Are you ready?" {
        conn.Write([]byte("I am ready!!!"))
    } else {
        conn.Write([]byte("I don`t know what you say!!!"))
    }
}

并发的C/S模型通讯

并发Server

如今已经完成了客户端与服务端的通讯,可是服务端只能接收一个用户发送过来的数据,怎样接收多个客户端发送过来的数据,实现一个高效的并发服务器呢?

Accept()函数的做用是等待客户端的连接,若是客户端没有连接,该方法会阻塞。若是有客户端连接,那么该方法返回一个Socket负责与客户端进行通讯。因此,每来一个客户端,该方法就应该返回一个Socket与其通讯,所以,可使用一个死循环,将Accept()调用过程包裹起来。

须要注意的是,实现并发处理多个客户端数据的服务器,就须要针对每个客户端链接,单独产生一个Socket,并建立一个单独的goroutine与之完成通讯。

func main() {

    //建立监听套接字
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Listen() err:%v\n", err)
        return
    }
    defer listener.Close()

    //监听客户端链接请求
    for {
        fmt.Println("服务器等待客户端链接...")
        conn, err := listener.Accept()
        if err != nil {
            fmt.Printf("listener.Accept() err:%v\n", err)
            return
        }
        //具体完成服务器和客户端的数据通讯
        go HandleConnect(conn)
    }
}

将客户端的数据处理工做封装到HandleConn方法中,需将Accept()返回的Socket传递给该方法,变量conn的类型为:net.Conn。可使用conn.RemoteAddr()来获取成功与服务器创建链接的客户端IP地址和端口号:

客户端可能持续不断的发送数据,所以接收数据的过程能够放在for循环中,服务端也持续不断的向客户端返回处理后的数据。

func HandleConnect(conn net.Conn) {
    defer conn.Close()
    //获取链接的客户端 Addr

    addr := conn.RemoteAddr()
    fmt.Println(addr, "客户端成功链接!")
    //循环读取客户端发送数据
    buf := make([]byte, 1024)
    for {
        //注意,read读取时,会将命令行里的换行符也给读取了,在*Unix上换行符是\n,在Windows上时\r\n
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                fmt.Println("客户端退出了!!!")
                break
            } else {
                fmt.Printf("conn.Read() err:%v\n", err)
                return
            }
        }
        fmt.Printf("服务器读到数据:%v", string(buf[:n]))
        //小写转大写,回发给客户端
        conn.Write(bytes.ToUpper(buf[:n]))
    }
}

并发Client

客户端不只须要持续的向服务端发送数据,同时也要接收从服务端返回的数据。所以可将发送和接收放到不一样的协程中。

主协程循环接收服务器回发的数据(该数据应已转换为大写),并打印至屏幕;子协程循环从键盘读取用户输入数据,写给服务器。读取键盘输入可以使用 os.Stdin.Read(str)。定义切片str,将读到的数据保存至str中。

这样,客户端也实现了多任务。

客户端代码:

package main

import (
    "fmt"
    "io"
    "net"
    "os"
)

func main() {

    //发起链接请求
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial() err:%v\n", err)
        return
    }
    defer conn.Close()

    //获取用户键盘输入(os.Stdin),并将输入数据发送给服务器
    go func() {
        str := make([]byte, 1024)
        for {
            n, err := os.Stdin.Read(str)
            if err != nil {
                fmt.Printf("os.Stdin.Read() err:%v\n", err)
                continue
            }
            //写给服务器,读多少,写多少
            conn.Write(str[:n])
        }
    }()

    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                fmt.Println("服务端退出了!!!")
                return
            } else {
                fmt.Printf("conn.Read() err:%v\n", err)
                continue
            }
        }
        fmt.Printf("客户端读到服务器回发数据:%s",buf[:n])
    }
}

TCP通讯

下图是一次TCP通信的时序图。TCP链接创建断开。包含你们熟知的三次握手和四次挥手。

在这个例子中,首先客户端主动发起链接、发送请求,而后服务器端响应请求,而后客户端主动关闭链接。两条竖线表示通信的两端,从上到下表示时间的前后顺序。注意,数据从一端传到网络的另外一端也须要时间,因此图中的箭头都是斜的。

三次握手

所谓三次握手(Three-Way Handshake)即创建TCP链接,就是指创建一个TCP链接时,须要客户端和服务端总共发送3个包以确认链接的创建。比如两我的在打电话:

Client: "喂,你能听获得吗?"

Server: "我听获得,你听获得我吗?"

Client: "我能听到你,今天balabala..."

创建链接(三次握手)的过程:

  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。在这个过程当中,客户端和服务器分别给对方发了链接请求,也应答了对方的链接请求,其中服务器的请求和应答在一个段中发出。

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

数据传输的过程:

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

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

四次挥手

所谓四次挥手(Four-Way-Wavehand)即终止TCP链接,就是指断开一个TCP链接时,须要客户端和服务端总共发送4个包以确认链接的断开。在socket编程中,这一过程由客户端或服务器任一方执行close来触发。比如两我的打完电话要挂断:

Client: "我要说的事情都说完了,我没事了。挂啦?"

Server: "等下,我还有一个事儿。Balabala…"

Server: "好了,我没事儿了。挂了啊。"

Client: "ok!拜拜"

关闭链接(四次握手)的过程:

因为TCP链接是全双工的,所以每一个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的链接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP链接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另外一方执行被动关闭。

  1. 客户端发出段7,FIN位表示关闭链接的请求。
  2. 服务器发出段8,应答客户端的关闭链接请求。
  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭链接请求。
  4. 客户端发出段10,应答服务器的关闭链接请求。

创建链接的过程是三次握手,而关闭链接一般须要4个段,服务器的应答和关闭链接请求一般不合并在一个段中,由于有链接半关闭的状况,这种状况下客户端关闭链接以后就不能再发送数据给服务器了,可是服务器还能够发送数据给客户端,直到服务器也关闭链接为止。

下图是TCP状态转换图

UDP通讯

在以前的案例中,咱们一直使用的是TCP协议来编写Socket的客户端与服务端。其实也可使用UDP协议来编写Socket的客户端与服务端。

UDP服务器

因为UDP是“无链接”的,因此,服务器端不须要额外建立监听套接字,只须要指定好IP和port,而后监听该地址,等待客户端与之创建链接,便可通讯。

  1. 建立监听地址

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

    ResolveUDPAddr将addr做为UDP地址解析并返回。参数addr格式为"host:port"或"[ipv6-host%zone]:port",解析获得网络名和端口名;net必须是"udp"、"udp4"或"udp6"。
    IPv6地址字面值/名称必须用方括号包起来,如"[::1]:80"、"[ipv6-host]:http"或"[ipv6-host%zone]:80"。

  2. 建立用于通讯的socket

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

    ListenUDP建立一个接收目的地是本地地址laddr的UDP数据包的网络链接。net必须是"udp"、"udp4"、"udp6";若是laddr端口为0,函数将选择一个当前可用的端口,能够用Listener的Addr方法得到该端口。返回的*UDPConn的ReadFrom和WriteTo方法能够用来发送和接收UDP数据包(每一个包均可得到来源地址或设置目标地址)。

  3. 接受UDP数据

    func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)

    ReadFromUDP从c读取一个UDP数据包,将有效负载拷贝到b,返回拷贝字节数和数据包来源地址。

    ReadFromUDP方法会在超过一个固定的时间点以后超时,并返回一个错误。

  4. 写出数据到UDP

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

    WriteToUDP经过c向地址addr发送一个数据包,b为包的有效负载,返回写入的字节。

    WriteToUDP方法会在超过一个固定的时间点以后超时,并返回一个错误。在面向数据包的链接上,写入超时是十分罕见的。

服务器端代码:

package main

import (
    "fmt"
    "net"
)

func main() {

    //0.本应从步骤1开始,可是在写步骤1的时候发现,步骤1还须要*UDPAddr类型的参数,因此须要先建立一个*DUPAddr
    //组织一个udp地址结构,指定服务器的IP+port
    udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.ResolveUDPAddr()函数执行出错,错误为:%v\n", err)
        return
    }
    fmt.Printf("UDP服务器地址结构建立完成!!!\n")

    //1.建立用户通讯的socket
    //因为ListenUDP须要一个*UDPAddr类型的参数,因此咱们还须要先建立一个监听地址
    udpConn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        fmt.Printf("net.ListenUDP()函数执行出错,错误为:%v\n", err)
        return
    }
    defer udpConn.Close()
    fmt.Printf("UDP服务器通讯socket建立完成!!!\n")

    //2.读取客户端发送的数据(阻塞发生在ReadFromUDP()方法中)
    buf := make([]byte, 4096)
    //ReadFromUDP()方法返回三个值,分别是读取到的字节数,客户端的地址,error
    n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
    if err != nil {
        fmt.Printf("*UDPAddr.ReadFromUDP()方法执行出错,错误为:%v\n", err)
        return
    }
    //3.模拟处理数据
    fmt.Printf("服务器读到%v的数据:%s",clientUDPAddr, buf[:n])

    //4.回写数据给客户端
    _, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
    if err != nil {
        fmt.Printf("*UDPAddr.WriteToUDP()方法执行出错,错误为:%v\n", err)
        return
    }
}

运行代码

经过nc请求测试

服务端读取请求数据,并回写"I am OK!"

UDP客户端

udp客户端的编写与TCP客户端的编写,基本上是同样的,只是将协议换成udp.代码以下:

package main

import (
    "fmt"
    "net"
)

func main() {
    conn, err := net.Dial("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial()函数执行出错,错误为:%v\n", err)
        return
    }
    defer conn.Close()

    conn.Write([]byte("hello, I`m a client in UDP!"))

    buf := make([]byte, 4096)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("Conn.Read()方法执行出错,错误为:%v\n", err)
        return
    }
    fmt.Printf("服务器发来数据:%s\n", buf[:n])
}

并发

其实对于UDP而言,服务器不须要并发,只要循环处理客户端数据便可。客户端也等同于TCP通讯并发的客户端。

服务器:

package main

import (
    "fmt"
    "net"
)

func main() {

    //0.本应从步骤1开始,可是在写步骤1的时候发现,步骤1还须要*UDPAddr类型的参数,因此须要先建立一个*DUPAddr
    //组织一个udp地址结构,指定服务器的IP+port
    udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.ResolveUDPAddr()函数执行出错,错误为:%v\n", err)
        return
    }
    fmt.Printf("UDP服务器地址结构建立完成!!!\n")

    //1.建立用户通讯的socket
    //因为ListenUDP须要一个*UDPAddr类型的参数,因此咱们还须要先建立一个监听地址
    udpConn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        fmt.Printf("net.ListenUDP()函数执行出错,错误为:%v\n", err)
        return
    }
    defer udpConn.Close()
    fmt.Printf("UDP服务器通讯socket建立完成!!!\n")

    for {
        //2.读取客户端发送的数据(阻塞发生在ReadFromUDP()方法中)
        buf := make([]byte, 4096)
        //ReadFromUDP()方法返回三个值,分别是读取到的字节数,客户端的地址,error
        n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
        if err != nil {
            fmt.Printf("*UDPAddr.ReadFromUDP()方法执行出错,错误为:%v\n", err)
            continue
        }
        //3.模拟处理数据
        fmt.Printf("服务器读到%v的数据:%s\n",clientUDPAddr, buf[:n])

        //4.回写数据给客户端
        _, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
        if err != nil {
            fmt.Printf("*UDPAddr.WriteToUDP()方法执行出错,错误为:%v\n", err)
            continue
        }
    }
}

客户端:

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial()函数执行出错,错误为:%v\n", err)
        return
    }
    defer conn.Close()

    go func() {
        buf := make([]byte, 4096)
        for {
            //从键盘读取内容,放入buf
            n, err := os.Stdin.Read(buf)
            if err != nil {
                fmt.Printf("os.Stdin.Read()执行出错,错误为:%v\n", err)
                return
            }
            //给服务器发送
            conn.Write(buf[:n])
        }
    }()
    for {
        buf := make([]byte, 4096)
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Printf("Conn.Read()方法执行出错,错误为:%v\n", err)
            return
        }
        fmt.Printf("服务器发来数据:%s\n", buf[:n])
    }
}

UDP与TCP的差别

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

使用场景

TCP: 对数据传输安全性、稳定性要求高的场合。网络文件传输。下载、上传。

UDP: 对数据实时传输要求较高的场合。视频直播、在线电话会议。游戏

相关文章:

http请求是如何先创建的三次握手?

相关文章
相关标签/搜索