news 2026/10/17 14:07:41

Java网络编程入门:从TCP/IP基础到Socket与HTTP实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java网络编程入门:从TCP/IP基础到Socket与HTTP实战

翻到 Java 笔记的第十篇,正好是网络编程。不少朋友学 Java 时把集合、IO 练得很熟,一碰到 Socket 就发怵,总觉得 IP、端口、协议是另一个世界的东西。其实网络编程没有想象中那么神秘,核心就是让两个进程通过 TCP 或 UDP 互相搬数据。这篇笔记我按自己的学习路线整理:先从 TCP/IP 模型里最简单的那几层说起,然后直接写一个能跑的 Socket 例子,再延伸到多线程服务器、HTTP 请求、粘包拆包等实战中躲不开的问题。适合刚学完 Java 基础、想用 Java 做小项目或对接一些接口的朋友参考。

1. 网络编程前的必要准备

1.1 先理解 TCP/IP 模型里你真正用到的部分

很多教材上来就铺开整个分层模型,说实话容易把人劝退。你在 Java 里写网络程序,日常打交道最多的是传输层和应用层,再往下就是操作系统帮你处理了。所以我建议先用一个“寄快递”的类比把骨架搭起来:

  • IP 地址相当于收货地址,用来找到具体是哪台机器。
  • 端口相当于公司里的“某个部门”,机器收到数据之后要交给对应的进程。
  • 协议相当于快递包装上的交接规则,TCP 是“要签收的挂号件”,UDP 是“直接扔传达室的平信”。

TCP 和 UDP 的区别也很直观。TCP 要先建立连接,发送前要握手,收完要确认,数据歪了要重传,所以适合文件传输、网页访问这些不能丢数据的场景。UDP 不要连接,直接扔数据,速度快但可能丢包,适合视频通话、游戏同步这些丢几帧也无所谓的场景。

Java 提供了一整套 API 把底层细节挡住。你在代码里 new 一个 Socket,操作系统会帮你完成 DNS 解析、路由、握手,你只需要往输出流里写数据,再从输入流里读数据。所以我一直觉得,先跑起来,回头再补理论,效率高得多。

1.2 地址、端口与协议三者的分工

你可能遇到过这种代码:

InetAddress address = InetAddress.getByName("example.com"); System.out.println(address.getHostAddress());

InetAddress 就是 Java 里表示 IP 地址的类。本机回环地址 127.0.0.1 代表“我自己”,如果你只想本机测试,客户端连这个地址就行。生产环境里连接另一台服务器,就要填对方的公网 IP 或域名,同时还要配上端口。

端口是个 16 位整数,范围 0 到 65535。1024 以下很多是知名端口,比如 80 是 HTTP、443 是 HTTPS。你自己写练习程序时,通常选 1024 以上且没人占用的端口,比如 12345、8080、9000 这类。

写网络程序最烦的一种报错就是端口被占用。启动服务端时报java.net.BindException: Address already in use,说明端口已经被别的进程占了。排查命令我后面会专门写,这里你先记住一点:服务端要绑定一个固定端口,客户端只需要随机分配一个本地临时端口,所以客户端主动连接时一般不关心自己用的是哪个端口。

1.3 Java 网络编程到底能做什么

学这块最怕空泛。你至少要能在脑子里列出几个可落地的场景:

  • 多人在线聊天室,一条消息从客户端发给服务端,服务端广播给其他人。
  • 文件传输工具,把文件切成多个数据块,用 Socket 或者自定义协议传过去。
  • HTTP 接口调用,用 HttpURLConnection 或后来 Java 11 的 HttpClient 请求后端接口。
  • RPC 框架的底层通信,很多框架的消息收发本质就是 Socket 加自定义协议。
  • 网络爬虫简单版,用 HTTP 客户端抓页面再解析。

写这篇笔记时,我会先把 TCP 的 Socket 流程讲清楚,因为这是理解后面所有网络编程的基石。

2. 从 TCP Socket 开始写第一份通信代码

2.1 一次完整 TCP 通信的流程

先别急着复制代码,脑子里得有流程。服务端做的事情是:创建一个 ServerSocket,绑定端口,然后不停调用 accept() 等待客户端连接。客户端做的事情是:创建一个 Socket,指定服务器 IP 和端口,发起连接。

accept() 是一个阻塞方法,它会让当前线程停在那里,直到有客户端连上来才返回一个 Socket 对象。这个返回的 Socket 才是真正和客户端传输数据的通道。连接建立之后,就是普通的读写流操作了。

