网络通信协议
Go 实现网络编程
Go 网络编程 - 基础概念
基础概念:
- Socket:数据传输
- Encoding:内容编码
- Session:连接会话状态
- C/S模式:通过客户端实现双端通信
- B/S模式:通过浏览器即可完成数据的传输
网络轮询器
- 多路复用模型
- 多路复用模块
- 文件描述符
- Goroutine 唤醒
简单例子
- 通过TCP/UDP实现网络通信
Go 网络编程 - TCP简单例子
main
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:10000")
if err != nil {
log.Fatalf("listen error: %v\n", err)
}
for {
conn, err := listen.Accept()
if err != nil {
log.Printf("accept error: %v\n", err)
continue
}
// 开始goroutine监听连接
go handleConn(conn)
}
}
handleConn
func handleConn(conn net.Conn) {
defer conn.Close()
// 读写缓冲区
rd := bufio.NewReader(conn)
wr := bufio.NewWriter(conn)
for {
line, _, err := rd.ReadLine()
if err != nil {
log.Printf("read error: %v\n", err)
return
}
wr.WriteString("hello ")
wr.Write(line)
wr.Flush() // 一次性syscall
}
}
// bufio库 如果自己处理链接的读写 很容易导致syscall放大 bufio可以减少syscall 提前会copy数据到底层的byte数组中
// 例如
Go 网络编程 - UDP简单例子
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{Port: 20000})
if err != nil {
log.Fatalf("listen error: %v\n", err)
}
defer listen.Close()
for {
var buf [1024]byte
n, addr, err := listen.ReadFromUDP(buf[:])
if err != nil {
log.Printf("read udp error: %v\n", err)
continue
}
data := append([]byte("hello "), buf[:n]...)
listen.WriteToUDP(data, addr)
}
}
Go 网络编程 - I/O模型
推荐好书 UNIX NETWORK PROGRAMING
Linux下主要的IO模型分为:
-
Blocking IO - 阻塞I O
- scket中没有数据后 listen会阻塞住
-
Nonblocking IO - 非阻塞IO
-
IO multiplexing - IO 多路复用
-
Signal-driven IO - 信号驱动式IO(异步阻塞)
- 注册一个singal 基于拦截到某个信号
-
Asynchronous IO - 异步IO
- IO没有ready的话 继续往下走 可以注册一个callback函数 等待处理完成后回调
-
同步:调用端会一直等待服务端响应,直到返回结果
-
异步:调用端发起调用之后不会立刻返回,不会等待服务端响应
-
阻塞:服务端返回结果之前,客户端线程会被挂起,此时线程不可被 CPU 调度,线程暂停运行
-
非阻塞:在服务端返回前,函数不会阻塞调用端线程,而会立刻返回
Go 网络编程 - I/O多路复用
Go 语言在采用 I/O 多路复用 模型处理 I/O 操作,但是他没有选择最常见的系统调用 select。虽然 select 也可以提供 I/O 多路复用的能力,但是使用它有比较多的限制:
- 监听能力有限 — 最多只能监听 1024 个文件描述符;
- 内存拷贝开销大 — 需要维护一个较大的数据结构存储文件描述符,该结构需要拷贝到内核中;
- 时间复杂度 𝑂(𝑛) — 返回准备就绪的事件个数后,需要遍历所有的文件描述符;
I/O多路复用:进程阻塞于 select,等待多个 IO 中的任一个变为可读,select调 用返回,通知相应 IO 可以读。 它可以支持单线程响应多个请求这种模式。
Go 网络编程 - 多路复用模块
- 为了提高 I/O 多路复用的性能,不同的操作系统也都实现了自己的 I/O 多路复用函数 例如:epoll、kqueue 和 evport 等
- Go 语言为了提高在不同操作系统上的 I/O 操作性能,使用平台特定的函数实现了多个版本的网络轮询模块:
- src/runtime/netpoll_epoll.go
- src/runtime/netpoll_kqueue.go
- src/runtime/netpoll_solaris.go
- src/runtime/netpoll_windows.go
- src/runtime/netpoll_aix.go
- src/runtime/netpoll_fake.go
Goim 长连接 TCP 编程
Goim 长连接 TCP 编程 - 概览
参考项目
场景: 即时通讯IM,通过中心服务器 向某个/多个设备下发指令, 广播所有的机器 下发指令
弹幕穿越问题 加载离线弹幕 然后实时弹幕刷新
架构设计 kafka缓存任务数据 redis存储路由信息 discover心跳发现
Comet
长连接管理层,主要是监控外网 TCP/Websocket端口,并且通过设备 ID 进行绑定 Channel 实现,以及实现了 Room 合适直播等大房间消息广播。
Logic
逻辑层,监控连接 Connect、Disconnect 事件,可自定义鉴权,进行记录 Session 信息(设备 ID、ServerID、用户 ID),业务可通过设备 ID、用户 ID、RoomID、全局广播进行消息推送。 - logic单独处理逻辑, 无状态服务
Job
通过消息队列的进行推送消峰处理,并把消息推送到对应 Comet 节点。
各个模块之间通过 gRPC 进行通信。
Goim 长连接 TCP 编程 - 协议设计
主要以包/针方式:
- Package Length,包长度 4bytes
- Header Length,头长度
- Protocol Version,协议版本
- Operation,操作码
- Sequence 请求序号 ID
- Body,包内容
Operation对应操作:
- Auth
- Heartbeat
- Message
Sequence
- 按请求、响应对应递增 ID
Goim 长连接 TCP 编程-边缘节点(comet模块)
Comet 长连接连续节点,通常部署在距离用户比较近,通过 TCP 或者 Websocket 建立连接,并且通过应用层 Heartbeat 进行保活检测,保证连接可用性。
节点之间通过云 VPC 专线通信,按地区部署分布。 国内:
- 华北(北京)
- 华中(上海、杭州)
- 华南(广州、深圳)
- 华西(四川) 国外:
- 香港、日本、美国、欧洲
Goim 长连接 TCP 编程 - 负载均衡
问题 智能调度怎么做? 让用户连接就近的节点?
comet部署的时候 有一个外网IP 用户可以直连
长连接负载均衡比较特殊,需要按一定的负载算法进行分配节点,可以通过 HTTPDNS 方式,请求获致到对应的节点 IP 列表,例如,返回固定数量 IP,按一定的权重或者最少连接数进行排序,客户端通过 IP 逐个重试连接;
- Comet 注册 IP 地址,以及节点权重,定时 Renew当前节点连接数量;
- Balancer 按地区经纬度计算,按最近地区(经纬度)提供 Comet 节点 IP 列表,以及权重计算排序;
- BFF 返回对应的长连接节点 IP,客户端可以通过 IP直接连;
- 客户端 按返回 IP 列表顺序,逐个连接尝试建立长连接
Goim 长连接 TCP 编程 - 心跳保活机制
长连接断开的原因:
- 长连接所在进程被杀死
- NAT 超时
- 网络状态发生变化,如移动网络 & Wifi 切换、断开、重连
- 其他不可抗因素(网络状态差、DHCP 的租期等等 )
高效维持长连接方案
- 进程保活(防止进程被杀死) 移动端 会被安卓内核kill掉
- 有一些相互拉活的操作 app之间互相拉
- 心跳保活(阻止 NAT 超时)
- 定时发个ping 防止空闲链接被干掉
- 断线重连(断网以后重新连接网络)
自适应心跳时间
- 心跳可选区间,[min=60s,max=300s]
- 心跳增加步长,step=30s
- 心跳周期探测,success=current + step、fail=current - step
Goim 长连接 TCP 编程 - 用户鉴权和 Session 信息
用户鉴权,在长连接建立成功后,需要先进行连接鉴权,并且绑定对应的会话信息;
- Connect,建立连接进行鉴权,保存 Session 信息:
- DeviceID,设备唯一 ID
- Token,用户鉴权 Token,认证得到用户 ID
- CometID,连接所在 comet 节点
- Disconnect,断开连接,删除对应 Session 信息:
- DeviceID,设备唯一 ID
- CometID,连接所在 Comet 节点
- UserID,用户 ID
- Session,会话信息通过 Redis 保存连接路由信息:
连接维度,通过 设备 ID 找到所在 Comet 节点
用户维度,通过 用户 ID 找到对应的连接和 Comet所在节点
Goim 长连接 TCP 编程 - Comet
- Comet 长连接层,实现连接管理和消息推送:
- Protocol,TCP/Websocket 协议监听;
- Packet,长连接消息包,每个包都有固定长度;
- Channel,消息管道相当于每个连接抽象,最终TCP/Websocket 中的封装,进行消息包的读写分发;
- Bucket,连接通过 DeviceID 进行管理,用于读写锁拆散,并且实现房间消息推送,类似 Nginx Worker;
- Room,房间管理通过 RoomID 进行管理,通过链表进行Channel 遍历推送消息;
每个 Bucket 都有独立的 Goroutine 和读写锁优化: Buckets { channels map[string]*Channel rooms map[string]*Room}
Goim 长连接 TCP 编程 - Logic
Logic 业务逻辑层,处理连接鉴权、消息路由,用户会话管理;
主要分为三层:
- sdk,通过 TCP/Websocket 建立长连接,进行重连、心跳保活;
- goim,主要负责连接管理,提供消息长连能力;
- backend,处理业务逻辑,对推送消息过虑,以及持久化相关等;
Goim 长连接 TCP 编程 - Job
业务通过对应的推送方式,可以对连接设备、房间、用户 ID 进行推送,通过 Session 信息定位到所在的Comet 连接节点,并通过 Job 推送消息; 通过 Kafka 进行推送消峰,保证消息逐步推送成功;
支持的多种推送方式:
- Push(DeviceID, Message)
- Push(UserID, Message)
- Push(RoomID, Message)
- Push(Message)
Goim 长连接 TCP 编程 - 推拉结合
在长连接中,如果想把消息通知所有人,主要有两种模式:
- 一种是自己拿广播通知所有人,这叫“推”模式;
- 一种是有人主动来找你要,这叫“拉”模式。
在业务系统中,通常会有三种可能的做法:
- 推模式,有新消息时服务器主动推给客户端;
- 拉模式,由前端主动发起拉取消息的请求;
- 推拉结合模式,有新消息实时通知,客户端再进行新的消息摘取;
Goim 长连接 TCP 编程 - 读写扩散
一般消息系统中,通常会比较关注消息存储;
主要进行考虑“读”、“写”扩散,也就是性能问题;
在不同场景,可能选择不同的方式:
- 读扩散,在IM系统里的读扩散通常是每两个相关联的人就有一个信箱,或者每个群一个信箱。
- 优点:写操作(发消息)很轻量,只用写自己信箱
- 缺点:读操作(读消息)很重,需要读所有人信箱
- 写扩散,每个人都只从自己的信箱里读取消息,但写(发消息)的时候需要所有人写一份
- 优点:读操作很轻量
- 缺点:写操作很重,尤其是对于群聊来说
Goim 长连接 TCP 编程 - 唯一 ID 设计
唯一 ID,需要保证全局唯一,绝对不会出现重复的 ID,且 ID 整体趋势递增。
通常情况下,ID 的设计主要有以下几大类:
- UUID
- 基于 Snowflake 的 ID 生成方式
- 基于申请 DB 步长的生成方式
- 基于 Redis 或者 DB 的自增 ID生成方式
- 特殊的规则生成唯一 ID
Snowflake
Snowflake,is a network service for generating unique ID numbers at high scale with some simple guarantees.
id is composed of:
- time - 41 bits (millisecond precision w/ a custom epoch gives us 69 years)
- configured machine id - 10 bits - gives us up to 1024 machines
- sequence number - 12 bits - rolls over every 4096 per machine (with protection to avoid rollover in the same ms)
Sonyflake
Sonyflake,is a distributed unique ID generator inspired by Twitter's Snowflake.
id is composed of:
- 39 bits for time in units of 10 msec
- 8 bits for a sequence number
- 16 bits for a machine id
As a result, Sonyflake has the following advantages and disadvantages:
- The lifetime (174 years) is longer than that of Snowflake (69 years)
- It can work in more distributed machines (2^16) than Snowflake (2^10)
- It can generate 2^8 IDs per 10 msec at most in a single machine/thread (slower than Snowflake)
基于步长递增的分布式 ID 生成器,可以生成基于递增,并且比较小的唯一 ID;
服务主要分为:
- 通过 gRPC 通信,提供 ID 生成接口,并且携带业务标记,为不同业务分配 ID;
- 部署多个 id-server 服务,通过数据库进行申请 ID步长,并且持久化最大的 ID,例如,每次批量取1000到内存中,可减少对 DB 的压力;
- 数据库记录分配的业务 MAX_ID 和对应 Step ,供Sequence 请求获取;
Goim 长连接 TCP 编程 - IM 私信系统
在聊天系统中,我们几乎每个人都在使用聊天应用,并且对消息及时性要求也非常高;
对消息也需要有一致性保证; 并且都有着丰富的多媒体传输功能:
- 1 on 1
- Group chat
- Online presence
- Multiple device support
- Push notifications
在聊天系统中,主要是客户端和服务端之间进行通信;
客户端可以是 Android、iOS、Web 应用;
通常客户端之间不会进行直接通信,而是客户端连接到服务端进行通信;
服务端需要支持:
- 接收各个客户端消息
- 消息转发到对应的人
- 用户不在线,存储新消息
- 用户上线,同步所有新消息
在聊天系统中,最重要的是通信协议,如何有保证地及时送达消息;
一般来看,移动端基本都是通过长连方式实现,而 Web 端可以使用 HTTP、Websocket 实现实时通信;
常用通信方式:
- HTTP 定时轮询
- HTTP 长轮询
- WebSocket
- TCP
在聊天系统中,有着很多用户、消息功能,比如:
- 登录、注册、用户信息,可以通过 HTTP API 方式;
- 消息、群聊、用户状态,可以通过 实时通信 方式;
- 可能集群一些三方的服务,比如 小米、华为推送、APNs 等;
所以,主要服务可为三大类:
- 无状态服务
- 有状态服务
- 第三方集成
在聊天系统中,Goim 主要角色是 Real time service,实现对 连接 和 状态 的管理:
可以通过 API servers 进行系统之间的解耦;
各个服务的主要功能为:
- 聊天服务进行消息的 发送和接收
- 在线状态服务管理用户 在线和离线
- API 服务处理 用户登录、注册、修改信息
- 通知服务器发送推送通知(Notification)
- 通过 KV 存储进行 存储、查询 聊天信息
在聊天系统中,消息存储是最主要的,通常会有海量的消息需要存储,我们也会想到 关系数据库还是NoSQL 数据库;
而关系数据库主要进行存储用户信息,好友列表,群组信息,通过主从、分片基本满足;
由于消息存储比较单一,可以通过 KV 存储;
KV 存储消息的好处:
- 水平扩展
- 延迟低
- 访问成本低
一对一聊天,主要的消息发送流程:
- 用户 A 向聊天服务发送消息给用户 B
- 聊天服务从生成器获取消息 ID
- 聊天服务将消息发到消息队列
- 消费保存在 KV 存储中
- 如果用户在线,则转发消息给用户
- 如果用户不在线,则转发到通知服务(Notification)
群聊,较为复杂,通常有多写、多读两种方式;
单信箱(多写),每个用户都保存一份消息:
- 消息同步流程比较简单,每个客户端仅需要读取自己的信箱,即可获取新消息
- 当群组比较小时,成本也不是很高,例如微信群通常为 500 用户上限
- 对数组数量无上限
多信箱(多读),每个群仅保存一份消息:
- 用户需要同时查询多个信箱
- 如果信箱比较多,查询成本比较高
- 需要控制群组上限
References
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-netpoller/
https://www.liwenzhou.com/posts/Go/15_socket/
https://hit-alibaba.github.io/interview/basic/network/HTTP.html
https://cloud.tencent.com/developer/article/1030660
https://juejin.cn/post/6844903827536117774
https://xie.infoq.cn/article/19e95a78e2f5389588debfb1c
https://tech.meituan.com/2019/03/07/open-source-project-leaf.html
https://mp.weixin.qq.com/s/8WmASie_DjDDMQRdQi1FDg
https://www.imooc.com/article/265871
https://www.infoq.cn/article/the-road-of-the-growth-weixin-backgroundhttps://systeminterview.com/design-a-chat-system.php