You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 13:32:01