在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++中区别不大,同样是通过系统调用这些函数来完成。
1 2 3 4 5 6 7
| func Socket(domain, typ, proto int) (fd int, err error)
func Bind(fd int, sa Sockaddr) (err error)
func Listen(s int, n int) (err error)
func Accept(fd int) (nfd int, sa Sockaddr, err error)
|
在go开启tcp 服务需要用到的函数.
1 2 3 4 5
| func EpollCreate(size int) (fd int, err error)
func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error)
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
|
在go中使用epoll需要的函数
这些函数都是在syscall
包下,所以这些函数不是所有系统下都有的,
像epoll
相关的函数,就只能在linux下才能编译过。
所以,在这里就引申出一个东西就是 条件编译
在C/C++中可以通过宏定义来实现条件编译
1 2 3
| #ifdef linux cout<<"It is in Linux OS!"<<endl; #endif
|
这段代码就只能在linux系统上才会被编译
go虽然没有这么强大的宏命令来判断,但是go中可以通过编译标签 和 文件后缀来判断。
比如在文件的第一行加上
这样,这个文件就只能在linux上才会被编译(注意,// +build linux下面一定要有一个空行)
详细用法看这里 go条件编译
下面开始具体的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| type EpollM struct { conn map[int]*ServerConn
socketFd int epollFd int }
func (e *EpollM) Listen(ipAddr string, port int) error { fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP) if err != nil { return err }
var addr [4]byte copy(addr[:], net.ParseIP(ipAddr).To4()) net.ParseIP(ipAddr).To4() err = syscall.Bind(fd, &syscall.SockaddrInet4{ Port: port, Addr: addr, }) if err != nil { return err }
err = syscall.Listen(fd, 10) if err != nil { return err } e.socketFd = fd return nil }
|
这样就完成了监听
下面是 epoll 处理部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
func (e *EpollM) HandlerEpoll() error { events := make([]syscall.EpollEvent, 100) for { n, err := syscall.EpollWait(e.epollFd, events, -1)
if err != nil { return err } for i := 0; i < n; i++ { conn := e.GetConn(int(events[i].Fd)) if conn == nil { continue } if events[i].Events&syscall.EPOLLHUP == syscall.EPOLLHUP || events[i].Events&syscall.EPOLLERR == syscall.EPOLLERR { if err := e.CloseConn(int(events[i].Fd)); err != nil { return err } } else if events[i].Events == syscall.EPOLLIN { conn.Read() } } } }
|
从连接中读写数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| func (s *ServerConn) Read() { data := make([]byte, 100) n, err := syscall.Read(s.fd, data) if n == 0 { return } if err != nil { fmt.Printf("fd %d read error:%s\n", s.fd, err.Error()) } else { fmt.Printf("%d say: %s \n", s.fd, data[:n]) s.Write([]byte(fmt.Sprintf("hello %d", s.fd))) } }
func (s *ServerConn) Write(data []byte) { _, err := syscall.Write(s.fd, data) if err != nil { fmt.Printf("fd %d write error:%s\n", s.fd, err.Error()) } }
|
最后在依次调用这些函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main
func main() { epollM := NewEpollM() err := epollM.Listen("0.0.0.0", 8088) if err != nil { panic(err) }
err = epollM.CreateEpoll() if err != nil { panic(err) }
go func() { err := epollM.HandlerEpoll() epollM.Close() panic(err) }()
err = epollM.Accept() epollM.Close() panic(err) }
|
到这整个服务就运行起来了,代码已经上传到github了,传送门
整个过程看下来,和C/C++实现过程还是非常相似的,都是通过系统调用完成的。也没有什么难点,就这样吧