用生活里的场景类比:ServerSocket 像前台总机,一直守着电话;accept() 就是“有电话进来我就接,然后把这个客户转给具体坐席”。坐席挂了电话,前台还继续等着下一个电话。

2.2 服务端示例与逐行解读

下面这个是最原始的服务端,一次只处理一个客户端:

public class TcpServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(12345); System.out.println("服务端启动,端口 12345"); while (true) { Socket socket = serverSocket.accept(); System.out.println("客户端连入:" + socket.getRemoteSocketAddress()); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), "UTF-8")); PrintWriter out = new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); String line; while ((line = in.readLine()) != null) { System.out.println("收到客户端消息:" + line); out.println("服务端收到:" + line); } socket.close(); } } }

逐行说几个重点。new ServerSocket(12345)做了两件事:创建 Socket 并绑定端口。如果端口被占用,这行就会抛异常。accept()返回的 Socket 是已连接的对象,getRemoteSocketAddress()可以拿到客户端的 IP 和端口,方便打印日志。

读数据用了 BufferedReader 的 readLine(),它按行读取,所以客户端发送时要带换行符,否则 readLine() 会一直等。写出时我加了第二个参数 true,表示自动 flush,不然 PrintWriter 会缓存,客户端可能一直收不到消息。

这个版本有个明显问题:外层 while 每一轮只能处理一个客户端,处理完才 accept 下一个。如果第一个客户端连上来之后不发消息也不断开,第二个客户端就会卡在 accept() 上。

2.3 客户端示例

对应的客户端代码:

public class TcpClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 12345); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), "UTF-8")); PrintWriter out = new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); out.println("你好,服务端"); System.out.println("收到回复:" + in.readLine()); socket.close(); } }

这段代码里有个隐藏问题:in.readLine()会一直阻塞,直到服务端发来一行数据。如果服务端异常没回复,客户端就会卡死。所以生产代码里几乎一定要设置超时,调用socket.setSoTimeout(3000),超过 3 秒没数据就抛 SocketTimeoutException,借此可以及时退出或者重试。

2.4 资源关闭和超时为什么是重点

网络编程里出 bug 最多的不是我上面那些逻辑,而是资源没关干净。Socket 底层占的是操作系统文件描述符,数量是有限的。你每 new 一个 Socket,不 close,文件描述符就少一个。压测几次就会发现报Too many open files,这时候程序基本废了。

我建议养成两个习惯:

  • 能用 try-with-resources 就用,代码简洁,关闭还可靠。
  • 如果要手写 close,在 finally 里关,并且先关输出流,再关输入流,最后关 Socket。

超时也值得多说一句。连不上远端时,默认的 TCP 连接超时可能很久。你可以在 connect 之前设置:

socket.connect(new InetSocketAddress("192.168.1.10", 8080), 3000);

这样 3 秒连不上就直接报错,用户不用傻等一两分钟。读数据超时则用 setSoTimeout,它只对当前 Socket 的读操作生效。

3. 让服务端真正“并发”:多线程与线程池

3.1 单线程服务端的问题

前面的服务端代码在真实场景里基本不能用,因为一个客户端没断开,其他人全部排队。我最早练习时也踩了这个坑:开了两个终端同时连服务端,第二个客户端一直没反应,还以为是代码写错了,其实是第一个客户端阻塞了 accept 循环之后的处理逻辑。

要解决这个问题,思路很自然:每来一个客户端,就单独开一个线程去处理它,主线程继续 accept 下一个连接。

3.2 用线程池改写服务端

直接 new Thread 写起来简单,但客户端一多,线程数爆炸,系统先扛不住。更稳妥的方案是用线程池。改造后的服务端核心结构如下:

public class ThreadPoolServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(12345); ExecutorService pool = Executors.newFixedThreadPool(8); while (true) { Socket socket = serverSocket.accept(); pool.submit(new ClientHandler(socket)); } } static class ClientHandler implements Runnable { private final Socket socket; ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { try (Socket s = socket; BufferedReader in = new BufferedReader( new InputStreamReader(s.getInputStream(), "UTF-8")); PrintWriter out = new PrintWriter( new OutputStreamWriter(s.getOutputStream(), "UTF-8"), true)) { String line; while ((line = in.readLine()) != null) { System.out.println(Thread.currentThread().getName() + " 收到:" + line); out.println("服务端回显:" + line); } } catch (IOException e) { e.printStackTrace(); } } } }

newFixedThreadPool(8)表示最多同时执行 8 个客户端任务。第 9 个客户端连进来后,连接会被保存到任务队列,等前面的处理完再执行。这样可以防止线程数量无限增长。

