通过redis学网络(1)-用go基于epoll实现最简单网络通信框架
本系列主要是为了对redis的网络模型进行学习,我会用golang实现一个reactor网络模型,并实现对redis协议的解析。
系列源码已经上传github
https://github.com/HobbyBear/tinyredis/tree/chapter1
redis的网络模型是基于epoll实现的,所以这一节让我们先基于epoll,实现一个最简单的服务端客户端通信模型。在实现前,先来简单的了解下epoll的原理。
为什么不用golang的原生的netpoll网络框架呢,这是因为netpoll框架虽然底层也是基于epoll实现,但是它提供给开发人员使用网络io方式依然是同步阻塞模式,一个连接单独的拿给一个协程去处理,为了更加真实的感受下redis的网络模型,我们不用netpoll框架,而是自己写一个非阻塞的网络模型。
epoll 网络通信原理
通常情况下服务端的处理客户端请求的逻辑是客户端每发起一个连接,服务端就单独起一个线程去处理这个连接的请求,对于go应用程序而言,则是启用一个协程去处理这个连接。 而采用epoll相关的api后,能够让我们在一个线程或者协程里去处理多个连接的请求。
一个套接字连接对应一个文件描述符,当收到客户端的连接请求时,可以将对应的文件描述符加入到epoll实例关注的事件中去。
在golang里,可以通过syscall.EpollCreate1 去创建一个epoll实例。
func EpollCreate1(flag int) (fd int, err error)
其返回结果的fd就代表epoll实例的fd,当收到客户端的连接请求时,便可以将客户端连接的fd,通过EpollCtl 加入到epoll实例感兴趣的事件当中。
func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error)
EpollCtl 方法参数的epfd则是EpollCreate1 返回的fd,EpollCtl的第二个参数则是代表客户端连接的fd,通过我们在获取到客户端连接后,后续的行为便是查看客户端是否有数据发送过来或者往客户端发送数据,这些在epoll api里用event事件去表示,分别对应了读event和写event,这便是EpollCtl第三个参数所代表的含义。
将这些感兴趣事件添加到epoll实例中后,就代表epoll实例后续会监听这些连接的读写事件的到达,那么读写事件到达后,用户程序又是如何知道的呢,这就要提到epoll相关的另一个api,EpollWait。
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
EpollWait的第二个参数是一个事件数组,用户应用程序调用EpollWait时传入一个固定长度的事件数组,然后EpollWait会将这个数组尽可能填满,这样用户程序便能知道有哪些事件类型到达了,EpollEvent类型如下所示:
type EpollEvent struct {
Events uint32
Fd int32
Pad int32
}
其中fd则代表这些事件所关联的客户端连接的fd,通过这个fd,我们便可以对对应连接进行读写操作了。
而Events是个枚举类型,比较常用的枚举以及含义如下:
类型 | 解释 |
---|---|
EPOLLIN | 表示文件描述符可读。 |
EPOLLRDHUP | 表示 TCP 连接的远程端点关闭或半关闭连接 |
EPOLLET | 表示使用边缘触发模式来监听事件 |
EPOLLOUT | 表示文件描述符可写 |
EPOLLERR | 表示文件描述符发生错误时发生,这个事件不通过EpollCtl添加也能触发 |
EPOLLHUP | 与EPOLLRDHUP类似同样表示连接关闭,在不支持EPOLLRDHUP的linux版本会触发,这个事件不通过EpollCtl添加也能触发 |
虽然epoll event还有其他类型,不过一般情况下监控这几种类型就足够了,golang的netpoll框架在添加连接的文件描述符时事件时也只添加了这几种类型。netpoll的部分源码如下:
func netpollopen(fd uintptr, pd *pollDesc) int32 {
var ev epollevent
ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
*(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}
如何用golang创建基于epoll的网络框架
了解完epoll的一些概念以后,现在来看下我们需要实现的网络框架模型是怎样的。我们先实现一个最简单的网络通信框架,客户端发送来消息,然后服务端打印收到的消息。
如上图所示,我们收到新的连接后,会调用epoll实例的EpollCtl方法将连接的可读事件添加到epoll实例中,接着调用EpollWait方法等待客户端再次发送消息时,让连接变为可读。
下面是程序的效果测试结果
效果测试
启动了两个终端,其中右边的终端连接上redis以后,发送了1231,然后左边的终端收到后将收到的消息打印出来。
go代码实现
接着,我们来看看实际代码编写逻辑。
我们定义一个Server的结构体来代表epoll的server。
Conn是对golang原生连接类型net.Conn的包装,。
poll结构体是封装了对epoll api的调用。
type Server struct {
Poll *poll
addr string
listener net.Listener
ConnMap sync.Map
}
type Conn struct {
s *Server
conn *net.TCPConn
nfd int
}
type poll struct {
EpollFd int
}
接着来看下如何启动一个Server,NewServer是返回一个Server实例,Server 调用Run方法后,才算Server正式启动了起来。
在Run 方法里,构建监听连接的listener,构建一个epoll实例,用于后续对事件的监听,同时把监听握手连接和处理连接可读数据分成了两个协程分别用accept方法,和handler方法执行。
func NewServ(addr string) *Server {
return &Server{addr: addr, ConnMap: sync.Map{}}
}
func (s *Server) Run() error {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
return err
}
s.listener = listener
epollFD, err := syscall.EpollCreate1(0)
if err != nil {
return err
}
s.Poll = &poll{EpollFd: epollFD}
go s.accept()
go s.handler()
ch := make(chan int)
<-ch
return nil
}
accept 方法里执行的逻辑就是将握手完成的链接从全连接队列里取出来,将其连接的文件描述符和连接存储到一个map里, 然后将对应的文件描述符通过epoll的epollCtl 系统调用监听它的可读事件,后续客户端再使用这个连接发送数据时,epoll就能监听到了。
func (s *Server) accept() {
for {
acceptConn, err := s.listener.Accept()
if err != nil {
return
}
var nfd int
rawConn, err := acceptConn.(*net.TCPConn).SyscallConn()
if err != nil {
log.Error(err.Error())
continue
}
rawConn.Control(func(fd uintptr) {
nfd = int(fd)
})
// 设置为非阻塞状态
err = syscall.SetNonblock(nfd, true)
if err != nil {
return
}
err = s.Poll.AddListen(nfd)
if err != nil {
log.Error(err.Error())
continue
}
c := &Conn{
conn: acceptConn.(*net.TCPConn),
nfd: nfd,
s: s,
}
s.ConnMap.Store(nfd, c)
}
}
handler里的逻辑则是通过epoll Wait系统调用等待可读事件产生,到达后,根据事件的文件描述符找到对应连接,然后读取对应连接的数据。
func (s *Server) handler() {
for {
events, err := s.Poll.WaitEvents()
if err != nil {
log.Error(err.Error())
continue
}
for _, e := range events {
connInf, ok := s.ConnMap.Load(int(e.FD))
if !ok {
continue
}
conn := connInf.(*Conn)
if IsClosedEvent(e.Type) {
conn.Close()
continue
}
if IsReadableEvent(e.Type) {
buf := make([]byte, 1024)
rd, err := conn.Read(buf)
if err != nil && err != syscall.EAGAIN {
conn.Close()
continue
}
fmt.Println("收到消息", string(buf[:rd]))
}
}
}
}
主干代码是比较容易理解的,但是用golang使用epoll 时有几个点 需要注意下:
第一点是IsReadableEvent 的判断方式,epoll的每个event 都有一个位掩码,位掩码是什么意思呢?比如EPOLLIN 的值 是0x1,二进制就是00000001,EPOLLHUP 的值是0x10,二进制表示是00010000,那么epoll wait系统调用的event要如何同时表示同一个文件描述符同时拥有这两个事件呢? epoll 的event会将对应的位掩码设置为和对应事件一致,比如同时拥有EPOLLIN和EPOLLHUP,那么event的值将会是00010001,所以利用与位运算是不是就能判断event是否具有某个事件了。因为1只有与1进行与运算结果才为1。
func IsReadableEvent(event uint32) bool {
if event&syscall.EPOLLIN != 0 {
return true
}
return false
}
第二点是如何读取连接的数据, 我们后续要达到的目的是在同一个事件循环里能处理多个连接,所以要保证读取连接中的数据时不能阻塞,通过调用golang的net.Conn下的read方法是阻塞的,其read实现最终会调用到下面的这个方法。
func (fd *FD) Read(p []byte) (int, error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if len(p) == 0 {
// If the caller wanted a zero byte read, return immediately
// without trying (but after acquiring the readLock). // Otherwise syscall.Read returns 0, nil which looks like // io.EOF. // TODO(bradfitz): make it wait for readability? (Issue 15735) return 0, nil
}
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, err
}
if fd.IsStream && len(p) > maxRW {
p = p[:maxRW]
}
for {
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, err
}
}
这个方法会在for循环中判断系统调用syscall.Read 的返回,如果是syscall.EAGAIN 那么会让当前协程睡眠,等待被唤醒。
syscall.EAGAIN 错误是在非阻塞io进行读写时才有可能产生的,在读取数据时,如果发现读缓冲区没有数据到达,则返回这个syscall.EAGAIN错误,在写入数据时,如果写缓冲区满了,也会返回这个错误。
既然golang的net.Conn下的read方法是阻塞的,那么我们就自己实现下conn的Read方法。
func (c *Conn) Read(p []byte) (n int, err error) {
rawConn, err := c.conn.SyscallConn()
if err != nil {
return 0, err
}
rawConn.Read(func(fd uintptr) (done bool) {
n, err = syscall.Read(int(fd), p)
if err != nil {
return true
}
return true
})
return
}
的Read方法是我们自定义的Conn类型实现的Read方法,原生的连接类型是net.Conn,它有一个SyscallConn 能够获取到更加底层的连接类型,从这个类型能够获取到该网络连接的文件描述符fd,我们通过直接调用系统调用syscall.Read来从该网络连接读取数据。 并且碰到错误则直接返回。后续 syscall.EAGAIN错误会交给上层handler方法去进行处理。
总结
这节算是用golang去演示了下如何对epoll api的调用,并且能够实现最简单的客户端服务端通信,下一节我会讲解redis的网络模型是怎么样的,你可以从中了解到经常说的redis的单线程具体是指什么,了解到reactor网络模型是怎样的?
通过redis学网络(1)-用go基于epoll实现最简单网络通信框架的更多相关文章
- 【网络编程01】socket的基础知识-简单网络通信程序
1.什么是socket socket(套接字),简单来说是IP地址与端口(port)的组合,可以与远程主机的应用程序进行通信.通过IP地址可以确定一台主机,而通过端口则可以确定某一个应用程序.IP+端 ...
- (转载)基于Unity~UGUI的简单UI框架(附UIFramework源码)
此博客跟随siki老师的课程笔记生成,感谢siki老师的辛勤付出! 此框架功能较简单,适用于学习,可以很好的锻炼我们的设计思想 框架源码地址: UIFramework litjson.dll下载地址: ...
- 阶段4-独挡一面\项目-基于视频压缩的实时监控系统\Sprint1-基于Epoll架构的采集端程序框架设计\第2课-基于Epoll的采集端程序框架设计
回顾之前的整个程序架构 把epoll机制应用到这个架构上去 下面主要去分析我们的系统中有没有需要等待的事件,先看看采集子系统 在采集子系统当中,摄像头有数据,摄像头采集到图像数据可以作为一个等待事件. ...
- 使用JavaScript实现机器学习和神经学网络
欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 下载heaton-javascript-ml.zip - 45.1 KB 基本介绍 在本文中,你会对如何使用JavaScript实现机器学习这个 ...
- 入木三分学网络第一篇--VRRP协议详解第一篇(转)
因为keepalived使用了VRRP协议,所有有必要熟悉一下. 虚拟路由冗余协议(Virtual Router Redundancy Protocol,简称VRRP)是解决局域网中配置静态网关时,静 ...
- 一步一步学ZedBoard & Zynq(四):基于AXI Lite 总线的从设备IP设计
本帖最后由 xinxincaijq 于 2013-1-9 10:27 编辑 一步一步学ZedBoard & Zynq(四):基于AXI Lite 总线的从设备IP设计 转自博客:http:// ...
- Spring集成Redis方案(spring-data-redis)(基于Jedis的单机模式)(待实践)
说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点.并且会与一些低版本的Sp ...
- Fixed-Length Frames 谈谈网络编程中应用层(基于TCP/UDP)的协议设计
http://blog.sina.com.cn/s/blog_48d4cf2d0101859x.html 谈谈网络编程中应用层(基于TCP/UDP)的协议设计 (2013-04-27 19:11:00 ...
- 南京邮电大学网络攻防平台(NCTF)-MD5-Writeup
南京邮电大学网络攻防平台-MD5-Writeup 题干如下: 分析: 遍历 TASC?O3RJMV?WDJKX?ZM(?替换为其他),找到md5为e9032???da???08????911513?0 ...
- 南京邮电大学网络攻防训练平台(NCTF)-异性相吸-Writeup
南京邮电大学网络攻防训练平台(NCTF)-异性相吸-Writeup 题目描述 文件下载地址 很明显,文件之间进行亦或就可得到flag,不再多说,直接上脚本 #coding:utf-8 file_a = ...
随机推荐
- requests不带参数的get请求和带get参数请求
requests库常用的方法 requests.Request(url) 构造一个请求,支持以下各种方式 requests.get() 发送get请求 requests.post() 发送post请求 ...
- vulnhub靶场之HACKSUDO: 2 (HACKDUDO)
准备: 攻击机:虚拟机kali.本机win10. 靶机:hacksudo: 2 (HackDudo),下载地址:https://download.vulnhub.com/hacksudo/hackdu ...
- 使用GithubAction自动构建部署项目
目录 1.1 项目准备 2.1 GithubAction设置 3.1 运行测试 4.1 小结 GitHub Actions 是一种持续集成和持续交付(CI/CD) 平台,可用于自动执行生成.测试和部署 ...
- 二进制安装Kubernetes(k8s) v1.23.5
Github:https://github.com/cby-chen/Kubernetes/releases 前提说明:公主号不支持富文本,建议在Github查看. 1.23.3 和 1.23.4 和 ...
- [架构]辨析: 高可用 | 集群 | 主从 | 负载均衡 | 反向代理 | 中间件 | 微服务 | 容器 | 云原生 | DevOps | ...
词汇集 灾备 冷备份 双机热备份 异地容灾备份 云备份 灾难演练 磁盘阵列(RAID) 故障切换 心跳监测 高可用 集群 主从复制(Master-Slave) 多集群横向扩容(master-clust ...
- elasticsearch 官方优化建议
1.一般建议 a.不要返回过大的结果集.这个建议对一般数据库都是适用的,如果要获取大量结果,可以使用search_after api,或者scroll (新版本中已经不推荐). b.避免大的文 ...
- 异常:java.io.FileNotFoundException:e:\demo(拒绝访间。)
禁止向目录中写数据,只能向文件写数据
- csv数据集按比例分割训练集、验证集和测试集,即分层抽样的方法
一.一种比较通俗理解的分割方法 1.先读取总的csv文件数据: import pandas as pd data = pd.read_csv('D:\BaiduNetdiskDownload\weib ...
- 深入理解 Redis 新特性:Stream
该数据结构需要 Redis 5.0.0 + 版本才可用使用 概述 Redis stream 是 Redis 5 引入的一种新的数据结构,它是一个高性能.高可靠性的消息队列,主要用于异步消息处理和流式数 ...
- lua变量、数据类型、if判断条件和数据结构table以及【lua 函数】
一.lua变量[ 全局变量和局部变量和表中的域] Lua 变量有三种类型:全局变量和局部变量和表中的域. 全局变量:默认情况下,Lua中所有的变量都是全局变量. 局部变量:使用local 显式声明在函 ...