
很多学JavaEE的同学前期写Servlet、Spring MVC都挺顺一旦进入联调、部署、排查线上故障的阶段看见报错里那一堆IP、端口、协议栈信息就开始发懵。我带新人的时候发现一个共性代码能力没问题但网络基础是普遍软肋。这篇《JavaEE初阶网络初识》就是想把JavaEE开发必须掌握的网络知识做一个系统梳理。文章会覆盖网络分层模型、IP端口和DNS、HTTP请求完整流程、TCP三次握手、Socket编程以及日常调试工具和典型故障案例。适合刚入JavaEE的初学者补齐网络短板也适合工作一两年但对网络细节仍然含糊的开发当参考手册。1. 为什么JavaEE一定要懂网络它不只是网工的事1.1 网络是JavaEE应用的“隐形骨架”你写一个最简Spring Boot接口浏览器发一个请求Tomcat监听到Socket上的数据解析出HTTP报文再交给DispatcherServlet处理最后把JSON写回Socket。这个过程链路中除了你的Java代码剩下的全是网络通信。更不用说生产环境里应用要连接MySQL、Redis、消息队列服务间要做RPC调用JavaEE应用本质上就是一台通过端口与世界对话的机器。很多新手喜欢把问题隔离在“我只管业务逻辑”但现实是数据库连接超时要看网络接口响应慢要抓包看TCP重传跨网段调用要检查路由甚至改一个防火墙规则就能让整个服务挂掉。网络不是可选项而是JavaEE应用正常运转的骨架。不懂网络你写出的代码就像在流沙上盖楼看起来功能完好遇到联调排查就塌。1.2 JavaEE开发者的网络知识边界到底需要学到什么程度我见过有同学抱着《TCP/IP详解》啃一啃就是一个月最后连中间人攻击都在查。这其实跑偏了。JavaEE开发不是网络工程师不需要会配置交换机不需要精通BGP路由协议但下面这些是底线IP地址、子网掩码、公网私网的区分端口范围与常见服务端口DNS解析过程与常见问题TCP和UDP的区别TCP三次握手、四次挥手HTTP请求/响应结构状态码含义Socket通信原理常用命令ping、telnet、nc、curl、netstat、lsof基本抓包能力用Wireshark或tcpdump过滤关键报文按这个边界去学你会发现网络知识直接服务日常开发不会变成纯理论的负担。我在后文会围绕这些点展开尽量用开发场景做例子而不是背书式地讲原理。2. 网络分层模型数据在网络上到底怎么“跑”的2.1 OSI七层模型与TCP/IP四层模型的对应关系教科书上讲OSI七层但实际干活时大家用的是TCP/IP四层模型。我建议把两者对照着理解先记住每一层的职责再记对应协议。TCP/IP四层对应OSI层职责常见协议/工具应用层应用层、表示层、会话层生成业务数据、格式化、管理会话HTTP、HTTPS、DNS、FTP、SSH传输层传输层端到端通信、端口区分、可靠传输TCP、UDP网络层网络层逻辑寻址、路由选择、分组转发IP、ICMP、IGMP网络接口层数据链路层、物理层物理传输、MAC寻址、帧封装Ethernet、Wi-Fi、ARPJavaEE开发最常打交道的是应用层和传输层。应用层是你写的Controller、HTTP请求、返回的JSON传输层是决定数据能不能可靠抵达的TCP和UDP。网络层和网络接口层日常被框架和系统屏蔽了但排查网络不通时你会用到ICMP的ping、抓包看到IP报文所以至少要能看懂。2.2 数据封装与解封装一次HTTP请求要套几个“信封”很多同学不理解为什么抓包时看到的不是干净的JSON而是一个个十六进制的报文。其实网络通信就是不断加信封和拆信封的过程。假设你的浏览器向服务器发送一个JSON请求应用层生成HTTP报文请求行、请求头、请求体传输层给HTTP报文加上TCP头形成TCP段头里有源端口和目标端口、序列号等网络层加上IP头形成IP包头里有源IP和目标IP网络接口层加上MAC帧头尾形成帧在物理链路上传输服务器接收时逆序拆封网卡拆掉MAC帧头IP层拆掉IP头TCP层拆掉TCP头最后把HTTP报文交给应用层。这个过程和寄快递高度相似你写的是内容HTTP报文快递单是传输层信息端口地址是网络层信息IP快递员的配送路线是链路层信息MAC地址。理解这个你才知道MTU、TCP分段、粘包这些概念来自哪里。2.3 网络拓扑你们公司为什么会有测试环境和生产环境隔离热词里出现很多“网络拓扑”JavaEE入门也得知道一点。开发环境是一台机器上跑所有服务属于最简单的单节点。测试环境通常是几台服务器组成的小型局域网络Nginx、应用、数据库分开放用私网IP互联。生产环境则复杂得多可能跨机房、多可用区、负载均衡前还有防火墙和WAF。为什么要把测试环境和生产环境隔离因为网络域隔离是基本的安全原则。测试环境网络策略一般开放方便调试生产环境只开放必要的端口比如只允许负载均衡的IP访问应用端口应用服务器连数据库使用独立内网网段。这样即使应用被攻破攻击者也不能直接通过测试网络摸到生产数据。新人联调时要先搞清楚自己在哪个环境别上来就怀疑代码先确认目标IP和端口是否在当前网络可达。3. 核心概念拆解IP、端口、DNS、协议每个都要能吃透3.1 IP地址区分设备还分公网和私网IP地址是网络层的门牌号。IPv4是32位二进制数写成点分十进制就是192.168.1.100这种。IPv6是128位写成十六进制加冒号的形式地址耗尽之后它已经全面普及将来你会越来越多地见到240e:...这样的地址。JavaEE开发里你需要记住几个特殊地址127.0.0.1和localhost本机回环地址只有本机能访问0.0.0.0监听所有网卡地址服务器部署时常用10.0.0.0/8、172.16.0.0/12、192.168.0.0/16私网地址段内网服务器常用公网IP全球唯一但普通人家里的宽带一般不会直接分到公网IPv4很多同学在Spring Boot里配server.address0.0.0.0时不明白含义简单说配localhost只有本机能访问配0.0.0.0才能让局域网其他机器访问。线上部署如果是单机这两个差别不大但涉及Docker、云服务器时理解它们能省不少排错时间。3.2 端口一台机器上区分不同服务的“窗口”IP地址找到机器端口号找到机器上的进程。TCP/UDP头的端口号占16位范围是0到65535。0到1023是知名端口一般需要管理员权限绑定比如HTTP的80、HTTPS的443、MySQL的3306、Redis的6379。应用开发常用8080、9090这些高位端口。日常开发中端口坑非常多端口被占用比如两个Spring Boot项目都默认8080第二个就启动不起来防火墙放行云服务器安全组没开端口外部永远连不上进程监听地址不对服务监听了127.0.0.1其他机器即使同一局域网也访问不了排查的时候本地Linux用lsof -i:8080Windows用netstat -ano | findstr 8080就能查出端口被谁占用。我见过最经典的坑Tomcat启动看似成功但日志里显示“Port 8080 required by Tomcat v9.0 Server is already in use”一查是IDEA残留进程占着端口。这时候杀掉对应PID或者改配置切到其他端口就好。3.3 DNS把难记的IP翻译成好记的域名谁会去记百度的IP大家记的是www.baidu.com。DNS域名系统负责把域名解析成IP。完整解析过程大概是浏览器缓存、本地操作系统hosts文件、本地DNS服务商、逐级查询根域名服务器。开发调试时DNS最常见的操作是改hosts文件把域名强制解析到指定IP。这个技巧在本地连联调环境、模拟线上域名时特别有用。Windows的hosts在C:\Windows\System32\drivers\etc\hostsLinux和macOS在/etc/hosts格式都是IP 域名一行一个。如果遇到“我不知道为什么域名访问不了别人那可以”先ping 域名看解析出来是什么IP再用nslookup 域名看DNS服务器返回。绝大多数情况要么是本地hosts写错要么是公司内网DNS没配好。这里是JavaEE开发必会的基础因为很多企业内网应用就是用内网域名互相调用域名解析不对连接就失败。3.4 TCP、UDP、HTTP三个绕不开的协议TCP是面向连接的可靠协议有确认、重传、排序机制就像一个靠谱的快递员你寄的东西他保证送到丢了他会补寄。UDP是无连接的尽力而为协议不保证到达但快适合视频通话、DNS查询。JavaEE开发最直观的感受是HTTP跑在TCP上所以你调一个接口如果网络不好浏览器会反复重试这是因为TCP底层在帮你保证可靠性。而DNS查询使用UDP一次请求响应就完事丢了就再发一次不需要建立连接。HTTP本身是无状态的应用层协议你发一个GET /user/1的请求服务器回一个JSON就结束了。为什么购物车能记住你是谁靠的是Cookie和Session本质上是在无状态的协议上人为补了状态。HTTPS就是HTTPTLS加密在TCP之上加了安全层JavaEE的接口如果是HTTPS中间人抓包只能看到密文这对生产环境非常重要。4. 一次完整的网络通信实操从HTTP请求到Socket编程4.1 用curl和nc手动发一个HTTP请求理论知识讲再多不如动手抓一次。我用一个最简单的例子演示。假设本机有个Spring Boot服务跑在8080接口是GET /hello。用curl加上-v参数能看到完整报文交互curl -v http://localhost:8080/hello输出里最核心的内容分三块TCP连接建立、请求头、响应头。你会看到类似下面的信息* Connected to localhost (127.0.0.1) port 8080 GET /hello HTTP/1.1 Host: localhost:8080 User-Agent: curl/8.0 HTTP/1.1 200 Content-Type: application/json ...从这里你可以直观理解HTTP报文就是纯文本。如果你想更“原始”一点用ncNetcat直接往8080端口敲一段HTTP请求printf GET /hello HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n | nc localhost 8080TCP是流式协议HTTP报文在TCP里就是一段连续字节流服务端按\r\n\r\n来切分请求头和请求体。这个实操能让你彻底明白请求到后端时Spring MVC到底在解析什么就是这段有格式的文本。4.2 TCP三次握手和四次挥手连接不是瞬间建立的为什么TCP要三次握手简单说因为TCP是全双工的可靠传输双方必须确认对方的收发能力都没问题。第一次握手客户端发SYN告诉服务器“我要建立连接”。 第二次握手服务器回SYNACK表示“收到我也准备好了”。 第三次握手客户端再发ACK告诉服务器“我收到你的确认了连接建立”。为什么两次不行我经常用一个类比。A对B说“你能听见吗”B回“能听见”但A不知道B能不能听见自己。必须A再回一句“我也听见了”双方才确认链路双向通畅。三次握手算是网络基础里最常被问到的知识也是排查“连接超时”问题的理论依据如果你在一个能通IP但端口不通的环境里看到的往往就是握手无响应。四次挥手则是因为TCP连接是双向的每一方都要独立关闭自己的通道。断开时主动方发FIN被动方回ACK被动方再发FIN主动方再回ACK所以是四次。实际开发中你从日志看到很多TIME_WAIT状态就是因为主动断开的一方要等待足够时间确保对方收到最后的ACK这是正常的但堆积太多就可能占满端口。我在给新人讲这个知识点时会让他们用Wireshark抓一次握手包印象比看十遍图都深。Wireshark过滤规则记两个常用的tcp.port 8080抓指定端口的TCP报文http只抓HTTP报文。抓本机回环流量时Windows下要装Npcap勾选“loopback”才能抓127.0.0.1的包这个坑我踩过装完Wireshark死活抓不到localhost就是因为没装Npcap。4.3 Socket通信Tomcat的本质就是监听一个SocketJavaEE应用服务器Tomcat底层就是Java的ServerSocket监听端口循环接收客户端的Socket连接解析HTTP数据。写一个最简回显服务器能帮助你理解这个过程。import java.io.*; import java.net.*; public class SimpleServer { public static void main(String[] args) throws IOException { ServerSocket server new ServerSocket(9000); System.out.println(Server listening on 9000); while (true) { Socket socket server.accept(); // 阻塞等待客户端连接 BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out new PrintWriter(socket.getOutputStream(), true); String line; while ((line in.readLine()) ! null) { if (line.isEmpty()) break; // 读到空行HTTP头结束 System.out.println(line); } out.println(HTTP/1.1 200 OK); out.println(Content-Type: text/plain); out.println(); out.println(Hello, Client!); socket.close(); } } }客户端可以用刚才的nc命令去连printf GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n | nc localhost 9000你会发现服务端控制台打印了请求头客户端收到了“Hello, Client!”。Tomcat本质上就是把这段流程工业化了加了线程池、请求分发、Servlet容器等。理解了Socket你排查“为什么Tomcat启动不了但端口又开着”这种问题就有了基础思路先看端口是否真实被Java进程占用再看防火墙是否挡了外部访问。5. 常用网络排查工具与实战技巧把问题可视化5.1 ping、telnet、nc、curl四个命令覆盖大部分排查场景日常联调我习惯按这个顺序排查网络问题ping 目标IP先确认网络层通不通。注意很多云服务器禁pingping不通不代表不通但通了基本说明IP层没问题。telnet 目标IP 端口确认目标端口是否可达。端口不通会提示Connection refused或一直卡住后者多半被防火墙或安全组拦截。curl -v 地址看应用层交互情况能看到解析出的IP、使用的端口、请求响应细节。nc需要往端口发自定义数据时用比telnet更灵活。举个例子本地启动了一个Spring Boot项目端口8080同一内网的另一台机器访问不了。先在服务器上跑netstat -tlnp | grep 8080看监听地址是不是0.0.0.0如果监听的是127.0.0.1那别人当然访问不了。然后从客户端ping 服务器IP如果是通的再telnet 服务器IP 8080不通的话十有八九是云安全组或服务器防火墙没放行。5.2 抓包工具Wireshark和tcpdump的正确打开方式排查开发环境时Wireshark的图形界面非常直观。新手上手记住三件事选择正确的网卡、设置合理的过滤条件、知道怎么看关键报文。常用过滤表达式http.request tcp.port 8080 ip.addr 192.168.1.100 tcp.flags.syn 1抓HTTP请求能找到请求路径、User-Agent、Cookie、响应状态码。抓TCP能看到三次握手的SYN、SYN-ACK、ACK四次挥手的FIN报文以及大量重传时的TCP Retransmission。重传次数突然变多说明链路丢包严重这就是网络质量问题不是你Java代码能解决的。服务器上没法用图形界面就用tcpdump。抓包前先确认网卡名称比如eth0然后用sudo tcpdump -i eth0 tcp port 8080 -w /tmp/http.pcap-w保存成pcap文件拿回本地用Wireshark打开分析。我在生产上排查过一个“接口偶尔超时”的问题抓包后发现是TCP握手阶段有大量SYN重传定位到是IDC网络抖动而不是MySQL慢查询这个结论就是靠抓包得出的。5.3 服务器网络利用率别被“带宽大”骗了热词里提到“服务器网络利用率”这确实值得说。网络利用率是单位时间内实际传输的比特数与理论带宽的比值用iftop可以实时看各连接的流量用sar -n DEV 1可以看历史的网卡吞吐。sar -n DEV 1 5 iftop -i eth0 -n很多人以为网络利用率越高越好其实如果利用率长期超过80%到90%丢包率和延迟都会明显上升。JavaEE应用如果大量传输大JSON或文件万兆网卡也可能被打满。这时单纯看响应时间是查不出问题的要结合流量和CPU看先用top看进程CPU再用iftop看是不是某个IP在疯狂拉数据。我在调优一个导出接口时就是发现每次导出都会把千兆网卡打成100%才决定改成分批传输和压缩而不是继续在后端加缓存。5.4 Docker容器网络JavaEE本地开发必踩的坑很多人的开发环境现在都容器化Spring Boot跑在Docker里网络又套了一层。Docker默认bridge网络模式会分配一个172.17.0.x网段的IP容器内端口要靠-p 8080:8080映射到宿主机。常见问题就是“容器里服务正常宿主机访问不了”。先检查端口映射docker ps看PORTS列如果是0.0.0.0:8080-8080/tcp那宿主机8080应该能通。如果没映射容器IP是172.x宿主机要用docker exec -it 容器名 bash进容器curl 127.0.0.1:8080确认服务确实在监听。还有人会把应用配成连接localhost:3306的MySQL但在容器里localhost指向容器本身不是宿主机于是连接失败。此时要么用host.docker.internalDocker Desktop支持要么改成宿主机在docker网络的网关地址通常默认是172.17.0.1。热词里那个“宝塔内某个容器使用宿主机网络环境”本质就是把容器的网络模式改成--network host让容器共享宿主机网络栈这样端口直接暴露在宿主机上但这也牺牲了隔离性生产要慎用。6. 网络安全的初识认知开发也要有点防守意识6.1 为什么企业网络要做VLAN和ACL隔离热词里有一组“网络域隔离-VLAN划分与ACL配置”这听起来像网工内容但JavaEE开发也需要理解。VLAN可以把一个物理局域网切成多个广播域ACL则是对数据包的源IP、目标IP、端口等做访问控制。举个实际场景一个稍大的公司里测试环境的数据库密码和生产的并不同网络层面也会把测试网段和生产网段隔开甚至服务器之间东西向流量都要经过防火墙。为什么之前就有过因为测试环境Redis没设密码被扫描器抓了结果内网数据库跟着遭殃的事故。网络隔离不是为了难为开发而是把攻击半径缩小。如果所有机器都在同一个大二层网络里一台机器被入侵其他机器可以通过内网直接访问根本不用经过任何防护。JavaEE开发在容器和云环境里的“安全组”其实就是云化的ACL你给云服务器加一个允许规则就是在网络层做白名单控制。我建议入门阶段至少要知道对外开放的端口越少越好数据库和Redis绝不暴露到公网内部服务调用用内网IP。6.2 防火墙与安全组你以为的通路可能根本不通很多联调问题查到最后真相只有一个防火墙挡了。Linux上常见的是iptables或firewalld云服务器上还有安全组。我刚工作时被生产环境一个问题折磨很久应用日志显示连接数据库超时但命令行telnet数据库IP 3306却超时后来才发现别人在数据库服务器上配置了iptables只允许特定应用服务器IP访问3306。处理这个问题的标准排查顺序应该是应用服务器上先确认自己到数据库的路由和端口通不通数据库服务器上执行firewall-cmd --list-all或iptables -L -n看是否放行了3306如果都是放行状态再抓包看SYN包是否到了数据库服务器在这里给一个Linux常见开通端口命令供参考sudo ufw allow 8080/tcp # Ubuntu 用 ufw sudo firewall-cmd --add-port8080/tcp --permanent # CentOS 用 firewalld sudo firewall-cmd --reload麒麟系统这类国产Linux方法也类似热词里提到“麒麟系统网络端口怎么配置”其实就是用firewalld或直接改/etc/sysconfig/iptables然后重启网络服务。没把握的时候先关掉防火墙测试一把再精准放行但别为图省事长期关防火墙安全是底线。6.3 安全三原则最小权限、白名单、加密网络安全初识不需要你懂渗透测试但三个原则要刻进脑子里。最小权限指服务和用户只给必要权限比如数据库账号不给DROP权限。白名单指对外访问尽量用白名单而不是黑名单没有明确允许就默认拒绝。加密指的是数据传输要上HTTPS、SSH远程要上密钥登录内部敏感数据也要考虑加密传输。热词里有“网络攻防演练知识”这个在大型企业很常见演练中常用的手法就是扫描开放端口、尝试弱口令、Web服务漏洞利用。对于JavaEE开发而言你不一定要会攻击但要知道你们开发的系统每天都在被人扫描。我自己在云主机上查过/var/log/secure日志一周内被尝试SSH登录上千次这还是在IP被限制后。所以默认不要开放不必要的端口不要用弱密码密钥登录该配就配。7. 常见网络故障排查与避坑技巧实录7.1 连接超时和拒绝连接两个完全不同的诊断方向这是JavaEE开发遇到最多的两类报错。Connection refused通常是服务端口根本没监听或监听地址不对。connect timed out则是数据包出不去或到达不了。排查顺序和命令确认服务进程是否存在ps -ef | grep java确认监听端口lsof -i:8080或netstat -tlnp | grep 8080本机试一把curl localhost:8080通则说明服务正常从外部机器telnet 服务IP 8080不通则查防火墙和安全组如果是云服务器千万别忘了安全组。我犯过一个很蠢的错本地环境配好了云服务器的安全组没放行新端口客户那边一直连不上最后查了半天才发现端口没加白名单。7.2 Linux修改DNS后网络服务没生效怎么办热词里“Linux修改DNS后重启网络还原”是一个很实际的场景。在Ubuntu上修改DNS常见做法是改/etc/resolv.conf但很多系统这个文件会被systemd-resolved覆盖。正确做法之一是编辑/etc/systemd/resolved.conf设置DNS和FallbackDNS然后重启服务sudo systemctl restart systemd-resolved有的同学改完DNS还习惯性systemctl restart network或service network restart这个命令在现代Ubuntu里经常会报错因为网络管理已经交给Netplan或NetworkManager。如果用的是Netplan改完要执行sudo netplan apply重启后DNS变了但域名解析还是旧IP通常是DNS缓存没清。本机可以sudo systemd-resolve --flush-caches代码里的JVM也有自己的DNS缓存如果应用调用远程接口域名解析不更新可能要重启Java进程或调整networkaddress.cache.ttl参数。我见过一个线上问题就是内部服务迁移IP后旧连接还在Java进程一直用缓存的旧IP访问重启应用才好最后只能改代码配置TTL。7.3 Windows本机网络查看只能看到1个设备局域网发现的小坑热词里有一条“win7系统网络查看只能看见1个设备”这虽然不是JavaEE核心但在办公环境很常见。现象是网上邻居里看不到其他电脑。原因通常是网络发现被禁用、防火墙挡了NetBIOS和SMB端口、或者跨了不同网段/工作组。排查就两步看是否同一网段再开网络发现。如果是网络隔离环境看不到其他设备反而是好事。作为JavaEE开发你在联调时只要保证能ping通、能telnet端口就够了不用太纠结网上邻居显示什么那更多是Office网络管理的事。7.4 请求数据截断、粘包、乱码别什么都赖网络热词里“网络爬虫原理”和“网络协议分析”都涉及一个现象抓到的数据包不完整。JavaEE开发处理网络数据时要注意TCP是字节流应用层一次read不一定能拿到完整的HTTP报文或RPC消息。HTTP协议自己处理了Content-Length或Transfer-Encoding: chunked的边界但自定义Socket协议就需要自己定义消息边界比如用“长度内容”的格式否则就会出现半包和粘包。还有一个常被误判的是乱码。很多人以为乱码是网络传输问题其实HTTP报文是字节流只要不是链路被篡改正常的TCP传输不会修改字节内容。乱码绝大多数是字符编码问题比如服务端返回UTF-8客户端使用GBK解析。排查时先用curl -v看响应头里的Content-Type再检查两端代码里的字符集配置不要拿“网络问题”当背锅侠。7.5 从一次“网络异常990002”案例说起错误码背后的排查套路热词里有个“网络异常990002”虽然具体上下文不清楚但这种带错误码的网络问题很典型。遇到任何网络相关错误码我建议先拆解成五个维度错误发生在哪个层是本机到网关还是网关到目标服务器错误码是超时、拒绝还是未知解析客户端和服务端的日志各显示什么相同请求是否稳定复现网络抓包结果如何有一次我排查一个APP登录失败报错码类似网络异常。客户端日志只显示“网络请求失败”服务端根本收到不到请求抓包发现客户端发往服务器IP的SYN包一直无响应最后查到是网络出口侧做了策略变更。这种问题不看网络抓包光调代码肯定调不出来。学会把错误码转换成网络层的动作再逐层排查就能少走很多弯路。8. 初识网络的最后一点体会我以前很少给新人系统讲网络总觉得这些是基础课里应该会的。但后来发现网络知识只有在实战场景里讲才能真正吸收。这里分享我个人摸索出的一套学习顺序先会用curl和nc发请求再学抓包抓完包再回头看TCP握手搞明白了再学Socket编程最后把Socket和Tomcat的请求处理流程连起来。这条路线避开了枯燥的理论每一步都能看到“原来数据长这样”。另外一个小技巧平时排查问题养成记录习惯。我笔记本里有一条很简单的脚本专门用来快速探测端口通不通大概是function port() { (echo /dev/tcp/$1/$2) /dev/null 21 echo $1:$2 open || echo $1:$2 closed; }这利用了Bash的/dev/tcp特性免安装工具就能测端口虽然比不上nc和telnet正规但在紧急时候非常管用。网络初识不必一步吃成全懂先把命令跑顺把概念串上后面遇到真实问题才会有方向到时候你自然会回头把更深的知识补齐。