一文详解Go Http Server原理

目录
  • 从一个 Demo 入手
  • Http Server 如何处理连接?
  • Http Server 如何处理请求的?
    • 一些前置工作
    • serve 方法到底干了什么
    • 请求如何路由?
  • 总结

从一个 Demo 入手

俗话说万事开头难,但用 Go 实现一个 Http Server 真不难,简单到什么程度?起一个 Server,并且能响应请求,算上包名、导入的依赖,甚至空行,也就只要 15 行代码:

package main
import (
	"io"
	"net/http"
)
func main() {
	http.HandleFunc("/hello", hello)
	http.ListenAndServe(":81", nil)
}
func hello(response http.ResponseWriter, request *http.Request) {
	io.WriteString(response, "hello world")
}

这么简单,能与之一战的恐怕只有 Python 了吧,而且 Go 还能编译成可执行的二进制文件,你说牛啤不牛啤?

Http Server 如何处理连接?

我们从这一行代码看起

http.ListenAndServe(":81", nil)

从命名来看,这个方法干了两件事,监听并且服务,从方法的单一职责上来说,我觉得不ok,一个方法怎么能干两件事?但这是大佬写的代码,就很合理。

第一个参数Addr是要监听的地址和端口,第二个参数Handler一般是nil,它是真正的逻辑处理,但我们通常用第一行代码那样来注册处理器,这代码一看就感觉是把 path 映射到业务逻辑上,我们先大概了解,待会再来看它

http.HandleFunc("/hello", hello)

如果了解过一点网络编程基础,就会知道操作系统提供了bindlistenaccept这样的系统调用,我们只要按顺序发起调用,就能组合出一个 Server。

Go 也是利用这些系统调用,把他们都封装在了ListenAndServe中。

Listen 往下追究就是系统调用,所以我们重点看 Serve

把分支代码收起来,只看主干,发现是一个 for 循环里面在不停地 Accept,而这个 Accept 在没有连接时是阻塞的,当有连接时,起一个新的协程来处理。

Http Server 如何处理请求的?

一些前置工作

处理请求的一行代码是,可以看出是每个连接单开了一个协程处理:

go c.serve(connCtx)

这里的 connCtx 代入了当前的 Server 对象:

ctx := context.WithValue(baseCtx, ServerContextKey, srv)
...
connCtx := ctx

而且还提供了修改它的 hook 方法 srv.ConnContext,可以在每次 Accept 时修改原始的 context

if cc := srv.ConnContext; cc != nil {
	connCtx = cc(connCtx, rw)
	if connCtx == nil {
		panic("ConnContext returned nil")
	}
}

它的定义是:

// ConnContext optionally specifies a function that modifies
// the context used for a new connection c. The provided ctx
// is derived from the base context and has a ServerContextKey
// value.
ConnContext func(ctx context.Context, c net.Conn) context.Context

但是如果按照我开头给的代码,你是没法修改 srv.ConnContext 的,可以改成这样来自定义:

func main() {
	http.HandleFunc("/hello", hello)
	server := http.Server{
		Addr: ":81",
		ConnContext: func(ctx context.Context, c net.Conn) context.Context {
			return context.WithValue(ctx, "hello", "roshi")
		},
	}
	server.ListenAndServe()
}

同样的 c.setState 也提供了 hook,可采取如上的方法设置,在每次连接状态改变时执行 hook 方法:

c.setState(c.rwc, StateNew, runHooks) // before Serve can return
// ConnState specifies an optional callback function that is
// called when a client connection changes state. See the
// ConnState type and associated constants for details.
ConnState func(net.Conn, ConnState)

serve 方法到底干了什么

为了能看清楚 Accept 后,serve 方法到底干了什么,我们再简化一下:

func (c *conn) serve(ctx context.Context) {
	...
	for {
		w, err := c.readRequest(ctx)
		...
		serverHandler{c.server}.ServeHTTP(w, w.req)
		...
	}
}

serve 也是一个大循环,循环里面主要是读取一个请求,然后将请求交给 Handler 处理。

为什么是一个大循环呢?因为每个 serve 处理的是一个连接,一个连接可以有多次请求。

读请求就显得比较枯燥乏味,按照Http协议,读出URL,header,body等信息。

这里有个细节是在每次读取了一个请求后,还开了一个协程去读下一个请求,也算是做了优化吧。

for {
	w, err := c.readRequest(ctx)
	...
	if requestBodyRemains(req.Body) {
		registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
	} else {
		w.conn.r.startBackgroundRead()
	}
	...
}

请求如何路由?

当读取到一个请求后,便进入这一行代码:

