嘿,朋友!今天咱们不聊那些虚头巴脑的理论,直接动手。我见过太多人学网络编程,一上来就丢一句“HTTP是无状态的”、“请求头里带什么什么”,结果脑子一片空白。今天,咱们从最底层的socket开始,一步一步搭出一个能工作的HTTP服务器,顺便把客户端怎么抓数据、那些让人头疼的状态码到底啥意思,全给你捋顺了。你只管跟着敲,我保证你看完能自己写出一个像样的网络程序。
先别急着写代码,咱们先搞清楚HTTP到底是啥
想象一下,你(客户端)走进一家餐厅,对着服务员(服务器)说:“我要一份红烧肉,不要辣。” 服务员记下你的要求,转身进厨房,过一会儿端着一盘菜回来:“您的红烧肉,请慢用。”
这个过程,就是HTTP协议在干活。
- 你点的菜 = 请求(Request)
- 服务员记下的内容和传回的话 = 响应(Response)
- 餐厅的规则 = HTTP协议
HTTP全称HyperText Transfer Protocol,是互联网上应用最广泛的协议。它工作在一个简单的“请求-响应”模型上:客户端发一个请求,服务器回一个响应。没有中间状态,没有长期连接(默认情况下),用完即走。
关键来了:HTTP是无状态的。什么意思?服务器不记得你上一秒点了啥。每次你发起请求,都是一次全新的对话。所以如果你想“保持登录状态”,就得靠Cookie或者Session这些外加机制——但这不是今天我们重点。
现在,咱们用Python来复现这个过程。先从最底层:Socket。
用Python的socket建一个最简单的HTTP服务器
别被“Socket”这个词吓到。你把它想象成一条直通服务器内部的黑管。你从一头塞东西进去,另一头就会吐出来。Python的socket模块就是让你直接操控这条黑管。
咱们先写一个服务器,它能接收连接,然后原样把收到的东西打印出来。这样你就能看清客户端到底发了什么。
import socket
def run_http_server(host='127.0.0.1', port=8080):
# 创建一个TCP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 允许端口复用,避免重启时报错
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址和端口
server_socket.bind((host, port))
# 开始监听,最多同时排队5个连接
server_socket.listen(5)
print(f"🎉 服务器启动!正在监听 {host}:{port}")
print("打开浏览器访问 http://127.0.0.1:8080 试试看")
while True:
# 阻塞等待客户端连接
client_socket, addr = server_socket.accept()
print(f"✅ 来自 {addr} 的连接已建立")
try:
# 接收客户端发来的数据,这里我们最多收1024字节
request_data = client_socket.recv(1024).decode('utf-8')
print("\n" + "="*50)
print("📥 收到的原始请求:")
print(request_data)
print("="*50 + "\n")
# 解析请求行(第一行)
lines = request_data.split('\r\n')
if lines:
request_line = lines[0]
print(f"🔍 请求行:{request_line}")
# 构造一个最简单的HTTP响应
# HTTP/1.1 200 OK 是状态行
# Content-Type: text/plain 告诉浏览器这是纯文本
# Content-Length 告诉浏览器响应体有多长
response_body = "Hello from my DIY HTTP Server! 🐍"
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
f"Content-Length: {len(response_body)}\r\n"
"Connection: close\r\n"
"\r\n" # 空行分隔头部和体部
f"{response_body}"
)
# 把响应发回去
client_socket.sendall(response.encode('utf-8'))
print("📤 响应已发送")
except Exception as e:
print(f"❌ 出错了:{e}")
finally:
# 关闭连接
client_socket.close()
print(f"🔌 与 {addr} 的连接已关闭")
if __name__ == '__main__':
run_http_server()
好,代码写完了。你运行它,然后打开浏览器,访问 http://127.0.0.1:8080。
你会看到终端打印出一大段东西。别慌,咱们来拆解一下你浏览器发了什么:
GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
Accept: text/html,application/xhtml+xml,...
Accept-Language: zh-CN,zh;q=0.9,...
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
这就是一个典型的HTTP请求。第一行是请求行:
GET—— 方法,告诉服务器“我要获取资源”/—— 路径,我要访问根目录HTTP/1.1—— 协议版本
后面那些都是请求头,每一行都是“键: 值”的格式。它们告诉服务器:“我是Chrome浏览器”、“我支持中文”、“我喜欢gzip压缩”等等。
服务器收到后,按照我们写的代码,回了一个200 OK,里面包含一句“Hello from my DIY HTTP Server!”。浏览器把它显示出来,你就看到这句英文了。
是不是很简单?这就是HTTP的全部真相:你发一段文本,服务器回一段文本。其他那些花里胡哨的框架、库,底层都在这套规则上工作。
解析请求:咱们得听懂客户端到底要啥
刚才那个例子,服务器只回了个固定的“Hello”。但真实世界里,客户端会访问不同的路径:有的要 /index.html,有的要 /api/user,有的要 /image/logo.png。服务器得知道怎么走。
咱们来升级一下服务器,让它能解析请求,并返回不同的内容。
import socket
import os
def parse_request(request_data):
"""解析HTTP请求,返回方法、路径、headers"""
lines = request_data.split('\r\n')
if not lines:
return None, None, {}
request_line = lines[0]
parts = request_line.split(' ')
if len(parts) < 2:
return None, None, {}
method = parts[0] # GET, POST, etc.
path = parts[1] # /index.html, /api/data
# protocol = parts[2] # HTTP/1.1
headers = {}
for line in lines[1:]:
if line == '': # 遇到空行,说明header结束了
break
if ':' in line:
key, value = line.split(':', 1)
headers[key.strip()] = value.strip()
return method, path, headers
def handle_get_request(path):
"""根据路径返回不同的内容"""
# 模拟一个简单的路由
if path == '/' or path == '/index.html':
body = "<html><body><h1>欢迎来到首页!</h1><p>这是你DIY的HTTP服务器。</p></body></html>"
content_type = "text/html; charset=utf-8"
elif path == '/api/user':
body = '{"name": "张三", "age": 25, "city": "北京"}'
content_type = "application/json; charset=utf-8"
elif path == '/not-found':
body = "<html><body><h1>404 - 页面不存在</h1></body></html>"
content_type = "text/html; charset=utf-8"
return 404, content_type, body
return 200, content_type, body
def run_http_server(host='127.0.0.1', port=8080):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((host, port))
server_socket.listen(5)
print(f"🎉 服务器启动!正在监听 {host}:{port}")
while True:
client_socket, addr = server_socket.accept()
print(f"✅ 来自 {addr} 的连接已建立")
try:
request_data = client_socket.recv(4096).decode('utf-8')
method, path, headers = parse_request(request_data)
if method is None:
# 请求解析失败,返回400
status = 400
content_type = "text/html"
body = "<html><body><h1>400 Bad Request</h1></body></html>"
else:
print(f"📥 收到 {method} {path} 请求")
status, content_type, body = handle_get_request(path)
# 构造响应
response = (
f"HTTP/1.1 {status} {'OK' if status == 200 else 'Error'}\r\n"
f"Content-Type: {content_type}\r\n"
f"Content-Length: {len(body.encode('utf-8'))}\r\n"
"Connection: close\r\n"
"\r\n"
body
)
client_socket.sendall(response.encode('utf-8'))
print(f"📤 返回 {status} 状态码")
except Exception as e:
print(f"❌ 出错了:{e}")
finally:
client_socket.close()
if __name__ == '__main__':
run_http_server()
现在你再打开浏览器,分别访问:
http://127.0.0.1:8080/—— 你会看到“欢迎来到首页!”http://127.0.0.1:8080/api/user—— 你会看到JSON数据http://127.0.0.1:8080/not-found—— 你会看到404错误页
你发现没?服务器完全“听懂”了你的请求,并且根据路径给了不同的回应。这就是HTTP服务器最基本的逻辑。
状态码:服务器给你的“成绩单”
刚才代码里出现了 200、400、404。这些数字叫状态码,是服务器对客户端请求的“评分”。
你可以把状态码分成五类,每一类都有它的脾气:
1xx:信息性状态码(服务器在告诉你“我正在处理”)
100 Continue:继续发送剩余内容101 Switching Protocols:切换协议(比如WebSocket升级)
这类很少见,日常开发基本碰不到。
2xx:成功(请求办成了)
200 OK:最经典的成功!你发的GET、POST、PUT、DELETE都返回这个,表示“这事儿成了”201 Created:创建成功,常用于POST请求创建资源后204 No Content:成功,但没有返回内容(比如删除操作成功,但不用告诉你删了啥)
3xx:重定向(服务器说“别去那儿,去别处”)
301 Moved Permanently:永久重定向,URL已经变了,以后都去新的302 Found:临时重定向,下次可能还会回去304 Not Modified:缓存命中,浏览器说“我本地有这个文件”,服务器回“那你直接用本地的吧,不用重新下”
4xx:客户端错误(你发错请求了)
400 Bad Request:请求语法错误,服务器看不懂401 Unauthorized:没登录或登录过期403 Forbidden:知道你是谁,但不让你访问404 Not Found:路径不存在,最常见的错误405 Method Not Allowed:方法不对,比如这个接口只支持GET,你却发了POST
5xx:服务器错误(服务器自己出问题了)
500 Internal Server Error:服务器内部炸了,可能是代码bug502 Bad Gateway:网关错误,你访问的服务器本身是代理,但它背后的服务器挂了503 Service Unavailable:服务暂时不可用,可能是维护或者过载
记住:状态码第一数字决定类别,后面两位是具体原因。比如 404,4 表示客户端错误,04 表示路径不存在。
在刚才的例子里,handle_get_request 函数根据路径返回不同的状态码。实际项目中,你会把这部分逻辑做得更复杂,比如从数据库查数据,查不到就返回404,查到了返回200加数据。
客户端怎么抓网页?用urllib和requests
刚才咱们是服务器,现在换角色,当客户端。客户端用Python向服务器发请求,拿回数据,这就是“抓取网页数据”。
Python有两个常用库:urllib(标准库,不用装)和 requests(第三方库,要装)。咱们两个都讲。
用urllib抓取网页
import urllib.request
import urllib.error
def fetch_with_urllib(url):
try:
# 创建请求对象
req = urllib.request.Request(url)
# 可以添加请求头,模拟浏览器
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
# 发起请求
with urllib.request.urlopen(req, timeout=10) as response:
# 获取状态码
status = response.getcode()
print(f"📊 状态码:{status}")
# 获取响应头
headers = response.info()
print(f"📋 Content-Type:{headers.get('Content-Type')}")
# 读取响应体
data = response.read().decode('utf-8')
print(f"📝 响应内容长度:{len(data)} 字节")
print("📄 前200字符:")
print(data[:200])
return status, data
except urllib.error.HTTPError as e:
print(f"❌ HTTP错误:{e.code} - {e.reason}")
return e.code, None
except urllib.error.URLError as e:
print(f"❌ 网络错误:{e.reason}")
return None, None
# 测试
if __name__ == '__main__':
url = 'http://127.0.0.1:8080/api/user'
status, content = fetch_with_urllib(url)
运行后,你会看到:
📊 状态码:200
📋 Content-Type:application/json; charset=utf-8
📝 响应内容长度:40 字节
📄 前200字符:
{"name": "张三", "age": 25, "city": "北京"}
注意几点:
urlopen会自动处理重定向(3xx),你不用管- 如果状态码是4xx或5xx,会抛出
HTTPError,要单独捕获 timeout参数很重要,防止服务器卡住你的程序- 手动加
User-Agent头,有些服务器会拒绝没有头部的请求(比如反爬)
用requests库抓取网页(更简单)
import requests
def fetch_with_requests(url):
try:
# 发送GET请求
response = requests.get(url, timeout=10)
# 状态码
print(f"📊 状态码:{response.status_code}")
# 响应头
print(f"📋 Content-Type:{response.headers.get('Content-Type')}")
# 响应体(自动解码)
content = response.text
print(f"📝 响应内容长度:{len(content)} 字节")
print("📄 内容:")
print(content)
# 如果是JSON,可以直接解析
if 'application/json' in response.headers.get('Content-Type', ''):
data = response.json()
print(f"🔍 解析后的JSON:{data}")
return response
except requests.exceptions.HTTPError as e:
print(f"❌ HTTP错误:{e}")
except requests.exceptions.ConnectionError as e:
print(f"❌ 连接错误:{e}")
except requests.exceptions.Timeout as e:
print(f"❌ 超时:{e}")
except requests.exceptions.RequestException as e:
print(f"❌ 其他错误:{e}")
return None
# 测试
if __name__ == '__main__':
url = 'http://127.0.0.1:8080/api/user'
response = fetch_with_requests(url)
requests 比 urllib 简单太多了:
- 不用手动解码,
response.text自动用UTF-8解码 - 不用手动处理JSON,
response.json()一步搞定 - 异常处理更
