Java 网络编程套接字入门:从数据传输到并发服务器
介绍 Java 网络编程基础,涵盖 Socket 概念、UDP 与 TCP 编程模型及代码实现。通过回显(Echo)示例演示 DatagramSocket 与 ServerSocket 用法,解析阻塞 IO、端口占用、应用层协议及长/短连接区别。最后结合线程池说明并发处理方案,为学习 HTTP、RPC 及 NIO 打下基础。

介绍 Java 网络编程基础,涵盖 Socket 概念、UDP 与 TCP 编程模型及代码实现。通过回显(Echo)示例演示 DatagramSocket 与 ServerSocket 用法,解析阻塞 IO、端口占用、应用层协议及长/短连接区别。最后结合线程池说明并发处理方案,为学习 HTTP、RPC 及 NIO 打下基础。

网络编程最核心的目标:让不同主机(或同一主机的不同进程)通过网络传输数据。你在浏览器看视频、刷图片、读文章,本质都是'客户端进程'向'服务端进程'请求网络资源,然后接收响应数据。
先明确请求/响应、客户端/服务端,再落到 UDP/TCP 两条路线,最后讲到端口占用、并发处理、长短连接这些工程级坑点。下面按一条顺滑的学习路径串起来。
来看一个关键定义:网络编程就是网络上的主机,通过不同进程,以编程方式实现网络通信(网络数据传输)。只要是不同进程,哪怕在同一台机器上,通过网络协议栈收发数据,也算网络编程。
于是会出现三个高频'角色名词':
客户端 / 服务端:提供服务资源的一方是服务端,获取服务的一方是客户端。常见流程是:客户端请求 → 服务端处理业务 → 服务端响应 → 客户端展示结果。

来看 Socket 的定位:它是系统提供的一种网络通信技术,是基于 TCP/IP 的网络通信基本操作单元。基于 Socket 写出来的程序,就是网络编程。

按传输层协议,Socket 主要分三类:
来看 UDP 的'脾气':它不建立连接,发送一块数据就必须整体发送,接收也必须整体接收。Java 里主要靠两个类:
DatagramSocket:UDP Socket,用来 send/receive 数据报DatagramPacket:数据报本体(携带字节数组 + 目标/来源地址信息)

new DatagramSocket():绑定本机随机端口(更常见于客户端)new DatagramSocket(port):绑定本机指定端口(更常见于服务端)receive(packet):阻塞等待接收send(packet):发送(通常不阻塞等待)close():关闭套接字new DatagramPacket(byte[] buf, int length)new DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 或指定 InetAddress + portgetAddress()/getPort()/getData():获取对端地址、端口和数据回显(Echo)是网络编程里的'Hello World':客户端发什么,服务端回什么。下面这两段就是完整可运行版本(带注释)。
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
// 指定了一个固定端口号让服务器来使用
socket = new DatagramSocket(port);
// socket 对象代表网卡文件,读这个文件等于从网卡收数据,写这个文件等于让网卡发数据
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 循环一次,相当于处理一次请求
// 处理请求的过程,典型的服务器都分为三个步骤
// 1、读取请求并解析
// DatagramPacket 表示一个 UDP 数据报,此处传入的字节数组就保存 UDP 的载荷部分
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket); // receive 会触发阻塞,直到收到客户端的请求
// 把读取到的二进制数据转换成字符串
String request = (requestPacket.getData(), , requestPacket.getLength());
process(request);
(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf(, requestPacket.getAddress().toString(), responsePacket.getPort(), request, response);
}
}
String {
request;
}
SocketException, IOException {
();
server.start();
}
}
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
// UDP 本身不保存对端的信息,给自己的代码中保存一下
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
// 一定不能填写 serverPort,serverip 是目的 ip,serverport 是目的端口
// 源 ip 所在的客户端的主机 Ip,源端口,应该是操作系统随机分配一个端口
// 就像学生去食堂吃饭,食堂提供把饭做好了端过去的服务,学生在食堂是随便坐一样
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1.从控制台读取用户输入的内容
System.out.println("请输入要发送的内容!");
if (!scanner.hasNext()) break;
String request = scanner.next();
// 2.把请求发送给服务器,首先构造数据报
(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
( [], );
socket.receive(responsePacket);
(responsePacket.getData(), , responsePacket.getLength());
System.out.println(response);
}
}
IOException {
(, );
client.start();
}
}
读代码时重点看三件事:
receive() 会阻塞等待;DatagramPacket。来看一个非常实用的抽象:服务器主循环基本都一样,差异常常只在'如何处理 request 得到 response'。因此做英译汉字典服务时,核心就是把 process(request) 改成'查表并返回'。
(工程上常见做法是让 process 具备可扩展性:例如改成 protected,再用继承/组合注入不同处理逻辑。)
来看 TCP 的关键区别:它是面向连接的。通信前要建立连接;建立后双方通过 InputStream/OutputStream 像读写文件一样收发数据。

