Golang之我想写个"web框架"-1: 获取请求报文

本来是想学习go web框架的,我跟着beego文档一顿猛操作,结果发现自己根本学不下去,beego框架太难了,朋友们,可能是因为我的基础知识太弱鸡了吧,于是乎就想从头开始学一下。

什么是http协议

要讲清楚这个点,我们得先回顾一下TCP/IP4层协议,图示如下

我们http协议是应用层协议,是基于传输层的TCP来传输报文的,所以说,想要进行http请求之前,需要首先先建立TCP链接,从而发送请求。 我们通常将基于http协议的应用,称之为是web应用,目前我们主流使用的是http 1.0http 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.0http 1.1

请求头

http报文请求头主要是用于携带报文的附加信息,其格式为key: value(key后跟一个冒号:,然后是空格,接着是value,最后是\r\n)

首部结束符

我们使用\r\n\r\n来标注首行结束了,主要是用于分割首部行与报文主体

报文主体

报文主体的长度来源于请求头中keyContent-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来标注首行结束了,主要是用于分割首部行与报文主体

报文主体的长度来源于请求头中keyContent-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进制的1310其实CRLF就是这么来的。

http 报文攻击方式

这里有个有意思的事情,我在查询http报文资料的时候,发现有一种攻击方法,称之为slow headerslow post,这里来解释下是什么意思。

slow header:表示客户端连接到服务器后,通过慢速度发送数据,但是一直不发送\r\n\r\n,服务器一直在接收,所以始终占着服务器连接,当该种连接过多时,会导致服务器连接数满,从而不能接收新的请求。

slow post: 这里指的是,通过post发送数据,但是将Content-Length设置的很大,还是每次只发送很小的数据,和上述一样,当该种连接过多时候,会导致服务器连接数满,从而不能接收新的请求。

好了,动动你的手,来试试吧。

本文正在参加技术专题18期-聊聊Go语言框架

posted @ 2022-07-14 22:31  pdudos  阅读(0)  评论(0编辑  收藏  举报  来源