网络编程-

发布于:2025-09-10 ⋅ 阅读:(25) ⋅ 点赞:(0)

网络编程套接字(Socket)

1. Socket是什么?

  • Socket 是英文单词,原意是“插槽”,类似于电脑主板上插各种硬件的插槽。
  • 在网络编程中,socket 指的是操作系统给应用程序(比如你的Java程序)提供的一组API(接口),用来进行网络通信。

2. Socket的作用

  • 操作系统通过socket API,让应用程序能够像插插槽一样,连接到网络上,实现数据的发送和接收。
  • 传输层通过socket,把网络功能“插”给应用层使用。

3. Java中的Socket API

  • 学习Java网络编程时,主要用的是JDK封装好的socket API
  • 操作系统原生的socket API(如Linux的socket()bind()listen()等),JDK已经帮你封装好了,所以你只需要用Java提供的类和方法(如SocketServerSocket等)就能实现网络通信。

4. 补充说明

  • socket 就像一个“插口”,应用程序通过它和网络打交道。
  • 不同语言有自己的socket库,但底层原理都一样,都是利用操作系统提供的接口实现通信。

TCP和UDP的区别

UDP和TCP差别很大,所以socket api 提供了两组不同的api

TCP 有连接,可靠传输,面向字节流,全双工

UDP 无连接,不可靠传输,面向数据包,全双工

有连接 vs 无连接

此处的连接,是抽象的连接

通信双方,如果保存了通信对端的信息,相当于是"有连接",如果不保存对端信息,就是"无连接"

可靠传输 vs 不可靠传输

此处谈到的 “可靠” 不是指 100% 能到达对方, 而是尽可能

网络环境非常复杂,存在很多的不确定因素

相对来说,不可靠,就是完全不考虑,数据是否能够到达对方

TCP 内置了一些机制,能够保证可靠传输

  1. 感知对方是不是收到了
  2. 重传机制,在对方没收到的时候进行重试

UDP 则没有可靠性机制

UDP 完全不管发出去的数据是否顺利到达对方

面向字节流 vs 面向数据报

TCP 是面向字节流的,TCP传输过程就和文件流是一样的特点

UDP 是面向数据报

传输数据的基本单位,不是字节了,而是"UDP 数据报"

一次发送/接收, 必须发送/接受完整的 UDP 数据报

全双工 vs 半双工

全双工: 一个通信链路, 可以发送数据, 也可以接收数据 (双向通信)

半双工: 一个通信链路, 只能发送/只能接收 (单向通信)

Socket对象

1. 为什么要用Socket?

  • 直接用代码操作网卡很麻烦,因为网卡型号很多,每种网卡的接口(API)都不一样。
  • 为了让应用程序员不用关心各种网卡的差异,操作系统把网卡的操作统一“封装”成了socket接口。

2. Socket的作用

  • 应用程序只需要操作socket对象,不用关心底层硬件细节。
  • 操作系统负责把你的socket操作翻译成对具体网卡的操作,实现数据收发。
  • socket就像遥控器,你只需按按钮,具体怎么控制电视(网卡),操作系统帮你搞定。

3. Socket的抽象意义

  • 在操作系统里,socket可以看作是一种特殊的文件类型,属于“广义的文件”。
  • 这种“文件”其实是对网卡、端口等硬件设备的一个抽象和表示。
  • 就像你打开文件可以读写内容,打开socket可以收发数据。

4. Java中的Socket对象

  • 例如Java里的DatagramSocket对象,就是对底层网卡操作的一个抽象。
  • 你只需要创建和操作DatagramSocket,不用关心具体网卡型号和驱动。

5. 总结比喻

  • socket = 遥控器:你操作遥控器,系统帮你控制硬件。
  • socket = 抽象文件:在操作系统中,把复杂的硬件操作抽象成统一的“文件”,方便程序员使用。

UDP/TCP api 的使用

UDP

1. DatagramSocket

DatagramSocket 是 UDP Socket,用于发送和接收 UDP 数据报。

构造方法

方法签名 方法说明
DatagramSocket() 创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) 创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口(一般用于服务端)