serverHandler{c.server}.ServeHTTP(w, w.req)

ServeHTTP 找到我们注册的 Handler 去处理,如果请求的URI 是 *或请求 Method 是 OPTIONS,则使用globalOptionsHandler,也就是说这类请求不需要我们手动处理,直接就返回了。

对于我们注册的 Handler 也需要去寻找路由,这个路由的规则还是比较简单,主要由如下三条:

  • 如果注册了带 host 的路由,则按 host + path 去寻找,如果没注册带 host 的路由,则按 path 寻找
  • 路由规则匹配以完全匹配优先,如果注册的路由规则最后一个字符是/,则除了完全匹配外,还会以前缀查找

举几个例子来理解一下:

  • 带 host 的匹配规则

注册路由为

http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello", hello2)

此时如果执行

curl 'http://127.0.0.1:81/hello'

则会匹配到 hello2,但如果执行

curl 'http://localhost:81/hello'

就匹配的是 hello

  • 前缀匹配

如果注册路由为

http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello/", hello2)

注意第二个最后还有个/,此时如果执行

curl 'http://127.0.0.1:81/hello/roshi'

也能匹配到 hello2,怎么样,是不是理解了?

找到路由之后就直接调用我们开头注册的方法,如果我们往 Response 中写入数据,就能返回给客户端,这样一个请求就处理完成了。

总结

最后我们回忆下 Go Http Server 的要点:

  • 用 Go 起一个 Http Server 非常简单
  • Go Http Server 本质是一个大循环,每当有一个新连接时,会起一个新的协程来处理
  • 每个连接的处理也是一个大循环,这个循环里做了读取请求、寻找路由、执行逻辑三件大事

