手写Socket Web服务器:根治TCP/HTTP原理恐惧症
2026/9/15 12:27:53 网站建设 项目流程

手写Socket Web服务器:不是闲着没事,是真能治"原理恐惧症"

我最早接触Socket编程,是照着博客敲了一个局域网聊天室,两个进程互相发消息,跑通那刻觉得自己挺牛。直到后来面试被问"HTTP协议底层到底怎么工作的,你写的那个聊天室改成Web服务器行不行",我才发现自己对天天在用的HTTP协议几乎一无所知。那些框架、服务器软件把底层藏得太好了,好到让你觉得请求会自己飞到后端去。那次之后我花了两个周末,用Socket手写了一个能处理GET和POST请求的Web服务器,才算是把TCP、Socket、HTTP之间的关系彻底捋顺了。

这篇文章就是那次实践的完整复盘,包含了网络编程中所有值得关注的底层细节,包括三次握手到底发生在哪一步、HTTP报文长什么样、服务器怎么解析请求、端口被占用和TIME_WAIT这些坑从哪来、单线程模型怎么一步步变成能用的并发服务器。适合想看透Web服务器原理的人、在面试前突击网络编程的人,以及那些被各种诡异网络报错折磨过的运维和开发。项目用Python实现,代码量不大,但每个函数背后都值得说道说道。

1. 为什么放着现成的Nginx不用,偏要手写一个

1.1 一次"被框架惯坏"之后的反思

当时我在排查一个线上接口超时问题,排查到最后发现是连接被反复建立和销毁,性能被握手开销拖垮了。同事随口问了一句:"你确定TCP连接真的被复用了?"我盯着配置里的Keep-Alive,却答不上来。因为我从没在自己的代码里观察过一个连接从建立到关闭的全过程,我知道的都是别人告诉我的结论。

手写Web服务器的第一个价值,就是逼你把"我以为"变成"我看到"。当你自己控制Socket的生命周期时,你自然会看到一条TCP连接什么时候建立、什么时候被复用、什么时候进入TIME_WAIT。这些东西用框架时是隐形的,手写时全是可见的。

1.2 手写Web服务器能换来什么

说实在的,手写这个服务器,不是要造一个Nginx的替代品。Nginx处理高并发的能力是靠事件驱动、多进程、epoll这一整套机制撑起来的,Python里写个玩具服务器在性能上完全没法比。但写一遍之后,你得到的不是一个能上生产的软件,而是几个原本根本接触不到的理解:

  • 理解了Socket、TCP、HTTP三者之间的层次关系。Socket是传输层的编程接口,TCP是传输层协议,HTTP是应用层协议。一个HTTP请求最终要靠TCP连接承载,TCP连接则要依赖Socket这套API来创建和管理。
  • 理解了端口、IP、进程之间的关系。端口不是随便绑的,两个进程绑同一个端口会报错,这个报错背后有一条完整的排查链路。
  • 理解了HTTP报文为什么是那个格式。"空行分隔头和体""Content-Length必须准确"这些规定不是为了折磨人,而是因为TCP是字节流,没有"一条消息"的概念,协议方必须自己定义消息边界。
  • 理解了并发模型的基本矛盾。单线程串行处理请求和阻塞式IO之间的矛盾,是所有服务器性能问题的根源,理解了这一点,再看Nginx、Redis的模型就会有豁然开朗的感觉。

所以这篇文章更适合把"搞懂原理"当成目标的人。如果你只是想快速搭一个接口服务,那直接用框架就好,没必要自己造轮子。但如果你已经写了几年业务代码,始终觉得网络协议这块有一层窗户纸没捅破,手写一个服务器就是那根手指头。

2. 先搞懂Socket:HTTP要走路的那条"路"是怎么铺出来的

2.1 Socket到底是什么

很多初学者把Socket理解成一个API函数集合,这没错,但太片面了。更准确地说,Socket是一套由操作系统提供的网络编程接口,它把TCP/IP协议栈的复杂性封装成了几个系统调用。你可以把Socket想象成一根水管两端的接头:内核负责把数据从一端搬运到另一端,你的代码只需要负责拿着两端接头往里面倒水、接水。

