我们都知道以太网层和IP 层是不可靠的,想要可靠的传输就必须到传输层。而传输层的重要协议就是TCP 协议了
如果我们想要在应用层上开发,也需要在基于TCP 层上开发,所以我们需要了解Go官方是如何去实现传输层的
TCP 经典的三次握手和四次挥手过程,我们这节主要讲解go 的相关知识,这里的细节就不一一展开了
我们可以想象如果手动实现TCP 的三次握手和四次挥手会怎么样? 答案是很麻烦的, 于是在计算机中就有了socket 这个概念, 它能将三次握手和四次挥手简化几个函数connect(), close(), 屏蔽了系统底层的操作,使应用开发者更加便捷
其中简单的通信过程如下:
从socket 连接个数看客户端通信过程,可以画成这样
从图中可看到,通过socket 屏蔽了底层通信。如果一个server 服务两个客户端, 这时会有3个socket , 其中2个是的建立连接的socket, 另外一个是监听socket。这个概念对后面很重要,也对理解整个tcp 层很关键
这时上面两个客户端连接服务器的例子, 那么问题出现了
如果多个客户端连接服务器,在计算机看来就是多个socket 连接到服务器,这个服务端需要同时处理,于是这个同时处理的方法就产生了IO 模型
简单理解就是服务端处理3个socket 时,需要开辟三个线程
步骤:
优缺点:
优点: 开发难度小,代码简单
缺点: 内核态切换开销大 (如果有1000个线程, 开销就很大了)
正因为这个缺点就有了非阻塞IO 模型诞生了
现在使用非阻塞模型,避免了阻塞模型的缺点,但是必须时刻进行轮询,
那么问题来了,有没有一种方法不用轮询,就能帮助系统监控有没有数据过来,于是就诞生了多路复用模型
linux 有epoll, windows 和mac 中也有类似的东西;
这里以epoll 为例: 全称叫event poll。它可以将每个socket 的可读 可写的事件注册到里面, 现在这个场景是将三个socket 的可读事件注册到事件池里。放入里面后就是操作系统帮助我们实现的,然后我们会非阻塞的调用epoll,去询问这三个事件发生了什么, 然后epoll 返回socket2 可读事件,socket1、3 都没有数据。然后我们的业务直接调用第2个socket,然后处理对应的客户端就行了。也就是多路复用 是将 监听多个socket 的任务从业务转移到了操作系统。
步骤:
扩展知识:
Mac : kqueue
windows: IOCP
现在提出问题:
有没有能结合阻塞模型和多路复用的方法?(也就是说将两者的优点合二为一)
在go 中 类似架构如下:
这里可以将每一个协程对应一个socket,起到阻塞模型的特点
然后利用go 的网络层进行对epoll 层的封装,最终达到上面的效果
接着上面的思考,更加细化go 中的架构图,我们把go 中的协程和业务系统联系起来,如下图:
这样就是把原来的线程阻塞模型换成协程阻塞模型底层也不是休眠线程了, 是休眠协程。而协程相比于线程来说,所消耗的系统资源小得多。
在go 中想要实现epoll, 必须得考虑到不同平台的差异,而为了解决这一差异,官方就在架构中加入了epoll 抽象层
这一节我们就思考在go 中阻塞模型和多路复用模型架构图中epoll抽象层是如何做的
为什么需要引入go 抽象层呢?目的就是为了统一各个平台之间的差异,其实就是go 官方针对不同平台接入了不同api
用术语来讲: epoll 抽象层是为了统一各个操作系统对多路复用器的实现
各个系统的多路复用器都有以下功能
go 的官方epoll 实现好了之后,应该具体做啥呢。
当然是实现epoll 得具体逻辑了
多路复用器得抽象适配:刚好对应各个系统应该拥有得功能
名字有混淆性, 不是打开一个epoll, 而是插入
返回的是协程列表
思考: pollDesc 是怎么来的呢?
Network Poller 层与上面的关系
上一节讲了多路复用抽象层, 主要是实现了三个方法, 屏蔽了不同平台的差异性这一节继续讲解上面一层 networkPoller 具体是怎么实现的
初始化之后就要工作了
不过在这之前需要了解两个数据结构 pollcache 和pollDesc
pollDesc 不是描述多路复用器的,而是描述一个socket 的
其中他们之间的关系如下:
Network Poller 新增监听socket
收发数据分为两个场景
runtime 循环调用netpoll()方法(g0 协程)
比如 垃圾回收器 会循环调用
这个不是业务调用的, 而是runtime 调用
步骤:
runtime 循环调用netpoll()方法(g0 协程)
此时可将之前的架构图画成这
思考:
我们知道了如何检测socket 状态,但是socket 从哪来,直到socket 可操作后,做什么呢
在network poller 的上一层就是net包,是go 官方实现的
net包:
回顾一下之前讲过的socket 通信过程
现在是执行了bind 和 listen ,就将socket 放到系统的epoll 中去监听新的连接,下一步就调用accept()去监听了
下一步可以有两个思路:
TCPListener.Accept() 函数
拿到conn 之后就可以和客户端进行通信了,下面讲解read 、write 函数
下面演示用go 中的net 实现一个简单tcp服务器, 监听9999端口
func main() {
lis, err := net.Listen("tcp", ":9999")
if err != nil {
panic(err)
}
conn, err := lis.Accept()
if err != nil {
panic(err)
}
var body [100]byte
for true {
_, err := conn.Read(body[:])
if err != nil {
break
}
fmt.Printf("recv msg = %s\n", string(body[:]))
_, err = conn.Write(body[:])
if err != nil {
break
}
}
}
整理一下之前的架构图:
由于之前的代码只能服务于一个客户端,也就是说只能和一个socket 进行通信
基于上面这种情况,go 推出了一个编程风格,可理解成每一个协程一个连接
思路:
func handleConnection(conn net.Conn) {
defer conn.Close()
var body [100]byte
for true {
_, err := conn.Read(body[:])
if err != nil {
break
}
fmt.Printf("recv msg = %s\n", string(body[:]))
_, err = conn.Write(body[:])
if err != nil {
break
}
}
}
func main() {
lis, err := net.Listen("tcp", ":9999")
if err != nil {
panic(err)
}
//实时处理新的连接
for true {
conn, err := lis.Accept()
if err != nil {
panic(err)
}
// 用新的协程处理新的连接
go handleConnection(conn)
}
}