以上就是一文详解Go Http Server原理的详细内容,更多关于Go Http Server原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • 使用Go语言写一个Http Server的实现

    目录 调试 功能1 功能2 功能3 功能4 Http Server 代码 go.mod: module goStudy1 go 1.17 main.go: package main import ( "fmt" "os" "strconv" //"github.com/thinkeridea/go-extend/exnet" "io" "log" "net/http"

  • Golang简单实现http的server端和client端

    介绍 HTTPS (Secure Hypertext Transfer Protocol)安全超文本传输协议,是一个安全通信通道,它基于HTTP开发用于在客户计算机和服务器之间交换信息.它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版,是使用TLS/SSL加密的HTTP协议. HTTP和HTTPS的区别 HTTPS是加密传输协议,HTTP是名文传输协议 HTTPS需要用到SSL证书,而HTTP不用 HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO HTTPS标准端

  • 深入分析Golang Server源码实现过程

    func (srv *Server) Serve(l net.Listener) error { ...... for { rw, err := l.Accept() if err != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { te

  • Golang实现http server提供压缩文件下载功能

    最近遇到了一个下载静态html报表的需求,需要以提供压缩包的形式完成下载功能,实现的过程中发现相关文档非常杂,故总结一下自己的实现. 开发环境: 系统环境:MacOS + Chrome 框架:beego 压缩功能:tar + gzip 目标压缩文件:自带数据和全部包的静态html文件 首先先提一下http server文件下载的实现,其实就是在后端返回前端的数据包中,将数据头设置为下载文件的格式,这样前端收到返回的响应时,会直接触发下载功能(就像时平时我们在chrome中点击下载那样) 数据头设

  • 一文详解Go Http Server原理

    目录 从一个 Demo 入手 Http Server 如何处理连接? Http Server 如何处理请求的? 一些前置工作 serve 方法到底干了什么 请求如何路由? 总结 从一个 Demo 入手 俗话说万事开头难,但用 Go 实现一个 Http Server 真不难,简单到什么程度?起一个 Server,并且能响应请求,算上包名.导入的依赖,甚至空行,也就只要 15 行代码: package main import ( "io" "net/http" ) fu

  • 一文详解Vue3响应式原理

    目录 回顾 vue2.x 的响应式 vue3的响应式 Reflect 回顾 vue2.x 的响应式 实现原理: 对象类型:通过object.defineProperty()对属性的读取.修改进行拦截(数据劫持) 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹) Object.defineProperty(data,'count ",{ get(){}, set(){} }) 存在问题: 新增属性.删除属性,界面不会更新 直接通过下标修改数组,界面不会自动更新 但是

  • 一文详解MySQL主从同步原理

    目录 1. MySQL主从同步实现方式 2. MySQL主从同步的作用 一主多从架构 双主多从架构 3. 主动同步的原理 4. 主从同步延迟问题 主从同步延迟的原因有哪些? 主从同步延迟的解决方案? 5. 如何提升主从同步性能 从库开启多线程复制 修改同步模式,改为异步 修改从库Bin Log配置 知识点总结 1. MySQL主从同步实现方式 MySQL主从同步是基于Bin Log实现的,而Bin Log记录的是原始SQL语句. Bin Log共有三种日志格式,可以binlog_format配置

  • 一文详解Golang中net/http包的实现原理

    目录 前言 http包执行流程 http包源码分析 端口监听 请求解析 路由分配 响应处理 前言 Go语言自带的net/http包提供了HTTP客户端和服务端的实现,实现一个简单的http服务非常容易,其自带了一些列结构和方法来帮助开发者简化HTTP服务开发的相关流程,因此我们不需要依赖任何第三方组件就能构建并启动一个高并发的HTTP服务器,net/http包在编写web应用中有很重要的作用,这篇文章会学习如何用 net/http 自己编写实现一个 HTTP Server 并探究其实现原理,具体

  • 一文详解Python中生成器的原理与使用

    目录 什么是生成器 迭代器和生成器的区别 创建方式 生成器表达式 基本语法 生成器函数 yield关键字 yield和return yield的使用方法 生成器函数的基本使用 send的使用 可迭代对象的优化 总结 我们学习完推导式之后发现,推导式就是在容器中使用一个for循环而已,为什么没有元组推导式? 原因就是“元组推导式”的名字不是这样的,而是叫做生成器表达式. 什么是生成器 生成器表达式本质上就是一个迭代器,是定义迭代器的一种方式,是允许自定义逻辑的迭代器.生成器使用generator表

  • 一文详解Golang 定时任务库 gron 设计和原理

    目录 cron 简介 gron 定时参数 源码解析 Cron Entry 按照时间排序 新增定时任务 启动和停止 Schedule 扩展性 经典写法-控制退出 结语 cron 简介 在 Unix-like 操作系统中,有一个大家都很熟悉的 cli 工具,它能够来处理定时任务,周期性任务,这就是: cron. 你只需要简单的语法控制就能实现任意[定时]的语义.用法上可以参考一下这个Crontab Guru Editor,做的非常精巧. 简单说,每一个位都代表了一个时间维度,* 代表全集,所以,上面

  • 一文详解凯撒密码的原理及Python实现

    目录 一.什么是恺撒密码 二.程序运行环境 三.恺撒密码:加密 3.1 恺撒密码加密实例程序 3.2 恺撒密码加密实例程序运行结果 四.恺撒密码:解密 4.1 恺撒密码解密实例程序 4.2 恺撒密码解密实例程序运行结果 五.完整程序 六.总结 一.什么是恺撒密码 恺撒密码是古罗马恺撒大帝用来对军事情报进行加密的算法,它采用了替换方法对信息中的每一个英文字符循环替换为字母表序列该字符后面第三个字符: 原文:A B C D E F G H I J K L M N O P Q R S T U V W

  • 一文详解 Compose Navigation 的实现原理

    目录 前言 1. 从 Jetpack Navigation 说起 2. 定义导航 3. 导航跳转 4. 保存状态 SaveableStateHolder & rememberSaveable 导航回退时的状态保存 底部导航栏切换时的状态保存 5. 导航转场动画 6. Hilt & Navigation 7. 总结 前言 一个纯 Compose 项目少不了页面导航的支持,而 navigation-compose 几乎是这方面的唯一选择,这也使得它成为 Compose 工程的标配二方库.介绍 

  • 一文详解Go语言单元测试的原理与使用

    目录 前言 为什么要引用单元测试类 单元测试基本介绍 优点 Testing规范 基本使用 Golang运行 命令行 案例 前言 为什么要引用单元测试类 传统方法的缺点分析 不方便,我们需要在main函数中去调用,这样就需要去修改main函数,如果现在项目正在运行,就可能去停止项目 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在main函数,不利于我们管理和清晰我们的思路 单元测试基本介绍 Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性

  • 详解PHP的执行原理和流程

    简介 先看看下面这个过程: • 我们从未手动开启过PHP的相关进程,它是随着Apache的启动而运行的: • PHP通过mod_php5.so模块和Apache相连(具体说来是SAPI,即服务器应用程序编程接口): • PHP总共有三个模块:内核.Zend引擎.以及扩展层: • PHP内核用来处理请求.文件流.错误处理等相关操作: • Zend引擎(ZE)用以将源文件转换成机器语言,然后在虚拟机上运行它: • 扩展层是一组函数.类库和流,PHP使用它们来执行一些特定的操作.比如,我们需要mysq

随机推荐