Golang之我想写个"web框架"-1: 获取请求报文
本来是想学习
go web框架的,我跟着beego文档一顿猛操作,结果发现自己根本学不下去,beego框架太难了,朋友们,可能是因为我的基础知识太弱鸡了吧,于是乎就想从头开始学一下。
什么是http协议
要讲清楚这个点,我们得先回顾一下TCP/IP4层协议,图示如下
我们http协议是应用层协议,是基于传输层的TCP来传输报文的,所以说,想要进行http请求之前,需要首先先建立TCP链接,从而发送请求。 我们通常将基于http协议的应用,称之为是web应用,目前我们主流使用的是http 1.0和http 1.1两个版本。由于本节我们将介绍如何获取报文,所以不展开讲协议内容。
通过上述对http协议的描述,我们知晓了想要进行http请求之前,首先需要建立TCP连接,从而发送请求报文,现在问题是,TCP建立之后,相当于建立了一根管道,我们怎么知道请求报文发送完毕了呢?这个就要先分析一下我们请求报文格式了。
http报文概述
具体的http格式大概是这样的
整个起始行 和 请求头 是依靠/r/n/r/n来判断的,当读取到这个字符的时候,就意味着整个起始行 和 请求头结束了,然后请求体的报文主体是依靠什么来结尾的呢? 我们判断是否有报文主体 以及 报文主体 如何判断结束的,若我们有报文主体需要发送的时候,需要在请求头中添加 Content-Length来记录报文主体的长度。
我们分别来解释一下含义
起始行
我们接着来看起始行,它由 请求方法 和 URL 以及 协议版本构成
请求方法有哪些呢?这里暂时简单列举一下
- GET: 请求读取由URL所标注的信息
- HEAD: 请求读取由URL所标识的首部信息
- POST: 向服务器添加信息
- PUT: 在指名的URL下存储一个文档
URL是指我们需要请求的资源,例如: /1.txt、/1/2.txt等
协议版本是指向服务器声明客户端所使用的版本,目前最流行的是 http 1.0和 http 1.1
请求头
http报文请求头主要是用于携带报文的附加信息,其格式为key: value(key后跟一个冒号:,然后是空格,接着是value,最后是\r\n)
首部结束符
我们使用\r\n\r\n来标注首行结束了,主要是用于分割首部行与报文主体
报文主体
报文主体的长度来源于请求头中key为Content-Length的记录值。
手写报文验证格式
我们可以通过telnet来验证下报文的正确性,我们可以先写一个简单的http服务器,然后通过telnet连接上服务器,然后键入报文,来获取数据。
我们http服务器定义路由: /pdudo,当访问后,返回字符 hello juejin\n
我们开启服务器后,使用telnet进行获取数据
以上足以验证,我们编写的报文可以获取服务器数据。
获取报文
建立TCP服务器
现在假设没有net.http包供我们调用,我们如何获取报文并且分析报文请求格式呢,我们回顾上面案例,应用层http协议,是基于传输层TCP协议的,所以,当前我们得先建立一个TCP服务器起来。
我们写出一个最简单的TCP程序
如上监听8081端口,获取TCP管道数据后,就输出出来
启动程序后
我们使用curl客户端来访问一下
当然,请求该连接,这个是不会有返回结果的。
服务器打印了输出
我们服务器接收报文成功
如上http协议所述,我们还可以请求带主体,我们尝试一下,查看请求报文长什么样子。
使用curl可以发送POST请求,并且携带请求主体
服务器输出
从结果来看,我们可以通过Content-Length来获取请求主体长度
获取报文详细数据
我们在获取报文之前,应当思考一个问题,如何确定报文结束 以及 请求主体长度 ,好在我们再介绍http协议的时候提及了,这里再重复一下。
我们使用\r\n\r\n来标注首行结束了,主要是用于分割首部行与报文主体
报文主体的长度来源于请求头中key为Content-Length的记录值。
那么在代码中,我们应当如何编写呢? 我们可以想一个笨办法,每次从TCP管道中读取一个字节,然后比对已经获取的后4个字节 是否 和 \r\n\r\n相等,若相等,则代表首行结束了,接着在首行中获取 起始行 和 请求头的数据。
我们拿到了请求头数据,就可以判断是否存在 Content-Length,若存在,我们就可以根据其长度去取主体数据了,这样,我们整个请求报文就可以拿出来了,我们尝试下。
我们编写代码如下(main方法省略了)
type httpHeaders struct {
Method string
Url string
HttpVersion string
RequestHeader map[string]string
Body []byte
}
func worker(conn net.Conn) {
defer conn.Close()
maxLen := 8182
headBuf := make([]byte,maxLen) // 申请内存用于接收报文
CRLF := "\r\n" // 定义行结束
CRLF2 := "\r\n\r\n" // 定义首部报文结束符
readSize := 0
// 每次读取一个字节数据
for i:=1;i<=maxLen;i++ {
n,err := conn.Read(headBuf[readSize:i])
if err != nil {
fmt.Println("error: " , err)
return
}
readSize = readSize + n
// 判断首部报文是否结束了
if len(CRLF2) <= len(string(headBuf[:readSize])) {
if string(headBuf[readSize-len(CRLF2):readSize]) == CRLF2 {
break
}
}
}
// 判断首部报文是否超过允许接收长度
if maxLen <= readSize {
fmt.Println("超过允许接收的最大长度")
return
}
// 声明 httpHeaders
var httpHeader httpHeaders
httpHeader.RequestHeader = make(map[string]string,1)
// 将获取的首部数据打印出来
for k,v := range strings.Split(string(headBuf[:readSize-len(CRLF)]),CRLF) {
if v == "" {
continue
}
if 0 == k {
// 请求行
header := strings.Split(v," ")
httpHeader.Method = header[0]
httpHeader.Url = header[1]
httpHeader.HttpVersion = header[2]
} else {
// 请求头
requestLine := strings.Split(v,": ")
httpHeader.RequestHeader[requestLine[0]] = requestLine[1]
}
}
// 获取报文主体数据
bodyLen , ok := httpHeader.RequestHeader["Content-Length"];if ok {
bodyLenInt , err := strconv.Atoi(bodyLen)
if err != nil {
fmt.Println("Content-Length值转化失败")
return
}
// 从管道中取 Content-Length 长度的数据
bodyBuf := make([]byte,bodyLenInt)
recvLen := 0
for recvLen < bodyLenInt {
n , err := conn.Read(bodyBuf[recvLen:bodyLenInt])
if err != nil {
fmt.Println("read error " , err)
return
}
recvLen += n
}
httpHeader.Body = bodyBuf[:]
}
// 暂时打印请求报文
fmt.Printf("请求方法:%s\nURL:%s\n协议版本:%s\n请求头:%v\n报文主体:%s", httpHeader.Method , httpHeader.Url,httpHeader.HttpVersion , httpHeader.RequestHeader , httpHeader.Body)
}
我们访问一下
我们查看服务器打印的请求报文
这里解释一下,curl之所以报错: curl: (52) Empty reply from server,是因为函数结束后,服务器将该连接断开了,并没有返回任何数据给客户端。
总结
为什么将/r/n称之为CRLF
后期我们将/r/n统称为CRLF,为什么呢,我们不妨打开ascii看一下就明白了
在linux中使用命令man ascii可以查看ascii编码,我们看10进制的13和10其实CRLF就是这么来的。
http 报文攻击方式
这里有个有意思的事情,我在查询http报文资料的时候,发现有一种攻击方法,称之为slow header和 slow post,这里来解释下是什么意思。
slow header:表示客户端连接到服务器后,通过慢速度发送数据,但是一直不发送\r\n\r\n,服务器一直在接收,所以始终占着服务器连接,当该种连接过多时,会导致服务器连接数满,从而不能接收新的请求。
slow post: 这里指的是,通过post发送数据,但是将Content-Length设置的很大,还是每次只发送很小的数据,和上述一样,当该种连接过多时候,会导致服务器连接数满,从而不能接收新的请求。
好了,动动你的手,来试试吧。
本文正在参加技术专题18期-聊聊Go语言框架

浙公网安备 33010602011771号