后端技术栈入门:从零搭建第一个服务
一、别从框架开始
学习后端最大的误区,就是把框架当作起点。Spring Boot、Django、Express、Gin,这些名字听起来很酷,教程里也都是“三分钟跑通Hello World”,但你真正动手时,会发现自己在复制粘贴一堆不理解的黑魔法。你连HTTP报文长什么样都没见过,却已经在处理拦截器和依赖注入;你连数据库连接都没建立过,却要理解ORM的懒加载和事务传播行为。框架不是后端的地基,而是后端的装修公司——地基没打好,装修得再漂亮也随时会塌。
所以我建议你从零搭建第一个服务,这里的“零”不是指一个空目录,而是指你自己掌控从网络端口到业务逻辑的那条完整链路。不用任何Web框架,只用语言自带的标准库,甚至可以用最朴素的socket编程,让浏览器能访问到你写的代码。听起来很原始?但正是这种原始,能让你看清后端到底在做什么:监听端口、解析请求、构造响应、处理并发。
你不需要理解一切才去动手,但你需要动手才能理解一切。 与其在框架的海洋里溺水,不如先在自己的代码里游一圈。
二、第一个服务:让端口活起来
打开你的终端,创建一个目录,随便叫什么名字。在里面新建一个文件,用Python的话叫server.py,用Node的话叫server.js,用Go的话叫main.go。选择什么语言不重要,重要的是接下来你要写的那几行代码。
我用Python标准库来演示,因为它最接近“伪代码”,你不需要处理花括号、分号、类型声明的噪音。这里用到的是http.server模块,它已经帮你处理了底层socket的细节,但依然把HTTP请求和响应暴露给你。
from http.server import HTTPServer, BaseHTTPRequestHandler class MyHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain; charset=utf-8') self.end_headers() self.wfile.write(b'Hello from your first server!') server = HTTPServer(('localhost', 8080), MyHandler) print('Server is running at http://localhost:8080') server.serve_forever()
保存,运行,打开浏览器访问http://localhost:8080。你看到那段英文了吗?恭喜你,你刚刚亲手创建了一个可以对外提供服务的进程。 这个进程监听在8080端口上,每当有请求进来,do_GET方法就会被调用,然后你通过send_response设置状态码,通过send_header设置响应头,通过wfile.write写入响应体。
这几行代码背后藏着后端服务的全部本质:任何后端服务,无论它多复杂,最终都逃不过“接收请求、处理逻辑、返回响应”这个循环。 你现在做的,就是亲手拆开了这个循环的最小单元。
三、解剖HTTP:你每天说却看不懂的语言
刚才那个服务能跑,但你未必知道浏览器到底发送了什么。做一个实验:在do_GET里加一句print(self.command, self.path, self.request_version),再打印self.headers。然后重启服务,刷新浏览器,观察终端输出。
你会看到类似这样的东西:
GET / HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 ... Accept: text/html,...
这就是HTTP请求的原始面貌。HTTP协议本质上就是一种约定好的文本格式,它告诉你三件事:方法(动词)、路径(名词)、版本(语气)。 GET表示你想获取资源,/是你要获取的资源路径,HTTP/1.1是你使用的协议版本。下面那些键值对是请求头,用来传递元信息,比如浏览器类型、能接受的内容类型、连接状态等。
而你的响应也需要遵循同样的格式。刚才你写的send_response(200)实际上生成了响应状态行HTTP/1.1 200 OK,send_header生成了响应头,wfile.write写入了响应体。当你能用肉眼看懂这些文本时,你就再也不会被那些所谓的“前后端联调问题”吓到了——所有的“接口不通”都能追溯到请求或响应格式不符合约定。
后端开发最基础也最重要的能力,就是熟练掌握HTTP协议。你不需要背下所有状态码,但你必须知道200、301、400、401、403、404、500意味着什么。 你不需要把所有请求头都记住,但你至少要理解Content-Type、Content-Length、Authorization、Cookie这几个关键项。因为路由、鉴权、缓存、跨域所有高级主题,全都是在HTTP协议上长出来的枝叶。
四、路由:别再写一长串if了
刚才的服务器无论你访问/还是/abc,都会返回同一个Hello。这当然不行,真实服务要根据不同路径返回不同内容。你可能会写:
def do_GET(self): if self.path == '/': self.wfile.write(b'Home page') elif self.path == '/user': self.wfile.write(b'User profile') else: self.send_response(404) self.wfile.write(b'Not Found')
这能跑,但这正是初学者最容易陷入的“if地狱”。当路由数量超过五个时,逐个if判断就会变得难以维护、难以测试、容易出错。 这时候你应该想到:路由本身就是“路径到函数的映射”,既然是映射,Java里有HashMap,Python里有字典,你为什么不直接用一个表来定义路由呢?
改造一下你的代码:先定义一个字典,将路径映射到处理函数,然后在do_GET里查字典。如果找到就调用对应的函数,找不到就返回404。这样你的路由表变得清晰可见,新增一个接口只需要往字典里加一行。
设计好的路由是你理解“后端架构分层”的第一次启航——你需要把“网络接收”和“业务逻辑”分离。 不一定真的要做多复杂的抽象,但只要你的请求处理函数能从一个路径参数变成一个独立函数,你就已经从“写脚本”迈向了“做工程”。
五、获取用户输入:Query字符串和请求体
光有路由还不够,用户还得能传参数。最常见的参数传递方式是Query字符串,也就是URL里?后面的部分。比如http://localhost:8080/search?q=python&page=2。在你自己的服务里,你可以用urllib.parse.urlparse(self.path)把路径和查询字符串分开,再用parse_qs把查询字符串解析成字典。
试着写一个/search接口:
from urllib.parse import urlparse, parse_qs def do_GET(self): parsed = urlparse(self.path) if parsed.path == '/search': params = parse_qs(parsed.query) q = params.get('q', [''])[0] page = params.get('page', ['1'])[0] # 处理搜索逻辑...
你马上会发现,Query字符串是无状态的,它只能通过URL传递,不适合上传大段文本或文件。这时候你需要POST请求和请求体。GET和POST的本质区别不是“大小”,而是“语义”和“安全性”。 GET用于获取资源,参数放在URL里,可被书签保存、可被缓存;POST用于提交数据,参数放在请求体里,不适合写在日志里。但很多新手把POST当成“防篡改的GET”来用,这是错误的。
处理POST请求,你需要重写do_POST方法,并且需要读取请求体。HTTP请求体是流式的,你需要读一定长度。你从self.headers.get('Content-Length')拿到字节数,然后调用self.rfile.read(length)。看,这又逼着你理解HTTP头的重要性了——没有Content-Length,服务器就不知道该读多少字节。
当你能手动解析GET和POST参数时,你就能深刻体会到为什么会有JSON这种格式。 请求体是一堆字节,你需要一套规则去解读它们。你可以用application/x-www-form-urlencoded的键值对格式,也可以用application/json。现代API几乎都用JSON,因为它嵌套能力强、类型丰富、跨语言方便。所以从你自己的服务开始,就应该主动接受JSON,用json.loads来解析请求体,用json.dumps来生成响应体。放心,标准库都有。
六、状态管理:服务端该记住什么
写到这里,你可以做一个最简单的“计数器”接口,每次访问数字加一。你可能会想,直接在全局变量里存一个count不就行了?确实,单进程单线程下可以,但你的服务器是多线程的(HTTPServer默认使用线程池),两个请求同时进来,同时读取count,同时加一,然后同时写回,就丢失了一次更新。这听起来像是教科书里才会出现的“竞态条件”,但你只要多刷新几次,就会遇到。
后端开发的一个重要教训是:不要假设你的服务是单用户、单线程的。 从零开始你就应该把共享状态交给数据库或专门的存储中间件去管。全局变量只能用来存一些不重要的常数配置。所以,当你觉得自己需要“记住某个用户上次做了什么”时,去用文件、SQLite,或者以后用Redis。
试着用SQLite做最简单的持久化。你不需要引入ORM,就通过sqlite3标准库,创建一张表,插入一条记录,查询出来。你会发现这个流程虽然啰嗦,但它让你理解了事务、连接、SQL语句这些概念。数据库不是复杂度的来源,它是复杂度的解决方案。 你越早习惯把状态交给数据库,就越能避免在业务代码里用各种诡异的方法去保存状态。
七、并发:你以为的快,其实是排队
你在浏览器里刷新自己的服务,感觉速度飞快。但如果你用两三个终端同时用curl去访问,或者写个脚本并发请求,你可能会看到响应时间变得不稳定。现代HTTPServer已经是多线程的,但你的CPU、内存、网络带宽都是共享资源。后端服务的核心艺术之一,就是管理并发下的资源分配。
但你不需要一上来就研究异步、协程、线程池。你只需要理解:每个请求到来时,服务器会创建一个新线程去处理它。如果处理函数的逻辑很快,比如只返回一句话,那并发是没问题的。但一旦你的处理函数里有一个time.sleep(1),模拟一个耗时操作,再同时发十个请求,有的请求就会排队等待,因为线程池是有上限的。
从零搭建第一个服务的意义,在于让你用身体去感受“慢”到底发生在哪里。 是网络传输慢?是CPU计算慢?是数据库查询慢?还是线程切换慢?框架帮你隐藏了所有这些细节,但隐藏不等于消失。当你以后用Spring Boot遇到一个接口响应缓慢时,如果你连“线程池中的线程数”和“阻塞”是什么都不知道,你会觉得一切都在黑箱里。
八、中间件与过滤器:用“切面思维”解放重复代码
你已经有了路由、参数解析、JSON响应,这算是一个能用的后端了。但你很快会发现一个烦心事:每个接口都要打印日志、设置CORS头、判断用户是否登录。你不想在每个处理函数里重复写这些代码,于是你想到了“装饰器”或者“包装函数”。
在Python里,你可以写一个装饰器来给函数加日志;在Go里,你可以用一个接受HandlerFunc的HandlerFunc;在Java里,这类东西叫Filter或Interceptor。所有现代框架里的中间件机制,本质上都是“洋葱模型”:请求在进入业务逻辑之前,先经过一层层外皮,然后再一层层离开。 你从零开始自己做一次,就能彻底理解Django的middleware、Express的app.use、Spring的HandlerInterceptor到底在搞什么。
试着写一个装饰器,记录每个请求的耗时:
import time def log_time(func): def wrapper(self, args, kwargs): start = time.time() result = func(self, args, kwargs) print(f'{self.command} {self.path} took {time.time()-start:.3f}s') return result return wrapper
把装饰器加到每个处理函数上,你会发现不用改业务逻辑,就完成了横切关注点的功能。这就是面向切面编程的价值:不侵入业务代码,就能添加系统级行为。 以后你在框架里配置一个鉴权中间件,理解起来就会像吃饭一样自然。
九、错误处理:让失败变得可见
你的服务现在运行良好,直到某个接口的代码抛出了异常。默认情况下,异常会导致服务器向客户端返回一个500状态码,并且打印一堆堆栈到终端。这对开发调试是好的,但对线上用户来说,看到“Internal Server Error”简直是在破坏信任。
你需要设计错误处理。至少做到三件事:第一,捕获所有未处理异常,返回一个JSON格式的错误信息,而不是让连接断开;第二,为常见的错误状态(404、405、400)提供统一的响应格式;第三,把错误日志记录下来,方便排查。
一个优雅的后端,不在于它不犯错,而在于它犯错时能让调用方快速知道发生了什么。 你可以在do_GET和do_POST的外层加一个try/except,返回500和错误消息。但你也可以更细粒度,在每个业务函数内部捕获特定异常。最好的错误处理是在“对的地方”捕获“对的异常”,既不过度捕获导致掩盖问题,也不遗漏导致系统崩溃。
从零开始,你会学会用traceback.format_exc()把堆栈打印出来,你会学会用logger.exception(e)来记录上下文,你会学会在响应中加入一个“error code”字段来区分“输入不合法”和“系统内部错误”。这些经验,都是框架不能直接教你的。
十、配置文件与部署:把服务交出去
终于,你的服务写好了。现在你打算把它部署到服务器上,让别人也能访问。你会发现自己的代码里有硬编码的8080端口,有硬编码的数据库路径,甚至可能还有硬编码的密码。这是一个必须改掉的习惯:任何环境相关的配置,都不该写在代码里。
你可以新建一个config.json或.env文件,然后在服务器启动时读取。用最简单的json或os.environ即可。你还需要区分开发环境和生产环境:开发时允许输出详细日志,生产时只输出警告;开发时开启自动重载,生产时用进程管理器守护。
部署是后端从“玩具”变成“产品”的试金石。 你可能会在部署时遇到端口被占用、防火墙拦截、进程死掉、内存不足等各种问题。但解决这些问题的过程,就是你理解操作系统、网络、进程管理的过程。试着用nohup或systemd把你的Python脚本变成一个常驻服务,让它开机自启、崩溃重启、日志回滚。这些琐事看起来不酷,但却是后端工程师价值的一部分。
十一、下一步:从手工到框架,从单机到分布式
你已经完成了一个不使用任何Web框架的、支持路由、参数解析、JSON、日志、错误处理、SQLite存储的后端服务。这个时候,你再去接触任何一个框架,都会觉得“啊,这个就是处理URL的那个东西”“这个就是帮我解析请求体的库”。框架不再是黑魔法,而是你熟悉的那套流程的优化版本。
你可以继续沿着两个方向深入。方向一是“广度”:把服务拆分成多进程、加消息队列、引入缓存、做负载均衡;方向二是“深度”:研究HTTP/2和gRPC、学习异步编程模型、分析数据库索引原理。无论哪个方向,你都已经有了自己的第一座灯塔。
回到最初的问题:为什么要从零搭建第一个服务? 因为只有亲手踩过那些笨拙的代码,你才能真正理解框架的价值;只有自己实现过一次路由表,你才能体会Spring Boot注解背后的工程权衡;只有被并发问题坑过,你才会敬畏每一行看似无关紧要的线程安全代码。后端技术栈的学习没有捷径,但你可以通过亲手搭建一个最小的“世界”来加速进化。
现在,关掉教程,去修改你自己的服务器的某个路径,让它返回你想要的任何东西。真正的学习,从你动手改变第一行代码开始。 你的第一个后端服务已经跑起来了,接下来,让它跑得更好、更稳、更优雅。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)