其中一端是客户端Socket,另一端是服务器端Socket。客户端负责发起连接,服务器端负责被动等待连接到来。两端都基于IP和端口来标识自己。IP地址知道你通向哪台机器,端口号知道你到了那台机器后要找哪个进程。所以当一个进程绑定了某个端口后,另一个进程再想绑同一个端口,操作系统就会报"只有一个Socket地址能使用一次"这类错误,这个后面会详细讲。

2.2 服务器端的Socket生命周期:bind、listen、accept都在忙什么

服务器端Socket的生命周期有五个阶段,每个阶段对应一个系统调用,我把它列成了一张表:

阶段系统调用作用关键点
创建socket()在内核中创建一个Socket对象需要指定地址族(AF_INET)和类型(SOCK_STREAM)
绑定bind()把Socket绑定到指定的IP和端口端口冲突就是在这里报出来的
监听listen()把主动Socket变为被动监听Socket内核在这里开始维护连接队列
接受accept()从连接队列中取出一个已完成握手的连接三次握手在此之前已经完成
收发recv()/send()读写数据默认是阻塞的,没数据时会一直等
关闭close()释放连接和资源主动关闭方会进入TIME_WAIT状态

这里最容易被误解的是listen()accept()的分工。很多人以为三次握手发生在accept()被调用的那一刻,这是错的。listen()之后,操作系统内核就开始在后台帮你监听端口了。当客户端发起SYN时,内核会自动完成三次握手,然后把这个已经就绪的连接放在一个全连接队列里。accept()只是从队列里"取出"一个已经准备好的连接。所以即使你还没调用accept(),客户端那边可能已经认为连接建立了。这个细节解释了为什么某些场景下你能在客户端看到连接建立成功,服务器端却还没有任何读写动作。

2.3 从浏览器到服务器:一个请求在路上经历了什么

假设你在浏览器地址栏输入http://127.0.0.1:8080/并回车,这条路径上发生的事情远比"发了一个请求"复杂:

  1. 浏览器解析URL,得到主机名127.0.0.1和端口8080以及路径/
  2. 浏览器调用connect()创建一个客户端Socket,向127.0.0.1:8080发起TCP握手请求。
  3. 服务器端的内核在listen()之后就在监听这个端口了,收到SYN包后完成三次握手,并把连接放入全连接队列。
  4. 服务器进程调用accept()取出这个连接,得到一个用于通信的客户端Socket描述符。
  5. 浏览器按HTTP协议格式,通过Socket发送一个报文,内容是"GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\n...".
  6. 服务器用recv()读到这些字节,按照HTTP协议去解析,拿到"方法GET、路径/、版本HTTP/1.1"。
  7. 服务器根据路由和处理函数生成响应报文,用send()发回去。
  8. 浏览器收到响应报文,解析HTTP头部和HTML正文,渲染页面。

整个过程你会发现,HTTP协议其实完全不知道TCP握手的存在,它只负责把数据按照约定格式塞给Socket层;TCP协议也完全不知道HTTP报文的内容是什么,它只保证字节流按顺序到达。这种分层设计的好处就是各司其职,HTTP改版不用动TCP协议,底层换协议也不会影响HTTP的语义。

3. 把HTTP请求报文摊开来看

3.1 请求行、请求头、请求体:三段式报文结构

一个标准的HTTP请求报文长这样,我用一个实际的报文来展示:

GET /index.html?page=2 HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Accept: text/html,application/json Connection: keep-alive

我把这个报文拆开逐行解释。第一行叫请求行,由三部分组成:请求方法(GET)、请求URI(/index.html?page=2)、协议版本(HTTP/1.1),中间用空格分隔。从第二行开始到空行之前,都是请求头,每行一个头字段,格式是"字段名: 值"。请求头和请求体之间由一个空行分隔,这个空行是关键的消息边界标识。空行之后如果有内容,那就是请求体,比如POST请求携带的表单数据或JSON字符串。

这个三段式结构是HTTP协议的骨架,解析HTTP报文本质上就是按这个格式切分字节流。你要注意,头和体用空行分隔,但请求体本身没有统一的结束标识,它靠请求头里的Content-Length字段来声明自己有多长。

3.2 请求头里藏着哪些关键信息

