go实践九 开发http服务器

HTTP 请求处理流程

一次完整的 HTTP 请求处理流程如上图所示。(图片出自《HTTP 权威指南》,推荐想全面理解 HTTP 的读者阅读此书。)

1. 建立连接

客户端发送 HTTP 请求后,服务器会根据域名进行域名解析,就是将网站名称转变成 IP 地址:localhost -> 127.0.0.1,Linux hosts文件、DNS 域名解析等可以实现这种功能。之后通过发起 TCP 的三次握手建立连接。TCP 三次连接请参考 TCP三次握手详解及释放连接过程,建立连接之后就可以发送 HTTP 请求了。

2. 接收请求

HTTP 服务器软件进程,这里指的是 API 服务器,在接收到请求之后,首先根据 HTTP 请求行的信息来解析到 HTTP 方法和路径,在上图所示的报文中,方法是 GET,路径是 /index.html,之后根据 API 服务器注册的路由信息(大概可以理解为:HTTP 方法 + 路径和具体处理函数的映射)找到具体的处理函数。

3. 处理请求

在接收到请求之后,API 通常会解析 HTTP 请求报文获取请求头和消息体,然后根据这些信息进行相应的业务处理,HTTP 框架一般都有自带的解析函数,只需要输入 HTTP 请求报文,就可以解析到需要的请求头和消息体。通常情况下,业务逻辑处理可以分为两种:包含对数据库的操作和不包含对数据的操作。大型系统中通常两种都会有:

  1. 包含对数据库的操作:需要访问数据库(增删改查),然后获取指定的数据,对数据处理后构建指定的响应结构体,返回响应包。数据库通常用的是 MySQL,因为免费,功能和性能也都能满足企业级应用的要求。
  2. 不包含对数据库的操作:进行业务逻辑处理后,构建指定的响应结构体,返回响应包。

4. 记录事务处理过程

在业务逻辑处理过程中,需要记录一些关键信息,方便后期 Debug 用。在 Go 中有各种各样的日志包可以用来记录这些信息。

需要用到的第三方包有:

go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql
go get github.com/spf13/viper

目录结构如下: 

项目根目录是 webproject

webproject的目录结构
├── conf                         # 配置文件统一存放目录
│   ├── config.yaml              # 配置文件
├── config                       # 专门用来处理配置和配置文件的Go package
│   └── config.go                 
├── handler                      # 类似MVC架构中的C,用来读取输入,并将处理流程转发给实际的处理函数,最后返回结果
│   ├── handler.go
├── model                        # 数据库相关的操作统一放在这里,包括数据库初始化和对表的增删改查
│   ├── init.go                  # 初始化和连接数据库
│   ├── model.go                 # 存放一些公用的go struct
│   └── user.go                  # 用户相关的数据库CURD操作
├── pkg                          # 引用的包
│   ├── errno                    # 错误码存放位置
│   │   ├── code.go
│   │   └── errno.go
├── router                       # 路由相关处理
│   ├── middleware               # API服务器用的是Gin Web框架,Gin中间件存放位置
│   │   ├── header.go
│   └── router.go                # 路由
├── service                      # 实际业务处理函数存放位置
│   └── service.go
├── database.sql                 # 数据库文件
├── main.go                      # Go程序唯一入口

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

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

common:
  runmode: debug               # 开发模式, debug, release, test
  addr: :6663                  # HTTP绑定端口
  name: apiserver              # API Server的名字
  url: http://10.10.87.243:6663   # pingServer函数请求的API服务器的ip:port
  max_ping_count: 10           # pingServer函数尝试的次数
  database:                     #数据库配置
    #最大空闲连接数
    max_idle_conns: 50
    #数据库名称
    dbname: test
    #数据库服务器ip
    host: 10.10.87.244
    #端口
    port: 3306
    #登录用户名
    username: root
    #登录密码
    password: 123456

建立文件夹和文件 webproject/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, 0766)
	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
}

建立文件夹和文件 webproject/handler/handler.go ,handler.go 内容如下:

package handler

import (
	"net/http"
	"webproject/pkg/errno"
	"github.com/gin-gonic/gin"
)

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

//返回json 格式
func SendResponse(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,
	})

}

//返回html 格式
func SendResponseHtml(c *gin.Context,err error,data string){
	c.Header("Content-Type", "text/html; charset=utf-8")
	//总是返回http状态ok
	c.String(http.StatusOK,data)
}

