好 我们开始第9课
第9课主要是讲网络编程
结合Go runtime的一些特性
尤其是像goroutine的创建非常方便
然后以及结合它的那种非阻塞的IO
然后和goroutine的结合
使得Go的网络编程变得非常的简单
这也是我为什么选择Go作为我们的后端的主力语言
以及一些中间件的主力语言的原因之一
因为它的整个编程模型跟我们
传统的C++写网络编程还是有很大区别
像C/C++他那种回调式的那种写法
就函数指针传来传去
大家如果去看开源的像Nginx也好
或者像看mproxy这种代理也好
基本上C/C++写法其实都会有点奇怪
你作为一个第三者去看这个代码的时候非常不好理解
就整个函数指针跳来跳去
如果你去看Go写的网络编程
它实际上是非常符合human readable
就是人阅读的这种非常友好的一种体验
因为你开两个goroutine
一个负责读 一个负责写
那么你整个同步逻辑代码都是在里面去去完善和描述的
使得整个写起来是非常顺手的
那么阅读起来也是非常顺手的
我觉得这个是Go非常重要的一个特点之一 对
所以有一句话说你用学C学C++
然后结合我们了解Linux的epoll
做一个高性能的这种100k的这种网络服务器
那可能需要你至少三四年以上的编程功力
去把整个网络框架网络库搭建好
但是如果你是用Go语言的话
这些事情其实已经在Go状态层面
或者是我们的net包
写一个高性能的网络程序
可能只需要花费我觉得10分钟就开两个goroutine
请大概处理一下包的粘包
就整个网络变成体验非常好
对我们再开始讲Go的网络变成之前
我们先回溯一下网络通信协议的一些基础的知识
我觉得这个是我们日常工作中也好
解决问题的时候都会涉及到啊
我们第二块可能会讲一下
用Go去实现网络编程的一些写法是什么样的
第三块呢会讲一讲我们goim的面向tcp server的场景
因为要讲im的话
其实很重要的一个场景就是id发号器
以及我们最后一个im的私信的系统的实现
这块也是非常经典的
那么看我们网络通信协议
互联网的核心实际上它是由一系列的协议组成
我们看到最底层就是物理层
物理层就是像我们的双绞线光纤无线电波
这就是底层的一些物理电路
那么我们到数据链路层的话
可能就涉及到以太网和网桥
我们可能像IP TCP
还有我们TCP之上的HTTCP
这些可能协议层你都要去了解
然后还有像我们的网络层
我们的网络成像
ARP 路由器这些知识
还有我们传输层TCP UDP
还有应用层7层的HTTCP FTCP SMTCP
这一些我们基本上大家其实多多少少都会知道
但是我比较建议大家去看一下
有一本叫做《UNIX Network Programming》
就是UNIX网络编程的一本书
它里面会把基础的一些这种网络知识
以及结合UNIX上的一些网络编程的系统库
syscall一些系统调用
它都会讲得非常的清楚
所以这些东西还是要去看一看
我不会每一个协议每一层我都会去展开讲
对于我们开发人员来说
除了了解它底层的这么一些概念
实际的到我们这一层的时候
我们面向开发编程去写的时候会变得非常简单
因为它已经做了一些抽象
比方说它会分为socket
我们所谓的接口抽象层
我们最终都是面向socket去编程的
那么它会屏蔽掉底层的像TCP/IP这些概念
其实都会被socket的接口抽象层所屏蔽
我们一般来建立socket的这层来做编程的时候
最常见的就是你的传输层到底是用什么
是TCP的 面向连接的可靠传输
还是无连接的UDP 不可靠的传输
以及我们基于TCP或UDP之上的应用层协议应该是什么
我经常讲我们大家每天调的接口HTTCP1.1
那么还有HTTCP2.0
包括我们前面讲的课程中的gRPC
其实也是HTTCP2.0
那么其实还有现在很多视频网站开始使用QUIC
来作为视频传输的一个重要协议的像QUIC协议
像爱奇艺 然后腾讯和b站
我印象中都使用了QUIC
所以QUIC其实也是下一代的HTTCP
那么它也叫正是可以命名为HTTCP3.0
超文本的一个传输协议
这一块我们都会介绍它的一个基本的原理和协议的一些细节
我们首先先看到整个socket的抽象层
那应用程序
我们经常听到的都是fd
打开一个文件句柄
或者说我们打开一个套接字
我们实际上最终都是通过套接字抽向我们的网络发出一些请求
或者说去获取网络上的一些响应
就发出请求和获取响应对吧
我们看一下分层就是我们的
应用层的话就是你的业务代码
我们使用Go的net包
它是做了一些封装
最终它是使用我们的是
Linux对外暴露的像socket一个抽象层
最终你选择的是传输是TCP还是UDP
你是选择可靠还是不可靠
是面向连接的还是无连接的
之后我们可能还会涉及到像我们的网络层 像IP
你要向对端发送一些数据
最终这些你发送的这些数据都是通过我们的IP
一个个IP包携带一些数据
那么这个IP包如果能够正确地路由到链路路上的设备
它最终就可以收到一些IP包
然后通过我们的传输层的协议的一些处理
你从你的应用程序
再通过socket的抽象层获取响应
然后在你的应用层感知到这些读出的内容是什么
当然还有我们更底层的就是像物理介质硬件接口
还有像ARP
我建议大家都要去了解一下
因为很有可能在某一类场景
你可能就会涉及到用底层的一些知识去解决问题
举个例子
我前段时间看到我们群里面有同事说了解LVS
但是他不知道LVS怎么做负载均衡
所以后来就了解到OSPF这个协议
可能可以解决LVS的一个负载均衡的问题
所以这些知识虽然是很底层的
但是实际工作中有可能哪一天就会用上
所以我们还是要非常重视
那么使用socket的抽象层以后
它是一种面向流的通用的网络接口
简单来说我们一般面向socket操作
就是说有建立一个连接
然后接收一个连接
我们建立连接
我们在Go里面一般叫dial
我去打电话dial
然后我们接收连接叫什么叫accept
我可以接收一个连接
那么dial其实面向的是客户端去拨号连对方
那么accept指的是服务端接收一个连接
那么接收完之后
我实际上拿到的是一个什么呢
对Linux来说
我最终其实就是打开了一个fd对吧
打开了一个文件描述符
这个文件描述符指向的是什么
指向的是一个socket的抽象层
其实它意味着代表的就是一个具体的物理连接
那么针对这个物理连接
我就可以进行读取 我们叫read write
我们知道io包里面有io.Reader io.Writer
我们还有io.Closer接口
那么可以关闭一个连接
那么关闭的行为其实是client要关闭
服务端也要关闭
或者说服务端要关闭client也要关闭
它是双都要去主动关闭的
那么还有超时的一些控制
比方说像Go里面的SetReaderDeadline和SetWriteDeadline
所以我们简单看一下我们的net包
我们看刚刚说的我们的demo里面
他实际上就包含像dial 连接
然后还有accept接收连接
接收连接
然后读写的话其实也是也是非常简单的
我们举例来说看一下TCP
我们看一下TCP
它是实现了一个io
实现了Read的方法
对它可以读
然后可以设定超时SetDeadline
当然它也可以设置
分别设置读写的超时可以SetReadDeadline
这些都是套件字的一些基本的一些操作
然后还有获取地址和端口
我们再切过去看一下
包含像我们的RemoteAddr获取地址端口
那么Addr可以获取到它是什么网络类型
比方说它是传输层的TCP还是UDP
那么string的话其实获取的就是我们的IP和端口
因为我们知道键连接的时候
你的client是要有一个 IP和端口
你的server也是有一个IP和端口的
那么服务端的IP和端口和客户端的IP和端口
加上我们使用的网络类型
也就是我们的传输类型
它最终变成一个5元组
那么5元组其实就是意味着你的client
和server之间建立了一条会话
基于你的client的IP和端口和服务的IP端口
那么你们之间就可以建立一条会话
然后还有网络类型
然后你就可以去发包和收包了
所以我们可以简单的把网络的一个连接想象成一个管道
这个管道是你们独占独有的
那么它的标识符其实就是刚刚说的5元组网络
然后双端的IP端口
我们再来看一下
我们再看一下TCP
这里面其实非常典型的就是TCP
我们知道它是一项面向连接的
然后可靠的一个传输
简单来说就是如果我的链路传输过程
中有一些中间环节出现了一些丢包
或者是超时导致部分包不见了
我们知道TCP因为它是可靠传输
所以它会什么
它会在某一些中间这个环节丢包的情况下
会做自动的包重传
比方我发了三个包123
发现第二个包不见了
它会把二包重新重传
其实它也是有序的
就是说123最终让你的应用层感知的时候
一定是你发的时候是123
那么我收的时候一定也是123
不会说三变成321或者312之类的这种乱序
虽然每一个IP包到达我们的
网络设备有可能是有快慢区分的
但是因为TCP的这种面向可靠连接的
以及这种流式的传输方式
让我们的整个顺序保障性是通过TCP协议实现来保障的
所以我们用起来会相对来说比较轻松
那么对于TCP的核心
我们一定要搞清楚它的建连过程和断开连接的过程
也就是说TCP/IP的一个接连的3次握手和断连的4次挥手
比方我们知道三次握手的时候先要发一个SYN包括做一个同步
然后它会回你一个我收到了并确认
然后之后再发一个ACK
就是服务端回包之后我要给它一个响应ACK
就是这样的话双方就已经建立握手了
TCP其实它的实现上有很多细节
包括TCP的慢启动
它的快速恢复
这些我建议还是刚刚说的UNIX编程里面
甚至是我们的TCP/IP编程
这些书里面其实都会介绍它的一些TCP的实现细节
举例子来说
我们在第五周可用性的时候
其实就提到过TCP的算法其实也是
我们当时用来做负载自适应保护的时候
也是参考它的思路
所以TCP带的一些协议设计
甚至它用的一些算法
很有可能可以移植到我们别的一些场景里面
所以它的底层的一些原理我们还是要去阅读
那么在建立完连接之后
它的数据传输其实蛮简单的
就是我们叫read和write对吧
就是发送数据和收数据
然后包括我们断开连接的时候
它是有所谓的看是哪一端先调close
但是不管怎么样
我这边先提一个大家经常会问到的一个
问题就是timewait
timewait过多怎么办
timewait实际上它就是一个正常的状态
如果你是个纯client
你在client出现大量的timewait
这是第一种
第二种有可能是你是一个proxy
你是一个中间层
那么你有别的client会连到你再作client转发到下游
那么你出现大量台timewait会怎么样
这两种场景我们其实要分开对待
后续在我们reference里面应该有一篇文章专门去介绍
就是TCP/IP的timewait也好
到底是否是应该要被处理的
我们知道可能通过一些改内核的参数去
快速回收掉timewait
未必就是正确的
所以我们一定要把它的握手和挥手的这些细节搞清楚
为什么会出现timewait
这些你搞清楚以后
你再去结合内核的一些参数去调优的时候
你就搞清楚你的服务处的角色是一个纯client
还是作为一个proxy
那么它都是有一些配置上的差异的
另外还是要举个例子
在网络编程里面我们还会遇到一个比较典型的错误
比较典型的TCP的连接状态叫closewait
closewait大量出现一定是不正常的
closewait一般意味着你的对端有关闭连接
但是因为你连接泄露
或者因为你的负载过高
导致你无法快速的把你的连接给回收掉
以至于堆积的大量的closewait
那么这个确实是不正常的
如果出现大量closewait你的监控应该基于
这种网络上的一些指标来进行指标的采集和监控
大量的closewait一般就是有bug
或者说负债过高
你的处理不够好
这个是要注意的
对于服务端我们说写Go的一个程序
是一个什么样的流程去handle一个连接呢
通常来说我们要监听端口
比方说我要起一个Go的一个rpx服务
我要起一个gRPC服务
我要起一个 HTTP的服务
我都是要先监听端口
一般我像我们的80端口
监听端口兼听完口之后
我的HTTP server也好
或者我的TCP server也好
如果有人来尝试建立连接
它就产生一个带有的行为
我们叫 dial TCP p对吧
我们知道net包里面有个dial tcp
对于client来说是尝试连接
对于server来说这个时候它其实就会开一个goroutine
会for循环的不断地去接收连接
它会不断的去调accept
也就是说如果没有连接
我会卡在accept这里
但是如果一旦有连接我的
accept就会返回
就是建立连接成功之后的一个connection对象
基于这个connection对象
我就可以去创建goroutine
一个负责读
一个负责写
注意一定是一个负责读 一个负责写
那么读和写之间它们之间
再进行数据的一些协议上的交互和操作
一般会使用Go channel
比方说建网连接我的服务端
这个时候我的read的goroutine
会去调一个read的方法
然后收到一些包
那么你如果对这个包进行解析之后得到一个指令
对方发了一个什么指令给我
我要做些什么处理
我可能会把这个消息发到哪
可能会去调一些业务逻辑
这是第一种
关键是调完一些业务逻辑之后
你可能需要驱动write的goroutine
负责写的goroutine
可能是阻塞在channel的读操作里面
那么当write的goroutine收到这个消息以后
会把这个消息取出来
然后做一些业务逻辑的判定
然后可能还会做一些消息的序列化
序列化之后的那些字节
然后再把这些流给写出去
所以一般创建两个goroutine
一个负责读一个负责写
通常是这么协作的
但我们讲goim的时候也会看一下
我们是怎么让他们两个人之间协作的
但是还有一种更简单的模型
你可能没有处理非常复杂的业务逻辑
比方说我们做一个ping
发一个ping消息然后回一个ping消息
这个时候其实就是我的读的goroutine解析完协议之后
我就通过channel发送到我的write的routine
我的write goroutine一旦收到消息以后
它的消息进行一些处理之后
它就回包回去了
就是最简单的一个ping的逻辑
当然了对这个场景有可能只需要一个goroutine读完立马处理
read完之后立马write
因为你的逻辑足够简单
但是如果我们reader write之间要做一些业务逻辑
那么通常都会把它读取之后的动作或者操作
我们会把它分发到下游的另外一个函数去处理
处理完之后再通知我们的write的goroutine去进行消息的接收
然后再把这个数据序列化之后发送回去
这是个比较典型的一个流程
对于client来说就比较简单了
client因为你连完之后
你无非就是说就是在一个goroutine里面串行的发包收包
这是最简单的一个做法
我们再看UDP的
我们也叫它为不可靠的一个连接
允许广播和多播
简单来看一下TCP和UDP的一个对比
首先TCP是需要面向连接的
所以他要握手 所以你看我们这个地方是然后再
SYN ACK然后再ACK
这个是什么TCP的三次握手
对于UDP来说就简单了
对于send和receive来说
发包获取响应
非常简单
它没有任何连接的概念
你可以理解为它是一个无状态的
无状态的一种无连接的一种传输层协议
这种传输层协议首先它不需要建连
另外它不可靠
没有持续的通讯
就是他们之间的每个包之间的到达的顺序
不能绝对的去确定和保障
包是有长度的
就是只能一次6k左右65515
然后支持广播和多播
然后它的低延迟和实时性比较好
因为它非常简单
但是其实需要打个问号的
因为我们现在很多网络设备对UDP的支持并不好友好
或者说UDP的处理上优先级非常低
以至于我们它的整个效果并不是特别好
当然就是说我们说了像HTTCP3.0
包括我们视频上经常用的QUIC协议
都用于我们视频的直播
甚至是游戏的一些对账的场景里面的一些
游戏场景的一些同步都是会使用的UDP的
我觉得随着未来可能越来越成熟
UDP应该是会发挥很大的一个作用
包括刚刚我们说的HTTP3.0
也是基于UDP来做的
我们再来看一下我们在传输TCP和UDP之上的应用层协议
第一个就是HTTP每天大家都会使用
我可能是用HTTP做个接口
可能是HTTP暴露给外网用户
可以使用这个HTTP接口来访问到前端的一些页面
它的报文非常简单
其实它是纯文本的
像Method就是我的HTTP请求到底是get还是post
然后换行
然后accept接收的是哪些编码方式
比方说json
以及它的content-type响应类型
比方说我们经常会以restfull返回json的方式
我的content-type可能就是aplication/json
接下来就是它的请求正文
就是我们所谓的body
加上如果看的话
整个HTTP基本就是靠\r\n来做分割的
响应报文也非常简单
就是状态行状态码status
然后还有你的response haader
你可以自己带些response header下去
还有你的响应正文就是我们的body
所以非常简单
你看我们举个例子
这个地方就是get某一个路径
然后HTTP1.1表示协议和版本
然后有host信息
有可能Content-Type
这些其实都是HTTP header
都是HTTP的request header
对于相应来说就是HTTP1.1
然后200 ok 这个是它的状态
然后是换行
包括一些HTTP的头
所以这个是非常简单的
我这里面其实也列了一些指令
为什么要列一些指令
我相信大家有时候也是需要去服务器上做一些流量诊断的
如果你要去诊断HTTP的一些流量
就是你想把这些头什么的这种文本协议给抓出来
你可以直接使用tcpflow
你可以尝试一下用tcpflow看看监听某一个端口
通过tcpflow去watch某一个端口
然后它可以把整个基于端口上的一些流量给截取出来
然后方便你去做debug
那么网络上还有很多好用的指令
像nload的我们可能看一看网卡的带宽
ss和netstat可以看看一些网络层面的统计信息
还有像nmon
nmon的话也是非常复杂的一个工具
这几个就是非常常用的一些Linux的一些指定
大家可以去了解一下
我们要讲一个非常典型的
就是gRPC的协议了
gRPC我们之前介绍过
它是基于HTTP2.0之上的协议扩展来实现的
所以我们看一下
对于request来说一样的也是
headers叫methods=POST
然后schema=https
然后它的path路径是什么
还有content-type
content-type就是Application/grpc
还有encoding 它的编码方式
之后可能我们的data部分
data部分有一个叫Length-Prefixed Message
简单来说它是有一个带有网络
protobuf序列化之后的消息长度的这么一个格式
我们看一下第一个字节
one bite of zero
这个字节表示的0是没压缩
1是表示压缩了
第二个就是叫network order 4 bytes of proto message
这个是网络续的四字节的protobuf的包协议的内容长度
然后下面就是序列化的protobuf的message
所以它是靠一个5个字节
第一个字节表示是否压缩
然后4个字节表示Prada Boff的包长度
然后后面就是Prada puff的具体的一个内容
response一样的也是非常简单
首先也是包含一些headers
比方状态码等于200
肯定有返回的是什么样的一个响应内容
然后包含data部分
data部分其实也类似于request data部分
也就类似于这个地方
它包含刚刚我们说的像你序列化之后会返回哪些protobuf
然后还有个Tailers
Tailers其实是一个是头一个是尾
它可以包含像gRPC的状态码message
其实Tailers目前是用于gRPC的 status
以及gRPC的details的实现
但如果用过gRPC应该知道有gRPC的状态码和gRPC的details
其实它是基于HTTP2.0的Tailers来实现的
因为我们这除了header就是头吗
Tailers就是伪头和尾
为什么gRPC要使用HTTCP2.0
就是为什么不使用1.x
其实我们知道spring cloud它使用HTTP1.1的
那么gRPC使用2.0
我觉得主要是几点
我们先看看1.1
虽然出现的时间非常久
服役了很多年
但是它为了网络效率也做过一些优化
比方说我们知道HTTP1.1支持keep-alive
长连接
它增加了持久连接
也就是说我每次发一个HTTP的请求
它不意味着就是一个短连接
它其实是可以是一个长连接的
也就是说我可以一直复用连接去串行的发送一些请求
第二就是说我记得以前我们做web开发的时候
通常要去优化页面打开速度
经常用到的一个我们会使用多域名
我们使用不同的静态资源
可能是使用随机选几个不同的域名
那是为什么
因为浏览器为每个域名最多维护6个TCP的长连接
也就是说我这个时候其实是有6个 HTTCP的连接
我可以并行加载一些图片静态资源
可能让我的首页可以打开的速度更快一些
另外还有一个就是可以使用像HTTP2的
它的核心的一个改变就是多路复用
多路复用我们其实听过很多次
简单来说啊就是HTTCP2.0两个改动
一个就是引入了二进制分帧层
也就是说我们的以前是文本协议
那么它现在其实变成了二进制的一个协议
我们的请求数据把它们打包好以后
会把请求数据或者说把帧放上一个编号
这个编号当我们的服务器收到以后
相同id相同编号的这些帧
就合并为一条完整的请求消息
所以多路复用指的是什么呢
指的是我一条连接
通过给不同的这些帧打上一个编号
那么相同id的这些帧实际上就表示的是某一类请求
它就是合并为一条某一类完整的一个请求信息
id可以有不同的编号
所以说意味着我可以使用HTTP2.0
同时发送多个请求
通过的是每一个人带上一个不同的id编号
所以这个就是核心就是多路复用
也就是意味着我在现在浏览器
我只需要开HTTP2.0开一个连接
我就可以并行发好多请求
所以其实大大减少了连接的开销
然后让我们的HTTP2.0更高效地使用网络
这个是非常关键的
我们也会来看一下
刚刚说的带id怎么去把id
在服务端怎么处理
在客户端怎么处理
我最早知道这个是怎么发现HTTP2.0这个特性
其实是我先知道了
Go标准库的net/rpc
然后才发现HTTP2.0也是类似的一个思路
我们现在看一下net/rpc的它的一个源码
我们现在看到的rpc的一个源码
大家可以在Go的runtime的目录src
就是我们Go源码的src/net/rpc目录里面
找到他的代码
打开client.go
首先有个Dial方法就是建立连接
比方这个地方我是有个本机的127.0.0.1
然后写上一个端口号
那么Dial完之后会掉NewClient去new一个Go的net/rpc的连接
我们看一下啊
应该是在这里
拿到连接以后 一般都会用我们所谓的readbuffer和writebuffer
就是用bufferio会包一下
我们最核心是什么
我们知道建立完client拿到一个rpc client以后
我们最重要的就是叫什么
调查它的Call
调一个rpc
这个地方就是填服务名和那个方法名
然后这个地方是获取响应
那么就这个方法我们看
这个Call这个方法它会掉Go
也就是这个地方
我们先看啊他会用一个Call对象
又一个调用者的一个对象
就是我要去掉发起rpc
我们看看首先它会把服务名保存进去
还有你的传递的参数
还有你的响应内容
我都会把它复制进去
然后我们再看一下
如果Done=nil
它Done是什么
是一个Call对像一个channel
我们看一下
在Call方法里面
它实际上make了长度为1的一个channel
意味着什么
一个buffer channel
为什么会要长度为一的呢
我们先看
也就是这个地方不需要再初始化的也就进到这个地方
然后我们再把call.Done对象复制到
你的make(chan. 1)的方法里面
然后会调一个send的方法发送
就把call request发送出去
然后在return一个call对象
那么return callt对象以后我们看一下
call方法是个同步调用的方法
说白了就是你发一个网络请求回去
利用channel等待response什么时候回来
然后一旦我的response回来以后
它会把我的Done对象肯定是会塞一个Call对象塞回来
然后我就拿Call对象
我就知道是成功还是失败的
所以是如此的简单
我们现在看一下Call对象的详细的内容是什么
服务名和方法名参数 响应内容
error是否报错还有Done
Done的话就是当我们收到Call的对象以后
就当我们的rpc调完以后
它是通过Done可以获取到Call对象的
我们再来看一下它的send方法发送
这些地方我们就先跳开
不管我们核心的就是什么
先拿到一个它这个地方是加锁的
这个client的是加锁的
client在加锁以后先获取一个序列号
client.seq是个序列号
就有点像我们的HTTP2.0的给每一个二进制帧分配一个编号一样的
这个地方是用自增的拿到这个编号然后自增
然后我们看看client rpc的client
还有一个pending对象
我们看这个pending对象是个什么东西
这个pending实际上是一个用uint64做key
value是一个Call的对象
也就是说你发一个rpc出去以后
我会在map里面维护编号加Call对象的引用
下面的话应该是把Call对象叫write request
把它发送出去
发送对应的rpc服务里面
我们再看看怎么去收取响应的
我们先看input的方法
我们先看一下
当你new一个client的时候
它会在后台goroutine开了一个input的background的goroutine
那么这个background goroutine我们看它干嘛
它会一直在这里
它会一直在这里收包
注意大家看它会一个for循环一直在这收包
意味着我就读对端的连接
因为那个net/rpc也是一样的
只有一个连接
我就不断的收包
收完包以后
我们注意看这个地方核心来了
重点来了
先从response的seq里面把序列号拿到
我们可以猜想一下
我们是不是服务端收到seq以后不做任何处理
当我的服务端真的调rpc方法调完之后
我再把seq序列号原封不动的可以塞回来
然后我们在查找client.pending这个map
把刚刚Call的对象暂存起来
我就把它找回来
找到以后
我们拿到Call对象以后我们看看
这些我们先不看
我们看核心的就是当我的rpc没有报错的情况下
这default rpc肯定要想一些内容
我会把用Call.Reply
把它复制到Reply里面去
那么处理完Body以后
我还会调个Done方法
我们再来看一下done方法它里面做什么
很简单
把call对象本身发送到Done channel里面去
所以我们结合刚刚这个Call方法
你实际上这个地方是拿到这个Done对象
然后我从里面取出来
所以这个就是个标准的rpc的同步调用的姿势
那么这里面的seq序列号做什么用的
应该理解的就是在client维度
维护了这么一个序列号 它是自增的
然后每次发rpc就用一个map存起来
那么发到服务端的时候
同样的是原封不动地
处理完之后再把seq这个号码带回来
所以你可以用一个连接
发送多个rpc 发多个rpc以后
在服务端可能有些人处理的快
有人处理的慢
然后这个时候不管
不一定是要按照严格顺序的
因为有些人处理的有些人处理的慢
那么只要seq的号码保证不变
那么回来的时候我就能找到之前是哪个channel
就哪个Call对象 在我的padding里面map里面我就找到之后
我就可以通过channel的发送把它唤醒了
所以HTTP2.0的核心思路也是一样的
也是这么一个做法
整个HTTP演进史基本上也是很长时间都是用HTTP1.1
直到现在还有很多1.1
那么包括2015年发布了HTTP2.0
优化了它的性能和安全性
主要是我们知道HTTP2.0它要强制使用TLS
所谓的HTTPS
其实是互联网现在基本上都会使用证书了
嗅探的这种问题和被篡改的这种问题风险就小得多了
所以我是非常鼓励大家都是用的HTTPS带证书的这种方式
还有它可以多路复用对吧
其实呢
一个就是二进制
以前是文本的现在是二进制
然后多路复用
还有头部压缩
头部压缩指的是什么
我们可以在想象一个场景
我们正常的HTTCP请求发很多自定义的一些头
什么host accept encoding
还有一些自定义的头
这些头其实在一个HTTP的长连接里面它其实不会变
因为你的浏览器不会变大概就是不变的
所以它可以使用HTTP去做压缩
那么在服务端就不需要来回的去传这些头了
还有HTTP2.0的支持server push
它可以直接从服务端push一些消息下来
那么在这种长连接订阅的一些场景里面
比方说有一个好友关注你了
有一些up主更新的视频内容
都可以使用server push的方式来下发一个通知
这也是非常好用的
那么HTTP2.0它是核心最致命的是什么问题
叫blocks of head line指的是什么
我们知道TCP可靠传输
那么你只有HTTP2.0来做这个多路复用的话
如果中间有一个包丢掉了
意味着会触发包重传
是不是后面的因为你顺序的一个原因
你后面那些包是不是都要排队
等前面处理完了才能去继续处理
所以导致它叫head line of blocking
它的中间有一个东西丢了
后面的全部会排队
这个是它的一个致命问题
所以HTTP3.0就出来了
它其实就是基于UDP来做的
它核心就是什么
就是延迟低一次方法就可以建立HTTPS连接
就是更激进的一些拥塞控制的算法
然后它的网络连接
因为你UDP是无连接状态的
那么从4G切换到WiFi等等切换各种网络设备
实际上不会重连的
因为UDP它是无连接的
它不需要重连
所以这些都是很大的一些优势和特性啊
那么讲到HTTPS我建议大家还是系统了解一下整个HTTPS的演进
从早期的SSL1.0
我们也叫什么
Secure Socket Layer
就是以前是secure它设计的Web的安全传输协议
后面就演进到把SSL更规范化
叫TLS1.0
然后之后的话又出了1.1 1.2
那么现在主流大部分还是使用1.2
一些较新的公司
或者是有些浏览器会支持的比较好
那么1.3也是支持的
其实了解HTTPS的人都知道涉及到证书
ssl它核心就几个作用
第一就是说认证用户和保护器
确保数据发送到正确的客户端和服务器
因为我使用证书的话我就知道我访问的IP
或者说给我相应内容的人一定是不是被什么被篡改掉
或者被劫持到其他IP的这个人受信任的一个接入方方法
我就知道一定是
我访问京东或者访问淘宝
我一定知道就是不太可能会被劫持到其他的IP去
所以我们要了解证书的话
就是了解一下CA的证书机构
CA其实就是负责帮发签发证书
认证证书以及管理颁发的证书的机关
我们操作系统一般都有一些默认信任的证书机构
因为证书它是有层级的
就是我们所谓的root证书可以发好多下游的二级证书
二级是可以颁发三级证书
HTTPS我们要理解它
其实核心就是要搞清楚对称加密和非对称加密
我们知道对称加密使用同一串密钥
把你的原文使用密钥去加密成密文
然后再传输到对方
再把密文用同样一个密钥把它解密出来
它的加密效率是非常高
那么非对称加密指的是有两个密钥
一个叫公钥一个叫私钥
我可以用公钥加密私钥解开
也可以用私钥加密公钥解开
这个都可以
那么非对称加密
我们知道它相当于两把钥匙才能打开一把锁
但是它的加密效率是非常低的
所以无论是TLS哪个版本
它的核心原理其实都没有变
就是用非对称的加密方式来传递对称加密的密钥
也就是说既然对称加密速度好
大家都是用一个密钥对
一切都就全完蛋
所以我先用非对称加密利用证书
公钥和私钥来传递
使用非对称加密的方式来传递我们握手之后的
使用对称加密的算法的一个密钥
说白了就是想办法搞定一个对称密钥的传输
那么它就引入了非对称加密对吧
然后引入了正数机构等等一系列的事情
这个其实大家简单搜一下
大概了解一下原理
现在其实1.2的话
它的核心思路也是一样的
就是使用RSA也好
或者是使用ECDHC这种加密
作为一个密钥传输的方式
其实就是我们1.1 1.2 1.3都是一样的
差不多的一个思想
客户端向服务端去索要我们的证书
通过签名认证一个证书没有被篡改
那么有了证书之后
我就知道某个人比方说一定是京东或者是淘宝
拿到这个证书以后
这证书实实际上是拿到了公要那么
私钥其实在服务器上维护
那么有的公私钥我就可以利用公私钥
来把我的对称加密的密钥给传递过去
核心就是要解决这个问题
那么现在其实越来越多也演进到1.3了
那么1.3和1.2有一些区别
核心它就是它的速度更快
然后更安全
速度更快指的是不支持重协商了
以前我要协商
你支持什么加密算法
我支持什么加密算法 现在不协商了
我们现在只有一种加密算法
使用Diffie-Hellman唯一的密钥交换的一个方法
应该叫密钥交换算法
不协商了
第二就是它更安全
因为它移除了RSA的一个算法
也是使用Diffie-Hellman来做唯一的密钥交换算法
这个感兴趣的也可以看到PPT的reference
会讲一下加密里面的一些细节
为什么它更安全 然后更快
这个就是我们网络通信相关的一些东西
那么涉及到Go网络编程的一些基础的话
我们也来看一下
我们还是一样先了解一下基础概念
Socket我们刚才已经知道了
那么对于Encoding是什么
我们经常看到Encoding
比方说你是使用json的Encoding
还是使用protobuf来做序列化
我们其实也可以叫序列化或者叫Encoding
然后我们一般来说建立一个连接以后
我们就要建立一个session建立一个会话
我们通常有两种模式
一种叫client/server模式
一种叫browser/server模式
client/server的思路指的是我用app用PC客户端
通过网络连到我们的服务器上的TCP的server
那么还有BS其实指的是通过浏览器来访问到你的服务
待会看一个简单的TCP的例子
这个就是个非常简单一个例子
我们先看一下服务端有一个handleConn
接收的是net.Conn对象
这个是一个接口 这个是interface
我们在handleConn
一般第一就是defer conn.Close()
就是把这个连接在处理完之后
这个函数退出以后要把连接关闭
第二步其实就是建立读写缓冲区
为什么要建立读写缓冲区呢
我们拿到一个连接以后
你可以读1个字节读5个字节
我们知道很频繁的去读1个5个字节
调一次read它就要陷入到我们叫上下文切换
切换到内核态
那么内核开始从网络设备里面读出这些东西出来
是不是很频繁的读一个字节上下文切换进入到内核
是不是很高频的一个syscall带来的上下文切换的开销
所以为什么要用bufio
比方我的buffer是4k
我是不是尽可能先让我的网络连接里面的这些内容
一次先填满整个buffer
一次把4k全捞出来
意味着通过这样的bufio的方式减少我的系统调用的次数
这个是非常典型的读写都一样
读指的是你不需要频繁的读1个读5个字节
写意味着我不会写两个字节往网络设备上发
写两个字在网络设备上发
这样的话也是很高频的一个系统调用
那么handleConn然后我们开始一个for循环
我们调bufio.NewReader后拿到reader对象
你可以到ReadLine就读一行吗
那么如果报错的话就直接return掉了
如果没报错
我再回一个hello
再打一个hello
把刚刚解出来 lines再加一下
然后在flush
这个地方如果你用buffio的话
只有这个地方会产生一次性的一个syscall
所以你写多次不会触发syscall
只有你flush的时候触发
当然有一种情况非常例外
如果你的buffio的write的buffio用完了
会提前flush掉
还有一种情况
如果你的写的内容超过了你的buffio大小
它也会直接把它叫directly write
就是把字节直接往net connection的底层的对象直接发送出去
因为你没必要再拷一次
你都超过我的buffer了
我也拷不进去
所以这个就鼓励大家去看看bufio的源码
我印象中就两三百行
非常快速的阅读完毕了
好 这个是服务端的
我客户端怎么接连呢
我是怎么去见监听呢
先Listen对吧
刚刚说的Listen完之后其实也是一样的
有个for循环不断的去accept的
那么每拿到一个连接注意这个地方
我都会开一个goroutine去handleConn
其实这个就是典型的一个Go监听accept之后拿到连接
马上开个goroutine把它扔出去
让它干别的活
我这个时候我的for就持续不断的可以接收连接
如果大量的短连接连进来
我就会开好多个goroutine
同学可能就问了我这边搞goroutine池
其实不需要
因为现在Go runtime已经对连接的goroutine的快速开启和销毁
都做了一些cache做了pool
那么它的分配效率会比以前高得多
这个是一个简单的demo
那么UDP就更简单了
UDP我刚刚说了我们TCP里面最终这个代码你可以用telnet
telnet 127.0.0.1:1000端口
你连上去以后你自己输一个
然后敲个回车
那么我们这个代码就简单的几行代码
它就可以回一个
大家看到这个简单代码
其实你去做benchmark短连接也好长连接也好
它的吞吐其实也不差的
吊打一个花了好几年C++写的一个网络库
绝对是可以超越他们的
如果写得不好
可能还没有Go的刚好
所以这个其实就是你看非常简单的一个编程
就十几行就搞定了
UDP其实类似
TCP刚刚说的是有所谓的握手
UDP可以直接发数据
所以我们看这个地方直接ListenUDP
你ListenUDP之后直接一个for循环
listen.ReadFromUDP直接读
因为它不需要accept的
直接读内容
1024这也是个buffer
1024的一个buffer
读完之后
在write回去
append完之后再write回去
所以也非常简单 我们看直接用echo -n "hahaha"
用nc这个shll用命令行
然后你可以去往127.0.0.1 20000号端口
你发一个haha他就回一个hello hahaha
非常的简单UDP的一个一个案例
那么再看个简单的HTTP
Go的HTTP其实也是非常简单的
核心就是几个点
就是第一你要有路由
你的HTTCP连接
你的HTTP的location
你的HTTP的路径映射到哪一个HTTP的Handler
还有就是要HTTP的server
你要负责监听对吧
大家如果你去看HTTP的源码
它的整个Listen它的HTTPServer的代码其实一样的
就是有一个人负责监听
一个for循环
然后一旦收到一个连接之后
马上开个goroutne出去
然后它就会负责读解析HTTCP的head
处理完之后就会去调write
我们看看这个地方就是设置路由
我们直接用路由HandleFunc根路径
然后这个是代码这个地方 welcome to home page
这个地方是http.Server
包括它监听地址 超时间
然后ListenAndServe
Client其实也非常简单
先用一个Transport对象
甚至你可以直接调http.Get
自己设定一个http.Transport对象
然后构建一个http.Client就发生一个的请求
都是非常简单的
我刚刚说的我们的net/http的源码我们也可以看一下
一个叫做Serve的方法
看它的核心代码是不是一个for的接收连接
接收完连接以后它就会去newConn
然后注意这个地方也是开了goroutine调它的serve方法
把它直接弄出去
这个地方里面其实就是解析
解析什么HTTP头的
前面一大坨的处理解析HTTP头的
我们看这个地方叫readRequest
这个地方就是解析HTTP头
然后这边地方处理完之后
因为我们知道HTTP1.1是一个长连接的
它这里面其实就包括处理keep-alive
然后包括怎么处理response
都是在这里面处理的
我们看一下这里有个叫serverHandler{c.server}.ServeHTTP(w. w.req)
其实就是我们路由注入进来的方法
它其实就是调接口
等处理完之后
你最终就要回包我们
回包这个地方没有
因为是在我们server HTTP里面其实就会有回包的
叫finishRequest
就这个地方调完之后就会finishRequest
所以也是非常简单的
我们再来了解一下Go的一个IO模型
那么Linux下主要的IO模型分为blocking IO
我们叫阻塞IO
还有Nonblocking IO非阻塞
还有IO的一个多路复用
还有一个就是叫信号驱动的IO
叫Signal-driven IO
还有异步IO
严格意义上来讲
除了最后一个异步IO叫异步的话
其他的上面的所有的其实都是叫同步IO
怎么定义
首先要搞清楚同步就是
你的调用端一直会等待
比方我发一个包
然后我要收一个包
如果我的调用端一直在等待回报
是不是要等待我的网络到达
它才会返回
我们把这种方式叫做同步IO
对于阻塞式的
就是我们比方说我去读一个套接字
读一个TCP的套件是读一个read的
对方如果什么都没发什么内容过来
我是不是意味着我的客户端的线程或者我goroutine会被挂起
这个时候我在线程或者goroutine其实不会被CPU或者Go runtime调度
意味着我线程或者goroutine会暂停运行
虽然它不会浪费资源
但是它可能会暂停
那么非阻塞是什么呢
我们实际上可以把一个套接字一个fd把它设置为什么
非阻塞模式打开
也就是说我会叫read的函数
其实有点像一个try read
我尝试去读一下
如果try完之后发现啥内容没有
我是不是可以报个错
你啥也没读到
欢迎你下次再来重试一下
这种就是叫非阻塞的
其实它内容什么时候到达
其实不知道
你得不断的轮询去判定有没有
那这两个模型用起来都不太爽
那么什么是异步呢
异步其实就是我的调用端发起调用之后
比方我AIO发起一个调用之后
其实把这个操作挂接给底层的设备
比方说DMA的设备
它去做一些后续的处理
给我的网络设备他去做一些后续的
这种IO的一些处理操作
当它好了之后
他会有一种机制
可能是一个线程或者goroutine的回调函数
或者说是通过产生一个信号
一个线程信号唤醒你
然后你再去处理
那么这种方式发完一个IO操作以后
你可以做别的事情
然后通过h回调的方式来告诉你
那么这种真正意义上的叫异步IO
那么这里面其实最容易搞混淆的是什么
是IO的多路复用
IO多路复用是不是异步的
其实严格意义上它不能算
只能叫是异步的阻塞式的一个模型
为什么
我们看一下
我们最熟悉的IO多路复用都用在Linux下select是非常常见的
当然另一个Linux下还有epoll性能可能更好
那么select的我以前学unp就是UNIX Network Programming
UND这本书的时候
包括Nginx也是支持select和epoll两种
我以前学select的发现它以下几个缺陷
首先他只能监听1024个文件描述符
比方你做一个HTTP server
你1024个连接肯定不够
它是内存拷贝开销很大
跟它内部实现原理有关
另外就是它时间复杂度很高
1024个文件描述符哪一个
是否好了之后
它要遍历所有的一个
扫一遍告诉你哪一个好了
这个可能效率是比较低下
那么io多路复用怎么理解
其实你可以理解为这个select函数相当于监听了一批fd
告诉你一个连接好了
你就可以去读它
其实select本身是有block的
它是会阻塞的
严格意义上多路复用它只是说也是同步的
因为你只有select返回
你再去掉read的函数
你才能把这个数据取出来
所以它其实本质上也是一个同步的一种方式
它也是会让你的线程或者进程会block住的
因为你没有数据ready
你没有数据ready
你的select肯定会阻塞
一旦你的数据ready了
你的数据会返回
告诉你哪个fd可以读
那么你再去调read函数
这样的一个模型
其实有点像一个什么
其实利用select来做同步
做一个等待数据ready
然后用read在读
这个read其实也是把它设定为是non-blocking的一个非阻塞式的一个IO
但是整体来说它本质上还是一种同步的IO模型
只有异步IO才真正的一个义务模型的
那么select单线程它其实也是可以响应多个网络请求的
以前还不太理解为什么单线程可以处理多个网络连接
其实相当于你现在有一批连接被epoll或者select托管了
当有一两个人好了以后
你就返回这个fd
然后你就可以read或者write
可以想象一下
其实是用一个线程不断的有人唤醒
你有数据好了
然后你可以去持续处理
所以它是串行一个线程不断地去处理
但是看上去有点像一个并行的效果
但是实际上它是单线程
也可以做到一个很好的处理的
如果有好多个线程
是不是我的整个IO
我的整个网络吞吐就会非常好
那么为了提高IO的一个多路复用
不同操作系统也有自己的IO多路复用的一个函数
比方说Linux的epoll
还有类似像kqueue
还有我们windows有完成端口叫compete port
好像是叫完成端口
还有evport等等
Go语言其实在不同操作系统上
它是做了多平台的一个适配
那么这个就是它的一个网络轮训的模块
大家感兴趣也可以去读一下
那上面的一些基础知识的部分
我们就讲到这里了
我们接下来会要讲一下
用goim的一个实践一个实战
就是我们goim是怎么设计的
它的原理是什么样的
然后做了一些什么优化