常用方法

方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字

2. DatagramPacket

DatagramPacket 是 UDP Socket 发送和接收的数据报。

构造方法

方法签名 方法说明
DatagramPacket(byte[] buf, int length) 用于接收数据报,数据保存在字节数组 buf 中,接收指定长度 length
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 用于发送数据报,数据为字节数组 buf 中,从 offset 到指定长度 length,address 指定目的主机的 IP 和端口号

常用方法

方法签名 方法说明
InetAddress getAddress() 获取发送端主机 IP 地址(接收时);获取接收端主机 IP 地址(发送时)
int getPort() 获取发送端主机端口号(接收时);获取接收端主机端口号(发送时)
byte[] getData() 获取数据报中的数据

3. InetSocketAddress

InetSocketAddress 是 SocketAddress 的子类,用于表示 IP 地址和端口号。

构造方法

方法签名 方法说明
InetSocketAddress(InetAddress addr, int port) 创建一个 Socket 地址,包含 IP 地址和端口号

UDP回显服务器 (echo server)

客户端发送什么请求,服务器就返回什么响应

没有任何业务逻辑,进行任何计算或处理

服务端代码:

class UdpServer {
    private DatagramSocket datagramSocket;

    public UdpServer(int port) throws SocketException {
        datagramSocket = new DatagramSocket(port);
    }

    public void start() {
        System.out.println("服务器开始运行!");
        while (true) {
            try {
                DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
                datagramSocket.receive(requestPacket);
                String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
                // 输出日志
                System.out.println("[" + new java.util.Date() + "] 收到来自 "
                        + requestPacket.getSocketAddress() + " 的请求: " + request);

                String response = process(request);
                DatagramPacket responsePacket = new DatagramPacket(
                        response.getBytes(),
                        response.getBytes().length,
                        requestPacket.getSocketAddress()
                );
                datagramSocket.send(responsePacket);
            } catch (IOException e) {
                System.out.println("服务器发生IO异常: " + e.getMessage());
            }
        }
    }

    private String process(String request) {
        return request;
    }

}


public class Server {
    public static void main(String[] args) throws IOException {
        new UdpServer(9090).start();
    }
}

客户端代码:

class UdpClient {
    private DatagramSocket datagramSocket;
    private String serverIp;
    private int serverPort;

    public UdpClient(String serverIp, int serverPort) throws SocketException {
        datagramSocket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        System.out.println("UDP客户端已启动,输入内容并回车发送给服务器。");
        while (scanner.hasNextLine()) {
            System.out.print("请输入要发送的内容:");
            try {
                String input = scanner.nextLine();
                DatagramPacket requestPacket = new DatagramPacket(
                        input.getBytes(),
                        input.getBytes().length,
                        InetAddress.getByName(serverIp),
                        serverPort
                );
                datagramSocket.send(requestPacket);

                DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
                datagramSocket.receive(responsePacket);
                String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
                // 输出日志
                System.out.println("[" + new java.util.Date() + "] 收到服务器回复: " + response);
            } catch (IOException e) {
                System.out.println("客户端发生异常: " + e.getMessage());
            }
        }
    }

}

public class Client {
    public static void main(String[] args) throws IOException {
        new UdpClient("127.0.0.1",9090).start();
    }
}
注意事项

datagramSocket.receive(): receive 是从网卡上读取数据, 但是调用 receive 的时候, 网卡没有数据,就会阻塞等待,一直等待到真正收到数据为止

String response = new String(responsePacket.getData(), 0, responsePacket.getLength());:

  • responseRequest.getData().length缓冲区的总长度(比如 4096),
  • 实际收到的数据长度应该用 responseRequest.getLength(),否则会把没用的缓冲区内容也转成字符串,可能会有很多乱码或多余字符。
DatagramPacket responsePacket = new DatagramPacket(
        response.getBytes(),
        response.getBytes().length,
        requestPacket.getSocketAddress()
);

DatagramSocket 这个对象中, 不持有对方的 ip 和端口的, 所以 send 的时候需要把 ip 和断开写进去

