本来是想学习go web
框架的,我跟着beego
文档一顿猛操作,结果发现自己根本学不下去,beego
框架太难了,朋友们,可能是因为我的基础知识太弱鸡了吧,于是乎就想从头开始学一下。
什么是http协议
要讲清楚这个点,我们得先回顾一下TCP/IP
4层协议,图示如下
我们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
设置的很大,还是每次只发送很小的数据,和上述一样,当该种连接过多时候,会导致服务器连接数满,从而不能接收新的请求。
好了,动动你的手,来试试吧。