网络编程套接字万字详解
一、网络编程基础
1.1 为什么需要网络编程
互联网存在海量网络资源,包含视频、图片、文本等各类数据。用户访问网络资源的本质,就是通过网络完成数据传输,而所有网络资源的数据传输,都依赖网络编程实现。
1.2 什么是网络编程
网络编程:网络中的不同主机,通过各自进程,以代码方式实现网络通信 / 数据传输。
- 通信主体:不同主机的不同进程;
- 特殊场景:同一台主机内的多个进程通过网络传输数据,也属于网络编程(开发测试常用);
- 核心角色:一类进程负责获取网络资源(客户端),一类进程负责提供网络资源(服务端)。
二、网络编程核心概念
2.1 发送端 & 接收端
一次数据传输中定义两端角色:
- 发送端:发送数据的进程,对应源主机;
- 接收端:接收数据的进程,对应目的主机;
- 特性:发送端、接收端是相对概念,数据流向反转则角色互换。
2.2 请求 & 响应
绝大多数网络交互包含两次数据传输:
- 请求:客户端主动发送数据,向服务端发起业务申请;
- 响应:服务端处理完成后,返回结果数据给客户端。
类比线下场景:到快餐店点餐,先发起 “点餐请求”,店家再给出 “出餐响应”。
2.3 客户端 & 服务端(C/S 模型)
这是网络编程最主流的架构模型:
- 服务端:持续运行、对外提供服务与资源的进程;
- 客户端:主动连接服务端、获取服务 / 提交数据的进程。
典型交互流程
- 客户端向服务端发送请求;
- 服务端执行业务逻辑;
- 服务端返回响应结果;
- 客户端解析并展示结果。
两类业务场景
- 客户端获取服务端资源(看视频、查资料);
- 客户端上传数据到服务端(上传文件、账号注册)。
类比银行场景:银行是服务端,用户是客户端;存款、取款均为客户端主动发起请求。
三、Socket 套接字概述与分类
3.1 Socket 概念
Socket(套接字)是操作系统提供的网络通信基础单元,基于 TCP/IP 协议簇实现,基于 Socket 开发网络程序,就是网络编程。
3.2 Socket 三大分类
按照传输层协议划分,日常开发主要使用前两类:
1. 流套接字(TCP)
基于 TCP 传输控制协议,核心特点:
- 有连接:通信前必须建立专属连接;
- 可靠传输:数据不丢失、不重复、有序到达;
- 面向字节流:数据无边界,流式传输,可多次收发;
- 具备发送缓冲区、接收缓冲区;
- 单次传输数据大小无严格限制。
2. 数据报套接字(UDP)
基于 UDP 用户数据报协议,核心特点:
- 无连接:无需提前建立连接,直接发送数据;
- 不可靠传输:不保证数据一定送达;
- 面向数据报:数据以 “数据包” 为单位,有边界;
- 仅有接收缓冲区,无发送缓冲区;
- 大小受限:单次最大传输 64KB;
- 规则:一个数据报必须一次性发送、一次性接收,不可拆分。
3. 原始套接字
用于自定义传输层协议,可读写内核未处理的 IP 数据包,日常业务开发不使用,仅作了解。
四、UDP 数据报套接字编程
4.1 UDP 通信模型(流程图)
单次收发(仅请求无响应)
发送端 接收端
│ │
1. 创建 DatagramSocket 1. 创建 DatagramSocket
│ │
2. 构造 DatagramPacket(数据+地址) 2. 准备空字节数组,构造接收Packet
│ │
3. socket.send(packet) 发送数据 3. socket.receive(packet) 阻塞等待
│ │
数据传输 ───────────────────────▶
│ │
4. 解析 Packet 中的数据、源IP、端口
标准 C/S 模型(请求 + 响应)
客户端(发送端) 服务端(接收端)
│ │
1. 创建 DatagramSocket 1. 创建 DatagramSocket(绑定固定端口)
│ │
2. 构造请求 Packet 2. 阻塞 receive() 等待客户端请求
│ │
3. 发送请求 ───────────────────▶
│ │
│ 3. 解析请求、执行业务逻辑
│ │
4. 阻塞 receive() 等待响应 4. 构造响应 Packet(携带客户端地址)
│ │
5. 发送响应 ◀───────────────────
│ │
5. 解析响应数据 │
4.2 Java UDP 核心 API
1. DatagramSocket(UDP 套接字对象)
| 构造 / 方法 | 说明 |
|---|---|
DatagramSocket() |
创建 UDP 套接字,绑定本机随机端口(多用于客户端) |
DatagramSocket(int port) |
创建 UDP 套接字,绑定指定端口(多用于服务端) |
void receive(DatagramPacket p) |
接收数据报,阻塞方法,无数据则一直等待 |
void send(DatagramPacket p) |
发送数据报,非阻塞 |
void close() |
关闭套接字,释放资源 |
2. DatagramPacket(UDP 数据报包)
| 构造 / 方法 | 说明 |
|---|---|
DatagramPacket(byte[] buf, int length) |
构造接收包,指定存储数据的字节数组与长度 |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress addr) |
构造发送包,指定数据、目标 IP + 端口 |
InetAddress getAddress() |
获取数据报中的对方 IP |
int getPort() |
获取数据报中的对方端口 |
byte[] getData() |
获取数据报内的字节数据 |
3. InetSocketAddress
SocketAddress 子类,用于封装 IP 地址 + 端口号,作为数据报的目标地址。
4.3 代码示例
示例 1:UDP 回显服务端(UdpEchoServer)
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
public class UdpEchoServer {
private DatagramSocket socket=null;
public UdpEchoServer(int pork) throws SocketException {
socket=new DatagramSocket(pork);
}
//启动服务器
public void start() throws IOException{
System.out.println("server start!");
while(true){
//读取请求并解析,此处的requestPackket是receive的输出型参数
DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
//此处把二进制数据转换成字符串
String request=new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应,(这里通常是一个复杂的过程,但是由于此处的回显服务器,没有计算的过程)
String response=process(request);
//3.把响应返回给客户端,再构造一个DatagramPacket
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),
response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志
//客户端的IP地址 客户端的端口号 从客户端接收到的请求信息 服务器根据这个请求返回的响应
System.out.printf("[%s:%d] req:%s,rep:%s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//根据请求计算响应
//由于当前是回显服务器,直接把request作为response返回了
//未来要写其他服务器,只需要把process里的逻辑进行调整即可
public String process(String request){
return request;
}
public static void main(String[] args)throws IOException {
//如果知道这个端口号和别人不重复
//如果重复了,就会报错
UdpEchoServer server=new UdpEchoServer(9080);
server.start();
}
}
示例 2:UDP 回显客户端(UdpEchoClient)
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class UdpEchoClient {
//创建socket对象
private DatagramSocket socket=null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp,int serverPort)throws SocketException{
socket=new DatagramSocket();
this.serverIp=serverIp;
this.serverPort=serverPort;
}
public void start()throws IOException {
System.out.println("client start");
Scanner scanner=new Scanner(System.in);
//用户通过控制台,输入字符串,把字符串发给服务器,从服务器读取响应
while(true){
//1.从控制台读取用户输入
System.out.print("->");
String request=scanner.next();
if(request.equals("exit")){
//说明退出控制台
break;
}
//2.把用户输入的字符串构成UDP数据报,进行发送
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIp),this.serverPort);
socket.send(requestPacket);
//3.从服务器读取响应
DatagramPacket responsePacket=new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),0,responsePacket.getLength());
//4.显示响应
System.out.println(response);
}
}
public static void main(String[] args) throws IOException{
UdpEchoClient client=new UdpEchoClient("127.0.0.1",5333);
client.start();
}
}
示例 3:UDP 英译汉词典服务端(继承改写)
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<String, String>();
public UdpDictServer(int port) throws SocketException {
// 调用父类构造方法这句代码, 必须放到子类构造方法的第一行
super(port);
dict.put("hello", "你好");
dict.put("world", "世界");
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
// 可以添加更多更多的数据.
}
@Override
public String process(String request) {
return dict.getOrDefault(request, "没有找到该单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(5333);
server.start();
}
}
五、TCP 流套接字编程
5.1 TCP 通信模型(流程图)
TCP 是面向连接协议,通信前必须建立连接:
plaintext
服务端 客户端
│ │
1. 创建 ServerSocket(绑定端口) 1. 主动创建 Socket,连接服务端IP+端口
│ │
2. serverSocket.accept() 阻塞监听 ───连接请求───▶
│ │
3. 接收连接,创建专属 Socket 连接建立成功
│ │
4. 通过 Socket 获取 输入/输出流 4. 通过 Socket 获取 输入/输出流
│ │
循环读写流(多次请求-响应) 循环读写流(多次请求-响应)
│ │
通信结束 通信结束
关闭 Socket 资源 关闭 Socket 资源
5.2 Java TCP 核心 API
1. ServerSocket(TCP 服务端套接字)
表格
| 构造 / 方法 | 说明 |
|---|---|
ServerSocket(int port) |
创建服务端套接字,绑定指定端口,开始监听 |
Socket accept() |
阻塞等待客户端连接,连接成功返回专属 Socket |
void close() |
关闭服务端监听 |
2. Socket(TCP 客户端 / 服务端通信套接字)
表格
| 构造 / 方法 | 说明 |
|---|---|
Socket(String host, int port) |
客户端:指定服务端 IP 和端口,发起连接 |
InputStream getInputStream() |
获取输入流,读取对方发送的数据 |
OutputStream getOutputStream() |
获取输出流,向对方发送数据 |
InetAddress getInetAddress() |
获取连接对端的 IP 地址 |
void close() |
关闭通信套接字 |
5.3 代码示例
示例 1:TCP 回显服务端
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;
public TcpEchoServer(int port)throws IOException {
serverSocket=new ServerSocket(port);
}
public void start()throws IOException{
System.out.println("server start");
//不能使用FixdThreadPool,线程数目是固定的
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//首先要接受客户端的连接,然后才能进行通信
//如果客户端和服务器建立好了连接,accept能返回
//否则accep会阻塞
//前面是有连接的
Socket socket=serverSocket.accept();
//通过这个方法处理这个客户端整个的连接过程
//直接调用processConnection,此时就会“顾此失彼”,一旦进入到processconnection方法
//就不能再调用aceept
// processConnection(socket);
// 此处创建新线程. 在新线程里, 调用 processConnection
// Thread t = new Thread(() -> {
// processConnection(socket);
// });
//t.start();
// 如果当前线程数目进一步增多, 创建销毁进一步频繁, 此时线程创建销毁开销不可忽视了.
// 使用线程池, 是进一步的改进手段.
executorService.submit(() -> {
processConnection(socket);
});
}
}
private void processConnection(Socket socket) {
// 在一次连接中, 客户端和服务器之间可能会进行多组数据传输.
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
// [面向字节流] & [全双工]
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 处理多次请求/响应的读写操作.
// 一次循环就是读写一个请求/响应
// 1. 读取请求并解析 (可以直接使用 Scanner 完成)
if (!scanner.hasNext()) {
// 客户端关闭了连接
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
// 假设这里有一系列的逻辑呢??
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回客户端
writer.println(response);
//冲刷缓冲区
writer.flush();
// 4. 打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 假如在这之前也有一系列逻辑呢?
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
示例 2:TCP 回显客户端
mport 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 {
// 客户端在 new Socket 的时候, 就会和服务器建立 TCP 连接.
// 此时少了服务器 IP 和 端口.
socket = new Socket(serverIp, serverPort);
// socket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort));
}
public void start() {
System.out.println("client start!");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取用户输入.
System.out.print("-> ");
String request = scanner.next();
// 2. 把请求发送给服务器.
writer.println(request);
writer.flush();
// 3. 从服务器读取响应
if (!scannerNetwork.hasNext()) {
break;
}
String response = scannerNetwork.next();
// 4. 把响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
示例 3:TCP英译汉词典服务端(继承改写)
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpEchoServer {
private Map<String, String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("hello", "你好");
dict.put("world", "世界");
dict.put("cat", "小猫");
dict.put("dog", "小狗");
}
public String process(String request) {
return dict.getOrDefault(request, "没有找到该单词");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}
六、TCP 服务端并发处理
单线程服务端一次只能处理一个客户端,无法支持并发,解决方案:多线程 / 线程池。
6.1 多线程版本(一连接一线程)
public void start() throws IOException {
System.out.println("TCP 服务端启动成功!");
while (true) {
Socket clientSocket = serverSocket.accept();
// 每个客户端单独开启线程处理
new Thread(() -> processConnection(clientSocket)).start();
}
}
特点:简单易实现;高并发下频繁创建 / 销毁线程,资源开销大。
6.2 线程池版本(优化并发性能)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public void start() throws IOException {
System.out.println("TCP 服务端启动成功!");
// 创建缓存线程池
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = serverSocket.accept();
// 提交任务到线程池
pool.submit(() -> processConnection(clientSocket));
}
}
特点:复用线程,减少资源损耗,适合高并发场景。
七、TCP 长短连接
根据连接关闭时机,分为短连接、长连接:
7.1 短连接
- 规则:一次请求 + 响应完成后,立即关闭 TCP 连接;
- 特点:每次通信都要重新建立、销毁连接,耗时较高;
- 适用场景:客户端请求频率低,例如网页浏览。
7.2 长连接
- 规则:连接建立后不主动关闭,双方持续多次收发数据;
- 特点:仅首次建立连接,后续直接传输数据,效率更高;服务端 / 客户端均可主动发数据;
- 适用场景:通信频繁,例如聊天室、实时游戏、即时通讯。
补充说明
本文示例基于 BIO(同步阻塞 IO) 实现长连接,每个连接独占一个线程,高并发场景资源占用极高。 生产环境高性能长连接,一般使用 NIO(同步非阻塞 IO) 实现。
八、Socket 编程常见问题与排查
8.1 端口被占用
报错信息
java.net.BindException: Address already in use: JVM_Bind
原因:一个端口同一时间只能被一个进程绑定,重复绑定触发异常。
排查步骤(Windows CMD)
- 根据端口号查询对应进程 PID:
netstat -ano | findstr 端口号 - 打开任务管理器 → 详细信息,根据 PID 定位占用进程。
解决方案
- 关闭占用该端口的进程,重新启动程序;
- 修改当前程序的绑定端口,使用空闲端口。
8.2 通用注意事项
- 开发可在单机模拟客户端、服务端,线上环境一般为不同主机;
- 必须明确目标 IP + 目标端口,才能定位到远端进程;
- Socket 基于传输层 TCP/UDP,复杂场景需要自定义应用层协议;
- 代码中务必在
finally中关闭套接字、流资源,避免资源泄漏。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)