socket 也是一种文件, 也是需要关闭的. 此处, 就算代码中没有 close, 进程关闭, 也会释放文件描述符表里的所以内容, 也就相当于 close 了

TCP

ServerSocket

ServerSocket 是创建TCP服务端Socket的API。

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口
方法签名 方法说明
Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close() 关闭此套接字

Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,以及用来与对方收发数据的。

方法签名 方法说明
Socket(String host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

服务端代码:

class TcpServer{
    private ServerSocket serverSocket;
    public TcpServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            System.out.println("收到新连接: " + clientSocket.getRemoteSocketAddress());
            Thread t =new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        try (
                InputStream in = clientSocket.getInputStream();
                OutputStream out = clientSocket.getOutputStream();
                Scanner scanner = new Scanner(in);
                PrintWriter writer = new PrintWriter(out, true);
        ) {
            while (scanner.hasNextLine()) {
                String request = scanner.nextLine();
                System.out.println("收到请求: " + request);

                String response = process(request);
                System.out.println("发送响应: " + response);

                writer.println(response);
            }
        } catch (IOException e) {
            System.out.println("处理连接时发生异常: " + e.getMessage());
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                System.out.println("关闭socket时发生异常: " + e.getMessage());
            }
            System.out.println("连接已关闭: " + clientSocket.getRemoteSocketAddress());
        }
    }


    private String process(String request) {
        return request;
    }
}

public class Server {
    public static void main(String[] args) throws IOException {

        new TcpServer(9999).start();
    }
}

客户端代码:

class TcpClient{
    private Socket socket;
    public TcpClient(String host, int port) throws IOException {
        socket=new Socket(host,port);
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        try (
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();
                Scanner serverScanner = new Scanner(in);
                PrintWriter writer = new PrintWriter(out, true);
        ) {
            while (scanner.hasNextLine()) {
                String input = scanner.nextLine();
                System.out.println("发送请求: " + input);
                writer.println(input);

                if (serverScanner.hasNextLine()) {
                    String response = serverScanner.nextLine();
                    System.out.println("收到响应: " + response);
                } else {
                    System.out.println("服务器已关闭连接。");
                    break;
                }
            }
        } catch (IOException e) {
            System.out.println("客户端发生异常: " + e.getMessage());
        }
    }
}

public class Client {
    public static void main(String[] args) throws IOException {
        new TcpClient("127.0.0.1",9999).start();
    }
}
注意事项

TCP 是面向字节流的. TCP 上传输数据的基本单位就是 byte.

TCP 是有连接的. 有连接, 就需要有一个 “建立连接” 的过程.

建立连接的过程就类似于打电话.

此处的 accept 相当于 “接电话”

由于客户端是 “主动发起” 的一方, 服务器是 “被动接受” 的一方

一定是客户端打电话, 服务器接电话.

构造 Socket 对象, 就是和服务器 “建立连接”

new 好对象之后, 和服务器的连接就建立完成了

如果建立失败, 就会直接再构造对象的时候, 抛出异常

TCP 建立连接的流程, 是在操作系统内核完成的. 我们的代码感知不到.

accept() 操作, 是内核已经完成了连接建立的操作, 然后才能够进行 “接通电话”

InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();

TCP 是全双工的, 一个 socket, 既可以读, 也可以写

PrintWriter writer = new PrintWriter(out, true);
  • 默认情况下,PrintWriter 的缓冲区会在调用 println() 后自动刷新(如果构造时设置了 autoFlush=true)。
  • 如果没有设置自动刷新,只有在缓冲区满或调用 flush()/close() 时才会把数据写出去。
finally {
    clientSocket.close();
}

ServerSocket 和 DatagramSocket 的生命周期是跟随整个进程的

服务端里面创建的 Socket 是"连接级别"的数据,当客户端断开连接后,这个 clientSocket 就不再使用了,需要主动关闭掉以避免资源泄露

while (true) {
    Socket clientSocket = serverSocket.accept();
    System.out.println("收到新连接: " + clientSocket.getRemoteSocketAddress());
    Thread t =new Thread(()->{
        processConnection(clientSocket);
    });
    t.start();
}

