go socket通信+守护进程启动服务器+平滑重启

什么是Socket?

Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

Socket有两种:TCP Socket和UDP Socket

golang TCP的服务端会经历Listen,Accept,之后才可以通过连接进行通信,Listen阶段,服务器监听主机的IP和端口,而Accept服务器阻塞等待客户端与之进行连接,当客户端与服务器经过三次握手建立连接后,就可以基于Accept返回的连接进行通信了。

需要用到的第三方包有:

go get -u github.com/spf13/viper

目录结构如下: 


项目根目录是 socket
 
socket的目录结构
├── socketserver                        # socket 服务器目录
│     ├── conf                               # 配置文件统一存放目录
│     │   ├── config.yaml              # 配置文件
│     ├── config                            # 专门用来处理配置和配置文件
│     │   └── config.go                 
│     ├── logs                               # 日志
│     ├── service                          # 实际业务处理函数目录
│     │   └── hotupdate.go           # 热重启和守护进程配置
│     │   └── socketserver.go       # socket服务器配置
│     ├── main.go                         #项目单入口
├── socketclient.go                     #socket客户端

下面,我们根据目录结构,从上往下建立文件夹和文件

建立文件夹和文件 socket/socketserver/conf/config.yaml ,config.yaml 内容如下:

common:
  server: #服务器配置
    addr: 127.0.0.1:6661           # 绑定地址

建立文件夹和文件 socket/socketserver/config/config.go ,config.go 内容如下:

package config

import (
	"github.com/spf13/viper"
	"time"
	"os"
	"log"
)

// LogInfo 初始化日志配置
func LogInfo() {
	file := "./logs/" + time.Now().Format("2006-01-02") + ".log"
	logFile, _ := os.OpenFile(file,os.O_RDWR| os.O_CREATE| os.O_APPEND, 0755)
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	log.SetOutput(logFile)
}

// Init 读取初始化配置文件
func Init() error {
	//初始化配置
	if err := Config();err != nil{
		return err
	}

	//初始化日志
	LogInfo()
	return nil
}

// Config viper解析配置文件
func Config() error{
	viper.AddConfigPath("conf")
	viper.SetConfigName("config")
	if err := viper.ReadInConfig();err != nil{
		return err
	}
	return nil
}

建立文件夹和文件 socket/socketserver/service/hotupdate.go ,hotupdate.go 内容如下:

package service

import (
	"errors"
	"flag"
	"log"
	"net"
	"os"
	"os/exec"
	"os/signal"
	"syscall"

	"github.com/spf13/viper"
)

/************************** 热重启 ***************************/

var (
	listener net.Listener = nil

	graceful =  flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)


//监听服务器
func Listenserver(){
	var err error

	//解析参数
	flag.Parse()

	//设置监听的对象(新建或已存在的socket描述符)
	if *graceful {
		//子进程监听父进程传递的 socket描述符
		log.Println("listening on the existing file descriptor 3")
		//子进程的 0 1 2 是预留给 标准输入 标准输出 错误输出
		//因此传递的socket 描述符应该放在子进程的 3
		f := os.NewFile(3,"")
		listener,err = net.FileListener(f)
		log.Printf( "graceful-reborn  %v %v  %#v \n", f.Fd(), f.Name(), listener)
	}else{
		//启动守护进程
		daemonProcce(1,1);
		//父进程监听新建的 socket 描述符
		log.Println("listening on a new file descriptor")
		add := viper.GetString("common.server.addr")
		listener,err = net.Listen("tcp",add)
		log.Printf("Actual pid is %d\n", syscall.Getpid())
	}
	if err != nil{
		log.Fatalf("listener error: %v\n",err)
	}
	tcp,_ := listener.(*net.TCPListener)
	go func(){
		fd,_ := tcp.File()
		log.Printf( "first-boot  %v %v %#v \n ", fd.Fd(),fd.Name(), listener)
	}()
	//监听socket
	go HandleServer(tcp)
	//监听信号
	handleSignal()
	log.Println("signal end")
}


//处理信号
func handleSignal(){
	//把信号 赋值给 通道
	ch := make(chan os.Signal, 1)
	//监听信号
	signal.Notify(ch, syscall.SIGINT,syscall.SIGTERM,syscall.SIGUSR2)
	//阻塞主进程, 不停的监听系统信号
	for{
		//通道 赋值给 sig
		sig := <-ch
		log.Printf("signal receive: %v\n", sig)
		switch sig{
		case syscall.SIGINT,syscall.SIGTERM:  //终止进程执行
			log.Println("shutdown")
			signal.Stop(ch)       //停止通道
			os.Exit(0)//关闭服务器窗口
			log.Println("graceful shutdown")
			return
		case syscall.SIGUSR2:  //进程热重启
			log.Println("reload")
			err := reload()  //执行热重启
			if err != nil{
				log.Fatalf("listener error: %v\n",err)
			}
			//server.Shutdown(ctx)
			log.Println("graceful reload")
			return
		}
	}
}

