第十一章:网络编程

2022-07-08  本文已影响0人  Benedict清水

一、互联网协议介绍

互联网的核心是一系列协议,总称为”互联网协议”(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
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

上一篇下一篇

猜你喜欢

热点阅读