serverSocket.accept():
等待客户端连接。这个方法会阻塞,直到有客户端(比如浏览器、其他程序)发起连接,才会返回一个 Socket 对象。

每有一个新连接,就创建一个新线程,在线程里调用 processConnection(clientSocket) 方法,专门处理当前客户端的通信。

public void start() throws IOException {
    System.out.println("服务器启动!");
    ExecutorService threadPool=Executors.newCachedThreadPool();
    while (true) {
        Socket clientSocket = serverSocket.accept();
        System.out.println("收到新连接: " + clientSocket.getRemoteSocketAddress());
        threadPool.submit(()->{
            processConnection(clientSocket);
        });
    }
}

为了避免频繁创建销毁线程, 也可以引⼊线程池.

长连接与短连接对比

1. 概念
  • 短连接
    每次发送数据前都要建立连接,数据收发完成后立即关闭连接。
    特点:一次连接只收发一次数据。
  • 长连接
    建立连接后,保持连接不断开,双方可以多次收发数据。
    特点:一个连接可多次收发数据,连接持续存在。

2. 区别
对比项 短连接 长连接
建立/关闭连接耗时 每次请求都需建立和关闭连接,耗时高 只需首次建立连接,后续直接传输,效率高
主动发送请求 一般由客户端主动发送 客户端和服务端都可以主动发送
适用场景 客户端请求频率低,如网页浏览 通信频繁,如聊天室、实时游戏等

3. 实现方式与资源消耗
  • BIO(同步阻塞IO)下的长连接
    每个连接都需要一个线程来阻塞等待数据,线程资源消耗大,难以支持高并发。
    • 每个线程长期占用,系统负担重。
  • NIO(同步非阻塞IO)下的长连接
    通过IO多路复用,用少量线程管理大量连接,大幅提升性能。
    • 适合高并发场景,资源利用率高。

4. 总结
  • 短连接:适合低频、简单请求场景,易于实现但效率低。
  • 长连接:适合高频、实时通信场景,推荐用NIO等高效方式实现,能极大提升并发性能。

线程池与IO多路复用

1. 线程池(Thread Pool)

使用场景

  • 客户端发请求后快速断开连接,服务器需要高效处理每个短连接请求。
  • 线程池可以避免为每个请求都创建新线程,提升资源利用率。

两种常见线程池

1.1 Executors.newCachedThreadPool()
  • 特点:线程池大小没有上限(最大线程数是 Integer.MAX_VALUE,约21亿)。
  • 适用场景:任务数量不确定、任务执行时间短,连接是短暂的。
  • 风险:如果短时间内有大量请求,会创建大量线程,导致系统压力极大,甚至崩溃。
1.2 Executors.newFixedThreadPool(nThreads)
  • 特点:线程池大小固定,比如10个线程。
  • 适用场景:服务器能同时处理的客户端数量有限,超出的请求会被排队等待。
局限性
  • 如果客户端持续发送请求且连接保持很久(长连接),每个连接分配一个线程会导致线程数暴增,资源耗尽。
  • 当客户端数量达到上万、几十万,线程池(无论是缓存还是固定)都不合适。

2. IO多路复用(IO Multiplexing)

解决方案

  • 针对大量长连接客户端,采用IO多路复用技术(如 selectpollepoll)。
  • 可以用少量线程高效地服务于大量客户端连接,极大减少资源消耗。

应用

  • 各种高性能服务器框架(Java NIO、Netty、C++中的libevent等)都已经封装了IO多路复用技术。
  • 在C++领域讨论更多,Java也有对应实现。

总结

  • IO多路复用适合高并发长连接场景,线程池适合短连接或任务型场景。

3. 总结流程
  1. 短连接/请求型服务:可以用线程池(如newCachedThreadPool),但要注意线程数量风险。
  2. 高并发长连接服务:建议用IO多路复用技术,避免线程池资源耗尽。

网站公告

今日签到

点亮在社区的每一天
去签到