//热重启
func reload() error{
	tl, ok := listener.(*net.TCPListener)
	if !ok {
		return errors.New("listener is not tcp listener")
	}
	//获取socket描述符
	currentFD, err := tl.File()
	if err != nil {
		return err
	}
	//设置传递给子进程的参数(包含 socket描述符)
	args := []string{"-graceful"}
	//args = append(args, "-continue")
	cmd := exec.Command(os.Args[0],args...)
	cmd.Stdout = os.Stdout  //标准输出
	cmd.Stderr = os.Stderr  //错误输出
	cmd.ExtraFiles = []*os.File{currentFD} //文件描述符

	err = cmd.Start()
	log.Printf("forked new pid %v: \n",cmd.Process.Pid)
	if err != nil{
		return err
	}
	return nil
}
/*
我们在父进程执行 cmd.ExtraFiles = []*os.File{f} 来传递 socket 描述符给子进程,子进程通过执行 f := os.NewFile(3, "") 来获取该描述符。值得注意的是,子进程的 0 、1 和 2 分别预留给标准输入、标准输出和错误输出,所以父进程传递的 socket 描述符在子进程的顺序是从 3 开始。
*/


//nochdir 是 程序初始路径 1是当前路径,0是系统根目录
//noclose 是 错误信息输出 1是输出当前, 0是不显示错误信息
func daemonProcce(nochdir, noclose int) (int,error){
	// already a daemon
	log.Printf("syscall.Getppid() %+v\n",syscall.Getppid())
	//如果是守护进程 syscall.Getppid() = 1
	if syscall.Getppid() == 1 {
		/* Change the file mode mask */
		syscall.Umask(0)

		if nochdir == 0 {
			os.Chdir("/")
		}

		return 0, nil
	}

	files := make([]*os.File, 3, 6)
	if noclose == 0 {
		nullDev, err := os.OpenFile("/dev/null", 0, 0)
		if err != nil {
			return 1, err
		}
		files[0], files[1], files[2] = nullDev, nullDev, nullDev
	} else {
		files[0], files[1], files[2] = os.Stdin, os.Stdout, os.Stderr
	}

	dir, _ := os.Getwd()
	sysattrs := syscall.SysProcAttr{Setsid: true}
	attrs := os.ProcAttr{Dir: dir, Env: os.Environ(), Files: files, Sys: &sysattrs}

	proc, err := os.StartProcess(os.Args[0], os.Args, &attrs)
	if err != nil {
		return -1, err
	}
	proc.Release()
	os.Exit(0)

	return 0, nil
}

建立文件夹和文件 socket/socketserver/service/socketserver.go ,socketserver.go 内容如下:

package service

import (
	"log"
	"net"
	"strings"
)

func connHandler(c net.Conn) {
	//1.conn是否有效
	if c == nil {
		log.Panic("无效的 socket 连接")
	}

	//2.新建网络数据流存储结构
	buf := make([]byte, 4096)
	//3.循环读取网络数据流
	for {
		//3.1 网络数据流读入 buffer
		cnt, err := c.Read(buf)
		//3.2 数据读尽、读取错误 关闭 socket 连接
		if cnt == 0 || err != nil {
			c.Close()
			break
		}

		//3.3 根据输入流进行逻辑处理
		//buf数据 -> 去两端空格的string
		inStr := strings.TrimSpace(string(buf[0:cnt]))
		//去除 string 内部空格
		cInputs := strings.Split(inStr, " ")
		//获取 客户端输入第一条命令
		fCommand := cInputs[0]

		log.Println("客户端传输->" + fCommand)

		switch fCommand {
		case "ping":
			c.Write([]byte("服务器端回复-> pong\n"))
		case "hello":
			c.Write([]byte("服务器端回复-> world\n"))
		default:
			c.Write([]byte("服务器端回复" + fCommand + "\n"))
		}

		//c.Close() //关闭client端的连接,telnet 被强制关闭

		log.Printf("来自 %v 的连接关闭\n", c.RemoteAddr())
	}
}

//开启serverSocket
func HandleServer(tcp *net.TCPListener){
	log.Println("正在开启 Server ...")
	for {
		//2.接收来自 client 的连接,会阻塞
		conn, err := tcp.Accept()

		if err != nil {
			log.Println("连接出错")
		}

		//并发模式 接收来自客户端的连接请求,一个连接 建立一个 conn,服务器资源有可能耗尽 BIO模式
		go connHandler(conn)
	}

}

建立文件夹和文件 socket/socketserver/main.go ,main.go 内容如下:

package main

import (
	"log"

	"github.com/spf13/viper"

	"socketserver/config"
	"socketserver/service"
)

func main() {
	if err := config.Init();err != nil{
		panic(err)
	}

	log.Printf("开始监听服务器端口: %s\n", viper.GetString("common.server.addr"))

	service.Listenserver()

}

建立文件夹和文件 socket/socketclient.go ,socketclient.go 内容如下:

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