建立文件夹 webproject/logs 存放日志文件, logs文件夹的权限 0777

建立文件夹和文件 webproject/model/init.go ,init.go 内容如下:

package model

import (
	"database/sql"
	"log"
	"fmt"
	"os"
	"errors"
	"strings"
	"io/ioutil"
	_ "github.com/go-sql-driver/mysql" //这个引用是必不可少的,因为需要调用driver.go文件里的init方法来提供一个数据库驱动程序
	"github.com/spf13/viper"
)

var DB *sql.DB     //全局变量,这样可以在别处调用

func Init() error{
	var err error
	var connectstring string
	max_idle_conns := viper.GetInt("common.database.max_idle_conns")
	username := viper.GetString("common.database.username")
	password := viper.GetString("common.database.password")
	host := viper.GetString("common.database.host")
	port := viper.GetInt("common.database.port")
	dbname := viper.GetString("common.database.dbname")
	// 账号:密码@tcp(IP:端口号)/数据库名?parseTime=true&charset=utf8&loc=Local
	connectstring = fmt.Sprintf("%s%s%s%s%s%s%d%s%s%s",username,":",password,"@tcp(",host,":",port,")/",dbname,"?parseTime=true&charset=utf8&loc=Local")

	//这行代码的作用就是初始化一个sql.DB对象
	DB,err = sql.Open("mysql",connectstring)
	if err != nil{
		return err
	}

	//设置最大超时时间
	DB.SetMaxIdleConns(max_idle_conns)

	//建立连接
	err = DB.Ping()
	if err != nil{
		return err
	}else{
		log.Println("数据库连接成功")
	}
	return nil
}

//检查是否安装数据库
func CheckInstalled() error{
	//先判断是否已经生成 install.lock文件,表示已经安装过了
	exists,_ := PathExists("install.lock")
	if exists {
		//fmt.Println("已经安装过数据库")
		return errors.New("已经安装过数据库")
	}
	return nil

}

//安装数据库
func CreateDatabase() error{
	//读取 database.sql 全部内容
	database, err := ioutil.ReadFile("database.sql")
	if err != nil {
		//fmt.Println("ioutil ReadFile database.sql error: ", err)
		return err
	}
	// 读取的数据转换成字符串类型
	databasestring := string(database)
	//去除换行符
	databasestring = strings.Replace(databasestring, "\n", "", -1)
	databasestring = strings.Replace(databasestring, "\r", "", -1)
	//strings.Split(string , "切割符") 把字符串转换成 数组
	databasearr := strings.Split(string(databasestring),";")
	//fmt.Printf("%v\n", reflect.TypeOf(databasearr))
	//fmt.Printf("%v\n", databasearr)

	//循环sql 数组,使用事务提交到数据库
	if(len(databasearr) > 0){
		transaction_con,err := DB.Begin()
		if err != nil{
			return err
		}
		for i := 0;i < len(databasearr);i++{
			if databasearr[i] == ""{
				continue
			}
			_, err = transaction_con.Exec(databasearr[i])
			if err != nil{
				// 失败回滚
				transaction_con.Rollback()
				return err
			}
		}
		//提交数据库
		err = transaction_con.Commit()
		if(err != nil){
			// 失败回滚
			transaction_con.Rollback()
			return err
		}
		//写入install.lock文件
		lock := []byte("1\n")
		err = ioutil.WriteFile("install.lock", lock, 0777)
		if err != nil{
			return err
		}
	}

	//
	//stmt,err := DB.Prepare(databasestring)
	//if err != nil{
	//	return err
	//}
	//defer stmt.Close()
	//_,err = stmt.Exec()
	//if err != nil{
	//	return err
	//}
	return nil
}

//检查文件是否存在
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

建立文件夹和文件 webproject/model/model.go ,model.go 内容如下:

package model

import (
	"fmt"
	"log"
)
var code int64
//  Insert 插入操作
func Insert(sql string,args...interface{})(int64,error){
	stmt,err := DB.Prepare(sql)
	defer stmt.Close()
	code,err = CheckErr(err,"sql语句设置失败",1)
	if(code != 1){return code,err}

	result,err := stmt.Exec(args...)
	code,err = CheckErr(err,"参数添加失败",1)
	if(code != 1){return code,err}

	id,err := result.LastInsertId()
	code,err = CheckErr(err,"插入失败",1)
	if(code != 1){return code,err}

	fmt.Printf("插入成功,id为%v\n", id)
	return id,err
}

