一、网络编程基础

1.1 为什么需要网络编程

互联网存在海量网络资源,包含视频、图片、文本等各类数据。用户访问网络资源的本质,就是通过网络完成数据传输,而所有网络资源的数据传输,都依赖网络编程实现

1.2 什么是网络编程

网络编程:网络中的不同主机,通过各自进程,以代码方式实现网络通信 / 数据传输

  • 通信主体:不同主机的不同进程;
  • 特殊场景:同一台主机内的多个进程通过网络传输数据,也属于网络编程(开发测试常用);
  • 核心角色:一类进程负责获取网络资源(客户端),一类进程负责提供网络资源(服务端)

二、网络编程核心概念

2.1 发送端 & 接收端

一次数据传输中定义两端角色:

  • 发送端:发送数据的进程,对应源主机;
  • 接收端:接收数据的进程,对应目的主机;
  • 特性:发送端、接收端是相对概念,数据流向反转则角色互换。

2.2 请求 & 响应

绝大多数网络交互包含两次数据传输:

  1. 请求:客户端主动发送数据,向服务端发起业务申请;
  2. 响应:服务端处理完成后,返回结果数据给客户端。

类比线下场景:到快餐店点餐,先发起 “点餐请求”,店家再给出 “出餐响应”。

2.3 客户端 & 服务端(C/S 模型)

这是网络编程最主流的架构模型:

  • 服务端:持续运行、对外提供服务与资源的进程;
  • 客户端:主动连接服务端、获取服务 / 提交数据的进程。
典型交互流程
  1. 客户端向服务端发送请求;
  2. 服务端执行业务逻辑;
  3. 服务端返回响应结果;
  4. 客户端解析并展示结果。
两类业务场景
  1. 客户端获取服务端资源(看视频、查资料);
  2. 客户端上传数据到服务端(上传文件、账号注册)。

类比银行场景:银行是服务端,用户是客户端;存款、取款均为客户端主动发起请求。


三、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)
  1. 根据端口号查询对应进程 PID:
    netstat -ano | findstr 端口号
    
  2. 打开任务管理器 → 详细信息,根据 PID 定位占用进程。
解决方案
  1. 关闭占用该端口的进程,重新启动程序;
  2. 修改当前程序的绑定端口,使用空闲端口。

8.2 通用注意事项

  1. 开发可在单机模拟客户端、服务端,线上环境一般为不同主机;
  2. 必须明确目标 IP + 目标端口,才能定位到远端进程;
  3. Socket 基于传输层 TCP/UDP,复杂场景需要自定义应用层协议;
  4. 代码中务必在 finally 中关闭套接字、流资源,避免资源泄漏。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