什么是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