3.3 并发下的共享状态与坑

线程池版本能解决并发连接,但如果你在 Handler 里访问共享变量,就要注意线程安全。比如统计在线人数:

private final AtomicInteger onlineCount = new AtomicInteger(); // 连接建立时 onlineCount.incrementAndGet(); // 连接关闭时 onlineCount.decrementAndGet();

用普通 int 在多线程下会有可见性问题,最好用 AtomicInteger 或者加锁。

还有几个容易忽略的点:

  • 客户端断开时,readLine() 返回 null,循环结束,但你不一定知道客户端是正常断开还是异常断开。服务端可以在 finally 里做清理。
  • 线程池如果用 newCachedThreadPool,短连接场景没问题;长连接且量大时,可能导致线程无限创建。固定线程池更可控。
  • 服务端处理某个客户端逻辑时抛异常,不能影响 accept 主循环。你可以在 run() 里 catch 掉,主线程继续接客。

4. HTTP 客户端编程与 URL 处理

4.1 URL、URLConnection 与 HttpURLConnection 的区别

Socket 是底层传输,但平时大家很少直接拿 Socket 访问网页,更多是用 HTTP。Java 里最原始的一套类就是 URL 和 URLConnection。URL 负责表示一个网络地址,URLConnection 是连接的抽象,实际用的时候通常强转成 HttpURLConnection,因为只有它提供了设置请求方法、响应码这些 HTTP 特有的能力。

如果你用的是 Java 11 及以上,我更推荐官方后来推出的 HttpClient,API 更顺手,还支持异步。不过这里仍然讲 HttpURLConnection,因为很多老项目还在用,而且它能帮助你理解 HTTP 请求的本质。

4.2 一个请求网页内容的示例

下面是用 HttpURLConnection 发起 GET 请求的例子:

public class HttpClientDemo { public static String get(String urlStr) throws Exception { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(3000); conn.setReadTimeout(3000); conn.setRequestProperty("User-Agent", "Mozilla/5.0"); int code = conn.getResponseCode(); System.out.println("响应码:" + code); if (code != 200) { conn.disconnect(); return "HTTP " + code; } StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8"))) { String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } } finally { conn.disconnect(); } return sb.toString(); } public static void main(String[] args) throws Exception { System.out.println(get("https://example.com")); } }

这里有几个关键点。openConnection()这个动作并不会真的发起网络请求,只有调用 getResponseCode() 或 getInputStream() 时,请求才真正发出。这是一个容易概念混淆的地方。

设置 User-Agent 也很有必要。很多服务端会拒绝没有浏览器标识的默认 Java 请求,你放一个正常浏览器的 User-Agent 可以避免一部分拦截。

4.3 HTTP 响应解析与常见响应码

判断请求是否成功不能只看有没有返回内容,还得看响应码。常见的状态码:

