尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java网络编程从入门到避坑:Socket、TCP/UDP与并发模型实战
网络编程这块内容我前前后后翻过不下五遍每次带新人或者自己复习感受都不一样。第一次看的时候满脑子问号Socket到底是个啥、为什么要有端口、TCP和UDP差在哪、什么时候该用哪个。后来自己动手写了几个小Demo又踩了一堆坑才慢慢把这些概念串起来。这篇笔记我打算按“一个从业者重新梳理网络编程知识”的思路来写把JavaSE里网络编程这部分从底层概念到代码落地讲透重点放在那些书本上一笔带过、但实际写代码时一定会卡住的地方。不管你是刚学完IO流准备进军网络编程的新手还是想回头把基础夯实的老手应该都能从里面找到点有用的东西。核心关键词就围绕Socket编程、TCP与UDP、IP与端口、网络通信模型这些展开咱们一个一个拆。1. 先搞清楚网络编程到底在解决什么问题很多人学网络编程上来就背“三要素IP、端口、协议”背完也不知道这三样东西到底在程序里怎么体现。我的建议是先别急着看API先想明白一件事网络编程的本质是让两台机器上的两个程序能够互相发送数据。就这么简单。1.1 从“两个程序聊天”这个场景切入你写了一个程序A跑在自己电脑上另一个人写了个程序B跑在另一台电脑上。现在A想给B发一句“你好”这句话怎么从A的内存里跑到B的内存里中间要经过网卡、网线、路由器、交换机……这一大串硬件设备它们可不认识你Java里的String对象。所以必须有一套约定数据用什么格式打包、怎么找到对方、对方怎么确认收到了。这套约定就是网络协议而JavaSE网络编程要做的就是把这套复杂的底层通信封装成几个好用的类让你不用管网线里跑的是电信号还是光信号。我常跟新人打一个比方网络编程就像寄快递。IP地址是收件人所在的小区地址端口号是具体的门牌号协议则是快递公司定的规矩比如顺丰和邮政的流程不一样。你只管把东西打包好交给快递员剩下的运输过程不用你操心。Java里的Socket和ServerSocket就是你手里的“快递寄件/收件窗口”。1.2 IP、端口、协议三要素在代码里长什么样光说概念容易飘落到Java代码里这三样东西各有对应的类和方法IP地址用InetAddress类表示。注意它没有公开构造方法得通过InetAddress.getByName(主机名或IP)这种静态方法拿对象。这个设计挺有意思因为它要把域名解析也包进来你传个域名它帮你查IP。端口号就是个int值范围0~65535。0~1023是系统保留端口你自己写程序别去占一般用1024以上的。代码里就是new ServerSocket(8888)里的那个8888。协议JavaSE层面主要接触TCP和UDP两套分别对应Socket/ServerSocket和DatagramSocket/DatagramPacket两组API。这里有个新手特别容易懵的点InetAddress的getByName方法会抛UnknownHostException很多人第一次写的时候不知道要处理编译直接报错。其实这个异常的意思是“这个主机名我解析不了”可能是网络不通也可能是名字拼错了。处理方式要么try-catch要么往上层抛实际项目里一般会做降级处理或者给用户一个友好提示。1.3 为什么Java要把网络通信封装成流这是我觉得最值得琢磨的一个设计。你看Socket拿到之后调getInputStream()和getOutputStream()拿到的就是普通的InputStream和OutputStream。这意味着什么意味着你之前学的所有IO流操作什么缓冲流、转换流、对象流全都能直接套上去用。发字符串就用BufferedWriter包一层传对象就用ObjectOutputStream序列化读写文件的那套经验原封不动搬过来。这个设计的妙处在于降低学习成本。网络通信的底层细节TCP三次握手、数据分包、校验和全被藏起来了暴露给你的就是一个“能读能写的管道”。你把它想象成一根连接两台机器的水管一头往里灌水另一头就能接到水。至于水在水管里怎么流的不用你管。当然真出了粘包、半包问题的时候你还是得回头理解TCP是字节流协议这件事这个后面会专门讲。2. TCP通信的完整落地从服务端监听到客户端连接TCP这部分是网络编程的重头戏考试考、面试问、实际项目也用得最多。我把它拆成服务端和客户端两条线来讲最后再合起来跑一个能实际收发消息的Demo。2.1 服务端为什么要先启动并阻塞在accept服务端的核心类是ServerSocket标准写法是这样ServerSocket serverSocket new ServerSocket(8888); Socket socket serverSocket.accept(); // 阻塞等待accept()这个方法很关键它会阻塞当前线程直到有客户端连上来。什么叫阻塞就是程序执行到这一行就停住了卡在那儿不动直到条件满足才继续往下走。很多新人第一次跑程序发现服务端“没反应”其实不是没反应是它在老老实实等连接。这里有个实操经验accept()返回的是一个新的Socket对象这个Socket专门用来和刚才连上来的那个客户端通信而ServerSocket本身继续负责监听新的连接。也就是说一个ServerSocket可以accept出无数个Socket每个Socket对应一个客户端。理解这一点后面讲多客户端并发才不会乱。端口的选择也有讲究。我一般用8888、9999这种好记的但要注意别和本机已占用的端口冲突。Windows下可以用netstat -ano | findstr 8888查端口占用情况Linux/Mac用lsof -i:8888。如果启动时报BindException: Address already in use八成就是端口被占了换个端口或者把占用的进程干掉就行。2.2 客户端连接时到底发生了什么客户端这边简单得多一行代码Socket socket new Socket(127.0.0.1, 8888);这行代码执行的时候底层其实做了三件事一是和指定IP的指定端口建立TCP连接就是那个著名的三次握手二是如果连不上会抛ConnectException三是连接成功后返回一个Socket对象这个对象和服务端accept出来的那个Socket是“一对”的它们之间建立了一条双向通道。我踩过的一个坑客户端new Socket()的时候如果服务端没启动会直接抛异常而且这个异常来得挺快。所以实际写代码时客户端连接最好放在try-catch里给用户一个“连接失败请检查服务端是否启动”的提示而不是让程序直接崩掉。还有个细节new Socket(127.0.0.1, 8888)里的127.0.0.1是本机回环地址意思是“我自己这台机器”。做本地测试的时候用它最方便不用管防火墙。真要跨机器通信就得填对方的真实局域网IP而且对方防火墙得放行对应端口这个后面避坑部分会细说。2.3 一个能跑通的收发消息Demo光说不练假把式我把服务端和客户端的最小可运行代码贴出来你可以直接复制到两个类里跑。服务端public class TcpServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8888); System.out.println(服务端启动等待连接...); Socket socket serverSocket.accept(); System.out.println(客户端已连接 socket.getInetAddress()); InputStream in socket.getInputStream(); byte[] buffer new byte[1024]; int len in.read(buffer); System.out.println(收到消息 new String(buffer, 0, len)); OutputStream out socket.getOutputStream(); out.write(收到你好客户端.getBytes()); socket.close(); serverSocket.close(); } }客户端public class TcpClient { public static void main(String[] args) throws IOException { Socket socket new Socket(127.0.0.1, 8888); OutputStream out socket.getOutputStream(); out.write(你好服务端.getBytes()); InputStream in socket.getInputStream(); byte[] buffer new byte[1024]; int len in.read(buffer); System.out.println(收到回复 new String(buffer, 0, len)); socket.close(); } }跑的时候先启动服务端再启动客户端顺序反了客户端会直接抛连接异常。这个Demo虽然简陋但把TCP通信的骨架完整走了一遍服务端监听、客户端连接、双向收发、关闭资源。你把这个跑通TCP的基本盘就稳了。2.4 关闭资源的正确姿势与顺序上面Demo里我用了close()但实际写的时候更推荐try-with-resources省得忘了关。这里有个顺序问题先关Socket再关ServerSocket。因为Socket是具体连接ServerSocket是监听器先断连接再停监听逻辑上更顺。如果反过来正在通信的Socket可能会出问题。另外getInputStream()和getOutputStream()拿到的流其实不用单独关关Socket的时候它们会跟着关。但如果你在外面套了BufferedWriter之类的包装流那包装流要单独flush一下确保缓冲区里的数据真的发出去了再关Socket不然可能丢数据。这个坑我在传文件的时候踩过文件总是少最后几个字节查了半天才发现是缓冲流没flush。3. UDP通信轻量但不省心的另一种选择TCP讲完UDP必须单独拎出来说因为它的使用场景和TCP完全不同而且API设计思路也不一样。很多人学完TCP就以为网络编程就这一套结果遇到广播、实时音视频这类需求就懵了。3.1 UDP和TCP的本质差异在哪一句话概括TCP是面向连接的可靠传输UDP是无连接的不可靠传输。这话谁都会背但背后的含义得掰开说。TCP建立连接要三次握手断开要四次挥手传输过程中有确认应答、超时重传、流量控制、拥塞控制保证数据不丢、不重、按序到达。代价是开销大、速度相对慢。UDP呢发出去就不管了不建立连接不保证到达不保证顺序甚至可能重复。听起来很坑对吧但它快啊而且没有连接维护的开销适合那种“丢一两个包无所谓但延迟必须低”的场景比如视频通话、在线游戏、直播。我打个比方TCP像打电话先拨通握手确认对方在听说一句对方应一句挂了还要说再见挥手。UDP像发短信编辑好直接发出去对方收没收到、什么时候收到、短信会不会乱序你都不管。3.2 DatagramSocket和DatagramPacket的配合逻辑UDP的API就两个核心类DatagramSocket负责收发DatagramPacket负责装数据。和TCP的流式读写不同UDP是以数据包为单位的一次发一个包一次收一个包。发送端DatagramSocket socket new DatagramSocket(); String msg UDP消息; byte[] data msg.getBytes(); DatagramPacket packet new DatagramPacket( data, data.length, InetAddress.getByName(127.0.0.1), 9999); socket.send(packet); socket.close();接收端DatagramSocket socket new DatagramSocket(9999); byte[] buffer new byte[1024]; DatagramPacket packet new DatagramPacket(buffer, buffer.length); socket.receive(packet); // 阻塞等待 String msg new String(packet.getData(), 0, packet.getLength()); System.out.println(收到 msg); socket.close();注意几个细节发送端的DatagramSocket不用指定端口系统随机分配接收端必须指定端口不然别人不知道往哪发。DatagramPacket构造时发送用的包要带目标地址和端口接收用的包只给缓冲区就行地址端口是receive之后从包里读出来的。3.3 UDP实际使用中的三个坑第一个坑缓冲区大小。UDP一个包最大理论上是64KB左右65507字节但实际网络环境里超过MTU通常1500字节的包会被分片分片越多丢包概率越大。所以传大数据别用UDP或者自己在上层做分片重组。我一般缓冲区开1024或2048够用且不容易出问题。第二个坑receive是阻塞的。和accept()一样receive()没收到包就一直卡着。如果你的程序还要干别的事得把接收逻辑放到单独线程里或者用setSoTimeout()设置超时时间超时抛SocketTimeoutException这样能跳出阻塞做其他判断。第三个坑中文乱码。getBytes()默认用平台编码new String()也是。如果发送端和接收端编码不一致中文就成乱码了。稳妥做法是两边都显式指定StandardCharsets.UTF_8别偷懒用默认的。4. 那些文档里不写、但一写就出问题的地方这部分是我觉得整篇笔记最有价值的地方。前面讲的都是“应该怎么做”这里讲“实际做的时候会怎么翻车”。网络编程的坑特别多而且很多坑报错信息很模糊不提前知道能查半天。4.1 端口被占用BindException的排查链路java.net.BindException: Address already in use: bind这个异常我敢说每个写网络编程的人都遇到过。它的意思是你要绑定的端口已经被别的程序占了。排查步骤我总结成一套固定流程第一步确认端口号看代码里写的多少第二步查占用Windows用netstat -ano | findstr 端口号拿到PID后去任务管理器对Linux/Mac用lsof -i:端口号第三步决定是换端口还是杀进程。如果是自己之前跑的程序没关干净杀掉就行如果是系统服务占着那就老老实实换端口。有个隐蔽的情况程序异常退出时Socket可能没正常关闭端口会处于TIME_WAIT状态短时间内不能复用。这时候你重启程序还是会报占用。解决办法是等一会儿通常几十秒到两分钟或者在ServerSocket上设置setReuseAddress(true)允许端口复用。这个参数我一般创建完ServerSocket就顺手设上省得反复调试时被卡。4.2 连接超时与读超时两个容易混淆的概念ConnectException: Connection refused和SocketTimeoutException是两码事很多人分不清。Connection refused是连接阶段就失败了通常是服务端没启动、端口不对、或者防火墙拦了。这个异常来得快因为TCP握手直接被拒绝。SocketTimeoutException是连接建立之后读数据的时候超时了。比如你调in.read()等了好久对方没发数据过来超过设定时间就抛这个。默认情况下read()是无限等待的要主动设置socket.setSoTimeout(5000)才会超时。这两个要分开处理连接失败就提示用户检查服务端读超时就可能是对方处理慢或者网络卡可以重试或者给个“响应超时”的提示。混在一起处理用户看到的错误信息就不准确。4.3 粘包和半包TCP是字节流不是消息流这是TCP编程里最经典也最容易忽视的问题。TCP保证字节按序到达但不保证一次read就能读到一条完整消息。你发“你好”和“世界”两条消息对方可能一次read读到“你好世界”也可能分两次读到“你”和“好世界”。前者叫粘包后者叫半包。为什么会这样因为TCP是流式协议它只认字节不认消息边界。发送端两次write底层可能合并成一个TCP段发出去接收端缓冲区大小、网络状况都会影响read的结果。解决办法是在应用层自己定协议。常见的有三种一是固定长度每条消息定长不够补空格二是分隔符比如每条消息以换行符结尾接收端按行读三是长度前缀消息头几个字节表示消息体长度先读长度再读内容。实际项目里第三种最通用我一般用DataInputStream的readInt()读长度再readFully()读内容稳得很。4.4 防火墙和局域网IP跨机器通信的拦路虎本机测试用127.0.0.1一切正常换到两台机器就死活连不上这种情况太常见了。原因通常有两个一是IP填错了二是防火墙拦了。先说IP。跨机器通信不能用127.0.0.1那是回环地址只指自己。要用服务端机器的局域网IPWindows下ipconfig看IPv4地址Linux/Mac用ifconfig或ip addr。拿到类似192.168.x.x的地址填到客户端。再说防火墙。Windows默认会拦入站连接服务端程序第一次跑的时候会弹窗问“是否允许”很多人手快点了取消后面就连不上了。解决办法是去防火墙设置里给Java或者对应端口加个入站规则。这个步骤稍微麻烦但一次配好后面就省心了。测试阶段实在不想折腾可以临时关掉防火墙验证一下是不是这个原因验证完记得开回来。5. 从单客户端到多客户端并发模型怎么选前面所有Demo都是“一个服务端对一个客户端”跑完就结束。但真实场景里服务端要同时服务多个客户端这就涉及到并发处理。JavaSE层面主要有两种思路我分别说说适用场景。5.1 主线程accept加子线程处理最直观的方案思路很简单主线程死循环调accept()每accept到一个Socket就丢给一个新线程去处理读写。代码骨架while (true) { Socket socket serverSocket.accept(); new Thread(() - handleClient(socket)).start(); }这个方案优点是直观、好理解新手也能快速写出来。缺点是线程开销大每个连接一个线程连接数一多比如上千线程创建和上下文切换的成本就上来了而且线程栈默认占内存几百个线程就能把内存吃不少。实际用的时候如果客户端数量可控几十个以内这个方案完全够用别过度设计。我见过一些项目明明就十几个客户端非要上复杂的线程池加NIO结果代码复杂度飙升维护成本反而更高。5.2 线程池版本控制并发的稳妥做法把new Thread换成线程池能控制最大并发数避免连接暴增把服务器拖垮ExecutorService pool Executors.newFixedThreadPool(50); while (true) { Socket socket serverSocket.accept(); pool.submit(() - handleClient(socket)); }线程池大小怎么定这个没有标准答案得看任务类型。如果是IO密集型网络通信基本都是线程数可以设大一点比如CPU核数的几倍到十几倍如果是CPU密集型设成核数加一就行。我一般先设个50压测一下看瓶颈在哪再调。这里有个坑线程池的队列如果用的是无界队列任务堆积太多会OOM。Executors.newFixedThreadPool底层用的就是无界队列连接数不可控的时候要小心。稳妥做法是自己new ThreadPoolExecutor指定有界队列和拒绝策略。5.3 NIO的定位知道它解决什么就够了JavaSE里还有一套NIONew IO用Selector、Channel、Buffer实现非阻塞通信一个线程能管成千上万个连接。这部分内容比较深黑马笔记里通常也会讲到但我建议初学阶段先别陷进去。你只要知道NIO解决的是“高并发连接下线程开销大”的问题就行。它的核心思想是一个线程通过Selector监听多个Channel的事件哪个Channel有数据可读才去处理没数据就干别的不阻塞。等你的项目真遇到连接数瓶颈了再回头深入学NIO不迟。上来就啃NIO很容易被Buffer的flip、clear、rewind搞晕反而把TCP基础给耽误了。6. 学完这些之后我建议你这样练网络编程这东西看十遍不如写一遍。我给几个循序渐进的练习方向都是我自己练过觉得有效的。第一个练习把前面的TCP Demo改造成支持多次收发的版本。现在的Demo发一条就关了你改成用while循环客户端能连续发多条消息服务端能连续回直到客户端发“bye”才退出。这个练习能让你彻底理解“连接是持续的”这件事。第二个练习做一个简易聊天室。服务端维护一个客户端列表收到某个客户端的消息后转发给列表里所有其他客户端。这个练习会逼你处理多线程共享集合的线程安全问题很有价值。第三个练习实现文件传输。客户端发文件服务端接收存盘。这个练习会让你真正遇到粘包问题文件内容可不会乖乖按你的缓冲区大小对齐逼着你去设计长度前缀协议。做完这个你对TCP字节流的理解会上一个台阶。第四个练习用UDP做一个局域网广播发现。一台机器发广播包其他机器收到后回复自己的信息实现自动发现。这个练习能让你体会UDP广播和TCP点对点的区别。每个练习都别只看不写一定要自己敲一遍报错了自己查。网络编程的报错信息有时候很模糊查错的过程本身就是最好的学习。我当初做文件传输那个练习因为没处理粘包接收的文件总是比原文件大一圈或者小一截折腾了一整天才想明白问题出在哪但想明白之后这块知识就再也没忘过。最后分享一个我个人的习惯写网络程序的时候日志一定要打足。连接建立、数据收发、异常捕获每个关键节点都打一行日志。网络问题排查最怕“黑盒”有了日志至少能知道卡在哪一步。这个习惯帮我省了无数次调试时间比任何调试工具都管用。
RELATED

