Go web 基础相关知识
Go web
Web工作方式
浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务
URL和DNS解析
URL是“统一资源定位符”的英文缩写,用于描述一个网络上的资源
scheme://host[:port#]/path/.../[?query-string][#anchor] scheme 指定低层使用的协议(例如:http, https, ftp) host HTTP服务器的IP地址或者域名 port# HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/ path 访问资源的路径 query-string 发送给http服务器的数据 anchor 锚
DNS 是**“域名系统”的英文缩写,是一种组织成域层次结构的计算机和网络服务命名系统,他用于TCP/IP网络,从事将主机名或域名转换为实际IP地址**的工作
DNS解析过程:
1.在浏览器中输入www.qq.com域名,操作系统会检查自己本地的hosts文件是否由这个网址映射关系,如果有,直接返回域名解析
2.如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系
3.如果本地DNS缓存器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫他本地DNS服务器,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析
4.如果要查询的域名,不由本地DNS服务器区域解析
举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是xx系的;然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学,助理同学说是xx系yy班的,然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程
HTTP协议详解
HTTP协议是Web工作的核心
HTTP是一种让Web服务器与浏览器通过Internet发送与接收数据的协议,它建立在TCP协议之上,一般采用TCP的80端口。
HTTP协议是无状态的,同一个客户端的这次请求和上次请求时没有对应关系,对HTTP服务器来说,它并不知道这两个请求
HTTP请求包(浏览器信息)
GET /domains/example/ HTTP/1.1 //请求行: 请求方法 请求URI HTTP协议/协议版本 Host:www.iana.org //服务端的主机名 User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 //浏览器信息 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 //客户端能接收的mine Accept-Encoding:gzip,deflate,sdch //是否支持流压缩 Accept-Charset:UTF-8,*;q=0.5 //客户端字符编码集 //空行,用于分割请求头和消息体 //消息体,请求资源参数,例如POST传递的参数
HTTP协议定义了很多与服务器交互的请求方法,最基本的有4种,分别是GET,POST,PUT,DELETE。一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,增,改,删4个操作。我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息。
这个是GET信息
这个是POST信息
GET和POST信息:
1.GET请求信息体为空,POST请求带有消息体
2.GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连
3.GET提交的数据大小有限制,而POST方法提交的数据没有限制
4.GET方式提交数据,会带来安全问题
如果输入用户名和密码提交数据,用户名和密码将出现在URL上,如果页面可以被缓存,将从历史记录获得该用户的账号和密码
HTTP响应包(服务器信息)
response包:
HTTP/1.1 200 OK //状态行 Server: nginx/1.0.8 //服务器使用的WEB软件名及版本 Date:Date: Tue, 30 Oct 2012 04:14:25 GMT //发送时间 Content-Type: text/html //服务器发送信息的类型 Transfer-Encoding: chunked //表示发送HTTP包是分段发的 Connection: keep-alive //保持连接状态 Content-Length: 90 //主体内容长度 //空行 用来分割消息头和主体 r.ParseForm() //解析参数,默认是不会解析的 fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, "")) } fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端 } func main() { http.HandleFunc("/", sayhelloName) err := http.ListenAndServe(":9090", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } } //用来处理接收客户端的请求信息 defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay max { tempDelay = max } log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c, err := srv.newConn(rw) //创建一个Conn if err != nil { continue } go c.serve() //单独开了一个goroutine } }
如何分配handler?
如何具体分配到相应的函数来处理请求?
conn首先会解析request:c.readRequest,然后获取对应的handler:
handler :=c.server.Handler
Go的http详解
Go的http有两个核心功能:Conn,ServeMux
Conn的goroutine
Go为了实现高并发和高性能,使用了goroutine来处理Conn的读写事件,这样每个请求都能保持独立
c, err := srv.newConn(rw) if err != nil { continue } go c.serve()
客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到对应的handler,该handler中可以读取到相应的header信息,这样保证了每个请求的独立性
ServeMux的自定义(路由器的实现)
并发(Concurrency)是指在计算机科学中,指的是同时执行多个独立的任务或操作的能力。这些任务可能在同一时间段内交替执行,也可能在同一时间段内同时执行,但它们都是在同一个时间段内并发地进行。并发通常涉及到多个线程、进程或任务同时执行,并且这些执行之间可能相互独立,也可能存在一定的交互关系。并发通常被用来提高系统的性能、资源利用率和响应能力。
需要锁机制是为了防止只有一个进程能访问共享资源
type ServeMux struct { mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制 m map[string]muxEntry // 路由规则,一个string对应一个muxEntry实体,这里的string就是注册的路由表达式 hosts bool // 是否在任意的规则中带有host信息 }
type muxEntry struct { explicit bool // 是否精确匹配 h Handler // 这个路由表达式对应哪个handler pattern string //匹配字符串 } //每个路由表达式都对应着一个handler
type Handler interface { ServeHTTP(ResponseWriter, *Request) // 路由实现器 }
我们调用了HandlerFunc(f),强制类型转换f称为HandlerFunc类型,这样f就拥有了ServeHTTP方法
HandlerFunc(f)-因为HandlerFunc实现了Handler接口类型
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
默认路由器实现了ServeHTTP:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { w.Header().Set("Connection", "close") w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r) }
通过对http包的分析之后,现在让我们来梳理一下整个的代码执行过程。
-
首先调用Http.HandleFunc
按顺序做了几件事:
1 调用了DefaultServeMux的HandleFunc
2 调用了DefaultServeMux的Handle
3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则
-
其次调用http.ListenAndServe(“:9090”, nil)
按顺序做了几件事情:
1 实例化Server
2 调用Server的ListenAndServe()
3 调用net.Listen(“tcp”, addr)监听端口
4 启动一个for循环,在循环体中Accept请求
5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()
6 读取每个请求的内容w, err := c.readRequest()
7 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux
8 调用handler的ServeHttp
9 在这个例子中,下面就进入到DefaultServeMux.ServeHttp
10 根据request选择handler,并且进入到这个handler的ServeHTTP
mux.handler(r).ServeHTTP(w, r)
11 选择handler:
A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)
B 如果有路由满足,调用这个路由handler的ServeHTTP
C 如果没有路由满足,调用NotFoundHandler的ServeHTTP
表单
表单是一个包含表单元素的区域
通过表单我们可以让客户端于服务器进行数据的交互
表单元素是允许用户在表单中输入信息的元素
... input 元素 ...
4.1处理表单的输入
package main import ( "fmt" "log" "net/http" "text/template" ) func sayhelloName(w http.ResponseWriter, r *http.Request) { r.ParseForm() //解析url传递的参数,对于POST则解析相应包的主体 //如果没有调用ParseForm方法,下面无法获取表单的数据 fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("value:", v) } fmt.Fprintln(w, "Hello astaxie!") //这个写入到w的是输出客户端的 } func login(w http.ResponseWriter, r *http.Request) { fmt.Println("method:", r.Method) //获取请求的方法 if r.Method == "GET" { t, _ := template.ParseFiles("login.gtpl") log.Println(t.Execute(w,nil)) }else{ //请求的要么是登录数据,要么是执行登录的逻辑判断 //如查询数据库,验证登录信息等 fmt.Println("username:",r.Form["username"]) fmt.Println("password:",r.Form["password"]) } } func main() { http.HandleFunc("/",sayhelloName) // 设置访问的路由 http.HandleFunc("/login",login) err := http.ListenAndServe("9090",nil) if err!=nil{ log.Fatal("ListenAndServe:",err); } }
4.2验证表单的输入
如何在服务器端验证我们从一个表单元素中得到了一个值。
//如果username是map的值必须通过这种形式 if len(r.Form["username"][0])==0{ //为空的处理 }
我们通过r.Form.Get()来获取单个值
只能是数字
如果想要确保一个表单输入框中获取的只能是数字
getint,err:=strconv.Atoi(r.Form.Get("age")) if err!=nil{ //数字转化出错了,那么可能就不是数字 } //接下来就可以判断这个数字的大小范围了 if getint >100 { //太大了 }
只能是中文
于中文我们目前有两种方式来验证,可以使用 unicode 包提供的 func Is(rangeTab *RangeTable, r rune) bool 来验证,也可以使用正则方式来验证,这里使用最简单的正则方式,如下代码所示
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m { return false }
英文
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { return false }
电子邮箱地址
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m { fmt.Println("no") }else{ fmt.Println("yes") }
手机号码
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m { return false }
下拉菜单
想要知道元素生成的下拉菜单中是否有被选中的项目
假设我们的select是这样一些元素
apple pear banana
slice:=[]string{"apple","pear","banana"} v := r.Form.Get("fruit") for _, item := range slice { if item == v { return true } } return false
单选按钮
当我们需要判断一个单选按钮是否选中的时候
男 女
和下拉菜单的做法一样
slice:=[]string{"1","2"} for _, v := range slice { if v == r.Form.Get("gender") { return true } } return false
复选框
就是多选的
足球 篮球 网球
slice:=[]string{"football","basketball","tennis"} a:=Slice_diff(r.Form["interest"],slice) if a == nil{ return true } return false
func Slice_diff(slice1, slice2 []interface{}) (diffslice []interface{}) { for _, v := range slice1 { if !In_slice(v, slice2) { diffslice = append(diffslice, v) } } return }
日期和时间
你想确定日期时间是否有效
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) fmt.Printf("Go launched at %s\n", t.Local())
身份证号码
身份证有十五位和十八位
//验证15位身份证,15位的是全部数字 if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m { return false } //验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。 if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m { return false }
4.3预防跨站脚本
我们的网站包含了大量的动态内容以提高用户体验,比过去要复杂的多,所谓动态内容,就是根据用户环境的需要,Web应用程序能够输出相应的内容 。
对XSS(跨站脚本攻击)最佳的防护应该结合以下两种方法:
1.对所有输入数据进行检验
2.进行适当的处理,防止任何已经成功注入的脚本在浏览器端运行
HTML转义
HTML 转义是指将 HTML 特殊字符转换为对应的 HTML 实体,以防止恶意用户插入 HTML 或 JavaScript 代码导致的安全漏洞。HTML/template 包提供了几个函数来帮助进行 HTML 转义。
在 web 应用程序中,如果允许用户输入的内容直接嵌入到 HTML 页面中而没有经过转义处理,就可能会导致安全漏洞。这种情况被称为跨站脚本攻击(Cross-Site Scripting,XSS)。
XSS 攻击的原理是攻击者向网页中注入恶意的客户端脚本,当其他用户访问包含恶意脚本的页面时,这些脚本就会在用户浏览器中执行,从而达到攻击的目的,例如盗取用户的会话信息、篡改页面内容等。
举个例子,如果一个网站允许用户在评论中输入任意内容,并且将这些内容直接显示在网页上,而不进行转义处理,那么攻击者可以在评论中插入恶意的 JavaScript 代码。当其他用户浏览这些评论时,恶意脚本就会在其浏览器中执行,可能导致用户的账户被盗取或者其他安全问题。
因此,对用户输入的内容进行适当的转义处理是防范 XSS 攻击的重要手段之一。HTML 转义函数就是一种常见的转义处理方式,它将 HTML 特殊字符转换为对应的 HTML 实体,从而使得恶意脚本失效,保护了网站的安全。
脚本
脚本(Script)是一种用于自动化和控制计算机程序行为的编程语言。脚本通常以文本形式编写,并且可以被解释器直接执行,而不需要预先编译成二进制代码。脚本语言通常用于快速编写小型程序或自动化任务,如网页交互、系统管理、数据处理等。
在 web 开发中,脚本通常指的是客户端脚本(Client-side Script),它是嵌入在网页中的脚本代码,由浏览器解释执行。常见的客户端脚本语言包括 JavaScript、VBScript 等。这些脚本可以在用户浏览网页时在浏览器中执行,用于实现网页的交互功能、动态效果等。
举个例子,当你在网页上点击一个按钮,触发了一些动作或者弹出了一个对话框,很可能是因为网页中嵌入了 JavaScript 脚本,它在用户与网页交互时被触发执行。
然而,如果恶意用户能够将自己的恶意脚本注入到网页中,并成功执行,就可能导致安全问题,比如跨站脚本攻击(XSS)或其他类型的安全漏洞。因此,开发者需要注意对用户输入数据进行适当的过滤和转义,以防止这类安全风险的发生。
MD5(Message Digest Algorithm 5)是一种常用的哈希函数,用于将任意长度的消息(或数据)转换为固定长度的哈希值(通常为128位的二进制字符串,或者32个十六进制字符)。
MD5
MD5 哈希对象通常在计算机安全、数据完整性验证等领域中使用,其主要用途包括:
数据完整性验证:** MD5 哈希函数可以将任意长度的数据转换为固定长度的哈希值。通过对原始数据进行哈希处理,可以生成一个唯一的标识符,称为哈希值。如果数据的任何部分发生了改变,其哈希值也会发生变化。因此,可以通过比较数据的哈希值来验证数据的完整性,确保数据在传输或存储过程中没有被篡改。
package main import ( "crypto/md5" "fmt" "html/template" "io" "log" "net/http" "strconv" "strings" "time" ) func sayhelloName(w http.ResponseWriter, r *http.Request) { r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body) //注意:如果没有调用ParseForm方法,下面无法获取表单的数据 fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息 fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, "")) } fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 } func login(w http.ResponseWriter, r *http.Request) { fmt.Println("method:", r.Method) if r.Method == "GET" { //如果时GET请求,就返回登录页面 crutime := time.Now().Unix() //获取当前时间戳(当前时刻) h := md5.New() //创建一个MD5,MD5对象可以保证这个时间戳的安全性 io.WriteString(h, strconv.FormatInt(crutime, 10)) //将当前时间戳转换成字符串并写入哈希对象h token := fmt.Sprintf("%x", h.Sum(nil)) //fmt.Sprintf用来返回格式化的字符串 //计算哈希值并将其格式化为十六进制字符串 t, _ := template.ParseFiles("vote.html") //解析模板文件并赋值给t t.Execute(w, token) //将token(格式化的字符串)渲染到模板中 //发送给客户端 } else { //如果时POST就需要处理 用户提交的登陆数据 r.ParseForm() //解析表单数据 token := r.Form.Get("token") if token != "" { //验证token的合法性 } else { //不存在token则报错 } fmt.Println("username length:", len(r.Form["username"][0])) fmt.Println("username:", template.HTMLEscapeString(r.Form.Get(("username")))) //打印用户名,并适应HTML转义 fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) template.HTMLEscape(w, []byte(r.Form.Get("username"))) //将用户名转移之后,输出到客户端 } } func main() { http.HandleFunc("/", sayhelloName) //设置访问的路由 http.HandleFunc("/login", login) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil { log.Fatal("ListenAndServe: ", err) } }
4.5处理文件上传
要使表单能够上传文件
我们要添加form的enctype属性,enctype属性有三种情况:
- application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
- multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
- text/plain 空格转换为 "+" 加号,但不对特殊字符编码。
package main import ( "crypto/md5" "fmt" "html/template" "io" "net/http" "os" "strconv" "time" ) func upload(w http.ResponseWriter, r *http.Request) { fmt.Println("method", r.Method) //获取请求的方法 if r.Method == "GET" { crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) t, _ := template.ParseFiles("upload.html") t.Execute(w, token) } else { r.ParseMultipartForm(32 fmt.Println(err) return } defer file.Close() fmt.Fprintf(w, "%v", handler.Header) f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Println(err) return } defer f.Close() io.Copy(f, file) } }