/**
   client 发送端 程序
   问题:如何区分  c net.Conn 的 Write 与 Read 的数据流向?
       1. c.Write([]byte("hello"))
          c <- "hello"
       2. c.Read(buf []byte)
          c -> buf (空buf)
   客户端 和 服务器端都有 Close conn 的功能
 */

func cConnHandler(c net.Conn,message string) {
	//缓存 conn 中的数据
	buf := make([]byte, 1024)
	var breakfor int

	//服务器重连后,自动重新发送上次消息
	if message != ""{
		fmt.Println("客户端自动重连")
		messageHandle(c,message,buf)
	}

	//返回一个拥有 默认size 的reader,接收客户端输入
	reader := bufio.NewReader(os.Stdin)

	fmt.Println("请输入客户端请求数据...")

	for {
		//客户端输入
		input, _ := reader.ReadString('\n')
		//发送消息
		breakfor = messageHandle(c,input,buf)
		if breakfor == 1 {
			message = input
			c.Close() //关闭连接
			break; //打断循环
		}
	}
	connect(message) //重新连接服务器
}

//消息处理
func messageHandle(c net.Conn,message string,buf []byte) int{
	//去除输入两端空格
	message = strings.TrimSpace(message)
	//客户端请求数据写入 conn,并传输
	c.Write([]byte(message))
	//服务器端返回的数据写入空buf
	cnt, err := c.Read(buf)

	if err != nil {
		fmt.Printf("客户端读取数据失败 %s\n", err)
		return 1 //退出当前循环
	}

	//回显服务器端回传的信息
	fmt.Print("服务器端回复" + string(buf[0:cnt]))
	return 0;
}

func main() {
	//第一次连接服务器
	connect("")
}

func connect(message string){
	conn, err := net.Dial("tcp", "127.0.0.1:6661")
	if err != nil {
		fmt.Println("客户端建立连接失败")
		return
	}

	cConnHandler(conn,message)
}

切换到socketserver目录中,模型初始化,并打包启动 socket服务器

[root@izj6c4jirdug8kh3uo6rdez socketserver]# go mod init socketserver
go: creating new go.mod: module socketserver
[root@izj6c4jirdug8kh3uo6rdez socketserver]# go build
[root@izj6c4jirdug8kh3uo6rdez socketserver]# ./socketserver

启动成功

2019/08/07 14:51:20 main.go:17: 开始监听服务器端口: 127.0.0.1:6661
2019/08/07 14:51:20 hotupdate.go:133: syscall.Getppid() 26931
2019/08/07 14:51:20 main.go:17: 开始监听服务器端口: 127.0.0.1:6661
2019/08/07 14:51:20 hotupdate.go:133: syscall.Getppid() 1
2019/08/07 14:51:20 hotupdate.go:45: listening on a new file descriptor
2019/08/07 14:51:20 hotupdate.go:48: Actual pid is 13892
2019/08/07 14:51:20 hotupdate.go:57: first-boot  6 tcp:127.0.0.1:6661-> &net.TCPListener{fd:(*net.netFD)(0xc0000e0300)} 
 
2019/08/07 14:51:20 socketserver.go:54: 正在开启 Server ...

启动客户端,并发送数据请求

[root@izj6c4jirdug8kh3uo6rdez socket]# go run socketclient.go
请输入客户端请求数据...
ping
服务器端回复服务器端回复-> pong
hello
服务器端回复服务器端回复-> world
test
服务器端回复服务器端回复test

服务器端热重启

[root@izj6c4jirdug8kh3uo6rdez ~]# netstat -tunlp | grep 6661
tcp        0      0 127.0.0.1:6661          0.0.0.0:*               LISTEN      13892/./socketserve 
[root@izj6c4jirdug8kh3uo6rdez ~]# kill -USR2 13892

热重启成功

2019/08/07 14:54:18 hotupdate.go:77: signal receive: user defined signal 2
2019/08/07 14:54:18 hotupdate.go:86: reload
2019/08/07 14:54:18 hotupdate.go:118: forked new pid 14073: 
2019/08/07 14:54:18 hotupdate.go:92: graceful reload
2019/08/07 14:54:18 hotupdate.go:63: signal end
2019/08/07 14:54:18 main.go:17: 开始监听服务器端口: 127.0.0.1:6661
2019/08/07 14:54:18 hotupdate.go:35: listening on the existing file descriptor 3
2019/08/07 14:54:18 hotupdate.go:40: graceful-reborn  3   &net.TCPListener{fd:(*net.netFD)(0xc0000e0300)} 
2019/08/07 14:54:18 hotupdate.go:57: first-boot  7 tcp:127.0.0.1:6661-> &net.TCPListener{fd:(*net.netFD)(0xc0000e0300)} 
 
2019/08/07 14:54:18 socketserver.go:54: 正在开启 Server ...

客户端重新连接服务器

ping
客户端读取数据失败 EOF
客户端自动重连
服务器端回复服务器端回复-> pong
请输入客户端请求数据...
hello
服务器端回复服务器端回复-> world
gogo
服务器端回复服务器端回复gogo

参考:https://www.jianshu.com/p/6db6dffb04e5

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值