最近后台收到不少关于Socket编程的提问,有人在期末复习计算机网络时卡在“传输层和应用层之间”这段描述上,有人说自己照着教程写了个echo服务端,一换到真实项目里就报 socket connection was closed unexpectedly,还有人在排查 MySQL 连接时报 can't connect to local MySQL server through socket 直接把“Socket”和“端口”混成一团。这些问题本质上都指向同一个点:很多人学过Socket的API,但没有把计算机网络里的协议状态机和写代码时遇到的异常行为对应起来。
这篇文章就围绕“计算机网络 + Socket编程”这个主线展开。我会先讲清楚Socket在网络分层里到底处于什么位置,再把TCP连接从三次握手到四次挥手的整个生命周期映射到代码层面,然后给出一个能实际运行的文件传输示例,最后分享几个真实项目里高频出现的异常排查思路和并发模型选型。适合正在复习计算机网络的学生、刚开始接触网络编程的开发者,以及那些被线上报错折腾过的后台开发。
1. Socket不是协议,而是内核暴露给应用层的网络接口
1.1 应用进程没法直接操作TCP协议栈
计算机网络的分层模型里,TCP和UDP被划在传输层,IP在网络层,以太网帧在链路层。注意一个关键事实:这些协议栈的实现都在操作系统内核里。应用进程跑在用户态,它不能直接去操作网卡、不能直接访问内核的协议状态机、更不能自己维护TCP的重传队列和拥塞窗口。进程想发数据,只能通过操作系统提供的系统调用,把数据交给内核,由内核协议栈去封装、路由、重传、确认。
Socket就是这组系统调用的封装。你可以把它理解为内核开在应用层和传输层之间的一个“窗口”:窗口这边是你的进程,窗口那边是内核的TCP/UDP协议栈。你通过这个窗口递出数据,内核负责打包发送;内核收到对端的数据,也从窗口递给你。正因为Socket是“应用层和传输层之间的API”,而不是一个独立的协议层,很多教材才把它放在传输层的章节里讲,但又没说透它本质上是操作系统接口。
1.2 一套方法调用对应一个五元组
一个TCP Socket在内核里其实是五元组标记的:源IP、源端口、目的IP、目的端口、协议类型。我们平时写 socket.socket(socket.AF_INET, socket.SOCK_STREAM) 只是创建了一个“未连接”的套接字,真正让它具备通信能力,需要经历 bind、connect/listen、accept 这套流程。
很多教程喜欢用“打电话”来类比Socket:socket() 是装电话机,bind() 是给电话机分配号码,listen() 是开机待机,accept() 是接听来电,connect() 是拨号,send() 和 recv() 是通话内容,close() 是挂电话。这个类比能把API顺序记住,但有一个容易误导人的地方:电话接通后是一条独占线路,而TCP连接是虚拟的,数据在网络上共享带宽。打电话的“接通”感觉像建立了一条物理链路,TCP的连接只是通信双方在内核里共同维护了一套状态信息,并没有一条专线。
1.3 SOCK_STREAM 和 SOCK_DGRAM 的选择
创建套接字时第二个参数决定了传输层协议。SOCK_STREAM 对应TCP,面向连接、可靠、字节流;SOCK_DGRAM 对应UDP,无连接、不可靠但轻量。对绝大多数业务系统来说,选TCP不会有错,因为可靠性和字节流语义让应用层不用处理丢包和乱序。但如果你做的是低延迟游戏同步、实时音视频,可以评估UDP或者基于UDP的QUIC,因为TCP的可靠重传机制在丢包严重时会带来明显延迟。
这里有一个新手容易忽略的点:选TCP不代表应用层不需要关心数据边界。TCP是字节流协议,没有消息边界,你 send 一个字符串“hello world”,对端一次 recv 可能只收到“hel”,也可能收到“hello world”加上你下次发送的数据。后面第3章要解决的核心问题之一就是“消息边界”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一个连接的一生:三次握手、四次挥手、TIME_WAIT在代码里长什么样
2.1 三次握手不是调用三个函数,而是内核帮你完成的
学计算机网络时背过TCP三次握手:客户端发送SYN,服务端回复SYN+ACK,客户端再回复ACK。很多初学者会去代码里找“哪个函数对应SYN、哪个函数对应SYN+ACK”,结果发现根本没有对应的API。原因是三次握手是由内核协议栈自动完成的,应用层感知不到中间的每个报文。
具体映射关系是这样的:
- 客户端调用
connect()时,内核协议栈发出SYN报文,然后客户端进入SYN_SENT状态。 - 服务端进程调用
listen()后,内核就在监听队列里等SYN。收到SYN后,内核自动回复SYN+ACK,服务端进入SYN_RCVD状态。 - 客户端收到SYN+ACK后,内核自动回复ACK,并让
connect()返回成功,客户端进入ESTABLISHED状态。 - 服务端等到三次握手完成,会把这条连接放入全连接队列,此时应用层调用
accept(),才能够从队列里取出这条已建立的连接。
所以 accept() 返回时,连接已经是可用状态,不需要再额外“握手”。这也是为什么 accept 不负责三次握手,它只负责从内核的全连接队列里“领取”建好的连接。网上有些教程说“accept完成三次握手”,严格来说不准确:三次握手在内核里已经完成,accept 只是取走结果。
2.2 backlog参数到底是干什么的
listen(sock, backlog) 这里的 backlog 是很多教程没讲透的点。它和“最大连接数”是两回事。现代Linux内核里,backlog 的值同时影响两个队列:半连接队列(SYN等待ACK)和全连接队列(握手已完成、等待accept)。半连接队列存的是那些已经收到SYN、还没有完成握手的连接;全连接队列存的是握手完成、但应用层还没调 accept 拿走的连接。
如果全连接队列满了,新的握手完成后会被丢弃或直接拒收,表现为客户端 connect 成功,但服务端处理速度跟不上,或者干脆连不上。线上排查时,如果发现客户端连接超时、服务端CPU不高但请求堆积,可以看一眼全连接队列溢出情况,命令是 ss -lnt,查看 Send-Q 和 Recv-Q 的值。这在“连接打满”场景下比单纯看进程内存更直观。
2.3 四次挥手和半关闭:close不一定真的“挂电话”
四次挥手在代码里的表现更隐蔽。主动关闭的一方调用 close(),内核发送FIN报文,对端 recv() 会返回0(EOF),表示“你读不到更多数据了”。这只是四次挥手的第一阶段,紧接着对端也要关闭,发送FIN回来,主动关闭方收到后进入 TIME_WAIT,整个连接才会释放。
这里有个特别实用的概念:半关闭。close() 会把写方向和读方向一起关闭,但有时候你只想告诉对端“我的数据发完了”,还想继续读对端的响应。这个时候要用 shutdown(SHUT_WR),它只关闭发送方向,发送FIN,接收方向仍然保留。这对设计“请求-响应”型协议很有用:客户端发送完请求后调用 shutdown(SHUT_WR),服务端就能通过 recv() 返回0判断请求结束,同时服务端还可以往客户端回数据。
Python里对应 shutdown(socket.SHUT_WR)。很多做网络编程的人不知道这个函数,导致客户端发送完请求后必须靠“约定长度”或“约定分隔符”来判断请求是否结束,代码复杂不少。
2.4 TIME_WAIT:主动关闭方的“冷静期”
四次挥手里最容易被忽略的是 TIME_WAIT。主动关闭的一方在发送最后一个ACK后,不会立刻进入 CLOSED,而是进入 TIME_WAIT 状态,等待2MSL(Maximum Segment Lifetime,报文最大存活时间)后才完全关闭。这个机制的初衷有两个:一是防止最后一个ACK丢失后,对端重发FIN时没有应答;二是防止旧连接的迟到报文串扰到新连接里。
但高并发的服务端频繁主动关闭连接时,TIME_WAIT 会堆积,占用大量端口和内存。解决办法里最有名的是 SO_REUSEADDR,它允许新启动的进程立即绑定 TIME_WAIT 状态的端口。程序里设置一次:
python复制server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
这个操作在开发环境里作用不明显,但在生产环境服务重启时非常关键。如果没加,服务刚停掉就立刻重启,会报 Address already in use,原因就是旧连接还在 TIME_WAIT 状态没释放完。
3. 用Python从零跑通一个TCP文件传输:从echo到带协议头的传输
3.1 为什么不直接写到文件里就行?因为TCP没有消息边界
先写一个最简单的echo服务端,只能把客户端发来的数据原样送回:
python复制import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 9000))
server.listen(128)
print("server listens on 0.0.0.0:9000")
while True:
conn, addr = server.accept()
print(f"connection from {addr}")
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
conn.close()
客户端:
python复制import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 9000))
client.sendall(b"hello socket")
resp = client.recv(1024)
print(resp.decode())
client.close()
这段代码跑通后,如果你直接拿它来传文件,很快就会碰到问题:比如传一张2MB的图片,服务端调 recv(1024) 时不保证一次拿到完整的文件内容,也可能上一次 recv 已经把下一段数据一起拿回来了。因为TCP是字节流,内核缓冲区里的数据是连续的,应用层读多少、什么时候读,完全由自己控制。没有“这条消息从哪开始、到哪结束”的天然边界。
所以设计文件传输协议时,必须在数据前面加一个协议头,明确告诉对端“后面是消息头,再后面是文件内容”。最朴素的方案是固定8字节头:前4字节存文件名长度,后4字节存文件大小,再跟文件名和文件内容。
3.2 文件传输服务端实现
python复制import os
import socket
import struct
SAVE_DIR = "./received"
HOST = "0.0.0.0"
PORT = 9000
def recv_exact(conn, n: int) -> bytes:
"""接收恰好 n 字节,不满足就继续读,直到连接关闭或出错。"""
data = b""
while len(data) < n:
chunk = conn.recv(n - len(data))
if not chunk:
raise ConnectionError("对端在发送完数据之前关闭了连接")
data += chunk
return data
def handle_conn(conn: socket.socket):
try:
# 1. 读固定8字节头:文件名长度 + 文件大小
header = recv_exact(conn, 8)
name_len, file_size = struct.unpack("!II", header)
# 2. 读文件名
file_name = recv_exact(conn, name_len).decode("utf-8")
file_name = os.path.basename(file_name)
# 3. 循环读文件内容,避免一次性读入超大文件
save_path = os.path.join(SAVE_DIR, file_name)
received = 0
with open(save_path, "wb") as f:
while received < file_size:
chunk = conn.recv(65536)
if not chunk:
raise ConnectionError("文件传输过程中连接被关闭")
f.write(chunk)
received += len(chunk)
print(f"文件 {file_name} 接收完成,共 {received} 字节")
finally:
conn.close()
def main():
os.makedirs(SAVE_DIR, exist_ok=True)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen(128)
print(f"服务端监听 {HOST}:{PORT}")
while True:
conn, addr = server.accept()
print(f"收到连接: {addr}")
handle_conn(conn)
if __name__ == "__main__":
main()
这里几个细节值得展开说。
recv_exact 这个名字可能有人没用过。TCP的 recv 不保证一次返回你请求的那么多字节,所以必须写一个“收满指定长度”的辅助函数,循环读取直到凑够,再交给上层解析。这几乎是所有TCP应用协议的标配。
struct.unpack("!II", header) 里的 ! 代表网络字节序(大端),这是跨平台通信必须遵守的约定。如果你用本机小端序打包整数,发到另一台大端序机器上就会解析出完全错误的值。Socket通信的协议头标准做法就是用网络字节序。
os.path.basename(file_name) 是为了防止客户端把路径设为 ../../etc/passwd 之类的内容,服务端如果直接拼接,就存在目录穿越风险。文件传输服务哪怕只是自己试验,也要习惯性做这一层防护。
最后一点:边收边写文件,不一次性 f.write(file_data)。一个超大文件全部读进内存再落盘,服务端很容易被拖垮。正确的做法是循环 recv,每收到一块写一块。
3.3 客户端实现
python复制import os
import socket
import struct
HOST = "127.0.0.1"
PORT = 9000
def send_file(file_path: str):
if not os.path.isfile(file_path):
raise ValueError(f"文件不存在: {file_path}")
file_name = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
name_bytes = file_name.encode("utf-8")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(10)
try:
client.connect((HOST, PORT))
header = struct.pack("!II", len(name_bytes), file_size)
client.sendall(header)
client.sendall(name_bytes)
sent = 0
with open(file_path, "rb") as f:
while sent < file_size:
data = f.read(65536)
if not data:
break
client.sendall(data)
sent += len(data)
print(f"文件 {file_name} 发送完成,共 {sent} 字节")
finally:
client.close()
if __name__ == "__main__":
send_file("./a.txt")
注意客户端用了 sendall 而不是 send。TCP的 send 在非阻塞模式或缓冲区不足时,可能只发送部分数据,返回值是实际写入的字节数。sendall 内部帮你循环发送,直到全部发完或抛异常。如果自己手写循环,记得语义一定要和 sendall 一致,否则大文件传输时会出现“最后一段没发完就对端开始处理”的bug。
3.4 用这个示例验证TCP的粘包现象
跑一遍上面的程序,然后故意改一下:把服务端一次 recv 的内容直接打印出来,不做 recv_exact 解析。你会发现一次收到的内容可能同时包含“下一个文件的头部”或“当前文件名的后半段”。这就是TCP粘包现象的直观体现。
解决粘包有两种成熟思路:一是像这里用固定长度协议头定义消息边界;二是用特殊分隔符(比如HTTP的 \r\n\r\n)。固定长度头的优点是解析效率高、边界清晰,缺点是设计协议时要考虑扩展性,头部字段不够用就得升级协议版本。分隔符方案优点是容易调试,缺点是消息体里不能出现分隔符,否则需要转义,复杂度反而更高。实际网络协议用得最多的是“长度字段方案”,HTTP/2、gRPC、WebSocket基本都是这个思路。
4. connection refused与closed unexpectedly:两种高频异常的完整排查链路
4.1 ConnectionRefusedError / connect ECONNREFUSED
这个错误在Python里通常长这样:
code复制ConnectionRefusedError: [Errno 111] Connection refused
含义很明确:你 connect 的目标IP有主机响应,但目标端口上没有进程在监听,或者监听被防火墙策略拒了。常见原因有几个:
- 服务端进程没起来,或者监听的不是这个端口。
- 服务端监听在
127.0.0.1,客户端连的是内网IP或公网IP,自然连不上。 - 防火墙直接丢弃SYN。这种情况通常表现不是“refused”,而是“超时”,因为防火墙丢弃报文后,客户端收不到任何响应,只能等超时。所以看到
Connection refused反而是好消息:网络通,端口没服务。 - 服务端的全连接队列已满,内核把新连接拒了,也可能体现为 refused。
排查链路我习惯这样走:
先用 ss -lntp 确认服务端监听地址和端口:
bash复制ss -lntp | grep 9000
如果输出里 Local Address:Port 是 127.0.0.1:9000,那只能本机访问,外部连不上;改成 0.0.0.0:9000 才会监听所有网卡。
再用 telnet 127.0.0.1 9000 或 nc -vz 127.0.0.1 9000 从本机测端口。本机能通、远端不通,优先想防火墙和安全组。本机都不通,再看监听进程是否存活。
如果是C/S架构的程序,服务端用 bind(("", port)) 表示监听所有接口,这个和 bind(("0.0.0.0", port)) 在效果上一致,但代码可读性不如后者直观。
4.2 socket connection was closed unexpectedly 的根因
这个报错在Java、Go、Node.js的HTTP客户端里经常出现,Python调用第三方服务也会遇到。它指的通常是:连接建立后,读或写过程中,对端突然关闭了连接,而且不一定是正常完成协议交互后关闭。
常见场景是:
- 服务端设置了空闲超时。比如网关/负载均衡的
keep-alive timeout是60秒,客户端连接池里的连接空闲超过60秒,网关主动断开。客户端不知道,下一次复用这个连接发请求,收到的就是“连接关闭”或Connection reset by peer。 - 服务端业务处理线程池满,代理层在等待超时后主动断开。
- 客户端发送的请求体太大,服务端或中间层直接断开。
- 服务端进程崩溃或被kill,客户端还在等响应。
- 跨网络设备时,中间设备的连接表过期,把长连接清掉。
排查这种问题,抓包是最直接的。在服务端和客户端之间抓包,看断开时是FIN还是RST:
bash复制tcpdump -i any host <服务端IP> and port 9000 -w /tmp/socket.pcap
抓完用Wireshark打开,看断开前的最后一个TCP报文。如果是FIN(Flags里带 F),说明对端是正常调用 close() 关闭;如果是RST,说明对端异常终止,可能是崩溃、超时主动重置,也可能是内核发送了RST。
区分RST和FIN是定位“closed unexpectedly”的关键一步。FIN说明有人主动优雅关闭,业务层可能知道原因;RST说明数据还没处理完连接就没得了,问题往往出在异常分支、超时或内核级强制关闭。
4.3 还有一个容易混淆的“socket文件”概念
搜索记录里有人遇到 MySQL 报错:
code复制ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'
这里的“socket”不是网络Socket,而是Unix Domain Socket。MySQL在Linux上默认通过本地socket文件通信,不走TCP/IP。报这个错通常是 MySQL 服务没启动,或者 socket 文件路径不对。排查方式先看进程:
bash复制ps aux | grep mysqld
systemctl status mysql
进程没起来就启动服务;进程起来了再看配置文件里 socket 路径和客户端连接的路径是否一致。这是“Socket”一词在不同语境下的典型歧义,做网络编程的人看到这类报错第一反应应该是确认它走的是哪种Socket,别一上来就抓包。
4.4 客户端怎么设计才能更抗揍
排查完根因,正经的客户端代码还要做几层防护:
- 设置连接超时和读写超时。Python里
socket.settimeout(10)设置的是阻塞模式下的超时时间,超过10秒还没数据就抛socket.timeout。Java里对应connectTimeout和readTimeout。 - 做有限次重试,采用指数退避。比如第一次失败后等200ms重试,第二次等400ms,第三次800ms,最多重试3-5次。不要“失败立刻无限重试”,会把已经不堪重负的服务端压垮。
- 空闲连接要有心跳。TCP的
SO_KEEPALIVE默认周期太长,很多应用层协议会设计自己的心跳包,比如每30秒发一个ping,对端回pong。这样连接断开后,客户端最迟30秒内就能感知。 - 重试前要确认请求是幂等的。如果重试会导致重复扣款、重复下单,那必须引入幂等键,否则宁可报错也不要盲目重试。
5. 并发上升后怎么办:多线程、事件驱动、协程的选型思考
5.1 阻塞IO加多线程:最简单的并发方案
前面第3章的服务端是单线程串行处理连接,一次只能处理一个客户端,后面的连接只能排队。最简单的改进是“每来一个连接,开一个线程处理”:
python复制import threading
while True:
conn, addr = server.accept()
t = threading.Thread(target=handle_conn, args=(conn,))
t.start()
这个模型的优点是代码完全不用改业务逻辑,handle_conn 里是阻塞的 recv,线程自己等数据,不会卡住主循环。缺点是线程有创建和切换成本,如果同时在线连接数到几千甚至几万,线程数量多得足以把系统拖垮。再往上加线程池,能缓解频繁创建线程的问题,但每个线程阻塞等待IO时,CPU资源和内存资源还是在白白消耗。
所以这个模型适合连接数在几百以内、单连接交互时间较长的业务。比如办公内部系统、或者设备管理类的长连接。
5.2 事件驱动:用Selector管好一堆连接
IO多路复用的思路是:一个线程管所有连接,告诉内核“你帮我盯着这些socket,哪个有数据来了告诉我”。Python标准库的 selectors 模块封装了底层 select/epoll/kqueue,在不同平台上自动选最优实现。
核心逻辑是用 select 注册可读事件,然后循环 select(),返回有事件的连接,再逐个处理:
python复制import selectors
import socket
sel = selectors.DefaultSelector()
def accept(server):
conn, addr = server.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn):
data = conn.recv(1024)
if data:
conn.sendall(data)
else:
sel.unregister(conn)
conn.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(128)
server.setblocking(False)
sel.register(server, selectors.EVENT_READ, accept)
while True:
events = sel.select()
for key, _ in events:
callback = key.data
callback(key.fileobj)
这段代码的核心思想是“非阻塞IO + 事件回调”。所有连接都注册到 Selector 里,内核有事件时再回调对应处理函数,单个线程就能管理几千个连接。缺点也很明显:代码结构从“顺序执行”变成了“回调驱动”,业务逻辑一旦复杂,回调嵌套会让维护性直线下降,这就是常说的“回调地狱”。
5.3 协程异步:把“等待”写成同步代码的样子
协程模型从代码形态上解决了回调地狱的问题。用Python的 asyncio,可以写出几乎和同步代码一样的网络代码,但底层是事件循环驱动,遇到IO等待时自动让出控制权,让其他协程执行。
python复制import asyncio
async def handle(reader, writer):
data = await reader.read(1024)
writer.write(data)
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle, "0.0.0.0", 9000)
async with server:
await server.serve_forever()
asyncio.run(main())
asyncio.start_server 内部就是IO多路复用加事件循环,但你在代码里看到的 await reader.read() 和“先读、再写”的顺序完全一致,不需要再手写回调。这个模型适合高并发、IO密集的场景,比如网关、聊天服务、消息推送。
Java侧也类似,CompletableFuture、虚拟线程,本质都是“不要占着线程等IO”。核心思想不是“更快的IO”,而是“在等待IO时把执行权让给其他任务”。如果你有一批IO请求要并发发起,用异步编成能显著减少线程占用,但并发这里要特别注意异常处理:异步任务里的未捕获异常不会像同步代码那样直接打印堆栈,必须显式 exceptionally 或 handle,否则一个 TimeoutException 会让整个任务“静默失败”。
5.4 选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 连接数少(<100),交互简单 | 单线程阻塞或短连接 | 工作量最小,不会引入复杂度 |
| 连接数中等,每个连接处理时间长 | 多线程/线程池 | 编程模型直观,线程数可控 |
| 连接数高(>1000),IO密集 | Selector事件驱动或协程异步 | 单线程管理大量连接,资源消耗低 |
| 高并发且CPU密集 | 多进程/多线程 + 事件驱动混合 | 协程和事件循环只能解决IO等待,算力还得靠多核 |
我自己做过的一个长连接网关,一开始用多线程,每连接一个线程,跑了一周后线程数稳定在800,内存占用高,GC压力大。改成 asyncio 后单进程撑住了一万左右的连接,内存占用反而稳定很多。但代价是代码必须严格控制阻塞点,一旦在协程里混入耗时同步操作(比如一次 requests.get),整个事件循环都会被卡住,其他连接全部等它。这个坑一定要记住。
最后说点实操体会
做Socket编程最容易让人轻敌的地方,是只在localhost上跑通一次happy path就觉得自己会了。真实网络环境里有丢包、半包、对端崩溃、代理断开、半关闭、TIME_WAIT堆积,任何一件都能让一个“看起来正常”的程序突然挂掉。我这些年写网络代码养成了一个习惯:先写对端异常关闭的测试,再写正常流程。把客户端发一半就断开、服务端读一半就重启、连接空闲再复用这几种场景全部跑一遍,比记住一百个API都有用。
另外一个心得是:定位Socket问题别靠猜,别靠反复加日志。先从状态确认——连接是在哪个状态断的?报错是拒绝还是重置还是超时?如果是连接被重置,去抓包看是FIN还是RST。把这些“是什么”搞清楚,再谈怎么修代码。稳定不是写出来的,是测出来和查出来的。