请求头里有些字段对服务器逻辑非常关键:

  • Content-Length:请求体的字节长度。服务器读取请求体时必须循环读到这个长度,才能确定请求体已经读完了。如果这个值算错,服务器会多读或少读,连接就会错乱。
  • Connection:告诉服务器客户端希望保持连接还是请求完毕后关闭,keep-alive表示复用连接,close表示用完之后关闭。HTTP/1.1默认是keep-alive,HTTP/1.0默认是close
  • Host:在HTTP/1.1中,Host是必填项。因为一台服务器可能托管多个域名,虚拟主机靠Host字段区分请求该路由到哪个站点。

很多人在解析HTTP报文的时候会犯一个低级错误:用recv()读一次数据就当读完了。这不对。TCP是字节流协议,一次recv()拿到的不一定是一个完整的HTTP请求,可能是一部分,也可能是粘着了好几个请求。正确的做法是循环读取,直到读取到的字节满足某个结束条件——比如头部区域出现了\r\n\r\n,或者Content-Length指示的字节数已经读够了。

3.3 响应报文的格式和状态码语义

服务器返回的响应报文,结构和请求报文高度对称,也分三段:

HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 61 Connection: keep-alive <html><body><h1>Hello from my socket server</h1></body></html>

第一行是状态行,由协议版本、状态码、状态描述组成。状态码是服务器用来告诉客户端"这次的请求到底怎么样了"的标准化语言,2xx表示成功,3xx表示重定向,4xx表示客户端问题,5xx表示服务器问题。我在实现迷你服务器时至少要处理三个:200 OK404 Not Found405 Method Not Allowed。别看状态码只是个三位数字,HTTP请求能不能被正确处理,很大程度上靠它对齐双方的认知。

响应头和请求头格式一样,常用的有Content-Type告诉客户端响应体的类型,Content-Length告诉客户端响应体有多长。注意,响应体里如果包含中文,Content-Type里的charset=utf-8一定要带上,否则浏览器可能按默认编码解析,出现乱码。

我顺手整理了一个GET和POST的对比表,细节都在里面:

维度GETPOST
语义获取资源提交数据并触发处理
请求参数位置请求行的URI里,如?page=2请求体里
请求体通常为空可能有,用Content-Length声明长度
幂等性幂等,重复执行结果一致不保证幂等,重复提交可能重复生效
缓存浏览器可能缓存通常不缓存
请求报文示例GET /search?q=socket HTTP/1.1POST /login HTTP/1.1\r\nContent-Length: 21\r\n\r\nusername=admin&pass=123

4. 动手写一个能跑起来的迷你Web服务器

4.1 先搭最核心的骨架:接收请求并返回固定响应

我选Python来实现,因为它语法直白,方便把注意力放在网络原理上。环境就是Python 3,不需要任何第三方库,从标准库里的socket开始。

一个最基础的服务器骨架只需要这些代码:

import socket 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'服务器已启动: http://{HOST}:{PORT}') while True: client_socket, client_addr = server_socket.accept() print(f'收到来自 {client_addr} 的连接') request = b'' while True: chunk = client_socket.recv(4096) if not chunk: break request += chunk if b'\r\n\r\n' in request: break print('收到请求:') print(request.decode('utf-8', errors='ignore')) response_body = '<html><body><h1>Hello from my socket server</h1></body></html>' response_header = 'HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n'.format(len(response_body.encode('utf-8'))) client_socket.send(response_header.encode('utf-8') + response_body.encode('utf-8')) client_socket.close()

这段代码里有两个细节值得说一下。第一个是setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),这行代码解决的是端口复用问题,我后文专门讲。第二个是接收请求时的循环,我判断头结束的条件是字节流里出现\r\n\r\n\r\n是HTTP协议里规定的换行符,网络报文里不能使用平台相关的\n\r,必须严格按照\r\n来,否则解析会出错。

4.2 解析请求行与请求头

上面这个骨架可以运行了,但只会返回固定内容。要成为一个"服务器",必须能看懂请求。第一步是解析请求行和请求头。我写了一个简单的解析函数:

import socket import urllib.parse def parse_request(request_bytes): # 先把字节流转成字符串,按行切分 text = request_bytes.decode('utf-8', errors='ignore') lines = text.split('\r\n') # 解析请求行 request_line = lines[0] method, path_with_query, version = request_line.split(' ') # 分离路径和查询参数 if '?' in path_with_query: path, query_string = path_with_query.split('?', 1) else: path = path_with_query query_string = '' # 解析查询参数 query_params = {} if query_string: for pair in query_string.split('&'): if '=' in pair: key, value = pair.split('=', 1) query_params[key] = urllib.parse.unquote(value) # 解析请求头 headers = {} for line in lines[1:]: if ':' in line: key, value = line.split(':', 1) headers[key.strip().lower()] = value.strip() return { 'method': method, 'path': path, 'version': version, 'query_params': query_params, 'headers': headers, 'body': ... }

我特意把查询参数单独解析出来,因为做路由的时候经常会用到。比如GET /search?q=socket HTTP/1.1,解析完成后path/searchquery_params{'q': 'socket'},这样路由判断就很方便了。

注意headers里我把字段名处理成了全小写。HTTP头字段名是不区分大小写的,Hosthost是同一个意思。统一转小写可以避免路由和处理逻辑里做大小写判断的麻烦。

4.3 支持路由分发和POST请求与静态文件

有了解析函数,接下来做路由分发。我用一个简单的字典来存路由表:

routes = { '/': 'index_handler', '/hello': 'hello_handler', '/now': 'time_handler', }

每个handler是一个函数,接收请求字典,返回响应状态码、响应头、响应体。路由分发就是根据请求里的path查找对应的handler,找不到就返回404。这样写的好处是扩展新页面很方便,加一个路径对应一个函数就行。

POST请求的解析核心在于请求体。之前说过,请求体的结束边界靠Content-Length来决定。所以在接收请求时,我不能只判断\r\n\r\n就完事,还要检查Content-Length

def read_http_request(client_socket): buffer = b'' while b'\r\n\r\n' not in buffer: chunk = client_socket.recv(4096) if not chunk: break buffer += chunk # 解析头区域,获取 Content-Length head, _, body_start = buffer.partition(b'\r\n\r\n') headers_text = head.decode('utf-8', errors='ignore') content_length = 0 for line in headers_text.split('\r\n'): if line.lower().startswith('content-length:'): content_length = int(line.split(':', 1)[1].strip()) # 继续读,直到请求体长度够 while len(buffer) - len(head) - 4 < content_length: chunk = client_socket.recv(4096) if not chunk: break buffer += chunk return buffer

这一段是HTTP解析里最容易出错的地方。很多初学者在上一个循环里读到\r\n\r\n就停了,没有继续读请求体,导致POST数据丢失。当Content-Typeapplication/x-www-form-urlencoded时,请求体的内容形如username=admin&pass=123,我用和查询参数一样的逻辑去解析它就行。

处理静态文件时,最常见的一个问题就是路径遍历攻击。比如用户请求/../../etc/passwd时,如果你直接把路径拼到本地文件路径上,就会把系统文件读出来发给用户。我在实现时做了防护,只允许访问指定目录下的文件,并且用os.path.realpath把路径里的..解析后再次校验前缀。

import os WEB_ROOT = './static' def serve_static_file(path): # 拼接路径 file_path = os.path.realpath(os.path.join(WEB_ROOT, path.lstrip('/'))) # 校验最终路径必须在 WEB_ROOT 内 if not file_path.startswith(os.path.realpath(WEB_ROOT)): return 403, 'Forbidden', 'text/plain' if os.path.isfile(file_path): with open(file_path, 'rb') as f: content = f.read() return 200, content, guess_type(file_path) return 404, 'Not Found', 'text/plain'

Web服务器安全这件事表面上很高深,但最基本的一道防线就是这种路径校验。生产级的服务器实现里还会处理符号链接、权限检查、特殊字符编码等等,但"路径规范化后再做前缀校验"是无论如何都不能少的一步。

5. 实测过程中的血泪坑:bind报错、TIME_WAIT与端口占用

5.1 "bind: only one usage of each socket address"是怎么来的

我运行这个服务器时遇到的最经典的报错,在标题相关的热搜词里也出现了,就是这句:

OSError: [Errno 98] Address already in use

或者在某些语言里你会看到:

bind: only one usage of each socket address (protocol/network address/port)