// Delete删除
func Delete(sql string,args...interface{})(int64,error){
	stmt,err := DB.Prepare(sql)
	defer stmt.Close()
	code,err = CheckErr(err,"sql语句设置失败",1)
	if(code != 1){return code,err}

	result,err := stmt.Exec(args...)
	code,err = CheckErr(err,"参数添加失败",1)
	if(code != 1){return code,err}

	num,err := result.RowsAffected()
	code,err = CheckErr(err,"删除失败",1)
	if(code != 1){return code,err}

	fmt.Printf("删除成功,删除行数为%d\n",num)
	return num,err
}

// Update 更新
func Update(sql string,args...interface{})(int64,error){
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	code,err = CheckErr(err, "SQL语句设置失败",1)
	if(code != 1){return code,err}

	result, err := stmt.Exec(args...)
	code,err = CheckErr(err, "参数添加失败",1)
	if(code != 1){return code,err}

	num, err := result.RowsAffected()
	code,err = CheckErr(err,"修改失败",1)
	if(code != 1){return code,err}

	fmt.Printf("修改成功,修改行数为%d\n",num)
	return num,err
}

// 检查错误
func CheckErr(err error,msg string,code int)(int64,error){
	if err != nil{
		log.Panicln(msg,err,code)
		return 0,err
	}
	return 1,nil
}

建立文件夹和文件 webproject/model/user.go ,user.go 内容如下:

package model

import (
	"errors"
	"webproject/pkg/errno"
	"encoding/json"
	"log"
)

type User struct {
	UserName    string `json:"user_name"`
	Password    string `json:"password"`
}

//查询
func (user *User) SelectUserByName(name string) error {
	stmt,err := DB.Prepare("select user_name,password from user where user_name = ?")
	if err != nil{
		return err
	}
	defer stmt.Close()
	rows,err := stmt.Query(name)
	defer rows.Close()
	if err != nil{
		return err
	}
	//数据处理
	for rows.Next(){
		rows.Scan(&user.UserName,&user.Password)
	}
	if err := rows.Err();err != nil{
		return err
	}
	return nil
}

//验证字段
func (u *User) Validate() error{
	if u.UserName == "" || u.Password == ""{
		return errors.New(errno.ErrValidation.Message)
	}
	return nil
}

//创建用户
func (u *User) Create() (int64,error){
	id,err := Insert("insert into user(user_name,password) values(?,?)",u.UserName, &u.Password)
	if err != nil{
		return 0,err
	}
	return id,nil
}

//转换user 作为json字符串
func (user *User)UserToJson() string{
	jsonStr,err := json.Marshal(user)
	if err != nil{
		log.Println(err)
	}
	return string(jsonStr)
}

//转换json 作为user构造体
func (user *User)JsonToUser(jsonBlob string) error{
	err := json.Unmarshal([]byte(jsonBlob),&user)
	if err != nil{
		return err
	}
	return nil
}

建立文件夹和文件 webproject/pkg/errno/code.go ,code.go 内容如下:

package errno

var (
	// Common errors
	OK                  = &Errno{Code: 0, Message: "OK"}
	INSTALLERROR        = &Errno{Code: -1, Message: "安装失败"}
	INSTALLED        = &Errno{Code: -1, Message: "你已经安装过数据库"}
	InternalServerError = &Errno{Code: 10001, Message: "接口服务器错误"}
	ErrBind             = &Errno{Code: 10002, Message: "绑定数据错误"}

	ErrValidation = &Errno{Code: 20001, Message: "验证失败"}
	ErrDatabase   = &Errno{Code: 20002, Message: "数据库出错"}

	// user errors
	ErrUserNotFound      = &Errno{Code: 20101, Message: "该用户不存在"}
	ErrPasswordIncorrect = &Errno{Code: 20102, Message: "密码错误"}
)

建立文件夹和文件 webproject/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 IsErrUserNotFound(err error) bool{
	code,_ := DecodeErr(err)
	return code == ErrUserNotFound.Code
}

// 解析 错误信息, 返回字符串
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()
}

建立文件夹和文件 webproject/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")
}

建立文件夹和文件 webproject/router/router.go ,router.go 内容如下:

package router

