
我先确认一下最终要求:输出一篇围绕“TCP 协议核心与 Socket 编程入门从理论到代码实战”的博文标题从二级标题开始编号主体不少于5000字至少4个H2包含实操细节、经验心得、常见问题不使用mermaid、emoji不出现AI套话不使用任何敏感内容结尾自然收尾。另外用户要求开头前100字融入核心关键词TCP、Socket、编程并以从业者口吻引入。下面直接开始写正文。1. 先说为什么每个写网络代码的人都绕不开 TCP我做了这么多年后端面试过不少人也带过不少新人发现一个规律凡是能在线上环境里快速定位网络问题的人TCP 基础一定扎实凡是遇到连接超时只会重启服务的人多半连 Socket 和 TCP 的关系都没理清楚。所以这篇内容不是给你背概念用的而是帮你在动手写代码之前把 TCP 协议核心机制和 Socket 编程之间的关系彻底打通。TCP 全称是 Transmission Control Protocol传输控制协议它解决的核心问题是在不可靠的网络上可靠地传输字节流。而 Socket 并不是协议它是操作系统给应用层提供的一组编程接口让你能通过代码创建连接、收发数据。简单说TCP 是交通规则Socket 是方向盘和油门。今天这篇博文从三次握手、四次挥手、可靠传输原理讲起再到用 Python 和 C 语言分别实现一个完整的 TCP 通信程序最后把我这些年踩过的坑全部抖出来新手可以直接照着抄老手也可以当排查手册用。2. TCP 协议核心机制连接怎么建立数据怎么保证不丢2.1 三次握手不是形式而是双方同步状态的过程很多人背过三次握手的流程客户端发 SYN服务端回 SYNACK客户端再回 ACK。但如果你只背到这个层面遇到真实问题还是会懵。我换个角度讲三次握手的本质是让通信双方各自确认两件事——自己的发送能力正常对方的接收能力正常以及对方的发送能力正常自己的接收能力正常。第一次握手客户端发送 SYN携带一个初始序列号 ISNInitial Sequence Number。服务端收到后能确认客户端的发送能力没问题但客户端此时还不知道服务端能不能收、能不能发。第二次握手服务端同时回复 SYN 和 ACKACK 是对客户端 SYN 的确认SYN 则是服务端自己的同步请求这样客户端收到后就能确认服务端的收发都正常。第三次握手客户端再回一个 ACK服务端收到后确认客户端的接收能力正常。到这一步双方才真正进入 ESTABLISHED 状态。我在实际抓包时经常看到一种现象客户端发了 SYN服务端也回了 SYNACK但客户端没有回最后的 ACK连接就挂在那。这种半连接状态如果大量堆积服务端的 SYN 队列会被占满新连接全部超时。这就是经典的 SYN Flood 攻击原理也是你为什么要在服务端设置 tcp_syncookies 和合理的 backlog 的原因。2.2 四次挥手为什么客户端要等 TIME_WAIT连接关闭比建立更复杂因为 TCP 是双工的两条数据通道要分别关闭。四次挥手的过程是主动关闭方发送 FIN被动方回 ACK被动方发送 FIN主动方回 ACK。其中被动方回完 ACK 之后可能还有数据没发完所以 FIN 和 ACK 是分开发送的这就比三次握手多了一次。这里有个特别值得注意的点主动关闭方在发送最后一个 ACK 之后会进入 TIME_WAIT 状态持续 2 个 MSLMaximum Segment Lifetime报文最大生存时间。为什么要等两个原因第一确保最后一个 ACK 能到达对方如果丢了对方会重发 FIN你还能再回应第二让网络中残留的旧数据包过期消失避免污染新的连接。我遇到过一个很典型的线上问题服务端频繁重启每次重启后启动失败报错信息是bind: only one usage of each socket address。原因就是上一个进程关闭后端口仍处于 TIME_WAIT 状态新进程立即 bind 同一个端口就冲突了。解决办法要么等 2 MSL 时间要么在代码里设置 SO_REUSEADDR 选项。这个坑后面还会详细说。2.3 可靠传输的三个底层机制TCP 的可靠性不是靠网络保证的而是靠一系列控制机制自己争取来的。最重要的三个是确认应答ACK、超时重传、流量控制与拥塞控制。确认应答的意思是每收到一个数据段接收方要回一个 ACK告诉发送方“我收到了”。如果发送方迟迟没收到 ACK就会触发超时重传。重传的超时时间不是固定的而是根据 RTTRound-Trip Time往返时间动态计算的这就是 RTORetransmission Timeout。我在实测中见过太多次因为 RTO 设置不合理导致的假死现象网络稍微抖动一下重传风暴直接把带宽打满。流量控制是接收方根据自己接收缓冲区剩余空间通过窗口大小字段告诉发送方“你一次最多能发多少”。如果你写代码时没注意接收缓冲区设置对方发太快你的进程来不及读内核缓冲区满了TCP 就会把窗口通告为 0发送方就会停住。拥塞控制则是发送方根据网络状况自行调节发送速率慢启动、拥塞避免、快速重传、快速恢复这四个算法是 TCP 性能的核心但一般业务开发不需要手动干预了解即可。3. Socket 编程模型服务端和客户端各自要做什么3.1 Socket 到底是什么Socket 翻译过来是“套接字”但它本质上就是一个文件描述符。在 Linux 下一切皆文件网络连接也可以看作一个文件你通过 read/write 来读写数据只不过读写的是内核网络协议栈帮你缓冲的数据。Socket 编程的标准流程可以用两个词概括服务端“被动等”客户端“主动连”。服务端要经历 socket()、bind()、listen()、accept() 四个步骤客户端只需要 socket() 和 connect() 两步。accept() 返回的是一个新的 socket 描述符后面收发数据都用这个新 fd而不是监听 fd。很多新手会在这里犯迷糊把监听 fd 拿去做读写结果永远收不到数据。我建议所有初学者先在纸上把这几个系统调用的关系画清楚socket 创建的是端点bind 是给端点绑定地址listen 是把端点变为被动监听accept 是从已完成握手的队列里取出一个连接connect 是主动发起连接。理解了这个链路代码写起来就不会乱。3.2 核心函数与状态转换服务端的 listen() 有一个 backlog 参数它决定了内核中已完成握手但还没被 accept 取走的连接队列长度。这个值不是越大越好因为每个连接都要占内存而且如果应用处理不过来队列满了之后新连接会被内核直接丢弃客户端看到的可能就是 connect timeout。connect() 返回成功不代表对端应用已经接收数据它只代表三次握手完成了。所以如果你用 connect 来判断服务端健康状态会漏掉一种情况服务端的 accept 循环已经卡死或者线程池耗尽但内核握手依然可以成功。正确做法是连接建立后发一个探测请求真正读到响应才认为服务端可用。这里我特别想提一下 epoll。单线程 accept 处理的方式只能应付并发很低的场景高并发下必须用事件驱动模型。epoll 是 Linux 下最高效的 I/O 多路复用机制它通过内核事件表直接告诉应用哪些 fd 可读、可写避免了轮询所有连接的开销。如果你用 Python 写服务端可以直接用 asyncio底层就是封装了 epoll用 C 或者 Go 的话原理也类似。但我不建议新手一上来就玩 epoll先把阻塞式流程写通再去看事件驱动思路会清晰得多。4. 从零写一个 TCP 通信程序完整实例4.1 编程语言选择和项目规划为了照顾不同阶段的人我这里用 Python 写一个标准示例因为 Python 的代码量最少新手能看懂每一行的作用同时我会穿插讲解 C 语言里对应的系统调用。项目规划很简单一个服务端监听 127.0.0.1 的 9000 端口收到客户端消息后原样返回并打印日志一个客户端连接服务端发送几条消息后关闭。这个回显服务虽然简单但包含了 TCP 通信的全部核心步骤。开发环境上Python 3 就行无需额外安装任何库。Windows、Linux、macOS 都能跑只是 Windows 下的防火墙可能会拦截第一次运行时允许即可。我下面的代码是基于阻塞式 socket 写的目的是让你看清每个调用过程性能问题后面再谈。4.2 服务端代码逐行拆解import socket SERVER_HOST 127.0.0.1 SERVER_PORT 9000 BACKLOG 5 server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((SERVER_HOST, SERVER_PORT)) server_socket.listen(BACKLOG) print(f服务端已启动监听 {SERVER_HOST}:{SERVER_PORT}) while True: client_socket, client_addr server_socket.accept() print(f收到新连接来自 {client_addr}) try: while True: data client_socket.recv(1024) if not data: break print(f收到数据: {data.decode(utf-8)}) client_socket.sendall(becho: data) except ConnectionResetError: print(f客户端 {client_addr} 异常断开) finally: client_socket.close() print(f连接 {client_addr} 已关闭)逐行说明一下重点socket.socket 第一个参数 AF_INET 表示使用 IPv4 地址第二个参数 SOCK_STREAM 表示使用流式套接字对应 TCP。如果你写 SOCK_DGRAM那就是 UDP通信模型完全不同。setsockopt 设置 SO_REUSEADDR这个非常关键。不设置的话如果服务端刚关闭端口会处在 TIME_WAIT 状态立刻重启就会报“Address already in use”也就是前面提到的 bind 错误。设置之后即使有 TIME_WAIT 连接占用端口bind 也能成功。accept() 是阻塞调用没有新连接时线程会挂起等待。这里要注意的是accept 返回的 client_socket 才是和这个客户端通信的套接字server_socket 继续监听新连接。我用的是单线程循环所以一次只能处理一个客户端。要支持并发要么用多线程要么用 select/epoll。recv(1024) 的参数 1024 表示最多读取 1024 字节注意不是“必须读满 1024 字节”。TCP 是字节流协议recv 可能返回 10 字节也可能返回 3000 字节完全看内核缓冲区里有多少数据。所以实际项目中如果没有自定义消息边界光靠 recv 无法判断一条消息是否完整。这是下面要讲的粘包问题的根源。4.3 客户端代码与运行验证import socket SERVER_HOST 127.0.0.1 SERVER_PORT 9000 client_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.connect((SERVER_HOST, SERVER_PORT)) print(f已连接服务端 {SERVER_HOST}:{SERVER_PORT}) try: messages [hello tcp, socket test, exit] for msg in messages: client_socket.sendall(msg.encode(utf-8)) response client_socket.recv(1024) print(f服务端响应: {response.decode(utf-8)}) finally: client_socket.close() print(客户端已关闭)客户端流程很简单创建 socketconnect 到服务端地址然后发送数据。connect 是阻塞的连接失败时会有超时时间具体时长由操作系统决定一般 Linux 默认在 2 分钟左右。运行顺序是先启动服务端再启动客户端。如果先启动客户端会抛出 ConnectionRefusedError这在后面常见问题里会专门讲。我在验证时会把服务端和客户端分别放在两个终端里跑。服务端日志显示“收到新连接”客户端打印出“服务端响应: echo: hello tcp”之类的结果就说明整个链路通了。如果看到乱码检查一下编解码是否一致我代码里统一用 UTF-8。这里提醒一句网络编程中编解码不一致是最常见的问题之一尤其是在混用 GBK 和 UTF-8 的环境里。4.4 深挖一下内核缓冲区与 sendall 的行为很多人对 sendall 有误解以为它一定把整条消息发出去了。其实 sendall 的内部实现是循环调用 send直到全部发送成功或者抛出异常。普通 send 一次可能只发了一部分这是由发送缓冲区大小决定的。所以我建议在应用层一律使用 sendall而不是 send。同理recv 也要用循环读取直到读取到预期的长度或者对端关闭。关于缓冲区我再多说一句TCP 接收缓冲区的大小可以通过 recv 的第三个参数或者 setsockopt 的 SO_RCVBUF 来调整。默认值通常在几十 KB 到几百 KB 之间。如果你要传大文件默认缓冲区可能不够需要在应用层做分块发送和累积接收。我曾经见过一个同事用一次 recv 去接一个 5MB 的数据结果只收到一部分然后程序就卡死了。TCP 不是消息协议它是字节流协议这句话请默读三遍。5. 并发模型与常见误区的深度解析5.1 单线程循环的问题在哪里上面的回显程序能跑通但它有个致命问题如果某个客户端连接后一直不发数据accept 之后的 recv 就会一直阻塞后续所有连接都在队列里等着。也就是说只要有一个人恶作剧连上来不发消息你的服务端就瘫痪了。这就是为什么实际项目中不会用单线程来同时处理多个连接。解决的思路有几种第一种是多线程每 accept 到一个连接就开一个线程去处理逻辑上最直观但线程创建和上下文切换有开销连接数一高就撑不住。第二种是线程池限制线程数量连接处理完回到池里避免反复创建销毁线程。但线程池的瓶颈在于一个线程同一时刻只能处理一个阻塞的 socket一个连接卡住线程就被占住了。第三种是事件驱动用 select、poll、epoll 监听所有 socket 的可读事件哪个有数据就处理哪个整个过程用单线程就能扛住上万连接。Nginx、Redis、Netty 都是这个思路。我先给出一段使用 select 模块的示例帮你理解它和多线程的本质区别import socket import select server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((127.0.0.1, 9000)) server_socket.listen(5) server_socket.setblocking(False) read_list [server_socket] while True: readable, _, _ select.select(read_list, [], [], 1) for sock in readable: if sock is server_socket: client_socket, addr server_socket.accept() client_socket.setblocking(False) read_list.append(client_socket) print(f新连接: {addr}) else: data sock.recv(1024) if data: sock.sendall(becho: data) else: print(f关闭连接: {sock.getpeername()}) read_list.remove(sock) sock.close()注意我给 socket 都设置了 setblocking(False)。非阻塞模式下如果没有数据可读recv 不会阻塞而是直接抛异常或者返回空配合 select 的监听才能避免空转。select 本身也是阻塞的但它可以同时等待多个 fd有事件才返回这样就不会卡在单个连接上。这段代码虽然能跑但 select 有最大 fd 数量限制性能随着 fd 数量增长而下降。Linux 环境下建议直接用 epoll但 epoll 的接口要复杂一些新手可以先从 select 理解概念生产环境换 epoll 或者直接用框架。5.2 粘包与拆包TCP 字节流的天然属性粘包问题几乎是每个新手都会遇到的。你在客户端连续执行两次 send分别发送“hello”和“world”对端 recv 的时候可能一次就读到了“helloworld”看起来像粘在一起了。但实际上“粘包”这个说法容易误导人。TCP 不保证消息边界它只保证字节顺序。你发送的数据进入发送缓冲区后内核可能把两段数据合并成一个 TCP 报文发出去也可能把一段数据拆成多个报文。对端内核缓冲区接受到的字节流没有任何标记告诉你哪里是一条消息的结束。解决粘包问题有两个常用方案一个是固定长度。每条消息统一为 N 字节不足的用空格填充接收方循环读读满 N 字节就当作一条消息解析。优点是实现简单缺点是不适合长度差异大的业务浪费流量。另一个是长度前缀法。每条消息前面用 4 字节整数表示消息体长度接收方先读 4 字节解析出长度 L再继续读 L 字节的数据。这种方案在真实项目里用得最多。我的做法通常是用 struct 模块把长度打包成网络字节序的大端格式这样不同机器之间解析也不会出错。我遇到过有人拿着 UDP 的思维写 TCP 代码期待 send 一次接收就完整。但从上面的分析你应该理解了写 TCP 应用层协议时必须自己设计消息格式。这是从“会写 demo”到“能写生产级代码”的关键一步。5.3 并发模型选择建议与 IO 多路复用的适用场景我简单说一下并发模型的选择策略帮你少走弯路并发连接数低于 1000业务逻辑简单用多线程加上合理的线程池上下限就够了。连接数上万但每个连接交互频率不高比如物联网设备上报用 epoll 配非阻塞 IO 是标准方案。有大量长连接且频繁通信比如实时聊天可以基于 epoll 实现事件驱动或者直接用成熟的网络框架。纯计算密集型服务不要用单线程事件循环因为 CPU 密集型任务会阻塞事件循环导致所有连接都没响应。这种情况下要么用多进程要么把计算任务丢到线程池。Go 语言里你不需要直接操作 epoll它的 goroutine 配合 net 包内部已经处理了 Io 多路复用。但我还是建议无论用什么语言都要理解底层模型否则思维会被框架限制住。框架能帮你挡掉 90% 的复杂度但剩下 10% 的疑难杂症全靠底层原理去排查。6. 真实开发中的常见问题与排查方法6.1 必知的报错信息速查表我整理了一张在实际生产环境中出现频率最高的报错表你遇到同类问题可以先按这个方向去排查报错信息常见原因排查思路Connection refused服务端没有监听对应端口或防火墙拦截telnet/ nc 测试端口连通性ss -lntp 查看端口监听状态Address already in use端口被占用或有 TIME_WAIT 连接未释放lsof -i:端口或 netstat -ano服务端设置 SO_REUSEADDRConnection reset by peer对端进程崩溃或主动关闭了连接看服务端日志检查对端是否有异常退出Broken pipe对端已经关闭连接后你仍然调用 send代码中捕获 SIGPIPE或发送前判断连接状态Operation timed out网络不通、对端无响应、防火墙丢包抓包看 SYN 是否发出、SYNACK 是否回来No buffer space available文件描述符耗尽或缓冲区设置过大ulimit -n 查看 fd 限制ss -s 查看 socket 统计这么多年来我发现排查网络问题最靠谱的搭档是 tcpdump 和 Wireshark。比如 Connection reset by peer你用 tcpdump 抓包能看到对端发来了 RST 标志位的报文RST 的意思是“我不认这个连接别发了”。相比之下如果对端只是正常关闭但没发 RST你可能会收到 FIN 而不是 RST。区分这两个就能判断是正常关闭还是异常崩溃。6.2 小心 Time_wait 和文件描述符耗尽前面说过 TIME_WAIT 是主动关闭方进入的状态。如果一个服务的连接生命周期很短比如一秒钟几万次连接创建销毁那系统里 TIME_WAIT 连接会剧增占用大量内存和端口。一个可行的优化是把 TIME_WAIT 的端口复用打开也就是 tcp_tw_reuse但要注意这个参数只对发起连接的一方有效并且需要配合时间戳选项使用。文件描述符耗尽也是高并发场景下的常见问题。每个 TCP 连接都对应一个 fdLinux 默认的 ulimit -n 常常是 1024也就是说你一个进程最多只能维持一千个左右的连接。提高到 65535 甚至更高是常规操作。如果你看到服务端 accept 返回 EMFILE 或者日志里全是 socket 相关的 Cannot assign requested address大概率就是 fd 或端口耗尽。真实生产里我遇到过 MySQL 报Cant connect to local MySQL server through socket的问题这类报错和 TCP/IP 无关它是 Unix domain socket 文件找不到或没权限。这个错误提示网上很多但很多人会误以为 MySQL 服务本身挂了。先检查 /var/run/mysqld 目录是否存在、mysql 用户有没有权限以及 my.cnf 里 socket 路径是否配置正确比盲目重启数据库靠谱得多。6.3 心跳机制与断线检测TCP 本身有一个 keepalive 机制但默认周期是 2 小时对绝大多数业务来说太慢了。你要做的是在应用层实现心跳客户端每隔一段时间发送一个自定义的 Ping 包服务端如果在 N 秒内没有收到任何数据就判定连接失效主动清理。我自己的做法是在消息格式里加一个 type 字段区分数据包和心跳包。服务端收到心跳包时更新最近活跃时间然后用一个后台线程定时扫描所有连接的活跃时间超过阈值就强制关闭。这样可以检测到那些半开状态的连接。什么叫半开连接比如客户端突然拔了网线服务端不知道还在等数据实际上这个连接已经不存在了。如果没有心跳机制这个 fd 会一直被占用积累到一定程度服务端就瘫痪了。这里还要提醒一句千万不要在心跳包路径里加复杂的业务逻辑心跳包必须是最小的、独立的、不参与业务处理的包。不然业务一阻塞心跳也发不出去健康连接会被误杀。6.4 服务端重启的经典坑我在第 2 节提过 bind 报错这里展开讲一下。假设你有一个服务监听 9000 端口运行中你 CtrlC 终止了它紧接着马上重新启动很可能会报bind: only one usage of each socket address。原因就在于刚才那条连接的主动关闭方可能是客户端而不是服务端但如果服务端也有主动关闭的行为它自己就进入了 TIME_WAIT。解决方案有两个第一个是在代码里设置 SO_REUSEADDR。这个选项允许 bind 到处于 TIME_WAIT 状态的地址。在 Python 里是 setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)C 里是 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, optval, sizeof(optval))。要注意SO_REUSEADDR 不能解决两个进程同时 bind 同一个地址的问题它只解决 TIME_WAIT 下的端口复用。第二个是监听一个单独的端口用命令先查看端口是否被占用确认释放后再启动。很多时候手动管理等 2 MSL 不太现实所以在代码里设置 SO_REUSEADDR 几乎是所有服务端程序的标配。7. 一个比较完整的可靠消息传输示例到这一步我觉得可以用一个更完整、更接近真实生产环境的示例把前面的理论串起来固定长度消息头 长度字段 心跳 多线程处理。我这里用 Python 实现一个简化版重点是展示思路。服务端import socket import struct import threading MAGIC bTCP HEADER_LEN 8 # 4字节魔数 4字节长度 def recv_exact(conn, n): data b while len(data) n: chunk conn.recv(n - len(data)) if not chunk: return None data chunk return data def handle_client(conn, addr): print(f新连接 {addr}) try: while True: header recv_exact(conn, HEADER_LEN) if header is None: break magic, msg_len struct.unpack(4sI, header) if magic ! MAGIC: print(f非法消息关闭连接 {addr}) break body recv_exact(conn, msg_len) if body is None: break print(f收到消息 [{addr}]: {body.decode(utf-8)}) conn.sendall(bACK) except Exception as e: print(f连接异常 {addr}: {e}) finally: conn.close() print(f连接关闭 {addr}) server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((127.0.0.1, 9100)) server.listen(128) print(服务端已启动端口 9100) while True: conn, addr server.accept() threading.Thread(targethandle_client, args(conn, addr), daemonTrue).start()这里我定义了一个简单的应用层协议4 字节魔数 4 字节长度 消息体。recv_exact 是封装的循环读取保证读满指定字节数。这样每条消息都固定了边界不会再出现粘包问题。多线程部分用了 daemon 线程主线程挂掉后子线程也会退出demo 阶段这样写没问题生产环境建议用线程池。客户端import socket import struct MAGIC bTCP HEADER_LEN 8 def send_message(sock, msg): data msg.encode(utf-8) header struct.pack(4sI, MAGIC, len(data)) sock.sendall(header data) sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((127.0.0.1, 9100)) send_message(sock, 第一条消息) send_message(sock, 第二条消息而且这条比较长) response sock.recv(1024) print(响应:, response) sock.close()这个 demo 跑通的意义在于你理解了消息边界、长度解包、多线程连接处理就已经跨过了 TCP 编程最关键的一道坎。后面再学异步框架、底层优化、网络协议栈调参都是在这个基础上的延伸。8. 我最后想说的几个实际心得和工具推荐写了这么多年网络代码我最大的体会是TCP 的坑不是靠看书躲过去的是靠抓包和踩坑踩出来的。你第一次遇到 Connection reset by peer 可能会慌抓一次包你就懂了原来是对端主动发了 RST。你第一次遇到粘包可能会怀疑框架有 bug看一次字节流你就明白了原来是没设计消息格式。在工具方面我常用的排查命令很简单ss -lntp 看端口监听tcpdump 抓包nc 测连通性lsof 看文件描述符。Wireshark 用得不多全流量抓包信息量太大我一般只在需要分析协议细节或者和同事排查复杂问题时才用。开发环境里写 Python 的话我推荐直接在代码里打印日志加上封装的 recv_exact 辅助函数比反复调试省心很多。如果你刚入门先不要把精力花在看各种高深的内核参数调优上那些知识有价值但没有实战做支撑你根本记不住也不知道什么时候该用。先把一次完整的三次握手、收发数据、四次挥手跑通然后制造几个故障拔网线、杀进程、重启服务端、连续发大消息观察会出什么错再对照着排查。这个过程做完你对 TCP 和 Socket 的理解会比看二十篇文章都有用。