相关推荐

OpenClaw流式翻译延迟控制:动态分段策略与实测调优

OpenClaw流式翻译延迟控制:动态分段策略与实测调优

在实时翻译这个场景里摸爬滚打这几年,被问得最多的一句话就是:"延迟到底压到多少才算能用?" 尤其是最近在调 OpenClaw 做跨语言语音交互,很多人一上来就盯着流式翻译的延迟数字看。标题里问的这个问题其实问到了点子上&…

📅 2026/10/24 4:45:56
含储能微网双层能量管理模型:MPC日前调度与日内滚动实现

含储能微网双层能量管理模型:MPC日前调度与日内滚动实现

这段时间我把手头那个含储能微网的双层能量管理模型用MATLAB完整搭了一遍,核心控制器是模型预测算法(MPC)——上层做日前调度,下层做日内滚动修正。这个模型的目标很朴素:在电价、光伏、负荷都不完全确定的情况下&…

📅 2026/10/24 4:45:56
IDE评估过期提示解析:授权状态机与30分钟会话限制的应对策略

IDE评估过期提示解析:授权状态机与30分钟会话限制的应对策略

1. 这条弹窗到底在说什么:从现象到本质的拆解第一次看到Your idea evaluation has expired. Your session will be limited to 30 minutes这行提示,很多人第一反应是“我是不是装了个假版本”或者“是不是账号被盗了”。其实没那么玄乎,这条提…

