美文网首页
第十一章:网络编程

第十一章:网络编程

作者: Benedict清水 | 来源:发表于2022-07-08 14:18 被阅读0次

    一、互联网协议介绍

    互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协 议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。由于这些协议太过庞大 和复杂,没有办法在这里一概而全,只能介绍一下我们日常开发中接触较多的几个协议。

    1.1互联网分层模型

    互联网按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层 越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层 的模型。


    image.png
    物理层

    我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用双绞线、光纤、无线电波等方 式。这就叫做”实物理层”,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作 用是负责传送0和1的电信号。

    数据链路层

    单纯的0和1没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例 如:多少个电信号算一组?每个信号位有何意义?这就是”数据链接层”的功能,它在”物理层”的上方, 确定了物理层传输的0和1的分组方式及代表的意义。早期的时候,每家公司都有自己的电信号分组方 式。逐渐地,一种叫做”以太网”(Ethernet)的协议,占据了主导地位。

    以太网规定,一组电信号构成一个数据包,叫做”帧”(Frame)。每一帧分成两个部分:标头 (Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等 等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节, 最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多 个帧进行发送。

    那么,发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有”网卡”接口。数据 包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做 MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用 12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。

    我们会通过ARP协议来获取接受方的MAC地址,有了MAC地址之后,如何把数据准确的发送给接收方呢? 其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计 算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比 较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广 播”(broadcasting)。

    网络层

    按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就 可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播 方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网 络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,因为 如果互联网上每一台计算机都会收到互联网上收发的所有数据包,那是不现实的。

    因此,必须找到一种方法区分哪些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采 用广播方式发送,否则就采用”路由”方式发送。这就导致了”网络层”的诞生。它的作用是引进一套新的 地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网 址。

    “网络层”出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没 有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助我们确定计 算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定 是先处理网络地址,然后再处理MAC地址。

    规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议 第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的 十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。

    根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主 要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的 长度为20到60字节,整个数据包的总长度最大为65535字节。

    传输层

    有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会 有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如 何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪 个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每 个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

    “端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用 大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。

    我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就 是在数据前面,加上端口号。UDP数据包,也是由”标头”和”数据”两部分组成:”标头”部分主要定义了 发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包非常简单,”标头”部分一共只有8个字 节,总长度不超过65,535字节,正好放进一个IP数据包。

    UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否 收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它 的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是 为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再 分割。

    应用层

    应用程序收到”传输层”的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八 门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用 就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协 议就组成了互联网协议的应用层。

    如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到 数据包之后再依次根据协议解包得到数据。 image.png
    1.2 Sokcet 编程

    Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链 的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发 TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请 求。

    1.2.1 socket图解

    Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面 模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函 数,让Socket去组织符合指定的协议数据然后进行通信。


    image.png
    • Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请
    • 常用的Socket类型有两种:流式Socket和数据报式Socket,流式是一种面向连接的Socket, 针对于面向连接的TCP服务应用,数据报式Socket是一种无连接的Socket,针对于无连接的UDP 服务应用
    • TCP:比较靠谱,面向连接,比较慢
    • UDP:不是太靠谱,比较快
    1.2.2 传统的socket编程

    以前我们使用Socket编程时, 会按照如下步骤展开。
    (1) 建立Socket:使用socket()函数。
    (2) 绑定Socket:使用bind()函数。
    (3) 监听:使用listen()函数。或者连接:使用connect()函数。
    (4) 接受连接:使用accept()函数。
    (5) 接收:使用receive()函数。或者发送:使用send()函数。
    我们用先用python来演示:
    TCP编程:server.py

    # 以前我们使用Socket编程时, 会按照如下步骤展开。
    # (1) 建立Socket:使用socket()函数。
    # (2) 绑定Socket:使用bind()函数。
    # (3) 监听:使用listen()函数。或者连接:使用connect()函数。
    # (4) 接受连接:使用accept()函数。
    # (5) 接收:使用receive()函数。或者发送:使用send()函数。
    import socket
    
    
    def tcp():
        # 创建流式套接字
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
        # 绑定ip和端口号
        ADDR = ("127.0.0.1", 8888)
        s.bind(ADDR)
    
        # 设置监听
        s.listen(5)
    
        while True:
            print("waiting for Connection...")
            # 接收连接
            conn, addr = s.accept()
            print("connect to client:", addr)
            while True:
                # 接收
                data = conn.recv(1024).decode()
                if not data:
                    break
                print("receive:", data)
                # 发送
                n = conn.send('Receive your message!'.encode())
                print('feedback %d byte' % n)
            # 关闭连接
            conn.close()
        # 关闭套接字
        s.close()
    
    
    if __name__ == "__main__":
        tcp()
    

    TCP编程:client.py

    # 以前我们使用Socket编程时, 会按照如下步骤展开。
    # (1) 建立Socket:使用socket()函数。
    # (2) 绑定Socket:使用bind()函数。
    # (3) 监听:使用listen()函数。或者连接:使用connect()函数。
    # (4) 接受连接:使用accept()函数。
    # (5) 接收:使用receive()函数。或者发送:使用send()函数。
    
    import socket
    
    
    def tcp():
        # 创建套流式接字
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        ADDR = ("127.0.0.1", 8888)
    
        # 向服务端发起连接请求
        s.connect(ADDR)
    
        while True:
            data = input("发送消息>>>") or 'q'
            if data == 'q':
                break
            # 发送
            s.send(data.encode())
            # 接收
            back = s.recv(1024)
            print(back.decode())
    
        s.close()
    
    
    if __name__ == "__main__":
        tcp()
    
    运行结果: image.png

    UDP编程:server.py

    # 以前我们使用Socket编程时, 会按照如下步骤展开。
    # (1) 建立Socket:使用socket()函数。
    # (2) 绑定Socket:使用bind()函数。
    # (3) 接收:使用receive()函数。或者发送:使用send()函数。
    import socket
    
    
    def udp():
        # 创建套接字
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        addr = ("127.0.0.1", 8888)
        # 绑定
        s.bind(addr)
        while True:
            # 接收
            data, addr = s.recvfrom(1024)
            print("Receive from {} : {}".format(addr, data.decode()))
            # 发送
            s.sendto('Receive your message'.encode(), addr)
        # 关闭套接字
        s.close()
    
    
    if __name__ == "__main__":
        udp()
    

    UDP编程:client.py

    # 以前我们使用Socket编程时, 会按照如下步骤展开。
    # (1) 建立Socket:使用socket()函数。
    # (2) 绑定Socket:使用bind()函数。
    # (3) 接收:使用receive()函数。或者发送:使用send()函数。
    import socket
    
    
    def udp():
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        addr = ("127.0.0.1", 8888)
        while True:
            data = input("Send the message >>>") or 'q'
            if data == 'q':
                break
            # 发送
            s.sendto(data.encode(), addr)
            # 接收
            r_data, addr = s.recvfrom(1024)
            print(r_data.decode())
    
        # 关闭套接字
        s.close()
    
    
    if __name__ == "__main__":
        udp()
    
    运行结果: image.png
    1.2.3 Go的网络编程

    Go语言标准库对传统的过程进行了抽象和封装。无论我们期望使用什么协议建立什么形式的连接,都只需要调用net.Dial()即可。
    Dial()函数
    Dial()函数的原型如下:

    func Dial(net, addr string) (Conn, error)
    

    其中net参数是网络协议的名字,addr参数是IP地址或域名,而端口号以“:”的形式跟随在地址 或域名的后面,端口号可选。如果连接成功,返回连接对象,否则返回error。

    我们来看一下几种常见协议的调用方式。 TCP链接:

    conn, err := net.Dial("tcp", "192.168.0.10:2100")
    

    UDP链接:

    conn, err := net.Dial("udp", "192.168.0.12:975")
    

    ICMP链接(使用协议名称):

    conn, err := net.Dial("ip4:icmp", "www.baidu.com")
    

    ICMP链接(使用协议编号):

    conn, err := net.Dial("ip4:1", "10.0.0.3")
    

    这里我们可以通过以下链接查看协议编号的含义:http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xml。 目前,Dial()函数支持如下几种网络协议:"tcp"、"tcp4"(仅限IPv4)、"tcp6"(仅限 IPv6)、"udp"、"udp4"(仅限IPv4)、"udp6"(仅限IPv6)、"ip"、"ip4"(仅限IPv4)和"ip6"(仅限IPv6)。
    在成功建立连接后,我们就可以进行数据的发送和接收。发送数据时,使用conn的Write() 成员方法,接收数据时使用Read()方法。
    TCP编程:server.go

    //1.监听端口
    //2.接收客户端请求建立连接
    //3.创建goroutine处理连接
    package main
    
    import (
        "bufio"
        "fmt"
        "net"
    )
    
    func process(conn net.Conn) {
        defer conn.Close()
        for {
            reader := bufio.NewReader(conn)
            var buf [128]byte
            n, err := reader.Read(buf[:]) //读取数据
            if err != nil {
                fmt.Println("read from client failed, err:", err)
                break
            }
            revStr := string(buf[:n])
            fmt.Println("收到Client端发来的消息:", revStr)
            respStr := "Receive your message"
            conn.Write([]byte(respStr))
        }
    }
    
    func main() {
        listen, err := net.Listen("tcp", "127.0.0.1:9999")
        if err != nil {
            fmt.Println("Listen() failed, err:", err)
            return
        }
        fmt.Println("Waiting for connection...")
        for {
            conn, err := listen.Accept()
            if err != nil {
                fmt.Println("Accept() failed, err:", err)
            }
            go process(conn)
        }
    }
    

    TCP编程:client.go

    //1.建立与服务端的连接
    //2.进行数据收发
    //3.关闭连接
    package main
    
    import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strings"
    )
    
    // TCP 客户端
    func main() {
        conn, err := net.Dial("tcp", "127.0.0.1:9999")
        if err != nil {
            fmt.Println("err : ", err)
            return
        }
        defer conn.Close() // 关闭TCP连接
        inputReader := bufio.NewReader(os.Stdin)
        for {
            fmt.Print("发送消息:")
            input, _ := inputReader.ReadString('\n') // 读取用户输入
            inputInfo := strings.Trim(input, "\r\n")
            if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
                return
            }
            _, err := conn.Write([]byte(inputInfo)) // 发送数据
            if err != nil {
                return
            }
            buf := [512]byte{}
            n, err := conn.Read(buf[:])
            if err != nil {
                fmt.Println("recv failed, err:", err)
                return
            }
            fmt.Println(string(buf[:n]))
        }
    }
    
    运行结果: image.png

    UDP编程:server.go

    package main
    
    import (
        "fmt"
        "net"
    )
    
    func main() {
        listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9981})
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())
        data := make([]byte, 1024)
        defer listener.Close()
        for {
            n, remoteAddr, err := listener.ReadFromUDP(data)
            if err != nil {
                fmt.Printf("error during read: %s", err)
            }
            fmt.Printf("<%s> %s\n", remoteAddr, data[:n])
            _, err = listener.WriteToUDP([]byte("Receive your message!"), remoteAddr)
            if err != nil {
                fmt.Printf(err.Error())
            }
        }
    }
    

    UDP编程:client.go

    package main
    
    import (
        "bufio"
        "fmt"
        "net"
        "os"
        "strings"
    )
    
    func main() {
        ip := net.ParseIP("127.0.0.1")
        srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
        dstAddr := &net.UDPAddr{IP: ip, Port: 9981}
        conn, err := net.DialUDP("udp", srcAddr, dstAddr)
        if err != nil {
            fmt.Println(err)
        }
        defer conn.Close()
        inputReader := bufio.NewReader(os.Stdin)
        for {
            fmt.Print("发送消息:")
            input, _ := inputReader.ReadString('\n') // 读取用户输入
            inputInfo := strings.Trim(input, "\r\n")
            if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
                return
            }
            _, err := conn.Write([]byte(inputInfo)) // 发送数据
            if err != nil {
                return
            }
            buf := [512]byte{}
            n, err := conn.Read(buf[:])
            if err != nil {
                fmt.Println("recv failed, err:", err)
                return
            }
            fmt.Printf("<%s>:", conn.RemoteAddr())
            fmt.Println(string(buf[:n]))
        }
    }
    
    运行结果: image.png
    1.3websocket编程

    服务端

    package main
    import (
       "fmt"
       "github.com/gorilla/websocket"
       "log"
       "net/http"
    )
    var upgrader = websocket.Upgrader{
       ReadBufferSize:  1024,
       WriteBufferSize: 1024,
    }
    func homePage(w http.ResponseWriter, r *http.Request) {
       fmt.Fprintf(w, "Home page")
    }
    func reader(conn *websocket.Conn) {
       for {
          // read in a message
          messageType, p, err := conn.ReadMessage()
          if err != nil {
             log.Println(err)
             return
          }
          // print out that message for clarity
          fmt.Println(string(p))
          if err := conn.WriteMessage(messageType, p); err != nil {
             log.Println(err)
             return
          }
       }
    }
    func wsEndpoint(w http.ResponseWriter, r *http.Request) {
       upgrader.CheckOrigin = func(r *http.Request) bool { return true }
       ws, err := upgrader.Upgrade(w, r, nil)
       if err != nil {
          log.Println(err)
       }
       log.Println("Client Connected")
       err = ws.WriteMessage(1, []byte("Hi Client!"))
       if err != nil {
          log.Println(err)
       }
       reader(ws)
    }
    func setupRoutes() {
       http.HandleFunc("/", homePage)
       http.HandleFunc("/ws", wsEndpoint)
    }
    func main() {
       fmt.Println("Hello World")
       setupRoutes()
       log.Fatal(http.ListenAndServe(":8080", nil))
    }
    

    客户端

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta http-equiv="X-UA-Compatible" content="ie=edge" />
      <title>Go WebSocket Tutorial</title>
    </head>
    <body>
    <h2>Hello World</h2>
    <script>
      let socket = new WebSocket("ws://127.0.0.1:8080/ws");
      console.log("Attempting Connection...");
      socket.onopen = () => {
        console.log("Successfully Connected");
        socket.send("Hi From the Client!")
      };
      socket.onmessage = (evt) => {
        console.log(evt.data)
      }
      socket.onclose = event => {
        console.log("Socket Closed Connection: ", event);
        socket.send("Client Closed!")
      };
      socket.onerror = error => {
        console.log("Socket Error: ", error);
      };
    </script>
    </body>
    </html>
    

    参考文献:《Go语言编程》许式伟 吕桂华
    https://www.jianshu.com/p/7b1a0777629e
    https://zhuanlan.zhihu.com/p/455635795

    相关文章

      网友评论

          本文标题:第十一章:网络编程

          本文链接:https://www.haomeiwen.com/subject/vbwsbrtx.html