import (
	"net/http"
	"webproject/service"
	"webproject/router/middleware"
	"github.com/gin-gonic/gin"
)

//初始化路由
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...)

	//404处理
	g.NoRoute(func(c *gin.Context){
		c.String(http.StatusNotFound,"该路径不存在")
	})
	//健康检查中间件
	g.GET("/",service.Index)//主页
	g.GET("/install",service.Install)//创建数据库
	usergroup := g.Group("/user") //创建路由组
	{
		usergroup.POST("/addUser",service.AddUser)  //添加用户
		usergroup.GET("/selectUser",service.SelectUser)//查询用户
	}
}

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

package service

import (
	"github.com/gin-gonic/gin"
	"webproject/model"
	. "webproject/handler"
	"webproject/pkg/errno"
	"fmt"
)

//首页
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>
`
	SendResponseHtml(c,nil,html)
}

//新建用户
func AddUser(c *gin.Context){
	var r model.User
	if err := c.Bind(&r); err != nil{
		SendResponse(c,errno.ErrBind,nil)
		return
	}
	u := model.User{
		UserName: r.UserName,
		Password: r.Password,
	}
	//验证数据
	if err := u.Validate(); err != nil{
		SendResponse(c,errno.ErrValidation,nil)
		return
	}
	//插入数据
	if _,err := u.Create();err != nil{
		SendResponse(c,errno.ErrDatabase,nil)
		return
	}
	SendResponse(c,nil,u)
}

//查询用户
func SelectUser(c *gin.Context){
	name := c.Query("user_name")
	if name == ""{
		SendResponse(c,errno.ErrValidation,nil)
		return
	}
	var user model.User
	if err := user.SelectUserByName(name); err != nil{
		fmt.Println(err)
		SendResponse(c,errno.ErrUserNotFound,nil)
		return
	}
	//验证数据
	if err := user.Validate(); err != nil{
		SendResponse(c,errno.ErrUserNotFound,nil)
		return
	}
	SendResponse(c,nil,user)
}


//创建数据库
func Install(c *gin.Context){
	//检查是否安装数据库
	if err := model.CheckInstalled(); err != nil{
		fmt.Println(err)
		SendResponse(c,errno.INSTALLED,err)
		return
	}
	//安装数据库
	if err := model.CreateDatabase(); err != nil{
		fmt.Println(err)
		SendResponse(c,errno.INSTALLERROR,err)
		return
	}
	SendResponse(c,nil,"成功")
}

建立文件夹和文件 webproject/database.sql ,database.sql  内容如下:

DROP TABLE IF EXISTS user;
CREATE TABLE user (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  user_name varchar(20) DEFAULT "",
  password varchar(20) DEFAULT "",
  age tinyint(11) DEFAULT "0",
  PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
INSERT INTO user VALUES ("1", "admin", "12164","18");

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

package main

import (
	"webproject/config"
	"webproject/model"
	"github.com/gin-gonic/gin"
	"log"
	"github.com/spf13/viper"
	"webproject/router"
)

func main() {
	if err := config.Init();err != nil{
		panic(err)
	}
	if err := model.Init();err != nil{
		panic(err)
	}
	//g := gin.default

	//设置gin模式
	gin.SetMode(viper.GetString("common.runmode"))

	//创建一个gin引擎
	g := gin.New()

	router.InitRouter(g)
	log.Printf("开始监听服务器地址: %s\n", viper.GetString("common.url"))
	//log.Println(http.ListenAndServe(viper.GetString("common.addr"), g).Error())
	if err := g.Run(viper.GetString("common.addr"));err != nil {
		log.Fatal("监听错误:", err)
	}

}

 

初始化包

[root@localhost webproject]# go mod init webproject
go: creating new go.mod: module webproject

启动服务器

[root@localhost webproject]# go run main.go
[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] GET    /                         --> webproject/service.Index (5 handlers)
[GIN-debug] GET    /install                  --> webproject/service.Install (5 handlers)
[GIN-debug] POST   /user/addUser             --> webproject/service.AddUser (5 handlers)
[GIN-debug] GET    /user/selectUser          --> webproject/service.SelectUser (5 handlers)
[GIN-debug] Listening and serving HTTP on :6663

首先,安装数据库:http://10.10.87.243:6663/install

 

直接使用浏览器访问:

web服务器搭建参考:https://www.cnblogs.com/Survivalist/articles/10596106.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值