这个报错的意思是:你要绑定的IP和端口组合已经被另一个进程占用了。为什么会出现?最常见的原因是上次运行的服务器进程没退出,还占着这个端口。排查方法很简单:

# 查看端口被哪个进程占用 lsof -i :8080 # 或者用 ss ss -lntp | grep 8080

看到进程号后,可以用kill命令结束它,再重新启动服务器。但我后来还遇到一种更隐蔽的情况:进程明明退出了,端口还是报占用。这就牵扯出了TIME_WAIT

5.2 TIME_WAIT为什么存在,又为什么令人头疼

我前面在Socket生命周期表里写过:主动关闭连接的一方会进入TIME_WAIT状态。TCP连接关闭时要经历四次挥手,谁先发起close(),谁就要在最后进入TIME_WAIT状态,并等待2MSL时间后才完全释放连接资源。MSL是报文最大生存时间,典型值是30秒到2分钟,所以一个连接可能要等上1到4分钟才会从端口表里消失。

为什么要设计这个状态?核心是为了防止"旧连接的迟到报文"影响到新连接。比如你关闭了一个连接,但网络中还有一个延迟到达的数据包,恰好此时另一个连接用了同样的IP和端口,旧包就可能被新连接误收。TIME_WAIT让主动关闭方等着,确保旧包在网络中彻底消失后,端口才可以复用。

那处理这个问题的标准姿势是什么?就是我在骨架代码里写的那一行:

server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

SO_REUSEADDR允许你在端口还有TIME_WAIT连接残留时重新绑定同一个端口,避免服务器重启时的尴尬。但要注意,它不等于允许两个进程同时监听同一个端口,那是SO_REUSEPORT的职责。两者区别我放在表格里:

选项作用典型场景
SO_REUSEADDR允许端口在TIME_WAIT状态下被重新绑定服务器重启、关闭后立即重启
SO_REUSEPORT允许多个进程/线程绑定同一个端口,由内核做负载均衡多进程服务器、nginx的某些工作模式

这里特别提醒一句:开发阶段在绑端口前加上SO_REUSEADDR,能省掉很多"重启报错"的烦恼。但如果你的程序处理的是高安全场景,这个选项也不会带来安全问题,它只是让地址重用更友好而已。

5.3 用telnet和curl做最原始的调试

手写服务器时,浏览器不是最好的调试工具,因为浏览器会自动加很多头、会先请求/favicon.ico、还会缓存。我最喜欢的两个调试工具是curltelnet

curl -v展示真正的请求和响应细节,它能让你看见自己写的服务器到底返回了什么:

curl -v http://127.0.0.1:8080/hello

输出里会显示请求行和所有请求头,以及服务器返回的状态行、响应头、响应体。如果响应体比Content-Length声明得短,curl会报错提示你响应不完整,这个反馈非常有用。

telnet更"原始",你可以手工敲入HTTP报文,完全掌控发送的内容。排查服务器解析逻辑时用这个办法最直观:

telnet 127.0.0.1 8080

连接建立后,手动输入下面的内容,注意最后的空行一定要敲上:

GET /hello HTTP/1.1 Host: 127.0.0.1:8080

然后看服务器返回什么。如果没反应,先检查是不是空行没敲,或者Content-Length之类字段不符合要求。这样的调试方式特别适合从零开始理解协议:你亲手把一个报文从键盘上敲出去,看着服务器一行行解析它,所有抽象的概念都会变得非常具体。

6. 从单线程玩具进阶到真正能用的并发服务器

6.1 单线程模型的致命短板

我刚写的那个服务器主循环是单线程的,它接受了第一个请求、处理完、返回响应、关闭连接,然后才去accept()下一个连接。这意味着同一时间只能服务一个客户端。第二个客户端发起连接后,即使TCP三次握手完成了,连接也躺在内核队列里,要等第一个请求处理完才会被accept()

这个模型有两个问题。第一是慢请求会阻塞后面的所有人,比如请求里带了一个超大的文件上传,处理过程可能持续好几秒,期间所有其他请求全部排队。第二是连接本身可能因为等待超时被客户端掐掉。

解决思路是引入并发:一个请求来了,开启一个新线程去处理,主线程继续accept()下一个连接。Python里用threading.Thread就能实现:

import threading while True: client_socket, client_addr = server_socket.accept() t = threading.Thread(target=handle_client, args=(client_socket, client_addr)) t.start()

每个连接一个线程,模型简单,立刻就能解决"一个慢请求卡住所有人"的问题。但要注意两个细节:一是Python的GIL限制,多线程并不能利用多核并行执行CPU密集任务;二是线程数量不可能无限增加,当并发连接数很高时,线程切换和内存占用会成为新的瓶颈。所以生产级服务器不会用这种朴素的线程模型,而是用事件驱动加IO多路复用。

6.2 支持Keep-Alive连接复用

前面我只实现了Connection: close的逻辑,每次请求结束就关闭连接。但HTTP/1.1默认要求支持keep-alive——客户端希望用同一个TCP连接连续发送多个HTTP请求,省去反复握手的开销。

这看起来是个小改动,但实现起来有个非常关键的难点:在一个连接上,你要知道一个请求什么时候结束,下一个请求什么时候开始。好消息是请求头里的Content-Length就是分界依据。你只需要循环解析一个又一个请求,直到客户端关闭连接:

def handle_client(client_socket, client_addr): try: while True: # 读取完整请求 request_data = read_http_request(client_socket) if not request_data: break # 解析请求,生成响应 request = parse_request(request_data) status, body, content_type = route(request) # 发送响应 response = build_response(status, body, content_type, keep_alive=True) client_socket.send(response) # 如果客户端不打算复用了,跳出循环 if request['headers'].get('connection', '').lower() == 'close': break except Exception as e: print(f'连接处理出错: {e}') finally: client_socket.close()

别小看这个循环结构,它的正确性完全依赖于你前面解析请求时是否严格按照Content-Length读取了请求体。如果少读了,下一次循环就会把一个请求的后半截当成新请求的开头,报文解析就彻底乱了。所以我在实现时特意写了一个能精确计算每个请求完整长度的读取函数,这是Keep-Alive能不能稳定工作的前提。

6.3 和Nginx这类成熟服务器的差距在哪里

写完这个版本后,我拿它和Nginx对比了一下,很容易看到差距:

维度我的手写版Nginx
并发模型每连接一线程多进程加事件驱动(epoll)
静态文件发送Python读文件后拷贝给Socketsendfile系统调用,零拷贝
超时处理基本没有完整的读超时、写超时管理
请求体限制无限制,可能吃满内存可配置的client_max_body_size
HTTP协议支持最基础的GET/POST,Keep-Alive完整支持HTTP/1.1、HTTP/2、WebSocket等

这些差距本质上是处理能力和工程完备性的差距。Nginx的性能核心是epoll事件驱动模型,它用一个进程就能同时管理成千上万个连接,而不是给每个连接开一个线程。sendfile让静态文件直接在内核态从磁盘发送到网卡,不经过用户态内存拷贝,速度比我read到Python再用send发送快得多。

但这些差距不影响手写这个服务器的价值。理解单线程的瓶颈,你才知道多线程解决的是什么问题;理解了线程模型的资源消耗,你才知道事件驱动为什么高效;理解了recv()的阻塞特性,你才知道IO多路复用为什么是必要的。Nginx的文档不会把这些故事讲给你听,但你自己写一遍,全都能体会到。

我实验下来最推荐的后续扩展方向是:先给服务器加上线程池,限制最大并发线程数,然后给静态文件处理加上基于sendfile的实现,最后尝试引入selectepoll来管理多个连接。每走一步,你对服务器性能瓶颈的理解就会深一层。

最后唠叨一句我的个人体会。写完这个迷你服务器之后,再去看真实项目里的网络报错,思路完全不一样了。之前遇到"Address already in use",我只知道重启大法;现在会先去lsof看端口占用、检查程序的退出逻辑、考虑TIME_WAIT的残留。之前不理解为什么接口偶发超时,现在会去看连接是否被正确复用。网络编程的知识不一定要靠大项目来积累,亲手写一个能跑的Web服务器就够了。我建议你也找时间把自己常用的框架放下,从bind开始敲一遍,那个过程会有点绕,但绕完之后,很多费解的术语就都变成常识了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询