  • 200:成功。
  • 301、302:资源被临时或永久重定向,需要处理 Location 头或者让 HttpURLConnection 自动跟随。
  • 403:没有权限访问。
  • 404:资源不存在。
  • 500:服务端内部错误。

默认情况下,HttpURLConnection 对重定向是自动跟随的,但如果你用 POST 请求,部分场景需要手动处理。你可以这样读取重定向地址:

String location = conn.getHeaderField("Location");

如果服务端返回 403 或 500,getInputStream()会抛 IOException,这时候有些信息在getErrorStream()里,需要从错误流中读取。很多新手写爬虫时在这里栽跟头,只写了 getInputStream,没有处理非 200 的情况。

4.4 生产环境更推荐什么

如果只是学原理,HttpURLConnection 够了。但如果真要做一个小工具,我更建议使用 Java 11 的 HttpClient 或者第三方请求库。原始 HttpURLConnection 写起来啰嗦,对连接池、超时、上传下载的支持都比较原始。

不过我还是建议你把原理吃透。很多奇怪的线上问题,比如代理设置、DNS 缓存、连接复用,都跟底层实现有关。你理解了请求是怎么发出去的,排查问题时就不会瞎猜。

5. 粘包、拆包与自定义协议设计

5.1 为什么会粘包和拆包

TCP 是面向字节流的协议,它不保证一次 write 的数据会被对端一次性 read 出来。你可以把它想象成一根水管:你倒进去的是三杯水,但到了另一端可能混成了一杯;也可能倒一杯水要被接成两杯。这在网络编程里就叫粘包和拆包。

粘包是多个小消息被合并到了一个包;拆包是一条大消息被分成了多段。很多人刚写 TCP 时会遇到这种诡异现象:客户端发了两条消息,服务端一次 readLine 读出来两行,或者明明发了一条很长的消息,服务端却只读到前半段。

根本原因在于 TCP 没有“消息边界”。它只负责把收到的字节按照顺序交给你,至于这些字节应该分成几段,TCP 不管。UDP 则是有消息边界的,一个数据报就是一条完整消息,所以 UDP 反而很少遇到这种问题。

5.2 四种常见解决方案

既然 TCP 不帮你划分边界,那应用层就得自己设计。常见方案有四种:

方案思路优点缺点
固定长度每条消息都用固定字节数,比如 1024 字节实现简单浪费空间,短消息也占满长度
分隔符消息末尾加换行符或者特殊字符简单直观消息内容不能包含分隔符
长度前缀前面用 2 或 4 字节表示消息体长度灵活,业界常用需要多读一步
自定义协议魔数、版本、类型、长度等字段组合可扩展性强设计复杂

实际项目里用得最多的是长度前缀。它既能精确切分消息,又能支持变长内容,是很多 RPC 和消息框架的标准做法。

5.3 一个长度前缀协议的最小实现

下面用 DataOutputStream 和 DataInputStream 演示。发送端先写一个 int 表示长度,再写消息体。

发送端片段:

Socket socket = new Socket("127.0.0.1", 12345); DataOutputStream out = new DataOutputStream(socket.getOutputStream()); String message = "这是第一条消息"; byte[] bytes = message.getBytes("UTF-8"); out.writeInt(bytes.length); out.write(bytes); out.flush();

接收端片段:

DataInputStream in = new DataInputStream(socket.getInputStream()); int length = in.readInt(); byte[] buffer = new byte[length]; in.readFully(buffer); String message = new String(buffer, "UTF-8"); System.out.println("收到消息:" + message);

这里最关键的是readFully。如果你自己用普通 read 循环去读,可能只读了部分字节就返回;readFully 会保证读满 length 个字节才返回,正好配合长度前缀。

写这段代码时还有两个常见问题:

  • 如果用 BufferedReader 包了一层,再想读字节流就会很麻烦,所以做二进制协议时直接用 DataInputStream 更省心。
  • writeInt 写的是大端序的 4 字节整数,两端一致就行。如果跨语言通讯,要确认另一端也按同一字节序解析。

5.4 实际业务中的协议设计建议

到了真正的业务系统里,长度前缀通常还不够。较为稳妥的协议头至少包含:

