在测试HTTP服务时,如果我们忘记关闭进程,再次尝试启动一个新的服务进程,就会遇到类似如下的错误信息:$gorunmain。golistentcp:8000:bind:addressalreadyinuse这是因为默认情况下,操作系统不允许我们打开具有相同源地址和端口的套接字。但是如果我们要开启多个服务进程监听同一个端口,这样可以吗?如果可以,这能给我们带来什么?Socket五元组套接字编程是每个程序员都应该掌握的基础知识。所以大家应该都知道,一个socket连接是由一个五元组唯一标识的。对于任意两个连接,它们的五元组不可能完全相同。{,,,,}protocol指的是传输层TCP/UDP协议,在创建socket的时候就已经确定了。srcaddr/port和destaddr/port分别标识了请求者和服务器的地址信息。因此,只要请求者的destaddr/port信息不同,即使server有相同的srcaddr/port,仍然可以识别出一个唯一的socket连接。基于这个理论基础,其实我们可以在同一个网络主机上复用同一个IP地址和端口号。LinuxSO_REUSEPORT为了满足复用端口的需要,Linux3.9内核引入了SO_REUSEPORT选项(其实这之前也有类似的选项SO_REUSEADDR,但是没有实现真正的端口复用,详见参考链接1)。SO_REUSEPORT支持多个进程或线程绑定同一个端口,用于提高服务器程序的性能。它的特点包括以下几点:允许多个套接字绑定到同一个TCP/UDP端口每个线程都有自己的服务器套接字服务器套接字上没有锁的竞争,实现内核级的负载均衡安全级别,套接字监听同一端口只能位于同一用户下(同一有效UID)。通过SO_RESUEPORT,每个进程都可以绑定相同的地址和端口,每个进程都是独立平等的。让多个进程监听同一个端口。每个进程中的acceptsocketfd是不同的。当一个新的连接建立时,内核只会调度一个进程去接受,保证调度的平衡。其工作原理图如下:在SO_REUSEADDR的支持下,我们不仅可以创建多个具有相同IP:PORT的socket,还可以获得内核态的负载均衡能力。Go如何设定SO_REUSEPORTLinux经典设计理念:一切皆文件。当然socket也不例外,它也是文件的一种。如果我们想在Go程序中使用linux的SO_REUSEPORT选项,我们需要一个接口来修改内核socket连接选项,这可以依赖golang.org/x/sys/unix库来实现,具体在以下方法。import“"golang.org/x/sys/unix"”...unix.SetsockoptInt(int(fd),unix.SOL_SOCKET,unix.SO_REUSEPORT,1)因此,一个持有SO_REUSEPORT特性的完整Go服务代号如下packagemainimport("context""fmt""net""net/http""os""syscall""golang.org/x/sys/unix")varlc=net.ListenConfig{Control:func(network,addressstring,csyscall.RawConn)error{varopErrerroriferr:=c.Control(func(fduintptr){opErr=unix.SetsockoptInt(int(fd),unix.SOL_SOCKET,unix.SO_REUSEPORT,1)});err!=nil{returnerr}returnopErr},}funcmain(){pid:=os.Getpid()l,err:=lc.Listen(context.Background(),"tcp","127.0.0.1:8000")iferr!=nil{panic(err)}server:=&http.Server{}http.HandleFunc("/",func(whttp.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusOK)fmt.Fprintf(w,"Client[%s]ReceivedmsgfromServerPID:[%d]\n",r.RemoteAddr,pid)})fmt.Printf("ServerwithPID:[%d]isrunning\n",pid)_=server.Serve(l)}我我们编译成linux可执行文件main$CGO_ENABLED=0GOOS=linuxGOARCH=amd64gobuildmain.go在linux主机上,同时开启三个监听8000端口的进程。我们可以看到三个服务进程的PID分别是32687、32691和32697~$./mainServerwithPID:[32687]isrunning~$./mainServerwithPID:[32691]isrunning~$./mainServerwithPID:[32697]isrunning最后,通过curl命令,模拟多次http客户端请求~$foriin{1..20};docurllocalhost:8000;doneClient[127.0.0.1:56876]ReceivedmsgfromServerPID:[32697]Client[127.0.0.1:56880]ReceivedmsgfromServerPID:[32687]Client[127.0.0.1:56884]Receivedmsg2.Server187.[PID]:[30.1:56888]从服务器PID接收到的消息:[32687]客户端[127.0.0.1:56892]从服务器PID接收到的消息:[32691]客户端[127.0.0.1:56896]从服务器PID接收到的消息:[32697]客户端[127.0.0.1:56900][g2Serverived900]接收到的消息127.0.0.1:56904]ReceivedmsgfromServerPID:[32691]Client[127.0.0.1:56908]ReceivedmsgfromServerPID:[32697]Client[127.0.0.1:56912]ReceivedmsgfromServerPID:[32697]Client[127.0.0.1:56916]ReceivedmsgfromServerPID:[32687]客户端[127.0.0.1:56920]从服务器PID接收到消息:[32691]客户端[127.0.0.1:56924]从服务器PID接收到消息:[32697]客户端[127.0.0.1:56928]从服务器PID接收到消息:[32697]客户端[127.0.0.1:56932]ReceivedmsgfromServerPID:[32691]Client[127.0.0.1:56936]ReceivedmsgfromServerPID:[32697]Client[127.0.0.1:56940]ReceivedmsgfromServerPID:[32687]Client[127.0.0.1:56944]ReceivedmsgfromServerPID:[32691]Client[127.0.0.1:56948]ReceivedmsgfromServerPID:[32687]Client[127.0.0.1:56952]ReceivedmsgfromServerPID:[32697]可以看出20个客户端请求被平均发送到3个服务进程3.9提供的SO_REUSEPORT选项允许多个进程监听同一个端口。这种机制带来的好处:提高服务器程序的吞吐性能:我们可以运行多个应用实例,充分利用多核CPU资源,避免单个核处理数据包而其他核空闲的问题。内核级负载均衡:我们不需要在多个实例前面加一层服务代理,因为内核已经提供了简单的负载均衡。不停更新:当我们需要更新服务时,我们可以启动一个新的服务实例来接受请求,然后优雅地关闭旧的服务实例。如果你的Go项目在高峰期出现了请求堆积的问题,此时可以考虑使用SO_REUSEPORT选项。参考链接:[1。SO_REUSEADDR和SO_REUSEPORT有何不同?]https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ[2。SO_REUSEPORT性能测试】http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html【3.linuxsocket手册页】https://man7.org/linux/man-pages/man7/socket.7。网页格式