go語言性能如此之高程序員
無數程序員折斷了腰web
雲端時代獨領風騷服務器
惟獨JAVA欲比高websocket
書歸正傳,爲何go在服務器構建時性能如此牛逼呢?markdown
請接下回網絡
go在構建網絡服務時性能牛逼主要源自於net包內部採用的多路複用技術,再結合go獨特的goroutine構成了一種特點模式goroutine per connection。app
貼一段簡單的TCP服務端代碼,依次分析socket
func main() {
//監聽8080端口
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go read(conn)
}
}
func read(conn net.Conn) {
io.Copy(io.Discard, conn)
}
複製代碼
從net的Listen方法進入,一層層往下找就能夠找到下面這麼一段代碼async
// ------------------ net/sock_posix.go ------------------
// socket returns a network file descriptor that is ready for
// asynchronous I/O using the network poller.
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
//底層調用系統的socket並返回socket文件描述符fd
s, err := sysSocket(family, sotype, proto)
if err != nil {
return nil, err
}
//設置socket參數
if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {
poll.CloseFunc(s)
return nil, err
}
//建立net包下的netFD,其中netFD包含了一個poll.FD
if fd, err = newFD(s, family, sotype, net); err != nil {
poll.CloseFunc(s)
return nil, err
}
if laddr != nil && raddr == nil {
switch sotype {
case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
//設置監聽,由於在listenTCP函數中設置的sotype爲syscall.SOCK_STREAM
if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
case syscall.SOCK_DGRAM:
if err := fd.listenDatagram(laddr, ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
}
}
//客戶端模式的時候,會進入到這裏來
if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil {
fd.Close()
return nil, err
}
return fd, nil
}
func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
....
//進行socket綁定
if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
return os.NewSyscallError("bind", err)
}
//調用操做系統的監聽
if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
return os.NewSyscallError("listen", err)
}
//fd初始化,裏面會有poll.FD初始化,快要進入關鍵位置了
if err = fd.init(); err != nil {
return err
}
lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
fd.setAddr(fd.addrFunc()(lsa), nil)
return nil
}
// ------------------ net/fd_unix.go ------------------
func (fd *netFD) init() error {
//netFD的初始化竟然僅僅調用了pfd的初始化,說明pfd至關重要啊。火燒眉毛的爲她寬衣解帶了
return fd.pfd.Init(fd.net, true)
}
// 文件 Internal/poll/fd_unix.go
func (fd *FD) Init(net string, pollable bool) error {
// We don't actually care about the various network types.
if net == "file" {
fd.isFile = true
}
if !pollable {
fd.isBlocking = 1
return nil
}
//繼續初始化fd.pd是pollDesc的對象,這個是對poll文件描述符的一個包裝,咱們看看裏面是如何初始化的。
err := fd.pd.init(fd)
if err != nil {
// If we could not initialize the runtime poller,
// assume we are using blocking mode.
fd.isBlocking = 1
}
return err
}
//------------------ Internal/poll/fd_poll_runtime.go ------------------
func runtime_pollServerInit()
func runtime_pollOpen(fd uintptr) (uintptr, int)
func runtime_pollClose(ctx uintptr)
func runtime_pollWait(ctx uintptr, mode int) int
func runtime_pollWaitCanceled(ctx uintptr, mode int) int
func runtime_pollReset(ctx uintptr, mode int) int
func runtime_pollSetDeadline(ctx uintptr, d int64, mode int)
func runtime_pollUnblock(ctx uintptr)
func runtime_isPollServerDescriptor(fd uintptr) bool
type pollDesc struct {
runtimeCtx uintptr
}
var serverInit sync.Once
func (pd *pollDesc) init(fd *FD) error {
//服務初始化一次,經過sync.Onece,保障poll在全局只初始化一次
serverInit.Do(runtime_pollServerInit)
//
ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
....
}
// ------------------ runtime/netpoll.go ------------------
//此處poll_runtime_pollOpen綁定了上面的runtime_pollOpen
//go:linkname poll_runtime_pollOpen internal/poll.runtime_pollOpen
func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
....
var errno int32
//此處就區別於各個不一樣操做系統的定義了。kqueue/epoll
errno = netpollopen(fd, pd)
return pd, int(errno)
}
//------------------ runtime/netpoll_epoll.go ------------------
//這裏拿epoll的實現來看,其中調用了epollctl,至此就是真正的與操做系統交互了。這裏將當前服務端的鏈接自身加入到epoll中進行監聽,當Accept時,就是等待Epoll事件的回調
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)
}
複製代碼
至此一個TCP服務端的端口綁定及Epoll的監聽算是建立好了,整個路徑整理後就是tcp
net.Listen -> sysListener.listenTCP -> sysSocket -> netFD.listenStream -> syscall.Bind -> poll.pollDesc.init -> runtime_pollServerInit -> runtime_pollOpen
複製代碼
經過這一整套鏈路,建立socks描述符,綁定地址與端口,接着建立全局惟一的Poller,以後將socket加入到poller中。
至此初始化工做已經作完了,接着就是須要等待客戶端的鏈接了
// ------------------ net/tcpsock.go ------------------
func (ln *TCPListener) accept() (*TCPConn, error) {
//關鍵代碼仍是調用的fd的accept
fd, err := ln.fd.accept()
....
}
// ------------------ net/fd_unix.go ------------------
func (fd *netFD) accept() (netfd *netFD, err error) {
//net的fd最終調用的是pfd.Accept函數,也就是poll.FD的Accept繼續
d, rsa, errcall, err := fd.pfd.Accept()
if err != nil {
if errcall != "" {
err = wrapSyscallError(errcall, err)
}
return nil, err
}
// 接受到客戶端鏈接的fd以後,建立一個netFD對象,並初始化。實際上流程又和上面同樣,初始化poller,將fd加入到poller監聽,這樣就造成了一個客戶端鏈接和服務端監聽都在一個poller裏面管理。
if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
poll.CloseFunc(d)
return nil, err
}
if err = netfd.init(); err != nil {
netfd.Close()
return nil, err
}
lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
return netfd, nil
}
// ------------------ poll/fd_unix.go ------------------
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
....
for {
// 這裏是無阻塞的獲取客戶端的連接,若是返回爲syscall.EAGAIN,將會進入poll_runtime_pollWait進行等待poller的狀態變化。若是poller發生狀態變化就再次調用accept進行獲取客戶端連接並返回給調用方.
s, rsa, errcall, err := accept(fd.Sysfd)
if err == nil {
return s, rsa, "", err
}
switch err {
case syscall.EINTR:
continue
case syscall.EAGAIN:
//syscall.EAGAIN 前面的accept函數調用系統的accept,因爲是非阻塞的fd,因此就返回了一個再次嘗試的錯誤。
// 而咱們的pd是可pollable的,因此咱們不會繼續重試,而是等待系統poll的事件通知
if fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
case syscall.ECONNABORTED:
// This means that a socket on the listen
// queue was closed before we Accept()ed it;
// it's a silly error, so try again.
continue
}
return -1, nil, errcall, err
}
}
// ------------------ runtime/netpoll.go ------------------
// 檢查poll fd 是否已就緒 。這裏調用的都是和操做系統的交互層級了。有興趣的能夠繼續深刻
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
errcode := netpollcheckerr(pd, int32(mode))
if errcode != pollNoError {
return errcode
}
// As for now only Solaris, illumos, and AIX use level-triggered IO.
if GOOS == "solaris" || GOOS == "illumos" || GOOS == "aix" {
netpollarm(pd, mode)
}
for !netpollblock(pd, int32(mode), false) {
errcode = netpollcheckerr(pd, int32(mode))
if errcode != pollNoError {
return errcode
}
// Can happen if timeout has fired and unblocked us,
// but before we had a chance to run, timeout has been reset.
// Pretend it has not happened and retry.
}
return pollNoError
}
複製代碼
經過什麼的建立服務端、建立pollDesc、加入鏈接到poll監聽等一系列操做,從而實現了go底層網絡包高性能。
而咱們本身要實現一個高性能web服務、websocket,或者是tcp服務器都會變的很是簡單。固然這個高性能只是相對的,若是想繼續壓榨服務器性能得采用直接和epoll等技術直接交互,這樣能夠省去goroutine來單獨監聽鏈接的讀操做。
條理性有待增強,繼續努力