go语言从零学起(三) -- chat实现的思考
要通过go实现一个应用场景:
1 建立一个websocket服务
2 维护在线用户的链接
3 推送消息和接受用户的操作
列出需求,很显然的想到了chat模型。于是研究了revel框架提供的samples/chat代码,以及基于gorilla/websocket实现的chat。
他们实现的思路比较类似,大概代码如下:
package main import ( "github.com/go-martini/martini" "github.com/gorilla/websocket" "net/http" "time" ) const ( readBufferSize = 1024 writeBufferSize = 1024 ) type Client struct { conn *websocket.Conn } // 常住监听一个链接的读 func (c *Client) readPump() { for { mstype, message, err := c.conn.ReadMessage() // ....... 该处处理接受用户发来的数据 } } //监听一个链接的写 func (c *Client) writePump() { for { c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) // ....... 该处处理推送给用户的数据 } } // 基于martini实现的websocket服务 func main() { m := martini.Classic() m.Get("/ws", func(res http.ResponseWriter, req *http.Request) { conn, err := websocket.Upgrade(res, req, nil, readBufferSize, writeBufferSize) if err != nil { log.Println(err) return } client := &Client{conn: conn} go client.writePump() //建立一个goroutine client.readPump() }) m.Run() }
上述代码只是用来简述运用go语言实现chat的大概思路,在最后会贴出原代码的出处。
通过看上面代码会发现,每个链接进来就需要2个goroutine,去维护它的读跟写。如果要是1万个链接,相应的就要有2万个常驻而不会释放的goroutine去监听。这样的话,到最后会不会只是维护链接就会耗尽服务器的cpu跟内存。因为只是接触go的时间不长,看完代码我有些质疑这样实现的效率问题。为了佐证我的想法,我查了下c基于epoll对链接读写的维护,代码如下:
struct epoll_event ev, *events; for(;;) { nfds = epoll_wait(kdpfd, events, maxevents, -1); //等待I/O事件 for(n = 0; n < nfds; ++n) { if(events[n].data.fd == listener) { //如果是主socket的事件,则表示有新连接进入,需要进行新连接的处理。 client = accept(listener, (struct sockaddr *) &local, &addrlen); if(client < 0){ perror("accept error"); continue; } setnonblocking(client); // 将新连接置于非阻塞模式 ev.events = EPOLLIN | EPOLLET; //注意这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听, //如果有写操作的话,这个时候epoll是不会返回事件的, //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET。 ev.data.fd = client; // 并且将新连接也加入EPOLL的监听队列 if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) { // 设置好event之后,将这个新的event通过epoll_ctl //加入到epoll的监听队列里,这里用EPOLL_CTL_ADD //来加一个新的 epoll事件。可以通过EPOLL_CTL_DEL来减少 //一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。 fprintf(stderr, "epoll set insertion error: fd=%d"0, client); return -1; } } else // 如果不是主socket的事件的话,则代表这是一个用户的socket的事件, // 则用来处理这个用户的socket的事情是,比如说read(fd,xxx)之类,或者一些其他的处理。 do_use_fd(events[n].data.fd); } }
从代码层面对比来看,同是1万个链接,c基于epoll的实现只有一个常驻的线程去监听I/O事件,而go需要2万个goroutine去监听。这2种语言实现方式间会不会存在比较明显的性能差距,这是我开始学习的时候,一直存在的疑问。
不过看了这篇文章时,解除了我很多疑惑,也让我对go的goroutine有了深入一点的认识。
https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.2.html (goroutine的生老病死)
1 goroutine切换时的上下文信息是保存在结构体的sched域中的。goroutine是轻量级的“线程”或者称为协程,切换时变不必陷入到操作系统内核中,所以保存过程很轻量
2 goroutine创建完成完后,会放入相应的队列
3 go会建多个worker进程,worker做的事情就是不停的去任务队列中取一个任务出来执行
4 goroutine准备好的时候,会获取cpu去执行,执行完了会让出cpu然后挂起
模式大概是这样的:
func M() { for { sched.lock.Lock() //互斥地从就绪G队列中取一个g出来运行 if sched.allg > 0 { g := sched.allg[0] sched.allg = sched.allg[1:] sched.lock.Unlock() g.Run() //运行它 } else { sched.lock.Unlock() } } } for i:=0; i<GOMAXPROCS; i++ { go M() }
上面这些是刚开始学习的一些疑惑和感悟,希望之后深入学习后,对go有更全面的认识。
参考资料:
1 https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.2.html
2 https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.3.html
3 http://leejia.blog.51cto.com/4356849/1021066
revel的chat代码
https://github.com/revel/samples
gorilla/websocket的chat代码