浅谈Go网络编程

聊一聊Go网络编程开发,说到网络编程开发必然离不开网络协议,大家最熟知的莫过于TCP/IP体系,实际上开发中,大家接触的最多的还是HTTP协议。在理论上,OSI模型把网络分成7层,但实际应用中是分为4层,从下到上分别是物理接口层、网络层、传输层、应用层。

每一层做不同的事情,分工明确,同时便于扩展,比如物理层指的就是网卡,又被称为调制解调器,它负责把电信号和数字信号之间做转换,这一块其实很多人不理解,也是非常难理解的地方,下面咱就聊一聊。

1.OSI网络模型

物理层

首先,大家需要知道,我们在电脑上(虚拟世界)在键盘上敲的文字最终会被转换成01010011这样的形式去保存或传输。有人就杠了,我明明看到记事本里面存的是中文啊,不是0和1。你可以这么理解,记事本只是一个软件,它运行在操作系统之上,当你查看一个文档的时候,首先这个软件会通过IO调用,打开存储在你磁盘上的文件,这个文件必然是二进制的,如果软件直接显示给你,你肯定看不懂(不排除有高手可以人眼读懂二进制)。这个时候这个软件就做了一件事,它根据你保存文件时候的编码方式(GBK或者UTF8),把0和1转换成了你能看懂的字符(人类的文字)。

这个过程是必须的,实际上任何人类可读的文本都必须有编码方式,不然像记事本这类工具就歇菜了,比如以前中文还分为了GBK、GB2130等好几种编码,好在现在全世界基本上都统一用UTF8了,编码问题不常见了。

但是在物理上(现实世界)并没有0和1这2个东西,0和1这只是人类为了方便记忆虚构的一个概念,我们需要能够代表0和1意思的东西或者机制。

人类为了记录数据,最原始的做法就是刻字,后来发明了纸和笔。现代的机械硬盘得工作原理就像是刻字,只不过笔是磁头,纸是磁盘,它通过一种复杂的机制去保存0和1的状态。可以说,人类发明的所有计算机以及网络设备都是在以各种花样的形式处理、保存、读取0和1,二进制是所有计算机文明的基础。

磁盘解决了存储问题,我们只需要把磁盘插入电脑设备接口,然后读取里面的内容,但是通过网络传输就不太一样了,因为这个网线并不认识0和1,所以就有人发明了网卡,它能把通过网线传输的电信号转换成计算机认识的0和1,现在很多都是光纤网络,光猫的作用就算把光信号转换成数字信号。

网络层

越过物理层之后,来到网络层就相对简单很多了,这一层就开始有代码了,这也是IP协议所在的地方,IP协议本质上解决的是2个网络设备之间端到端的传输问题,至于包里面放的是TCP还是UDP它不管。

可能有人觉得这一层比较简单,实际上并不是,如果单纯的只是局域网里面2台机器端对端之间传输确实不复杂,但是如果放大到整个国家,两台电脑之间通信得经过无数台路由器、交换机等通信设备,如何选择最快的路径、如何高效的转发数据等也并非易事。

然而,作为程序员,一般也很少接触这一层,这一层更多还是基于硬件做一些开发,比如说交换机、路由器等通信设备。

传输层

到了这一层,基本上就算代码的世界了,也是程序员接触非常多的一层,比如TCP协议,TCP人如其名:传输控制协议,其中重点在于控制。说白了,就是怎么保证数据传输完整可靠,所以就有了三次握手、拥堵控制等机制。

当然能在这一层做开发的都是大神级别的程序员,很多时候我们只是去用、理解这个TCP协议,更多的普通程序员还在下一层。

应用层

这一层最知名的就是HTTP协议,相比上面的TCP协议简单多了,协议内容都是文本的,人眼直接看就明白了。

这块有个误区,有人说HTTP协议是超文本传输协议,是不是意味着传输的内容都是文本?肯定不是,虽然说HTTP经常用来做Web开发,并不是说人家只能用来传输HTML,二进制内容也是没问题的,这里的文本是指协议内容,具体就是指请求头和响应头。

应用层的协议非常之多,因为门槛太低了,是个人都能开发一个协议,算上那种私有和公有的协议,估计得有几千种,很多公司一言不合就搞个协议出来自己玩,但是这么多年过去了,成为事实标准的协议并不多,开发常用的也就那几个。

2.封包和拆包

理解了OSI模型之外,我们还需要知道一点,那就网络通信的时候是有2个端,虽然来说有server端和client端之分,实际上区别不大,因为TCP的通信是双向的,无论是client还是server都可以发送接受数据。

实际上,每一次完整的通信都需要经过2个过程,一个是封包,一个是解包,这个2个过程是相反的,这有点像是剥洋葱,从一边到另一边,必须先从外向里剥,然后再从里向外剥,洋葱芯就是物理层。

其实这个过程对于开发者来说是隐藏的,实际上开发中我们也不需要一步步一层层封装,但是我们需要了解这个过程。

3.Go编程实战

Go自带的net库有着非常丰富的功能,封装的非常强大,有时候我们也称之为Socket编程,它是介于传输层和应用层之间的抽象层,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

一张经典的TCP网络编程的流程图即可解释完整过程:

下面让咱一下实战代码:

一、server端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"net"
)

func main() {
// 1.Listen
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
panic("Listen error!")
}
defer listener.Close()
for {
// 2.Accept
conn, err := listener.Accept()
if err != nil {
panic("Accept error!")
}
buf := make([]byte, 1024)
// 3.Read
length, err := conn.Read(buf)
if err != nil {
panic("Read from conn error!")
}

fmt.Printf("Recived Data: %s\n", string(buf[:length]))

conn.Write([]byte("Hello from server\n"))
// 4.Close
conn.Close()
}
}

代码基本上没什么难度,就是那几个调用,但是这几个调用的背后才是网络通信的核心,今天先不说原理,单论这段代码,它实现了一个TCP服务器,能够接受请求,读取输入,写入一段输出,然后关闭连接。但是这个服务只是演示,实际应用存在很多问题,比如说并发能力弱、存在”粘包“问题。

怎么去验证这个服务呢?咱先拿Telnet试一下,Telnet发送的数据是纯TCP数据

不过我们也可以拿ssh试试,你会发现会报错,当然这很正常,我们又没有去实现ssh协议

1
2
jwang@jun:~$ ssh root@127.0.0.1 -p 8080
ssh_exchange_identification: Connection closed by remote host

同时server端这边的结果是:

1
Recived Data: SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.10

这说明一个事情,ssh客户端发的第一个包的内容是ssh的版本,当然由于服务端没有按照协议的方式正确的响应结果,所以自然无法正常连接。

二、client端

除了使用一些现成的工具之外,我们也可以自己实现一个客户端去连接我们刚刚写的服务端,也是非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"net"
)

func main() {
// 1.Dial
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
// 2.Write
conn.Write([]byte("Hello from client\n"))

buf := make([]byte, 1024)
// 3.Read
length, err := conn.Read(buf)
if err != nil {
panic(err)
}
fmt.Printf("Recived Data: %s\n", string(buf[:length]))
// 4.Close
conn.Close()
}

代码也非常简单不作多说,就是建立一个连接,然后发送数据、接受数据、关闭连接的过程。大家在使用这个net库的时候会发现还有ListenTCPDialTCP等方法,其实底层都一样,只是参数写法上ListenTCP更加严谨。虽然只是几个简单的函数调用,但是其背后却代表整个OSI网络模型,Go在这方面是大大简化了网络编程的复杂度。