• Golang网络编程-套接字(socket)篇


                Golang网络编程-套接字(socket)篇

                                   作者:尹正杰

    版权声明:原创作品,谢绝转载!否则将追究法律责任。

     

     

     

    一.网络概述

    1>.什么是协议

      从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。假设,A、B双方欲传输文件。规定:
        第一次,传输文件名,接收方接收到文件名,应答OK给传输方;
        第二次,发送文件的尺寸,接收方接收到该数据再次应答一个OK;
        第三次,传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。
    
      由此,无论A、B之间传递何种文件,都是通过三次数据传输来完成。A、B之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B之间达成的这个相互遵守的规则即为协议。
    
      这种仅在A、B之间被遵守的协议称之为原始协议。
    
      当此协议被更多的人采用,不断的增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为一个标准协议。最早的ftp协议就是由此衍生而来。
    
      典型协议:
        应用层:
          常见的协议有HTTP协议,FTP协议。
          HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
          FTP文件传输协议(File Transfer Protocol)
        传输层:
          常见协议有TCP/UDP协议。
          TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
          UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
        网络层:
          常见协议有IP协议、ICMP协议、IGMP协议。
          IP协议是因特网互联协议(Internet Protocol)
          ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
          IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
        链路层:
          常见协议有ARP协议、RARP协议。
          ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
          RARP是反向地址转换协议,通过MAC地址确定IP地址。

    2>.什么是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,针对于面向连接的TCP服务应用;     数据报式Socket(SOCK_DGRAM):       数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。      温馨提示:          套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。

    3>.网络应用程序设计模式

      C/S模式
        传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
        优点:
          1>.客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。
          2>.一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。
          因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。
        缺点:
          1>.由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。
          2>.从用户角度出发,需要将客户端安装至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。   B/S模式     浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。     优点:       1>.B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。
          2>.另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。     缺点:       1>.B
    /S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。
          2>.没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。
          3>.必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。
      综上所述,在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。

    4>.博主推荐阅读

      计算机网络基础之网络拓扑介绍:
        https://www.cnblogs.com/yinzhengjie/p/11846279.html
    
      计算机网络基础之OSI参考模型(理论上的标准):
        https://www.cnblogs.com/yinzhengjie/p/11846473.html
    
      计算机网络基础之网络设备:
        https://www.cnblogs.com/yinzhengjie/p/11853809.html
    
      计算机网络基础之TCP/IP 协议栈(事实上的标准):
        https://www.cnblogs.com/yinzhengjie/p/11854107.html
    
      计算机网络基础之IP地址详解:
        https://www.cnblogs.com/yinzhengjie/p/11854562.html

    二.TCP的socket编程实战案例

    1>.简单C/S模型通信

     

    package main
    
    import (
        "fmt"
        "net"
    )
    
    func main() {
    
        /**
        使用Listen函数创建监听socket,其函数签名如下:
            func Listen(network, address string) (Listener, error)
        以下是对函数签名的参数说明:
            network:
                指定服务端socket的协议,如tcp/udp,注意是小写字母哟~
            address:
                指定服务端监听的IP地址和端口号,如果不指定地址默认监听当前服务器所有IP地址哟~
        */
        socket, err := net.Listen("tcp", "127.0.0.1:8888")
        if err != nil {
            fmt.Println("开启监听失败,错误原因: ", err)
            return
        }
        defer socket.Close()
        fmt.Println("开启监听...")
        for {
            /**
            等待客户端连接请求
            */
            conn, err := socket.Accept()
            if err != nil {
                fmt.Println("建立链接失败,错误原因: ", err)
                return
            }
            defer conn.Close()
            fmt.Println("建立链接成功,客户端地址是: ", conn.RemoteAddr())
    
            /**
            接收客户端数据
            */
            buf := make([]byte, 1024)
            conn.Read(buf)
            fmt.Printf("读取到客户端的数据为: %s
    ", string(buf))
    
            /**
            发送数据给客户端
            */
            tmp := "Blog地址:[https://www.cnblogs.com/yinzhengjie/]"
            conn.Write([]byte(tmp))
        }
    }
    简单版本服务端代码
    package main
    
    import (
        "fmt"
        "net"
    )
    
    func main() {
    
        /**
        使用Dial函数链接服务端,其函数签名如下所示:
            func Dial(network, address string) (Conn, error)
        以下是对函数签名的各参数说明:
            network:
                指定客户端socket的协议,如tcp/udp,该协议应该和需要链接服务端的协议一致哟~
            address:
                指定客户端需要链接服务端的socket信息,即指定服务端的IP地址和端口
        */
        conn, err := net.Dial("tcp", "127.0.0.1:8888")
        if err != nil {
            fmt.Println("连接服务端出错,错误原因: ", err)
            return
        }
        defer conn.Close()
        fmt.Println("与服务端连接建立成功...")
        /**
        给服务端发送数据
        */
        conn.Write([]byte("服务端,请问博客地址的URL是多少呢?"))
    
        /**
        获取服务器的应答
        */
        var buf = make([]byte, 1024)
        conn.Read(buf)
        fmt.Printf("从服务端获取到的数据为:%s
    ", string(buf))
    }
    简单版本客户端代码
    package main
    
    import (
        "fmt"
        "net"
        "strconv"
    )
    
    func main() {
    
        /**
        使用Listen函数创建监听socket,其函数签名如下:
            func Listen(network, address string) (Listener, error)
        以下是对函数签名的参数说明:
            network:
                指定服务端socket的协议,如tcp/udp,注意是小写字母哟~
            address:
                指定服务端监听的IP地址和端口号,如果不指定地址默认监听当前服务器所有IP地址哟~
        */
        socket, err := net.Listen("tcp", "127.0.0.1:8888")
        if err != nil {
            fmt.Println("开启监听失败,错误原因: ", err)
            return
        }
        defer socket.Close()
        fmt.Println("开启监听...")
    
        for {
            /**
            等待客户端连接请求
            */
            conn, err := socket.Accept()
            if err != nil {
                fmt.Println("建立链接失败,错误原因: ", err)
                return
            }
            defer conn.Close()
            fmt.Println("建立链接成功,客户端地址是: ", conn.RemoteAddr())
    
            /**
            分两次接收客户端数据:
                第一次最终接收数据的长度;
                第二次根据第一次接受的长度,创建容量大小;
            */
            tmp := make([]byte, 2)
    
            conn.Read(tmp)
            dataLength, err := strconv.Atoi(string(tmp)) //把字节切片转换成整型
            if err != nil {
                fmt.Println("获取数据长度失败: ", err)
                return
            }
            fmt.Println("获取到的数据长度是: ", dataLength)
    
            conn.Write([]byte("已获取到数据长度"))
    
            /**
            开始读取数据
            */
            buf := make([]byte, dataLength)
            conn.Read(buf)
            fmt.Printf("读取到客户端的数据为: %s
    ", string(buf))
    
            /**
            发送数据给客户端
            */
            data := "Blog地址:[https://www.cnblogs.com/yinzhengjie/]"
            conn.Write([]byte(data))
        }
    }
    简单版本服务端代码(优化版)
    package main
    
    import (
        "fmt"
        "net"
        "strconv"
    )
    
    func main() {
    
        /**
        使用Dial函数链接服务端,其函数签名如下所示:
            func Dial(network, address string) (Conn, error)
        以下是对函数签名的各参数说明:
            network:
                指定客户端socket的协议,如tcp/udp,该协议应该和需要链接服务端的协议一致哟~
            address:
                指定客户端需要链接服务端的socket信息,即指定服务端的IP地址和端口
        */
        conn, err := net.Dial("tcp", "127.0.0.1:8888")
        if err != nil {
            fmt.Println("连接服务端出错,错误原因: ", err)
            return
        }
        defer conn.Close()
        fmt.Println("与服务端连接建立成功...")
    
        /**
        定义需要发送的数据,第一次给服务端发送要发的长度
        */
        data := []byte("服务端,请问博客地址的URL是多少呢?")
        lenData := len(data)
    
        /**
        给服务端发送数据的长度
        */
        conn.Write([]byte(strconv.Itoa(lenData)))
    
        /**
        获取服务器的应答
        */
        var buf = make([]byte, 1024)
        conn.Read(buf)
        fmt.Printf("从服务端获取到的数据为:%s
    ", string(buf))
    
        /**
        第二次给服务器发送数据
        */
        conn.Write(data)
        conn.Read(buf)
        fmt.Printf("获取到的数据为:%s
    ", string(buf))
    }
    简单版本客户端代码(优化版)

    2>.并发C/S模型通信

    package main
    
    import (
        "fmt"
        "net"
        "strings"
    )
    
    func HandleConn(conn net.Conn) {
        //函数调用完毕,自动关闭conn
        defer conn.Close()
    
        //获取客户端的网络地址信息
        addr := conn.RemoteAddr().String()
        fmt.Println(addr, " conncet sucessful")
    
        buf := make([]byte, 2048)
    
        for {
            //读取用户数据
            n, err := conn.Read(buf)
            if err != nil {
                fmt.Println("err = ", err)
                return
            }
            fmt.Printf("[%s]: %s
    ", addr, string(buf[:n]))
            fmt.Println("len = ", len(string(buf[:n])))
    
            //if "exit" == string(buf[:n-1]) {     // nc测试,发送时,只有 
    
            if "exit" == string(buf[:n-2]) { // 自己写的客户端测试, 发送时,多了2个字符, "
    "
                fmt.Println(addr, " exit")
                return
            }
    
            //把数据转换为大写,再给用户发送
            conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
        }
    }
    
    func main() {
    
        /**
          使用Listen函数创建监听socket,其函数签名如下:
              func Listen(network, address string) (Listener, error)
          以下是对函数签名的参数说明:
              network:
                  指定服务端socket的协议,如tcp/udp,注意是小写字母哟~
              address:
                  指定服务端监听的IP地址和端口号,如果不指定地址默认监听当前服务器所有IP地址哟~
        */
        socket, err := net.Listen("tcp", "127.0.0.1:8888")
        if err != nil {
            fmt.Println("开启监听失败,错误原因: ", err)
            return
        }
        defer socket.Close()
        fmt.Println("开启监听...")
    
        //接收多个用户
        for {
            /**
              等待客户端连接请求
            */
            conn, err := socket.Accept()
            if err != nil {
                fmt.Println("建立链接失败,错误原因: ", err)
                return
            }
    
            //处理用户请求, 新建一个go程
            go HandleConn(conn)
        }
    }
    服务端代码
    package main
    
    import (
        "fmt"
        "net"
        "strconv"
    )
    
    func main() {
    
        /**
        使用Dial函数链接服务端,其函数签名如下所示:
            func Dial(network, address string) (Conn, error)
        以下是对函数签名的各参数说明:
            network:
                指定客户端socket的协议,如tcp/udp,该协议应该和需要链接服务端的协议一致哟~
            address:
                指定客户端需要链接服务端的socket信息,即指定服务端的IP地址和端口
        */
        conn, err := net.Dial("tcp", "127.0.0.1:8888")
        if err != nil {
            fmt.Println("连接服务端出错,错误原因: ", err)
            return
        }
        defer conn.Close()
        fmt.Println("与服务端连接建立成功...")
    
        /**
        定义需要发送的数据,第一次给服务端发送要发的长度
        */
        data := []byte("服务端,请问博客地址的URL是多少呢?")
        lenData := len(data)
    
        /**
        给服务端发送数据的长度
        */
        conn.Write([]byte(strconv.Itoa(lenData)))
    
        /**
        获取服务器的应答
        */
        var buf = make([]byte, 1024)
        conn.Read(buf)
        fmt.Printf("从服务端获取到的数据为:%s
    ", string(buf))
    
        /**
        第二次给服务器发送数据
        */
        conn.Write(data)
        conn.Read(buf)
        fmt.Printf("获取到的数据为:%s
    ", string(buf))
    }
    客户端代码

    三.UDP的socket编程实战案例

    1>.UDPTCP的差异概述

    TCP和UDP的主要区别如下:
      1>.TCP是面向连接,UDP是面向无连接
        TCP在建立/端口连接时分别要进行三次握手/四次断开,所以我们说TCP是可靠的连接,而说UDP是不可靠的连接;
      2>.TCP是流式传输,可能会出现"粘包"问题,UDP是数据报传输,UDP可能会出现"丢包"问题
        "粘包"问题可以通过发送数据包的长度解决
        "丢包"问题可以通过每一个数据报添加标识位
      3>.TCP要求系统资源较多,UDP要求系统资源较少
        TCP需要创建连接再进行通信,所以效率要比UDP慢
      4>.TCP程序结构较复杂,UDP程序结构较简单
      5>.TCP可以保证数据的准确性,而UDP则不保证数据的准确性

    应用场景:
      TCP的应用场景:
        比如文件传输,重要数据传输等。
      UDP的应用常见:
        比如打电话,直播等.

    2>.简单C/S模型通信

    package main
    
    import (
        "fmt"
        "net"
    )
    
    func main() {
        /**
        创建监听的地址,并且指定udp协议
        */
        udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:9999")
        if err != nil {
            fmt.Println("获取监听地址失败,错误原因: ", err)
            return
        }
    
        /**
        创建数据通信socket
        */
        conn, err := net.ListenUDP("udp", udp_addr)
        if err != nil {
            fmt.Println("开启UDP监听失败,错误原因: ", err)
            return
        }
        defer conn.Close()
    
        fmt.Println("开启监听...")
    
        buf := make([]byte, 1024)
    
        /**
        通过ReadFromUDP可以读取数据,可以返回如下三个参数:
            dataLength:
                数据的长度
            raddr:
                远程的客户端地址
            err:
                错误信息
        */
        dataLength, raddr, err := conn.ReadFromUDP(buf)
        if err != nil {
            fmt.Println("获取客户端传递数据失败,错误原因: ", err)
            return
        }
        fmt.Println("获取到客户端的数据为: ", string(buf[:dataLength]))
    
        /**
        写回数据
        */
        conn.WriteToUDP([]byte("服务端已经收到数据啦~"), raddr)
    }
    简单版本服务端代码
    package main
    
    import (
        "fmt"
        "net"
    )
    
    func main() {
        /**
        使用Dial函数链接服务端,其函数签名如下所示:
            func Dial(network, address string) (Conn, error)
        以下是对函数签名的各参数说明:
            network:
                指定客户端socket的协议,如tcp/udp,该协议应该和需要链接服务端的协议一致哟~
            address:
                指定客户端需要链接服务端的socket信息,即指定服务端的IP地址和端口
        */
        conn, err := net.Dial("udp", "127.0.0.1:9999")
        if err != nil {
            fmt.Println("连接服务端出错,错误原因: ", err)
            return
        }
        defer conn.Close()
        fmt.Println("与服务端连接建立成功...")
    
        /**
        给服务端发送数据
        */
        conn.Write([]byte("Hi,My name is Jason Yin."))
    
        /**
        读取服务端返回的数据
        */
        tmp := make([]byte, 1024)
        n, _ := conn.Read(tmp)
        fmt.Println("获取到服务器返回的数据为: ", string(tmp[:n]))
    }
    简单版本客户端代码

     

     

  • 相关阅读:
    蓝书3.6 割点与桥
    蓝书3.5 强连通分量
    蓝书3.4 差分约束系统
    蓝书3.3 SPFA算法的优化
    蓝书3.2 最短路
    蓝书3.1 最小生成树
    luogu 4630 [APIO2018] Duathlon 铁人两项
    Codeforces Round #124 (Div. 1) C. Paint Tree(极角排序)
    dutacm.club Water Problem(矩阵快速幂)
    dutacm.club 1094: 等差区间(RMQ区间最大、最小值,区间GCD)
  • 原文地址:https://www.cnblogs.com/yinzhengjie2020/p/12717312.html
Copyright © 2020-2023  润新知