Featured image of post go 使用 epoll 实现高性能tcp服务器

go 使用 epoll 实现高性能tcp服务器

在本篇文章中,我们深入探讨如何在 Go 中实现一个简单的 TCP 服务器。与 C/C++ 相比,Go 的实现更为简洁,仅需一行代码即可启动 TCP 监听。我们将逐步展示如何使用系统调用(如 socket、bind、listen 和 accept)和 epoll 进行高效的多路复用,确保服务器能够处理多个连接。同时,介绍了条件编译的使用,以支持跨平台操作。

在go中实现一个tcp服务器还是很简单的,至少和C/C++相比还是很简单的了。

一个简单的例子

1	listen, err := net.Listen("tcp", "0.0.0.0:8088")

只需要这样一行就可以监听了,就能等待客户端连接了。是不是还是很简单的

在C/C++中,需要依次 调用socket() bind() listen() accept()函数,完成打开,绑定,监听,等待操作,才能完成等待客户端来连接。 这还没完,想要提高性能还需要自己通过 epoll等手段完成多路复用。

其实在C/C++中是通过调用系统函数来完成的,只是go把这部分东西都给包装了,只需要简单的一行就可以完成了。

其实在go中也可以通过系统函数自己来完成些事情。只是这些事情比较复杂,跨平台还不好弄,像使用了epoll就只能在linux系统上编译运行了。

废话不多说了,直接上函数吧。在go中和C/C++中区别不大,同样是通过系统调用这些函数来完成。

1func Socket(domain, typ, proto int) (fd int, err error)
2
3func Bind(fd int, sa Sockaddr) (err error) 
4
5func Listen(s int, n int) (err error) 
6
7func Accept(fd int) (nfd int, sa Sockaddr, err error) 

在go开启tcp 服务需要用到的函数.

1func EpollCreate(size int) (fd int, err error)
2
3func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) 
4
5func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) 

在go中使用epoll需要的函数

这些函数都是在syscall 包下,所以这些函数不是所有系统下都有的, 像epoll 相关的函数,就只能在linux下才能编译过。

所以,在这里就引申出一个东西就是 条件编译 在C/C++中可以通过宏定义来实现条件编译

1    #ifdef linux
2       cout<<"It is in Linux OS!"<<endl;
3    #endif

这段代码就只能在linux系统上才会被编译

go虽然没有这么强大的宏命令来判断,但是go中可以通过编译标签文件后缀来判断。

比如在文件的第一行加上

1// +build linux
2.....
3.....

这样,这个文件就只能在linux上才会被编译(注意,// +build linux下面一定要有一个空行) 详细用法看这里 go条件编译

下面开始具体的代码

 1//定义一个结构体存储相关的数据
 2type EpollM struct {
 3	conn map[int]*ServerConn
 4
 5	socketFd int //监听socket的fd
 6	epollFd  int //epoll的fd
 7}
 8
 9//开启监听
10func (e *EpollM) Listen(ipAddr string, port int) error {
11	//使用系统调用,打开一个socket
12	fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
13	if err != nil {
14		return err
15	}
16
17	//ip地址转换
18	var addr [4]byte
19	copy(addr[:], net.ParseIP(ipAddr).To4())
20	net.ParseIP(ipAddr).To4()
21	err = syscall.Bind(fd, &syscall.SockaddrInet4{
22		Port: port,
23		Addr: addr,
24	})
25	if err != nil {
26		return err
27	}
28
29	//开启监听
30	err = syscall.Listen(fd, 10)
31	if err != nil {
32		return err
33	}
34	e.socketFd = fd
35	return nil
36}

这样就完成了监听

下面是 epoll 处理部分

 1
 2//处理epoll
 3func (e *EpollM) HandlerEpoll() error {
 4	events := make([]syscall.EpollEvent, 100)
 5	//在死循环中处理epoll
 6	for {
 7		//msec -1,会一直阻塞,直到有事件可以处理才会返回, n 事件个数
 8		n, err := syscall.EpollWait(e.epollFd, events, -1)
 9
10		if err != nil {
11			return err
12		}
13		for i := 0; i < n; i++ {
14			//先在map中是否有这个链接
15			conn := e.GetConn(int(events[i].Fd))
16			if conn == nil { //没有这个链接,忽略
17				continue
18			}
19			if events[i].Events&syscall.EPOLLHUP == syscall.EPOLLHUP || events[i].Events&syscall.EPOLLERR == syscall.EPOLLERR {
20				//断开||出错
21				if err := e.CloseConn(int(events[i].Fd)); err != nil {
22					return err
23				}
24			} else if events[i].Events == syscall.EPOLLIN {
25				//可读事件
26				conn.Read()
27			}
28		}
29	}
30}

从连接中读写数据

 1//读取数据
 2func (s *ServerConn) Read() {
 3	data := make([]byte, 100)
 4	
 5	//通过系统调用,读取数据,n是读到的长度
 6	n, err := syscall.Read(s.fd, data)
 7	if n == 0 {
 8		return
 9	}
10	if err != nil {
11		fmt.Printf("fd %d read error:%s\n", s.fd, err.Error())
12	} else {
13		fmt.Printf("%d say: %s \n", s.fd, data[:n])
14		s.Write([]byte(fmt.Sprintf("hello %d", s.fd)))
15	}
16}
17
18//向这个链接中写数据
19func (s *ServerConn) Write(data []byte) {
20	_, err := syscall.Write(s.fd, data)
21	if err != nil {
22		fmt.Printf("fd %d write error:%s\n", s.fd, err.Error())
23	}
24}

最后在依次调用这些函数

 1package main
 2
 3func main() {
 4	epollM := NewEpollM()
 5	//开启监听
 6	err := epollM.Listen("0.0.0.0", 8088)
 7	if err != nil {
 8		panic(err)
 9	}
10
11	//创建epoll
12	err = epollM.CreateEpoll()
13	if err != nil {
14		panic(err)
15	}
16
17	//异步处理epoll
18	go func() {
19		err := epollM.HandlerEpoll()
20		epollM.Close()
21		panic(err)
22	}()
23
24	//等待client的连接
25	err = epollM.Accept()
26	epollM.Close()
27	panic(err)
28}

到这整个服务就运行起来了,代码已经上传到github了,传送门

整个过程看下来,和C/C++实现过程还是非常相似的,都是通过系统调用完成的。也没有什么难点,就这样吧

发表了56篇文章 · 总计128.44k字
本博客已稳定运行
© QX
使用 Hugo 构建
主题 StackJimmy 设计