十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java网络编程实战:从TCP/IP到Socket与UDP通信全解析

Java网络编程实战:从TCP/IP到Socket与UDP通信全解析 1. 网络编程这门课到底在学什么全栈视角下的底层逻辑很多人一开始学网络编程容易陷入一个误区上来就背 TCP 三次握手、四次挥手背了一堆状态码和协议名称结果写代码的时候发现自己连一个最简单的 Socket 服务端都搭不出来。这恰恰是 Java 全栈课程里网络编程“实战优先”的原因。第 21 课的核心目标很简单让一个会用 Spring Boot 写接口、会用 Vue 写页面的开发者真正搞清楚客户端和服务端之间的数据是怎么传输的并且能脱离框架独立实现一次网络通信。换句话说这一课补的是全栈开发里最底层的短板——HTTP 协议之上是业务逻辑HTTP 协议之下就是网络编程。这节课适合谁不管你是刚学完 Java 基础、准备接项目的新手还是已经能写 CRUD、但面试总被网络问题卡住的全栈开发者都建议把网络编程从头到尾串一遍。因为无论是平时调接口、排查线上超时还是面试被追问 TCP 和 UDP 的区别底子都在这一课里。2. 必会的网络基础TCP/IP 分层模型与实际开发的关系2.1 从浏览器输入 URL 到页面渲染网络包经历了什么我用一个最常见的场景来解释网络编程在干什么。你打开浏览器输入http://localhost:8080/api/user然后按下回车。这一步背后经历了 DNS 解析把域名变成 IP、TCP 连接建立三次握手、HTTP 请求报文组装、服务端处理之后返回响应报文、浏览器解析渲染页面。如果你访问的是本地地址又是在同一个机器上那连网卡都走的是回环地址 127.0.0.1。在网络编程里并不需要关注网线、交换机和路由器这些是网络设备的事。Java 程序员要关心的是从传输层往上的部分。传输层的 TCP/UDP 协议决定了数据怎么可靠地传过去应用层的 HTTP/WebSocket 协议决定了数据长什么样。所以你在写 Java 网络程序的时候其实是在和这两个层级打交道。2.2 面试和实战必须分清的TCP/IP 四层模型与 OSI 七层模型面试八股文里常问 OSI 七层模型实际开发工作中我们更多讨论 TCP/IP 四层模型。四层分别是网络接口层、网络层、传输层、应用层。网络层负责 IP 寻址和路由传输层负责端口到端口的通信应用层就是 HTTP、FTP、SMTP 这些协议。很多人在学网络编程时搞不清楚一个关键点TCP 和 IP 分别解决什么问题。IP 负责找到目标主机TCP 负责在主机上把数据准确交付给某个端口对应的进程。打个比方IP 地址就是写字楼地址端口号就是楼里的房间号TCP 就是那个保证快递员准确把包裹送到房间里还要签收确认的物流体系。3. Java 网络编程环境准备与第一个 Socket 程序3.1 JDK 安装与开发环境配置的坑在写网络编程代码之前先把 Java 开发环境准备好。我见过很多学习者卡在环境变量配置上。建议直接安装 JDK 17 或 JDK 21这两个版本是当前企业项目里用得最多的 LTS 版本。下载安装完成后最重要的是配置JAVA_HOME环境变量和PATH。这里有一个容易踩的坑安装 JDK 后在命令行输入java -version没反应多半是JAVA_HOME没配置对。JAVA_HOME要指向 JDK 的安装根目录而不是 bin 目录。配置完PATH之后记得新开一个命令行窗口再测试因为环境变量的修改不会热生效。3.2 第一个 TCP Socket 通信从代码层面理解连接Java 网络编程的核心类只有两个ServerSocket服务端和Socket客户端。我先给一个最朴素的例子让你快速感受通信过程。服务端代码import java.io.*; import java.net.*; public class TcpServer { public static void main(String[] args) throws IOException { // 监听 8888 端口 ServerSocket serverSocket new ServerSocket(8888); System.out.println(服务端已启动等待客户端连接...); // accept 是阻塞方法有客户端连接才会往下走 Socket socket serverSocket.accept(); // 读取客户端发送的数据 BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream())); String message reader.readLine(); System.out.println(收到客户端消息 message); // 给客户端回一句话 PrintWriter writer new PrintWriter(socket.getOutputStream(), true); writer.println(服务端已收到消息 message); // 关闭资源 reader.close(); writer.close(); socket.close(); serverSocket.close(); } }客户端代码import java.io.*; import java.net.*; public class TcpClient { public static void main(String[] args) throws IOException { // 连接本机 8888 端口 Socket socket new Socket(127.0.0.1, 8888); // 向服务端发送消息 PrintWriter writer new PrintWriter(socket.getOutputStream(), true); writer.println(你好服务端); // 读取服务端返回的消息 BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream())); String response reader.readLine(); System.out.println(收到服务端消息 response); reader.close(); writer.close(); socket.close(); } }这里有几个关键细节值得说明。首先是ServerSocket(8888)的端口选择8080、8888 这类端口经常被占用如果启动时报Address already in use可以用netstat -ano | findstr 8888Windows或lsof -i :8888Mac/Linux查看占用情况。其次是accept()的阻塞机制它是同步等待客户端接入有且只有一个客户端能连上一旦有第二个客户端连接服务端还来不及 accept就会在操作系统内核的连接队列里排队。另一个需要注意的细节是关闭资源。新手最容易忘记按顺序关闭流和 Socket导致端口一直处于 TIME_WAIT 状态。从 Java 7 开始可以用 try-with-resources 来简化资源管理代码更安全。这一点在写生产级代码时尤其重要。4. 进阶实战用线程池实现多客户端并发连接4.1 单线程 ServerSocket 的致命缺陷上面的单连接版代码一次只能服务一个客户端。真实场景下比如一个简单的聊天室或 IoT 设备接入服务瞬间可能有几十个客户端同时连接。如果继续用单线程accept()后面的客户端只能在连接队列里干等完全不可接受。解决方案很简单每接到一个客户端连接就开一个线程去处理。但如果直接用new Thread()来创建线程并发量一高就会导致线程数量爆炸CPU 上下文切换开销巨大。正确做法是用线程池来管理线程既能复用线程又能限制最大并发数。4.2 线程池改造的多客户端服务端改造后的服务端代码import java.io.*; import java.net.*; import java.util.concurrent.*; public class TcpServerWithThreadPool { public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8888); // 创建线程池核心线程数 5最大线程数 10队列长度 100 ExecutorService threadPool new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(100) ); System.out.println(服务端已启动线程池模式运行中...); while (true) { Socket socket serverSocket.accept(); // 把每个连接交给线程池处理 threadPool.execute(() - handleClient(socket)); } } private static void handleClient(Socket socket) { try ( BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter writer new PrintWriter(socket.getOutputStream(), true) ) { String message; while ((message reader.readLine()) ! null) { System.out.println(收到客户端消息 message); writer.println(服务端已收到消息 message); // 约定bye表示断开连接 if (bye.equalsIgnoreCase(message)) { break; } } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }这里我特意用了execute()而不是submit()因为execute()如果任务执行过程中抛出异常会直接抛出到线程池的任务队列里方便定位问题。submit()则会吞掉异常需要额外用Future.get()才能获取处理不当会白白增加排查难度。注意看代码里用了 while 循环来持续读取readLine()。这是因为在实际的 TCP 通信中客户端发送一条消息之后连接并没有关闭服务端需要持续监听下一条消息。如果只读一次就退出连接就会断开。约定bye作为断开信号这样客户端可以主动告诉服务端我要走了服务端才正常释放连接资源。4.3 TCP 粘包和拆包每一个 Java 网络程序员都会遇到的坑当你用多个客户端分别发送大量数据时会发现一个经典问题TCP 粘包和拆包。TCP 是面向字节流的协议它不像 UDP 那样有明确的消息边界。多次 send 的数据可能被合并成一次收到粘包一条完整消息也可能被拆成多次读拆包。简化理解你给朋友寄了三个快递物流公司可能把三个箱子捆在一起送过来粘包也可能把一个箱子的东西分成两批送拆包。接收方拿到东西后得靠自己判断哪几件是同一批的。避免粘包/拆包的常见方案有三种固定消息长度、使用分隔符、在消息头中声明长度。在实际开发中最常用的是第三种。比如定义一个协议前 4 个字节存消息长度后面是消息正文。服务端先读 4 个字节得到长度再读指定长度的字节作为一条完整消息。这个思路在很多底层框架中都能看到比如 Netty 的LengthFieldBasedFrameDecoder就是这样工作的。5. UDP 通信实战无连接协议的正确打开方式5.1 一个最小可用的 UDP 发送接收示例很多时候大家只重视 TCP把 UDP 忽略了。但像实时音视频、DNS 查询、游戏帧同步这类场景UDP 反而是首选。它不需要建立连接直接把数据打包成数据报发出去效率比 TCP 高得多代价是不保证数据一定到达、到达顺序可能乱、报文可能重复。Java 的 UDP 编程用的是DatagramSocket和DatagramPacket。先看发送端import java.net.*; public class UdpSender { public static void main(String[] args) throws Exception { DatagramSocket socket new DatagramSocket(); byte[] data UDP 数据报测试.getBytes(); InetAddress address InetAddress.getByName(127.0.0.1); DatagramPacket packet new DatagramPacket(data, data.length, address, 9999); socket.send(packet); socket.close(); } }接收端import java.net.*; public class UdpReceiver { public static void main(String[] args) throws Exception { DatagramSocket socket new DatagramSocket(9999); byte[] buffer new byte[1024]; DatagramPacket packet new DatagramPacket(buffer, buffer.length); System.out.println(UDP 接收端启动等待数据...); socket.receive(packet); String message new String(packet.getData(), 0, packet.getLength()); System.out.println(收到数据 message); socket.close(); } }注意接收端的DatagramPacket初始化后receive()方法会阻塞等待数据的到来。这里有一个细节new String(packet.getData(), 0, packet.getLength())而不是直接用packet.getData()的全部字节。因为buffer长度是 1024如果实际数据不到 1024 字节直接转成字符串就会在后面多出一堆空字节。这个毛病很容易犯我第一次写 UDP 程序就在这里吃过亏打印出的字符串尾部是一排空格。5.2 TCP 还是 UDP选型不再纠结在实际项目里选 TCP 还是 UDP要结合业务类型来判断。这里我整理了一个对比表面试和做方案时都用得上。对比维度TCPUDP连接状态面向连接需三次握手无连接直接发数据报可靠性可靠传输有确认、重传机制不可靠不保证送达数据边界字节流无边界需处理粘包拆包每个数据报是独立消息自带边界速度相对慢有确认开销相对快没有握手和确认应用场景HTTP、文件传输、数据库连接DNS、音视频、游戏、物联网传感器上报讲一个我经手过的实际案例。之前做过一个设备数据采集项目设备每秒钟上报一次电量数据。最开始用的 TCP结果因为设备数量多、网络偶尔抖动大量的连接重连把服务器资源吃满了。后来改造为 UDP 上报设备只负责把数据报发出去服务器端采用异步接收丢失一两条数据完全不影响整体业务系统稳定性和吞吐率都有明显提高。所以在做技术选型时不要迷信 TCP 万能的说法而是要问自己这个问题业务能不能容忍少量数据丢失如果答案是能优先考虑 UDP。6. 全栈场景下的 HTTP 通信连接与常见问题排查6.1 用 JDK 自带的 HttpClient 替代第三方依赖做全栈开发时后端服务和前端页面打交道走 HTTP 协议Java 后端对外调用第三方接口也走 HTTP 协议。很多同学一上来就引入 Apache HttpClient 或 OkHttp其实 JDK 11 以后自带的java.net.http.HttpClient已经足够日常使用而且不需要额外依赖。下面就是一个简单的 GET 请求示例import java.net.URI; import java.net.http.*; public class HttpDemo { public static void main(String[] args) throws Exception { HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(http://127.0.0.1:8080/api/user)) .timeout(Duration.ofSeconds(10)) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(状态码 response.statusCode()); System.out.println(响应体 response.body()); } }注意这里设置了两个超时时间一个是连接超时connectTimeout一个是请求超时timeout。连接超时指建立 TCP 连接最多等多久请求超时指从发送请求到收到完整响应最多等多久。这两个参数在实际生产环境中必须显式设置不然服务端不可用时线程会一直阻塞等到默认超时通常是无限期或很久很容易把连接池耗尽。6.2 全栈联调时最常见的 5 个网络问题在全栈开发中前端联调报错通常集中在几个典型的问题上我按排查优先级整理一下。第一个是跨域问题。前端从http://localhost:5173访问后端http://localhost:8080浏览器会拦截响应。解决方式是在后端配置 CORS或者在开发环境用代理转发请求。这个话题很多框架有自己的解决方案但根本原因就是浏览器的同源策略。第二个是 IPv4 和 localhost 的解析问题。有的机器上localhost会被解析为 IPv6 地址::1导致连接失败。排查方法是用ping localhost看输出结果如果显示的是::1可以在/etc/hostsMac/Linux或C:\Windows\System32\drivers\etc\hostsWindows中强制把 localhost 解析为 127.0.0.1。第三个是防火墙拦截端口。自己电脑上服务端已经启动但局域网内另一台电脑访问不了大概率是防火墙没放行对应端口。Windows 上可以在“高级安全 Windows 防火墙”中添加入站规则放行需要开放的 TCP 或 UDP 端口。第四个是端口占用导致服务启动失败。提示Port already in use时找到占用进程并确认是否可以结束不要盲目换个端口了事因为前端代码里可能已经写死了后端端口。第五个是服务端和客户端编码不一致导致的乱码。TCP 通信中如果发送端用 UTF-8接收端用 GBK就会出现中文乱码。规范的团队应该在项目早期就约定所有网络传输统一使用 UTF-8 编码。7. 面试必问的网络编程核心知识点与学习路线7.1 Java 网络编程面试八股文的正确作答姿势搜索热词里频繁出现“java 面试八股文”和“java 面试大全”说明这是很多人的刚需。关于网络编程面试官真正想考察的其实不是你能不能背出状态码而是你有没有写过多客户端并发程序、有没有处理过粘包和拆包、知不知道 TCP 和 UDP 的本质区别。我来列举几个高频问题以及怎么答才不出错。第一个TCP 为什么需要三次握手标准回答是确认双方的收发能力都正常。第一次握手客户端告诉服务端我能发第二次服务端告诉客户端我能收也能发第三次客户端告诉服务端我能收。两次握手无法保证服务端发送能力正常四次又多余三次是最小达成条件。第二个TCP 四次挥手为什么要等 TIME_WAIT核心原因是最后一个 ACK 报文可能丢失主动关闭方需要等待一段时间确保对端收到 ACK 并关闭。如果不等就直接关闭对端重传 FIN 时主动关闭方已经无法响应会造成对端一直卡在关闭状态。第三个UDP 比 TCP 快为什么 HTTP 还是用 TCP因为 HTTP 要求数据完整性页面资源丢失哪怕一个字节渲染都会出错。TCP 的确认重传机制换来了可靠性代价是性能稍低。但这也是为什么 HTTP/3 改用 QUIC基于 UDP的原因——在保持可靠性的前提下减少握手延迟。7.2 从入门到进阶Java 网络编程的学习路线建议如果让我给一条学习路径大概是这样的先掌握 Socket 和 ServerSocket 的基础用法配合多线程实现一个最简单的聊天室。然后了解线程池在网络编程中的应用同时自己在本地用 Wireshark 抓包观察 TCP 的三次握手和四次挥手过程这个可视化体验比任何文字描述都直观。之后再接触 NIO非阻塞 IO和 Netty理解 select、poll、epoll 这些事件驱动模型并把粘包拆包问题在 Netty 的 pipeline 里实践一遍。很多人会问都用了 Netty 和 Spring Boot还有必要手写 Socket 代码吗我觉得非常有必要。框架封装了太多细节如果不知道底层的连接建立、数据读取、异常断开是怎么发生的遇到线上问题时就会完全无从下手。曾经有一个上线的项目频繁出现连接超时排查了半天最终发现是业务线程手动创建了大量临时连接没有走连接池。如果不懂底层连接的生命周期这种问题能排查到凌晨。7.3 最后的实践练习把网络编程用到真实工具里如果你已经掌握了上面的内容可以给自己安排一个综合练习用 Java 的 ServerSocket 实现一个极简 Web 服务器能处理 GET 请求、返回 HTML 页面和静态资源。这个练习会逼你把 HTTP 报文解析、线程池并发、Socket 读写、资源关闭全部串起来。完成之后你再去看 Spring Boot 内嵌的 Tomcat 的原理会有一种豁然开朗的感觉。我在实际学习和带人的过程中发现很多人学网络编程最大的问题不是理解不了概念而是“没动手”。概念背得再熟练碰到 Socket 连接被 reset、输入流卡住不返回、端口被占用这类问题一上手就容易懵。网络编程本来就是一门动手的学问多写几个 Demo多看看抓包结果很多八股文里的抽象描述在脑子里就会有具体的画面。到那时候你才算是真正把这一课的内容消化掉了。
返回列表