Java 里 TCP 的两个核心类:
ServerSocket:用来创建 TCP 服务端监听套接字
new ServerSocket(port):绑定端口accept():阻塞等待客户端连接,返回 SocketSocket:客户端 socket;或服务端 accept 后得到的连接 socket
new Socket(host, port):客户端发起连接getInputStream()/getOutputStream():获取读写流下面是完整可运行版本(带注释)。
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 这里和 UDP 类似,也是在构造对象的时候绑定端口
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
// 这种情况一般不使用固定线程数的 fixedThreadPool
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// tcp 来说,需要先处理客户端发来的连接
// 通过读写 clientSocket 和客户端进行通信
// 如果没有客户端发起连接,此时 accept 就会阻塞
Socket clientSocket = serverSocket.accept();
// 每个客户端连接,都会创建一个新的
// 每个客户端断开连接,这个对象都可以不要了
executorService.submit(() -> {
processConnection(clientSocket);
});
}
}
{
System.out.printf(, clientSocket.getInetAddress(), clientSocket.getPort());
( clientSocket.getInputStream(); clientSocket.getOutputStream()) {
(inputStream);
(outputStream);
() {
(!scanner.hasNext()) {
;
}
scanner.nextLine();
process(request);
writer.println(response);
writer.flush();
System.out.printf(, clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} (IOException e) {
(e);
}
}
String {
+ request;
}
IOException {
();
server.start();
}
}
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 直接把字符串的 IP 地址设置进来
// 127.0.0.1 这种字符串
socket = new Socket(serverIp, serverPort);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
// 为了使用方便,套壳操作
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
() {
scanner.next();
writer.println(request);
writer.flush();
scannerNet.nextLine();
System.out.println(response);
}
} (IOException e) {
(e);
}
}
IOException {
(, );
client.start();
}
}
读 TCP 这套代码时,盯住三个工程细节:
accept() 是阻塞点:没有客户端连上来就会一直等。println + flush 相当于定义了一个很简单的'应用层协议':一行一个消息,否则对端读取边界会很痛苦。来看几个真实世界里最常见的坑:
一次数据传输里,目的 IP + 目的端口唯一标识了对端主机和对端进程。写错了就不是'收不到',而是'发给了别的地方'。
如果进程 A 已经绑定端口,再让进程 B 绑定同一端口会报错(典型错误:Address already in use)。排查方式:
netstat -ano | findstr 端口号 查到 PID就算底层用 TCP/UDP,应用层仍需要约定'数据怎么分隔、怎么解析、字段怎么定义'。否则双方读写会互相折磨。
来看一个经典分叉:TCP 发数据前要先建连接,什么时候关闭连接决定你是短连接还是长连接。
对比差异:
还有一个'扩展但很重要'的工程提醒:基于 BIO(同步阻塞 IO)的长连接会长期占用线程资源,并发高时成本非常昂贵;实际高并发长连接更常用 NIO(同步非阻塞 IO)来实现,性能能上一个量级。
来看最简单的本地运行方式(同机回环地址):
UdpEchoServer 或 TcpEchoServerUdpEchoClient 或 TcpEchoClient只要端口一致(示例里都是 9090)且未被占用,就能跑通。
把这些概念和代码吃透之后,你就拥有了写网络程序的'底盘能力':知道什么时候会阻塞、如何定义消息边界、怎么做并发、为什么端口会炸,以及长连接为什么不能傻用 BIO。剩下的就是在这个底盘上继续往上盖:HTTP、RPC、自定义协议、NIO/Netty——都只是更复杂、更工程化的版本而已。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online