一、互联网协议介绍
互联网的核心是一系列协议,总称为”互联网协议”(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等协议,这些协 议就组成了互联网协议的应用层。
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
网友评论