图源:
开一个新坑,用Go来做web开发。虽然已经从事多年基于LAMP的web开发,但最近学习了Go编程,所以打算借着学习《Go Web 编程》一书的同时撰写笔记,算是对web开发的复习和回顾。
作者为新加坡人,从事编程相关教学工作,此书主要内容为使用Go的标准库实现一个web应用,并借此阐述HTTP和web开发的相关理念和技能。
本篇笔记的大纲是我用幕布编写的思维导图,可以在查看。
Go开发web的优势
比如Java多用于大型企业的商业应用,PHP多用于中小企业建站,Python用于科学计算、人工智能、数据分析,而Go常用于游戏服务器和实时通信等。
这种现象是由语言特性决定的,比如PHP在LAMP环境下易于部署、开发、维护,对于人力资源紧张的中小企业友好,而Python的list
、set
、map
等数据结构设计的相当好用,且语言本身的简单易学,再加上繁荣的第三方库,就造成了其在科学领域的广泛使用。
而Go的优点在于标准库本身支持了处理HTTP响应和请求的组件,可以让Go开发的web应用无需借助额外的Apache等Web service实现web服务。这样做的好处一是web请求无需经过web service转发,执行效率高。二是可以定制化地实现web请求处理和线程调度,比如通过线程池保持长连接的web请求,以实现游戏服务器等需要长连接情景下的高效web服务。
此外,Go语言的另一优势是语言本身是编译型语言,执行效率高。
执行效率的高低是相对而言的,一般来说C>Go>PHP>Python,但各种语言也在采用各种手段来提高执行效率,比如PHP使用C编写的库作为标准组件,Python之父加入微软后也在推进Python效率的提升。总之一门主流的语言在效率方面都是合格的,它们之间的执行效率只会在某些个别应用场景下被方大,需要注意,大部分时间都是无需在意的。
上面是从语言本身的特性评价Go在Web开发上的优势,下面用一般性的Web应用需要满足的特征进行评价:
-
可扩展
-
模块化
-
可维护
Web应用的可扩展性包含两个方面:纵向和横向。纵向扩展指要能通过增加单台服务器的性能(CPU的数量和主频)提高应用的性能。横向扩展指要能通过增加服务器的数量提升应用的性能。
Go开发的Web应用在纵向可扩展性上的优势在于其支持的goroutine实现的并发,可以很好的在单个或者多个CPU线程上实现不错的调度,以实现一个不错的并发性能。横向可扩展性上的优势在于,Go开发的程序都是一个单独的包含了所需要的库的二进制文件,这意味着可以无需考虑部署所依赖的外部环境,可以简单地通过拷贝二进制文件的方式在多台服务器上部署同样的应用。
Web应用的模块化体现在将一个大型应用拆分为多个小应用,应用之间使用数据库、消息队列等方式进行通信,以便于进行维护。在近些年这样的做法也叫做“微服务”,可以让小型应用之间通过网络进行通信,以灵活地组成完整的可以服务的Web应用。
在这方面Go并没有单独的优势,大多数后台开发语言都具有相同的能力。
Web应用的可维护性体现在代码结构清晰、可读性强。这点对所有的编程语言的要求是相同的,不同的是Go非常独特的理念:提供一致性的语法格式化工具go fmt
以及定义严格(甚至可以说苛刻)的语法。这点在主流编程语言中非常罕见,一些其他语言转过来的开发者(包括我)一开始可能很难适应。但就多人协同开发而言,这样做无疑是有益的,可以带来一致性的代码风格,对提升代码的可维护性是有帮助的。
HTTP
Web开发中,HTTP协议是基础。对于HTTP协议而言,最基础的定义是:这是一个纯文本的无状态的请求-响应协议。
除此以外我们还需要了解HTTP的主要版本:
-
HTTP 0.9,这是一个早期版本,应用于基本都是静态网站的互联网上古时期。
-
HTTP 1.0,这是一个已经相当晚上的版本。
-
HTTP 1.1,在HTTP1.0基础上推进的版本,也是目前普遍使用的版本。
-
HTTP 2.0,也称为HTTP/2,在保持HTTP基本语法不变的基础上,提升了传输性能。
更多HTTP不同版本之间的差异,请阅读。
Web应用历史
在Web应用的发展史中,有两个技术相当关键:
CGI
CGI的全称是Common Gateway Interface(通用网关接口)。
最原始的Web应用只能提供一种服务,即将请求的文件内容原样返回。所以早期的互联网上的网站都是静态的,只能展示服务器上的文档。
CGI提供了一种可能:即通过这种CGI定义的接口,可以让Web应用和服务器上的其它本地应用进行“交互”,以提供额外的服务。
图源:《HTTP权威指南》
SSI
SSI全称Server Side Iclude(服务端嵌入)。
虽然通过CGI技术可以扩展Web服务器的功能,但其对于Web服务器的主业(展示HTML页面)并无多大帮助,依然只能展示内容固定的静态HTML页面。这时候就有了SSI技术,该技术的要点在于可以在HTML页面中嵌入“模版语言”,通过执行模版语言后用执行结果替换HTML中的相应标签来展示一个“动态页面”。
PHP就是从一门模版语言基础上发展而来的。
HTTP请求
HTTP的核心就是“请求”和“响应”,它们的具体都是以“报文”的形式存在。
一个HTTP报文包含这几部分:
-
首行
-
报文头
-
空白行
-
报文体
具体的定义中,请求和响应报文略有区别。
这里以https://blog.icexmoon.xyz/
请求的报文头进行说明:
GET / HTTP/1.1 Host: blog.icexmoon.xyz Connection: keep-alive Pragma: no-cache Cache-Control: no-cache ... Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh-TW;q=0.9,zh-HK;q=0.8,zh;q=0.7,en;q=0.6,und;q=0.5 Cookie: wordpress_logged_in_355d7dcb2fd1dfec694fc30226db4815=xxx ...
GET / HTTP/1.1
是请求报文的首行,GET
指定了使用的HTTP方法(HTTP method),HTTP/1.1
说明了使用了哪个版本的HTTP协议。
HTTP方法
在HTTP/1.1
中可以使用的HTTP方法主要有:
-
GET,最基础的HTTP方法,从服务器获取资源。
-
POST,HTTP 1.0加入,将内容发送给服务器,服务器如何处理取决于服务器。
-
PUT,HTTP 1.1加入,发送内容到服务器,让服务器更新指定的资源(没有就新增)。
-
DELETE,HTTP 1.1加入,从服务器删除指定的资源。
-
HEAD,HTTP 1.0加入,和GET类似,不过服务器只需要返回报文头,不需要返回报文体。
-
TRACE,服务器返回请求报文的转发过程,可以用于网络诊断。
-
OPTIONS,服务器返回支持的HTTP方法列表。
-
CONNECT,与服务器建立网络连接,用于创建SSL隧道。
-
PATCH,让服务器使用报文中的数据对资源进行修改。
比较常用的有GET
、POST
、PUT
、DELETE
、HEAD
,事实上RESTful架构就是在这几几种HTTP方法上架构的,更多RESTful相关内容可以阅读。
需要说明的是,这些方法的含义仅仅是一种“官方建议”,具体服务端是否支持以及如何实现完全取决于Web应用自己,你完全可以创建一个用GET
请求删除资源的Web应用,事实上鉴于HTML只支持GET和POST,大多数Web应用都是这么做的。
安全的方法
通常会将“不会对服务器数据造成改变”的HTTP方法称为安全的方法。按HTTP方法定义来讲,GET/HEAD/TRACE/OPTIONS/CONNETC是安全的方法。
幂等
幂等是一个数学概念,简单的说,如果f(x)=f^n(x)
就是幂等。也就是说对于一个HTTP请求,执行一次和执行N次结果是完全相同的。
HTTP方法中,GET/PUT/DELETE/HEAD都是幂等的,对于POST,因为其具体含义完全由服务器定义,所以即不是安全的也不是幂等的。
需要强调的是,这些概念都只是“官方建议”,具体是否真的安全或幂等都取决于服务器的具体实现。
浏览器支持
直到HTML5,都是只支持POST和GET的,所以大多数常见的网站都只支持这两种HTTP方法。但其实大多数浏览器都是支持几乎所有的HTTP 1.1中定义的方法的。如果要使用浏览器发送HTML不支持的方法的HTTP请求,可以通过XHR。
XHR全程XML HTTP Request,事实上是一个浏览器对象,通过该浏览器对象,我们可以发送自定义的HTTP请求。
虽然XHR名称中是XML,但实际上并不限制报文体的格式,比如常见的
json
,或者自定义格式。
实际操作中通常使用JQuery来简化调用AJAX和XHR的过程,这里展示一个发送PUT请求的示例:
<html>
<head>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script>
function update_data() {
$.ajax({
url: "./put.php",
type: "PUT",
data: JSON.stringify({
data: $("input[name='data_input']").val()
}),
dataType: "json",
contentType: "json",
async: true,
success: function (result, status, xhr) {
$("input[name='data_update']").val(result.new_data);
},
error: function (xhr, status, error) {
console.log("error");
}
});
}
</script>
</head>
<body>
data:<input name="data_input" type="text" value="" /><br />
after changed:<input name="data_update" type="text" value="" /><br />
<button onclick="update_data()">update data</button><br />
</body>
</html>
完整的前后台代码见。
报文头
HTTP请求中比较重要的报文头有:
-
Accept,客户端接受什么样格式的返回信息,常见的有
text/html,application/xhtml+xml
等。 -
Accept-Encoding,客户端接受什么样的压缩算法压缩后的信息,常见的有
gzip, deflate, br
。 -
Accept-Language,客户端接受什么地区语言,常见的有
zh-CN,zh-TW;q=0.9,zh-HK;
等。 -
Cache-Control,是否使用缓存机制。
-
Connection,是否使用长连接(Keep-Alive)。
-
Cookie,随报文发送的当前网站保存在客户端的cookie信息。
-
Host,主机名。
-
User-Agent,用户代理,实际上就是网络浏览器的“标签”,最简单的反爬机制就是通过这个
User-Agent
信息来分辨请求来自正常的浏览器还是爬虫程序,但显然这种信息爬虫也可以简单伪造。
HTTP响应
HTTP响应(HTTP response)和HTTP请求类似,也包含首行、报文头和报文体这几个部分。
这里以https://blog.icexmoon.xyz/
的响应报文进行说明:
HTTP/1.1 200 OK Date: Sat, 18 Dec 2021 09:07:27 GMT Server: Apache/2.4.41 (Ubuntu) Expires: Wed, 11 Jan 1984 05:00:00 GMT Cache-Control: no-cache, must-revalidate, max-age=0 Pragma: no-cache Vary: Accept-Encoding Content-Encoding: gzip Keep-Alive: timeout=5, max=99 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8
状态码
HTTP响应报文的首行最重要的是状态码,也就是那个200
。通过不同的状态码可以确定HTTP请求的结果是成功还是失败。
事实上HTTP状态码通过首个数字区分为几种类型:
-
1XX,情报状态码,表示服务器已经收到请求。
-
2XX,表示请求成功。
-
3XX,重定向,浏览器应当重定向到某个页面,通常会通过返回报文头指定跳转目标地址。
-
4XX,客户端错误,常见的有404(资源没找到)。
-
5XX,服务端错误,通常为服务端程序bug。
1XX
不常见,其它几种都会常遇到。
可能一些人不理解为什么404是一个客户端错误,实际上在服务端看来,请求的URL无效说明客户端请求的URL是错误的,所以是一个客户端错误。
报文头
常见的HTTP响应报文头有:
-
Cache-Control,缓存机制。
-
Connection,是否使用长连接。
-
Content-Encoding,响应报文体的压缩算法。
-
Content-Type,响应报问体的格式和编码,客户端会根据这个格式信息来处理响应,比如展示图片或者对World文档提供一个保存选项。
-
Date,时间(格林尼治时间GMT)。
-
Expires,返回信息的过期时间(可以用于缓存机制)。
-
Server,服务器信息。
URI
URI全称Uniform Resource Identifie,即统一资源标识符。作用是可以在互联网上唯一地确定一个资源。
其实URI包含两种:URN和URL。前者是一种分布式的资源部署和访问方式,即使资源丢失也可以从资源的另一个拷贝处获取,有点像是P2P协议的方式。但因为种种原因并没有广泛使用,所以如今提到的URI都是指URL。
URL的构成
URL的基本构成是:scheme://host/path?query#location
。
其中scheme是使用的网络协议,目前广泛使用的是http
和https
。host和path是网站的域名和路径,一般唯一对应到一段后台代码进行响应。query为查询字符串,用于承载查询条件,当然这并非必须,也可以通过报文体来传递。location为“锚点”,用于让浏览器在当前页面中移动到某个特定信息节点,事实上锚点通常不会真的通过请求报文发送,而是会被Web客户端抛弃。
URL编码
URL中只允许出现安全字符,即ASCII
字符且不能是?&
等特殊字符,同样的,因为URL是一个完整字符串,也不能出现空格。
所以如果URL中出现一些不安全的字符,就需要对其进行编码。URL的编码规则是,将不安全的字符转换为ASCII
中的编码数值,然后再转换为2位16进制数,前边加上%
。
举例来说,空格在ASCII中的编码是32
,对应的十六进制值是20
,所以URL编码后的结果是%20
。
Web应用
通常很多开发者会将Web应用等同于Web框架,实际上框架只是为了为Web开发提供统一的、一致性的组件整合。事实上降低了新手的开发门槛,以至于会出现会使用框架的ORM编程程序但不会写纯SQL的开发者。
Web应用实际上有两部分组成:处理器和模版引擎。
处理器也可以叫做路由或者多路复用器,都起到一个将URL请求分发到具体处理代码的作用。
大多数Web应用所做的都是:
-
接收URL请求,并分发到处理程序。
-
处理请求,生成响应数据。
-
调用模版引擎,结合响应数据生成返回的HTML。
-
输出报文头和HTML。
hello go
现在可以编写第一个Go Web应用了:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe(":8080", nil)
}
func hello(rw http.ResponseWriter, r *http.Request) {
fmt.Fprintf(rw, "hello world! path:%s", r.URL.Path[1:])
}
运行后访问http://localhost:8080/123/456
,就会看到类似hello world! path:123/456
的输出。
谢谢阅读。
文章评论