  • 魔数:比如 0x12345678,用来快速判断是不是合法数据。
  • 版本号:方便以后升级协议。
  • 消息类型:请求还是响应,是心跳还是业务数据。
  • 消息体长度:用来切分消息。
  • 可选的序列化方式:比如 JSON 还是二进制。

协议设计最忌讳“随手写、随便改”。一旦上线,客户端和服务端版本不同步,错误解析会引发各种玄学问题。建议在设计文档里把字节顺序、字段长度、字符编码都写清楚。

另外,如果你做的是长连接,最好加心跳机制。客户端定时发一个小包,服务端收到后返回。这样能及时发现断线,也能防止中间设备因为空闲太久把连接回收掉。

6. 常见问题与排查技巧实录

6.1 连接 refused 还是 timeout 该怎么判断

网络异常信息永远是排查的第一线索。最常见的两种异常:

Connection refused的意思是连接请求被拒绝。通常是对端机器存在,但对应端口没有程序在监听,或者防火墙直接把请求回绝了。你先确认服务端有没有启动,再用 telnet 或系统自带的工具试一下端口通不通。

Connection timed out的意思是请求发出去了,但长时间没有响应。这常见于网络不可达、对端防火墙把包悄悄丢掉、或者对端服务忙碌没来得及握手。

你可以用一个简单的命令测试端口通不通:

telnet 192.168.1.10 8080

如果端口通,会进入连接成功状态;如果显示连接失败或卡住,问题基本不在 Java 代码,而在网络环境。很多程序员一碰到连不上就开始改代码,其实先分层排查更高效。

6.2 端口占用如何快速定位

服务端报 BindException 时,先查端口。Windows 上可以执行:

netstat -ano | findstr 12345

Linux 上可以执行:

netstat -nltp | grep 12345

如果你知道进程名,也可以直接用:

lsof -i :12345

得到进程号之后,判断是不是自己的旧进程残留。如果是,可以直接结束进程,然后重新启动服务端。

还有个小经验:你写练习代码时经常按一次运行就启动一个服务端,老进程没关掉,新进程自然绑不上端口。IDE 里明明只启动了一次,但上次调试中断没销毁线程,一样会占着端口。遇到这种情况,先把所有 Java 进程列出来再排除。

6.3 最容易忽略的三个小细节

第一,写完数据忘记 flush。PrintWriter 如果不开启 autoFlush,数据可能还在缓冲区,对方当然收不到。排查这类问题,先检查有没有 flush 或 close。

第二,客户端和服务端的字符编码不一致。一边用 UTF-8,一边用 GBK,中文就会变成乱码。我建议两边统一使用 UTF-8,并且明确指定编码,不要依赖系统默认编码。

第三,关闭顺序和异常吞掉。有些代码 catch 之后只 e.printStackTrace(),根本不打印完整堆栈,问题就被掩盖了。排查网络问题时,日志宁可多打,也不能省。

6.4 用抓包手段看清数据流

如果你觉得网络问题像玄学,推荐你学会用抓包工具看底层数据。发一个请求,在抓包工具里看 TCP 三次握手是否完成,数据包有没有正常发出,重传是不是很频繁。看到这些,很多问题就一目了然。

不过也要提醒一句:抓包看到的不只是你应用层的数据,还包括底层的 ACK、SYN 等控制包。刚开始可能觉得乱,不用怕,先关注几条关键信息:源端口、目标端口、数据段长度、标志位。

我个人做网络编程排查时,会按这个顺序来:先看日志里有没有异常,再看端口通不通,接着用抓包工具确认数据有没有发出去,最后才怀疑代码逻辑。大部分“连不上”最终都指向防火墙、安全组、端口占用,而不是 Java 本身。

说实话,网络编程这块内容多,但真正需要掌握的硬通货,就是 Socket 读写、并发处理、HTTP 调用和协议设计这几个方向。我练的时候最笨的办法,就是故意把服务端搞挂、把粘包调出来,然后一个个解决。踩过坑之后,那些概念才真正变成自己的。你学完今天这些内容,可以试着给自己的小项目加一个心跳机制,或者做一个简单的文件传输工具,比单纯背 API 有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 14:06:41

从事故到治理:Kafka Streams实时计算中的Schema校验与多租户管控

1. 从三起现场事故说起:没有数据契约的流处理迟早要还债 先说一件上周真实发生过的事。凌晨两点我被电话叫醒,说实时订单看板全部归零了,但订单量明明在涨。排查到最后,原因极其朴素:上游同学在订单事件里把一个字段从…

作者头像 李华
网站建设 2026/10/17 14:04:54

s3cmd实战指南:对象存储命令行操作与自动化备份

做运维和开发的人,多多少少都跟对象存储打过交道。对象存储那套HTTP接口设计得相当简单,可一旦遇到批量上传日志、同步服务器目录、定期清理过期备份这类需求,光靠浏览器里的管理控制台一个文件一个文件地点,效率低到让人怀疑人生…

作者头像 李华
网站建设 2026/10/17 14:04:27

Objective-C短视频Demo拆解:AVPlayer封装与iPad适配实战

简介:面向苹果设备开发者的仿抖音应用类源码演示项目,使用Object-C语言编写,适配苹果手机与平板,兼容从八点零到十二点零的多个系统版本。资源压缩包内有文件总数一千一百三十七个,大小约七兆,主要包含源代…

作者头像 李华
网站建设 2026/10/17 14:04:21

一人团队如何用AI工具打造百万级军工风格广告

“百万级军工广告”,听起来像是某个大制作公司的项目总结,但实际上,这个项目的核心创意和视觉执行,只有我一个人,外加一台性能还不错的电脑和一堆AI工具。 先解释一下“军工广告”这个词。很多人一听就觉得沾着保密和…

作者头像 李华
网站建设 2026/10/17 14:03:49

智能网联汽车仿真测试白皮书:从在环测试到场景库的落地指南

简介:《中国智能网联汽车自动驾驶仿真测试白皮书(2023版)》聚焦智能网联汽车仿真测试,系统梳理了行业从传统实车测试转向人-车-路-云强耦合系统验证的背景,直面恶劣气象、复杂电磁环境等测试挑战,适合汽车研…

作者头像 李华
网站建设 2026/10/17 14:03:46

从Copilot到Agent:AI编程工具选型的关键路径与避坑指南

简介:《2025年全球AI Coding市场洞察研究报告》由亿欧智库出品,面向具备编程基础的研发人员、企业开发团队及关注AI编程的非技术人员。报告系统梳理了AI编程工具从代码补全向自主型AI Agent跃迁的演变路径,涵盖主流工具测评、中美市场差异、产…

作者头像 李华