📅 2026/10/24 4:45:56
MORE NEWS

更多资讯

📰

笔记软件推荐:Markdown 也好,双向链接也好,先找到能坚持写下去的那一款

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列AIGC(GPT、DeepSeek、豆包、千问、Kimi) 👉关于作者 专注于Android/Unity和各种游戏开发技巧&a…

📰

Linux:线程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

JavaScript 未排序数组中第 k 个最小/最大元素 (预期线性时间)

目录 例如 流程图 示例代码 参考资料 如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。 未排序数组中第 k 个最小/最大元素 (预期线性时间)(Kth Smallest/Largest Element in Unsorted Array | Set 2 …

📰

TVA智能体技术体系概述(58):TVA-World生成式具身智能全景解析

前沿技术探索:TVA智能体(简称TVA) TVA智能体(亦称“AI智能体视觉”)是依托Transformer架构与“因式智能体”理论构建的新型工业视觉系统,也是当前最具代表性的具身视觉技术之一。它有机融合深度强化学习(DRL)、卷积神经网络(CNN)与因式分解算法(FRA),构成了具身智…

📰

TVA智能体技术体系概述(54):TVA生成式智能物理建模的三大技术瓶颈研究

前沿技术探索:TVA智能体(简称TVA) TVA智能体(亦称“AI智能体视觉”)是依托Transformer架构与“因式智能体”理论构建的新型工业视觉系统,也是当前最具代表性的具身视觉技术之一。它有机融合深度强化学习&a…

📰

洛谷 B2161:十进制转二进制 ← 字符串 / 栈 / vector

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