尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

网络编程实战:从TCP/IP协议到socket编程的数据流动与抓包解析

网络编程实战:从TCP/IP协议到socket编程的数据流动与抓包解析 很多人在学网络编程时都有一种共同体验TCP三次握手、四次挥手背得滚瓜烂熟但真让你写一个能让两台电脑互发消息的小程序却不知道第一行代码落在哪里。这个问题不怪你因为教材讲的是协议怎么设计而实际工作要的是数据怎么流动。我当年也是从这种割裂感里爬出来的所以这篇想做的事很明确把“网络编程”这四个字拆开揉碎用一条能真正跑起来的代码链路把理论和实践之间的缝隙补上。内容会涉及socket编程、TCP/IP协议栈、UDP与TCP的选择、数据收发机制以及只有写完项目才会遇到的坑。适合刚开始接触网络编程的学生也适合写过一些代码但对底层机制始终含糊的开发者。这篇文章的计划是先建立一张数据流动的整体地图搞清楚网络编程到底在编程什么再回到TCP协议本身的可靠性机制看它和socket API之间是怎么对应的然后我会带着你写一个回显服务跑通本地到局域网的真实收发接着用抓包工具把三次握手和流量传输过程可视化最后集中讲实战项目里那些高频问题比如粘包、超时、异常断开该怎么处理。整个过程尽量不堆概念每讲一个原理都配一个你马上能验证的操作。1. 先把“数据流动地图”画清楚网络编程到底在编程什么网络编程本质上只做一件事让两个进程跨设备交换数据。听起来简单但真正落地时会面临一个巨大的鸿沟——你的程序看到的是“一条连接”而底层网络看到的是一堆被切成碎片、还会丢失重复乱序的网络包。要想写出靠谱的网络程序第一步不是学API而是把这条数据通路从顶到底看明白。1.1 一条消息从发送到接收经过了几道关卡假设你在浏览器里访问一个网页输入的URL最终会变成一个HTTP请求。这个请求从应用层出发先被TCP协议封装进一个带有端口号和序列号的数据段再被IP协议封装成带有源地址和目标地址的数据报最后通过网卡变成电信号或光信号发出去。对端收到后逐层剥掉这些头部信息最终把数据原样交给目标进程。这里有一个关键认知网络编程里你写的代码几乎感知不到链路层和物理层的存在但你的程序无时无刻不在依赖它们。计算机之间靠IP地址找到设备靠端口号找到进程靠TCP或UDP协议约定数据的组装方式。而socket就是程序员和这套协议栈之间的那扇门——你只需要往门里丢数据剩下的封装、路由、重传都是操作系统和网络设备在幕后完成。我见过很多新人上来就写socket代码却不知道自己调用的每个函数背后对应着哪些协议行为。结果就是代码看着能跑但一遇到粘包、断线重连、高并发场景就抓瞎。所以我想先给你一张简化版的数据流动地图应用层你的业务代码决定了发送什么数据如何解析收到的数据。传输层TCP/UDP负责端到端的传输TCP还要保证可靠、有序、不丢失。网络层IP负责跨设备的寻址和路由把数据包从一台机器送到另一台机器。链路层和物理层网卡、网线、Wi-Fi负责实际传输比特流。程序员的网络编程主战场在传输层绝大多数你的工作就是在这层选择合适的协议并通过socket API把数据交付给内核。1.2 先搞清楚你是哪一方再谈代码怎么写网络通信里角色的区分非常重要。写服务端和写客户端的思路差异非常大。服务端的核心动作是“监听、接收连接、处理请求”客户端的核心动作是“主动发起连接、发送请求、等待响应”。很多初学者把两端代码混在一起想自然会觉得乱。有个很形象的类比服务端像是前台接待处先把自己的电话号码端口公布出去然后一直坐在位置上等电话进来客户端则是一个想咨询问题的访客得先知道接待处的号码主动拨号进去。拨通之后两边才能开始对话。这个类比还能帮你理解一个基本概念TCP连接是四元组决定的即源IP、源端口、目标IP、目标端口。服务端listen的端口是固定的但每个客户端的源端口是随机分配的所以服务端可以同时“接待”成千上万个连接而不需要为每个连接单独开一个端口。很多新手误以为“一个客户端连接就要占用服务端一个新端口”这是错的。后面的代码实操里你会看到服务端accept一个连接后会拿到一个独立的socket但这个socket仍然属于同一个监听端口。理解了这个你才能理解为什么一个服务进程能支撑数万个并发连接。1.3 TCP还是UDP不是性能好坏的问题是需求匹配的问题关于TCP和UDP最容易出现的误解是“TCP可靠所以更好UDP不可靠所以更差”。这句话误导了很多人。TCP和UDP是面向不同场景设计的两种工具谈不上谁优于谁。TCP最核心的价值是提供了一条可靠的字节流管道。它做了三件大事保证数据按序到达、保证不丢失、提供流量控制和拥塞控制。代价是连接的维护成本高有三次握手和四次挥手的开销头部信息更重而且由于内核会自动重传延迟的抖动可能更大。UDP则只做一件事把数据报发出去发完就完事不管到没到、到得对不对。它的头部极短没有握手过程几乎没有额外延迟。所以它在实时音视频、游戏同步、DNS查询这些“宁可丢一帧也不要卡半秒”的场景里反而更合适。实际项目里你大概率会两个都要用或者用TCP作为主通道、UDP作为辅助通道。重要的是你想清楚业务对“实时性”和“可靠性”的权重。比如文件传输必须用TCP因为丢一个字节文件就废了直播画面就可以用UDP偶尔丢几帧观众根本感知不到。2. 从三次握手到API调用TCP的可靠性并不是靠魔法很多人背过TCP三次握手和四次挥手的图但转身就把它们和socket API的作用对应不上了。这一章我先把TCP可靠性的核心机制讲透再告诉你这些机制到底是在调用哪个API时被触发的。2.1 TCP为什么敢说自己可靠确认、重传和序号TCP保证可靠的思路本质上是“收到说一声没收到就再来一次”。发送方给每个数据字节编一个序号接收方收到后返回一个确认号ACK表示“我这边已经收到了多少字节”。如果发送方在一定时间内没收到ACK就认为包丢了重新发送一次。这套机制初看简单但里面有两个非常影响性能的细节。第一个是窗口接收方在ACK里会带上自己的接收缓冲区剩余容量告诉发送方“你最多还能再发这么多”这叫滑动窗口机制。这样发送方不会一口气把接收方撑爆。第二个是拥塞控制TCP会根据网络环境的丢包和延迟动态调整发送速度而不是机械地按接收方窗口来。这也是为什么TCP在网络拥堵时会自动降低速度UDP则无动于衷。听上去这些机制好像很复杂但对你写代码来说它们都是内核自动完成的。你在应用层调用send()时数据未必真的立刻离开本机它会先进入内核的发送缓冲区调用recv()时数据也未必是网络包刚到而可能是早就躺在接收缓冲区里了。理解这一点非常重要因为后面讲阻塞、粘包问题时你都要回到“缓冲区”这个视角。2.2 三次握手对应到APIconnect、accept和内核的无声配合很多教材直接画三根箭头就完了没有告诉你这三次握手在哪几个函数之间被触发。其实过程是这样的客户端调用connect()时内核开始向服务端发送SYN报文然后客户端进入SYN_SENT状态。服务端的内核收到SYN后会回复SYNACK同时把这条连接标记为SYN_RCVD状态。但这个状态下的连接服务端应用层是感知不到的因为它还没被accept()处理。客户端收到SYNACK后再回一个ACK同时connect()成功返回。服务端收到这个ACK后连接变为ESTABLISHED状态内核将它放入一个已完成连接队列等待应用层调用accept()把它取出来。这里有一个关键认知三次握手是操作系统内核完成的并不需要应用层参与。你的代码只负责发起连接、接受连接。所以当你看到TCP状态是SYN_RCVD时别慌可能是内核已经收到SYN但应用层还没调accept()。反过来如果服务端程序accept()循环写得慢已完成连接队列填满新连接就会连接超时但这和握手本身关系不大。2.3 四次挥手与TIME_WAIT一个容易被误读的“隐患”关闭连接的四次挥手同样重要但也同样容易被忽视。客户端调用close()时内核发送FIN报文对端收到后返回ACK同时应用层的recv()会返回0表示“对端关闭了写方向”。对端如果也调用close()会再发一个FIN发起方收到后返回ACK这时连接才真正关闭。最容易被误解的是TIME_WAIT状态。主动关闭连接的一方在发送完最后一个ACK之后不会立刻把连接状态清掉而是停留在TIME_WAIT状态持续大约2MSL报文最大存活时间——Linux上通常是60秒。这个设计有两个原因一是怕最后一个ACK丢失需要留着状态以便重发二是防止旧连接里的数据包混入新的同样四元组的连接里。实际项目中TIME_WAIT状态的连接一多会导致大量端口被占用尤其在服务端主动关闭连接的场景下。这也是为什么你在写服务端时经常会设置SO_REUSEADDR这个socket选项。它允许新启动的服务端进程绑定同一个端口即使还有一堆TIME_WAIT状态的旧连接残留。3. 动手写一个能跑起来的回显服务把理论换成代码理论讲得再多不落到代码上都是空谈。我从最简单的回显服务开始给你一份能直接跑通的最小实现然后逐步加上并发处理。全程用Python的socket模块因为它是标准库几乎任何机器上都能直接跑而且代码足够短能把网络编程的核心骨架完整暴露出来。3.1 服务端socket、bind、listen、accept的标准流程先看服务端代码。这一小段代码里浓缩了网络编程最关键的四步import socket server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((0.0.0.0, 9000)) server_socket.listen(5) print(server listening on 0.0.0.0:9000) while True: conn, addr server_socket.accept() print(fclient connected from {addr}) while True: data conn.recv(1024) if not data: break conn.sendall(data) conn.close()逐行解释一下。第一行的AF_INET表示使用IPv4地址SOCK_STREAM表示使用流式套接字也就是TCP。如果你想用UDP就改成SOCK_DGRAM但后续所有读写方式都会变。setsockopt里的SO_REUSEADDR前面已经说过专门用来对付TIME_WAIT状态下的端口被占用问题。bind((0.0.0.0, 9000)) 表示监听本机所有网卡上的9000端口。如果你只想让本机访问可以绑127.0.0.1如果想开放到局域网一般绑0.0.0.0。listen(5) 的参数是已完成连接队列的最大长度表示内核最多帮你缓存几个尚未被accept处理的连接超过之后新连接会被拒绝。accept() 是阻塞调用程序会停在这里直到有客户端连进来。一旦有连接到达它返回一个新的socket对象conn和客户端的地址addr。注意后面所有的数据收发都通过conn而不是原来的server_socketserver_socket只负责继续接收新连接。内部循环里的recv(1024)每次最多读取1024字节。如果客户端关闭了连接recv会返回空字节串b这时循环退出调用close()关闭这条连接。sendall(data)保证把data全部发送出去它和send()的区别在后面的坑里细说。3.2 客户端connect与send/recv的对称操作客户端代码要简单得多核心就是connect然后收发数据import socket client_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.settimeout(3) client_socket.connect((127.0.0.1, 9000)) client_socket.sendall(bhello, tcp) response client_socket.recv(1024) print(freceived: {response}) client_socket.close()connect()会触发三次握手成功返回后客户端和服务端的连接就建立了。这里重点提醒一下settimeout(3)这个调用。默认情况下socket是阻塞模式connect如果对端没响应会一直阻塞下去程序看起来像“卡死”了。设了超时后connect超过3秒就会抛socket.timeout异常。这在实战里极其重要因为真实网络环境里连接超时是常态不能无限期等下去。sendall(bhello, tcp)把数据发出去recv(1024)等服务端回显。因为服务端是把内容原样返回所以response里应该能看到hello, tcp。把服务端和客户端分别跑起来后你会看到服务端打印出客户端的地址客户端打印出收到的回显内容。这个最简单的闭环就是网络编程的基础模型:连接、发送、接收、关闭。3.3 从单连接到并发多客户端接入的问题与解决上面的服务端有个明显的缺陷它是单线程的当第一个客户端连上并进入内部while循环后整个程序就一直卡在recv那里其他客户端根本连不上。这是网络编程新手最容易踩的坑——你的服务端必须能同时处理多个连接。最直接的解法是用多线程每接收一个连接就创建一个线程去处理它。import socket import threading def handle_client(conn, addr): print(fclient connected from {addr}) try: while True: data conn.recv(1024) if not data: break conn.sendall(data) except ConnectionResetError: print(fclient {addr} reset the connection) finally: conn.close() server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((0.0.0.0, 9000)) server_socket.listen(5) print(server listening on 0.0.0.0:9000) while True: conn, addr server_socket.accept() thread threading.Thread(targethandle_client, args(conn, addr)) thread.start()这里有个细节要注意handle_client里我加了ConnectionResetError的捕获。真实网络环境里客户端可能因为断电、崩溃、强制关闭而造成连接被重置此时recv或send会抛异常。如果不做处理服务端线程会直接崩掉虽然主循环还能继续accept但日志里会不断刷异常堆栈线上排查问题时会非常难受。多线程方案在连接数几百个时完全没有问题但到几千个并发连接时线程本身的开销就会成为瓶颈。那时候就需要换成I/O多路复用方案这我在最后一章会讲。但对于大多数入门项目和学习场景多线程已经足够你理解“连接管理”的核心思路。4. 用抓包让协议自己说话验证比背诵更重要有一句话我想强调很多遍学网络编程抓包应该和写代码一样频繁。你背一百遍TCP状态图不如自己在Wireshark里看一次握手过程来得深刻。抓包工具会让协议从纸面的抽象概念变成屏幕上你能逐字节检查的报文这是打通理论和实践最直观的桥梁。4.1 Wireshark的入门操作过滤规则与界面布局Wireshark是全平台可用的开源抓包工具我默认你已经会安装。打开后选择要监听的网卡然后就可以开始抓包。如果客户端和服务端跑在同一台机器上要选择回环接口loopback通常叫lo或Loopback: lo0如果客户端和服务端在局域网里的两台电脑上就选择对应的物理网卡。抓包界面默认会滚动刷出大量数据需要立刻输入过滤规则。针对我们刚才写好的服务端最常用的过滤规则是tcp.port 9000这会只看端口9000上的TCP报文屏蔽所有噪声。如果你同时跑着大量其他服务也可以加一个ip地址过滤tcp.port 9000 ip.addr 127.0.0.1Wireshark的每一行都代表一个报文关键字段包括源地址、目标地址、协议类型、长度以及底部的详细标志位。你点开一个TCP报文就能看到Source Port、Destination Port、Sequence Number、Acknowledgment Number还有一堆FLAGS标志位。4.2 亲手捕获三次握手的全过程现在做一个小实验先启动抓包再运行客户端连接一次服务端然后停止抓包。你会在过滤结果里看到一连串TCP报文最前面的三个报文就是三次握手客户端向服务端发送一个SYN报文客户端自己进入SYN_SENT状态。服务端回复SYNACK表示“我收到了我也准备好建立了”。客户端再回一个ACK双方进入ESTABLISHED状态。在Wireshark里怎么快速识别这三个报文呢你不需要逐字节分析直接看下方面板里TCP标志位三个包分别是SYN、SYNACK、ACK。你也可以右键在对话过滤里选择“TCP Stream”把整个连接的所有报文按顺序列出来非常直观。我建议你重点做一件事找到第一个SYN报文的序号再看第二个报文的确认号你会发现第二个报文的Acknowledgment Number等于第一个报文的Sequence Number加1。这个细小的数字关系就是TCP“序号确认”可靠性机制的直接体现。当你在屏幕上亲眼看到这个数字变化比背十遍书都管用。4.3 观察实际传输过程拆包、确认与窗口更新三次握手之后就是正常的HTTP或自定义数据交换。如果你用一个长度较长的消息做实验比如让客户端发送1MB的数据你会在抓包里看到非常有意思的现象这1MB数据被TCP切分成多个报文段每个报文段都带有一个不同的Sequence Number每隔一段还会传来一个ACK以及接收窗口的更新。这就是TCP的“字节流滑动窗口”机制的现场。你在应用层调send()传入1MB数据内核会自动把它切成符合MSS最大报文段长度大小的分片逐块发送。对端的内核把分片重新按序组装再通过recv()交给应用层。所以从应用层视角看你可能感觉自己在读一个连续的文件但底层早被拆成了几百个网络包。这里有个值得动手验证的现象Wireshark会对乱序或重传的包做醒目的警告提示。如果你在同一个网络里长时间抓包尤其是无线网络环境几乎一定会看到TCP Retransmission或者TCP Out-of-Order。这能让你直观体会到TCP的可靠性不是天生免费它确实要处理网络层的丢包和乱序而这些在上层代码里你完全感知不到。4.4 四次挥手与TIME_WAIT的现场观察最后再做一个小实验跑完客户端程序后让它自然退出抓包工具的过滤结果里会看到两个FIN报文和对应的ACK报文。主动关闭方发出FIN后会进入FIN_WAIT_1、FIN_WAIT_2最后进入TIME_WAIT状态被动关闭方则走CLOSE_WAIT、LAST_ACK等状态。在Wireshark里观察后你可以在终端里执行一条命令看到TIME_WAIT的残留netstat -an | grep 9000你会发现连着的TCP连接状态是TIME_WAIT而且会持续约一分钟。很多人第一次看到这个都以为程序出bug了其实这只是TCP主动关闭方必须经过的状态目的是确保最后一个ACK能安全送达。理解了这个你以后写服务端遇到端口频繁重启报错时就能快速定位大概率就是SO_REUSEADDR没设置。5. 实战代码里绕不开的坑粘包、超时与异常断开跑通回显服务只是第一步真正让人头疼的是那些在教科书里一个章节都没提、但在生产环境里天天踩的问题。这一章我集中讲四个高频坑粘包和半包、发送和接收的缓冲区语义、超时与心跳、异常断开后的处理。5.1 粘包和半包TCP没有消息边界粘包是网络编程新手第一次真正感受到“理论与实践的差距”的地方。你用send()发送两次数据第一次发送“hello”第二次发送“world”对端recv()一次读到的可能是“helloworld”也可能只读到“hel”这就是粘包和半包。很多人第一反应是“TCP需要处理消息边界”但这句话其实不太精确。TCP本身是字节流协议它根本不关心你的应用层一条消息从哪里开始到哪里结束。就好比把一桶水倒进管道对端只能按自己水量接接多接少取决于两端缓冲区而不取决于你倒水时是不是分成两段倒的。要解决消息边界问题只能在上层协议层自己定义规则。实战里最常用的有三种固定长度法每条消息都是固定长度比如100字节不够就补空格。简单有效但浪费带宽。分隔符法每条消息末尾放一个特殊字符比如\r\n。HTTP协议用的就是这种思路。但如果消息体本身包含这个分隔符就会出问题。长度字段法在消息头里用4字节整型记录后面的载荷长度接收方先读取头部知道自己要读多少字节再循环recv直到读够。这是目前生产环境最通用的方案。长度字段法的实现逻辑不复杂发送方先把长度值struct.pack成4字节二进制紧接着发送载荷接收方先recv 4字节解析长度N然后循环recv直到收满N字节。你可以把这段逻辑封装成一个函数这样每次调用recv_exact(n)就能保证读满n字节不再被粘包半包困扰。5.2 recv和send的“看似对称实则不对称”教科书示例经常教你recv和send配对用仿佛每次send一定对应一次recv。但真实代码里必须清楚两个不对称性。第一个不对称性在接收侧recv(n)表示“最多读取n字节”它可能只返回几字节即使数据远不止这些。所以涉及大消息时必须循环recv才能拼出完整数据。这就是上面长度字段法里recv_exact要做的事。第二个不对称性在发送侧send(data)不保证一次把data全部发出去它返回的是实际发送的字节数可能小于len(data)。尤其在发送缓冲区快满、网络拥塞或者对端接收缓慢时这种情况会很常见。如果忽略这个返回值数据会静默丢失。Python的sendall()封装了这个循环但底层C接口和很多语言的原生socket都得你自己写循环。这也是为什么“能用现成框架就别徒手撸”在团队协作里更常见。5.3 阻塞、超时和心跳防止程序“假死”默认socket是阻塞模式recv会一直等数据connect会一直等连接建立。这在网络故障时会造成程序看似死锁实际只是卡在内核里。所以生产环境里每个socket都应该设置超时时间不让任何一次等待变成无限期等待。settimeout是Python里最便捷的设置超时的方式。更细粒度的方法是使用select模块它可以同时监控多个socket的可读、可写状态并支持指定超时时间。这可以作为从多线程到I/O多路复用的中间跳板。除了超时还有一个更现实的问题对方进程崩溃或网络断开时你这边什么时候才能感知答案可能让你意外如果对方直接断电你的socket可能很久都没任何反应因为TCP连接是“软状态”没有报文交互时双方都不知道对方是否还活着。为了及时发现死连接实践中普遍采用心跳机制常见做法是每N秒发送一个心跳包连续M次没收到响应就判定连接死亡。心跳机制有两种实现思路。应用层心跳是业务代码自己定时发送一个PING消息框架层心跳则像WebSocket或Netty那样在协议层面内置。对大多数服务来说最简单的方案是约定一种空消息作为心跳客户端超时未收到任何数据就主动断开重连。5.4 连接异常断开时的异常处理策略写网络服务端时千万别假设所有客户端都会礼貌地先发FIN再退出。断电、拔网线、程序崩溃都会导致连接被野蛮断开。对于服务端这通常表现为recv抛出异常或在握手阶段出现ConnectionResetError。处理策略应当明确收到异常就关闭这条连接释放对应的线程和资源不要在多线程里让异常穿透主流程。上面的代码里我用try-except-finally包住了整个客户端处理流程目的就是保证资源一定会被释放。对于客户端异常断开后的策略通常是“自动重连但要带退避”。直接粗暴地死循环重连会雪崩式打爆服务端甚至连自己的机器也会被拖垮。推荐使用指数退避算法第一次断开等1秒重连第二次等2秒第三次等4秒直到达到上限比如30秒然后保持该间隔继续尝试。这样既能快速恢复偶发故障又不会在网络大面积故障时造成额外的连接风暴。6. 从会写到会设计下一步的进阶路径如果上面这些内容你都已经跑通并理解恭喜你你已经具备了网络编程最核心的基础认知。但离企业级开发还有一段路要走。这一章我聊聊我实际项目里的进阶路径按顺序走通常效率最高。6.1 从多线程到I/O多路复用处理连接数的数量级跨越多线程方案在几百个连接时没问题但到几千上万个连接时线程上下文切换的开销会严重拖累服务端。这个时候你需要I/O多路复用核心思路是让一个线程同时监控成千上万个socket谁有数据就处理谁没有数据就去睡觉。Linux下的发展脉络是select - poll - epoll。select能监控的文件描述符有数量上限poll解决了上限问题但性能仍然随监听数量线性增长epoll则只返回“就绪”的socket复杂度降至O(1)。Java NIO里的Selector、Netty的EventLoop底层都是基于epoll这类机制做的封装。Python里也有selectors库建议从它开始练手。有些人不理解为什么这一步这么重要。简单说你写网络服务最终要面对的是“一台机器要扛住几万甚至几十万连接”的考验多线程模型到不了这个量级。学习IO多路复用时不要只停留在API上去看它的事件循环模型一个循环里注册事件、等待事件、分发处理。明白了这个你就明白了目前绝大多数高性能网络框架的地基。6.2 从原生socket到成熟框架什么时候该上框架原生socket适合学习适合做极简单的工具但生产项目里我强烈建议用成熟框架。原因很简单框架已经替你解决了粘包、断线重连、心跳、编解码、线程池、背压等一系列问题你不需要从零造轮子。Java生态用NettyPython生态可以用asyncio或TwistedC生态有Boost.AsioGo语言则直接内置了goroutine和net包写并发网络服务极其顺手。上框架之前先问自己一个现实问题你真的需要这些新特性吗如果你的业务只是几个客户端连一个服务端那多线程原生socket就够了引入框架反而增加复杂度。如果野心是做一个高吞吐、高并发的网关或消息推送服务那框架能帮你省下数以月计的调试时间。6.3 协议设计是网络编程的分水岭同样是通信为什么有的系统稳定跑十年有的线上天天出bug差异往往出在协议设计上。文本协议如JSON、XML便于排查问题但序列化开销大二进制协议如Protobuf、MessagePack性能好但调试时要借助专门工具。目前行业里的主流做法是外部接口、对内接口的分层协商控制面用可读性强的文本协议数据面用紧凑的二进制协议。无论选哪种都要把“协议版本号”和“消息类型”设计进头部为未来的演进留好余地。我见过太多团队第一版协议没有版本字段后面上线改协议时必须全量升级客户端那场面极其痛苦。如果你正在设计自己的协议强烈建议头部至少包含魔数、版本号、消息类型、消息长度四个基础字段。至于更高级的主题比如零拷贝、吞吐调优、全双工通信、连接池、流控算法这些东西不必一开始就啃但你至少要知道它们的存在等真正遇到性能瓶颈时再针对性深入。学网络编程最忌贪多嚼不烂把基础链路吃透后面任何框架和知识点都是水到渠成。我个人的体会是网络编程是所有后端技术里最不应该靠“背”来学的。它天生适合动手适合用抓包工具去验证适合用测试代码去压一压。如果你现在只能做一件事别急着背新概念先用Wireshark把你自己写过的程序完整看一遍从三次握手到数据传输再到挥手你会发现自己对整个协议栈的理解瞬间上升一个层级。之后再去读那些讲TCP/IP和socket源码的书你会觉得每一行都写在了你已经见到过的现实上。
返回列表