Socket的recv与readline在Windows和Linux上表现差异大,原因何在?
问题
我在修复一段Socket代码,它负责从服务器接收jsonlines格式(以\r\n分隔的JSON对象)的数据。原代码仅检查每次.recv调用返回的字节是否以\r\n结尾,无法处理单次返回多条消息的情况。我将原代码替换为使用socket.makefile()和.readline()的实现,修复了该问题,但出现了奇怪的性能差异:Windows系统上新代码速度大幅提升,Linux系统上两者性能差异极小,几乎在误差范围内可忽略。我怀疑这与基准测试有关,但不确定遗漏了什么?为何完全相同的代码在两个系统上表现差异如此之大?
基准测试代码
import datetime import json import socket import threading import time import timeit from typing import Optional BUFFER_SIZE = 1024 N_BATCHES_OF_DATA = 100 PORT = 5678 def server(n_batches_of_data, port): data = [ json.dumps({"a": "b"}).encode('utf-8') + b'\r\n', json.dumps({"c": "d", "e": "f"}).encode('utf-8') + b'\r\n', json.dumps({"g" * (512-5): "h" * (512-5)}).encode('utf-8') + b'\r\n', # 1022 bytes, with linefeed == 1024, exact size of buffer json.dumps({"i" * (512-4): "j" * (512-4)}).encode('utf-8') + b'\r\n', # 1024 bytes, with linefeed == 1026 - exceeds buffer, just json.dumps({"k" * (256-5): "l" * (256-5)}).encode('utf-8') + b'\r\n', # 510 bytes, with linefeed == 512, half of buffer json.dumps({"m" * (256-5): "n" * (256-5)}).encode('utf-8') + b'\r\n', # 510 bytes, with linefeed == 512, half of buffer json.dumps({"o" * (1024-5): "p" * (1024-5)}).encode('utf-8') + b'\r\n', # 2046 bytes, with linefeed == 2048, 2x size of buffer json.dumps({"q" * 100: "r" * 100})[0:25].encode('utf-8'), # part 1 json.dumps({"q" * 100: "r" * 100})[25:50].encode('utf-8'), # part 2 json.dumps({"q" * 100: "r" * 100})[50:75].encode('utf-8'), # part 3 json.dumps({"q" * 100: "r" * 100})[50:75].encode('utf-8'), # part 4 json.dumps({"q" * 100: "r" * 100})[75:].encode('utf-8') + b'\r\n', # part 5 json.dumps({"s": "t"}).encode('utf-8') + b'\r\n' + json.dumps({"u": "v"}).encode('utf-8') + b'\r\n' + json.dumps({"w": "x"}).encode('utf-8') + b'\r\n'+ json.dumps({"y": "z"}).encode('utf-8') + b'\r\n', # several small messages ] * n_batches_of_data server_socket = socket.socket() server_socket.bind(("localhost", port)) server_socket.listen(2) while True: conn, address = server_socket.accept() # accept new connection for d in data: conn.sendall(d) conn.close() def read_using_recv(buffer_size, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(10) s.connect(("localhost", port)) # this code has a bug where messages can get stuck in data if part doesn't end with \r\n (data, part) = ("", "") while True: try: part = s.recv(buffer_size) except (socket.timeout, socket.error) as e: return # an empty string indicates the server shutdown the socket if len(part) == 0: return data += part.decode('utf8') if data.endswith('\r\n'): data.split('\r\n') data = '' def read_using_readline(buffer_size, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(10) s.connect(("localhost", port)) f = s.makefile('r', buffering=buffer_size, newline='\r\n', encoding='utf8') while True: try: part = f.readline() except (socket.timeout, socket.error) as e: return # an empty string indicates the server shutdown the socket if len(part) == 0: return if __name__ == '__main__': t = threading.Thread(target=server, args=[N_BATCHES_OF_DATA, PORT], daemon=True) t.start() time.sleep(5) print('read_using_recv', timeit.timeit((lambda: read_using_recv(BUFFER_SIZE, PORT)), number=5000)) print('read_using_readline', timeit.timeit((lambda: read_using_readline(BUFFER_SIZE, PORT)), number=5000))
测试结果
Windows系统
read_using_recv: 134.77089489999344 read_using_readline: 78.2258090999967
Linux系统
read_using_recv 22.800755036994815 read_using_readline 20.403162219095975
分析与解答
1. 跨平台Socket底层实现差异
Windows和Linux的Socket API底层逻辑存在本质区别:
- Windows:WinSock的
.recv默认无内置缓冲,每次调用都会触发系统调用开销;而socket.makefile()创建的文件对象启用了用户态缓冲,大幅减少系统调用次数,同时.readline()在缓冲中完成换行符处理,比手动拼接字符串检查\r\n高效得多。 - Linux:Linux Socket默认启用内核级TCP缓冲,
.recv会尽可能读取可用数据,系统调用开销被大幅摊薄;同时Python在Linux上对.recv的封装已做优化,和makefile的缓冲机制差距极小。
2. 原recv实现的性能瓶颈
read_using_recv函数存在两个关键低效点:
- 每次收到数据都执行
data += part.decode('utf8'),Python字符串是不可变对象,频繁拼接会产生大量临时对象,这种内存操作的开销在Windows系统中被进一步放大。 - 仅当数据以
\r\n结尾时才拆分字符串,否则持续累积数据,导致后续拼接的字符串越来越长,效率持续降低;而.readline()会在缓冲中实时处理换行符,无需累积整个批次内容。
3. 基准测试场景的影响
测试中客户端重复建立5000次连接:
- Windows系统中,
makefile的缓冲机制减少了每次连接的系统调用次数,累积后性能提升更显著。 - Linux系统中,TCP快速连接复用(如TIME_WAIT状态优化)让连接建立开销本身很低,加上内核缓冲的加持,两种实现的差异被缩小。
此外,测试数据包含跨缓冲边界的消息,readline()可一次性处理这类情况,而原recv代码需要多次拼接才能识别换行符,这在Windows上的开销差异比Linux更突出。
内容的提问来源于stack exchange,提问作者Peter
相关产品推荐
相关产品推荐

