前面我们学习过如何搭建go socket服务器:https://blog.csdn.net/daily886/article/details/98744825
下面我们在此基础上,开发网页版的聊天界面
需要用到的第三方包有:
go get -u github.com/gin-gonic/gin
go get -u github.com/spf13/viper
go get -u github.com/unrolled/secure
目录结构如下:
项目根目录是 socket
socket的目录结构
├── socketweb # socket 网页聊天目录
│ ├── conf # 配置文件统一存放目录
│ │ ├── config.yaml # 配置文件
│ ├── config # 配置
│ │ └── config.go
│ ├── handler # 处理响应和请求
│ │ └── handler.go
│ ├── logs # 日志
│ ├── pkg # 引用的包
│ │ ├── errno # 错误码存放位置
│ │ │ ├── code.go
│ │ │ └── errno.go
│ ├── router # 路由相关处理
│ │ ├── middleware # API服务器用的是Gin Web框架,Gin中间件存放位置
│ │ │ ├── header.go
│ │ └── router.go # 路由
│ ├── service # 实际业务处理函数目录
│ │ └── hotupdate.go # 热重启和守护进程配置
│ │ └── service.go # 提供路由服务
│ │ └── socketclient.go # 连接socket服务器配置
│ ├── view # 视图
│ │ └── socketweb.html # 网页聊天模板
│ ├── main.go #项目单入口
下面,我们根据目录结构,从上往下建立文件夹和文件
建立文件夹和文件 socket/socketweb/conf/config.yaml ,config.yaml 内容如下:
common:
server: #服务器配置
runmode: debug # 开发模式, debug, release, test
addr: :6662 # HTTP绑定端口
name: socketweb # API Server的名字
url: :6662 # pingServer函数请求的API服务器的ip:port
max_ping_count: 10 # pingServer函数尝试的次数
pemPath: /usr/local/go/conf/cert/online/www.baidu.com.pem
keyPath: /usr/local/go/conf/cert/online/www.baidu.com.key
建立文件夹和文件 socket/socketweb/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/socketweb/handler/handler.go ,handler.go 内容如下:
package handler
import (
"bytes"
"net/http"
"io/ioutil"
"github.com/gin-gonic/gin"
"socketweb/pkg/errno"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
//返回json 格式
func SendResponseJSON(c *gin.Context,err error,data interface{}){
code,message := errno.DecodeErr(err)
//总是返回http状态ok
c.JSON(http.StatusOK,Response{
Code: code,
Message:message,
Data: data,
})
}
//返回string 格式
func SendResponseString(c *gin.Context,err error,data string){
c.Header("Content-Type", "text/html; charset=utf-8")
//总是返回http状态ok
c.String(http.StatusOK,data)
}
//返回html 格式
func SendResponseHtml(c *gin.Context,template string,h *gin.H){
c.HTML(http.StatusOK, template, h)
}
//http请求
func HttpRequest(api string,json string,method string) (string, error) {
jsonStr := []byte(json)
req, err := http.NewRequest(method, api, bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json") //使用json格式传参
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", errno.ApiServerError
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if !(resp.StatusCode == 200) {
return "", errno.ApiServerError
}
return string(body), nil
}
建立文件夹 socket/socketweb/logs
建立文件夹和文件 socket/socketweb/pkg/errno/code.go ,code.go 内容如下:
package errno
var (
// Common errors
OK = &Errno{Code: 0, Message: "OK"}
VALUEERROR = &Errno{Code: -1, Message: "输入错误"}
InternalServerError = &Errno{Code: 10001, Message: "服务器错误"}
ApiServerError = &Errno{Code: 20001, Message: "接口服务器错误"}
)
建立文件夹和文件 socket/socketweb/pkg/errno/errno.go ,errno.go 内容如下:
package errno
import "fmt"
type Errno struct {
Code int
Message string
}
//返回错误信息
func (err Errno) Error() string{
return err.Message
}
//设置 Err 结构体
type Err struct {
Code int
Message string
Err error
}
//声明构造体
func New(errno *Errno,err error) *Err{
return &Err{Code:errno.Code,Message:errno.Message,Err:err}
}
//添加错误信息
func (err *Err) Add(message string) error{
err.Message += " " + message
return err
}
//添加指定格式的错误信息
func (err * Err) Addf(format string,args...interface{}) error{
err.Message += " " + fmt.Sprintf(format,args...)
return err
}
//拼接错误信息字符串
func (err *Err) Error() string{
return fmt.Sprintf("Err - code: %d, message: %s, error: %s",err.Code,err.Message,err.Err)
}
// 解析 错误信息, 返回字符串
func DecodeErr(err error) (int,string){
if err == nil{
return OK.Code,OK.Message
}
switch typed := err.(type) {
case *Err:
return typed.Code,typed.Message
case *Errno:
return typed.Code,typed.Message
default:
}
return InternalServerError.Code,err.Error()
}
建立文件夹和文件 socket/socketweb/router/middleware/header.go ,header.go 内容如下:
package middleware
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
//无缓存头部中间件 ,
//要来防止客户端获取已经缓存的响应信息
func NoCache(c *gin.Context){
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Next()
}
//选项中间件
//要来给预请求 终止并退出中间件 ,链接并结束请求
func Options(c *gin.Context){
if c.Request.Method != "OPTIONS"{
c.Next()
}else{
c.Header("Access-Control-Allow-Origin","*")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Content-Type", "application/json")
c.AbortWithStatus(200)
}
}
//安全中间件
//要来保障数据安全的头部
func Secure(c *gin.Context){
c.Header("Access-Control-Allow-Origin", "*")
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
if c.Request.TLS != nil {
c.Header("Strict-Transport-Security", "max-age=31536000")
}
//也可以考虑添加一个安全代理的头部
//c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}
建立文件夹和文件 socket/socketweb/router/router.go ,router.go 内容如下:
package router
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/unrolled/secure"
"github.com/spf13/viper"
"socketweb/service"
"socketweb/router/middleware"
)
//初始化路由
func InitRouter(g *gin.Engine){
middlewares := []gin.HandlerFunc{}
//中间件
g.Use(gin.Recovery())
g.Use(middleware.NoCache)
g.Use(middleware.Options)
g.Use(middleware.Secure)
g.Use(middlewares...)
g.Use(TlsHandler())
//404处理
g.NoRoute(func(c *gin.Context){
c.String(http.StatusNotFound,"该路径不存在")
})
g.LoadHTMLGlob("view/*.html")
//健康检查中间件
g.GET("/",service.Index)//主页
g.GET("/socketweb",service.SocketWeb)//
g.POST("/sendmessage", service.SendMessage)
}
//监听路由,自动跳转https
func TlsHandler() gin.HandlerFunc {
sslhost := "www.daily886.com:"+viper.GetString("common.server.addr")
return func(c *gin.Context) {
secureMiddleware := secure.New(secure.Options{
SSLRedirect: true,
SSLHost: sslhost,
})
err := secureMiddleware.Process(c.Writer, c.Request)
// If there was an error, do not continue.
if err != nil {
return
}
c.Next()
}
}
建立文件夹和文件 socket/socketweb/service/hotupdate.go ,hotupdate.go 内容如下:
package service
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"github.com/spf13/viper"
)
/************************** 热重启 ***************************/
var (
listener net.Listener = nil
graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)
//监听服务器
func Listenserver(server *http.Server){
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")
listener,err = net.Listen("tcp",server.Addr)
log.Printf("Actual pid is %d\n", syscall.Getpid())
}
if err != nil{
log.Fatalf("listener error: %v\n",err)
}
go func(){
err = server.ServeTLS(listener,viper.GetString("common.server.pemPath"),viper.GetString("common.server.keyPath"))
log.Printf("server.Serve err: %v\n",err)
tcp,_ := listener.(*net.TCPListener)
fd,_ := tcp.File()
log.Printf( "first-boot %v %v %#v \n ", fd.Fd(),fd.Name(), listener)
}()
//监听信号
handleSignal(server)
log.Println("signal end")
}
//处理信号
func handleSignal(server *http.Server){
//把信号 赋值给 通道
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)
ctx,_ := context.WithTimeout(context.Background(),20*time.Second)
switch sig{
case syscall.SIGINT,syscall.SIGTERM: //终止进程执行
log.Println("shutdown")
signal.Stop(ch) //停止通道
server.Shutdown(ctx) //关闭服务器窗口
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/socketweb/service/service.go ,service.go 内容如下:
package service
import (
"log"
"strings"
"github.com/gin-gonic/gin"
. "socketweb/handler"
"socketweb/pkg/errno"
)
/*
1、使用接收单个参数各种方法:
c.Param()
c.Query
c.DefaultQuery
c.PostForm
c.DefaultPostForm
c.QueryMap
c.PostFormMap
c.FormFile
c.MultipartForm
2、使用各种绑定方法
c.Bind
c.BindJSON
c.BindXML
c.BindQuery
c.BindYAML
c.ShouldBind
c.ShouldBindJSON
c.ShouldBindXML
c.ShouldBindQuery
c.ShouldBindYAML
*/
//首页
func Index(c *gin.Context){
html := `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello world</title>
</head>
<body>
hello world
</body>
</html>
`
SendResponseString(c,nil,html)
}
//消息html
func SocketWeb(c *gin.Context){
param := gin.H{
"title": "web socket",
}
SendResponseHtml(c,"socketweb.html",¶m)
}
// 发送消息
// Binding from JSON
type FormMessage struct {
Message string `form:"message" json:"message" binding:"required"`
Timestamp string `form:"time" json:"time" binding:"required"`
}
type ResponseMessage struct {
Message string `json:"message"`
}
func SendMessage(c *gin.Context){
var form FormMessage
// 你可以显式声明来绑定多媒体表单:
// c.BindWith(&form, binding.Form)
// 或者使用自动推断:
var result interface{}
result = ResponseMessage{
Message: "参数错误",
}
var err error
//获取 content type 类型
contentType := c.Request.Header.Get("Content-Type")
semicol := strings.Index(contentType, ";")
if semicol <= 0 {
semicol = len(contentType)
}
contentType = contentType[0:semicol]
//绑定参数
switch contentType {
case "application/json":
err = c.ShouldBindJSON(&form)
case "application/x-www-form-urlencoded":
err = c.ShouldBind(&form)
}
log.Println(form)
if err != nil || form.Message == "" || form.Timestamp == "" {
result = ResponseMessage{
Message: "参数不能为空",
}
SendResponseJSON(c,errno.VALUEERROR,result)
return
}
response := SocketConnect(form.Message,2)
//替换无效字符
response = strings.Replace(response, "服务器端回复", "", -1)
response = strings.Replace(response, "->", "", -1)
//去除输入两端空格
response = strings.TrimSpace(response)
if response == ""{
result = ResponseMessage{
Message: "无法连接服务器",
}
SendResponseJSON(c,errno.VALUEERROR,result)
}
log.Println(response)
result = ResponseMessage{
Message: response,
}
SendResponseJSON(c,nil,result)
}
建立文件夹和文件 socket/socketweb/service/socketclient.go ,socketclient.go 内容如下:
package service
import (
"log"
"net"
"strings"
)
func cConnHandler(c net.Conn,message string,timeout int) string {
//缓存 conn 中的数据
buf := make([]byte, 1024)
response := ""
log.Println("请输入客户端请求数据...")
for {
//发送消息
response = messageHandle(c,message,buf)
if response != "" {
c.Close() //关闭连接
return response
}
}
timeout--
return SocketConnect(message,timeout)
}
//消息处理
func messageHandle(c net.Conn,message string,buf []byte) string{
//去除输入两端空格
message = strings.TrimSpace(message)
//客户端请求数据写入 conn,并传输
c.Write([]byte(message))
//服务器端返回的数据写入空buf
cnt, err := c.Read(buf)
if err != nil {
log.Printf("客户端读取数据失败 %s\n", err)
return "" //退出当前循环
}
//回显服务器端回传的信息
log.Print("服务器端回复" + string(buf[0:cnt]))
return string(buf[0:cnt]);
}
//timeout 超时次数
func SocketConnect(message string,timeout int) string {
if(timeout == 0){ //0是没有次数
return ""
}
conn, err := net.Dial("tcp", "47.75.74.233:6661")
if err != nil {
log.Println("客户端建立连接失败")
return ""
}
return cConnHandler(conn,message,timeout)
}
建立文件夹和文件 socket/socketweb/view/socketweb.html ,socketweb.html 内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .title }}</title>
<script src="https://code.jquery.com/jquery.min.js"></script>
</head>
<body>
<form id="form">
发送信息:<input type="text" name="message" value="">
<input type="submit" value="提交">
</form>
<div>
<p>服务器回复:<span id="response" style="color:red;"></span></p>
</div>
<script>
$("#form").on("submit",function(){
var data = {
message: $(this).children('[name="message"]').val(),
time: new Date().getTime().toString()
}
data = JSON.stringify(data)
$.ajax({
url:"https://www.daily886.com:6662/sendmessage",
data: data,
type: "POST",
contentType: "application/json",
dataType:"json",
success: function(res){
console.log(res)
if(res.code == 0){
$('#response').html(res.data.message)
}
},
error:function(res){
console.log(res)
}
});
return false;
})
</script>
</body>
</html>
建立文件夹和文件 socket/socketweb/main.go ,main.go 内容如下:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"socketweb/config"
"socketweb/router"
"socketweb/service"
)
func main() {
if err := config.Init();err != nil{
panic(err)
}
//设置gin模式
gin.SetMode(viper.GetString("common.server.runmode"))
//创建一个gin引擎
g := gin.New()
router.InitRouter(g)
log.Printf("开始监听服务器地址: %s\n", viper.GetString("common.server.url"))
//使用热重启
// kill -USR2 pid 重启
// kill -INT pid 关闭
add := viper.GetString("common.server.addr")
srv := &http.Server{
Addr: add,
Handler: g,
}
log.Printf( "srv.Addr %v \n", srv.Addr)
service.Listenserver(srv)
}
切换到socketweb目录中,模型初始化,并打包启动 socketweb
[root@izj6c4jirdug8kh3uo6rdez socketweb]# go mod init socketweb
go: creating new go.mod: module socketweb
[root@izj6c4jirdug8kh3uo6rdez socketweb]# go build
[root@izj6c4jirdug8kh3uo6rdez socketweb]# ./socketweb
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] Loaded HTML Templates (2):
-
- socketweb.html
[GIN-debug] GET / --> socketweb/service.Index (6 handlers)
[GIN-debug] GET /socketweb --> socketweb/service.SocketWeb (6 handlers)
[GIN-debug] POST /sendmessage --> socketweb/service.SendMessage (6 handlers)
####################################这是守护进程引起的第二次启动###############
[root@izj6c4jirdug8kh3uo6rdez socketweb]# [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] Loaded HTML Templates (2):
-
- socketweb.html
[GIN-debug] GET / --> socketweb/service.Index (6 handlers)
[GIN-debug] GET /socketweb --> socketweb/service.SocketWeb (6 handlers)
[GIN-debug] POST /sendmessage --> socketweb/service.SendMessage (6 handlers)
启动成功
2019/08/08 14:51:56 main.go:26: 开始监听服务器地址: :6662
2019/08/08 14:51:56 main.go:37: srv.Addr :6662
2019/08/08 14:51:56 hotupdate.go:135: syscall.Getppid() 1642
2019/08/08 14:51:56 main.go:26: 开始监听服务器地址: :6662
2019/08/08 14:51:56 main.go:37: srv.Addr :6662
2019/08/08 14:51:56 hotupdate.go:135: syscall.Getppid() 1
2019/08/08 14:51:56 hotupdate.go:48: listening on a new file descriptor
2019/08/08 14:51:56 hotupdate.go:50: Actual pid is 22485
浏览器访问即可:https://www.daily886.com:6662/socketweb
参考:https://blog.csdn.net/u014361775/article/details/80582910
参考:https://www.jianshu.com/p/a31e4ee25305/