【Java 网络编程全解】Socket 套接字与 TCP/UDP 通信实战全解

文章目录

    • [一、 网络编程基础](#一、 网络编程基础)
      • [1.1 网络编程中的基本概念](#1.1 网络编程中的基本概念)
        • [1.1.1 发送端和接收端](#1.1.1 发送端和接收端)
        • [1.1.2 请求和响应](#1.1.2 请求和响应)
        • [1.1.3 客户端和服务端](#1.1.3 客户端和服务端)
    • 二、Socket套接字
    • 三、UDP数据报套接字编程
    • 四、TCP流套接字编程
        • [4.1 服务器引入多线程](#4.1 服务器引入多线程)
        • [4.2 服务器引入线程池](#4.2 服务器引入线程池)

一、 网络编程基础

  • 什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。

只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。

特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。

1.1 网络编程中的基本概念

1.1.1 发送端和接收端

在一次网络数据传输时:

  • 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
  • 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
  • 收发端:发送端和接收端两端,也简称为收发端。

注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

1.1.2 请求和响应

一般来说,获取一个网络资源,涉及到两次网络数据传输:

  1. 第一次:请求数据的发送
  2. 第二次:响应数据的发送
1.1.3 客户端和服务端
  • 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
  • 客户端:获取服务的一方进程,称为客户端。

对于服务来说,一般是提供:

  1. 客户端获取服务资源

    例如:客户端主机的客户端进程发送"获取服务资源"的请求到服务端主机的服务端进程,服务端进程发送"返回服务资源"的响应到客户端进程,如:传输视频资源、图片资源、文本资源。

  2. 客户端保存资源在服务端

    例如:客户端主机的客户端进程发送"保存用户资源"的请求到服务端主机的服务端进程,服务端进程保存资源后发送"返回处理结果"的响应到客户端进程。


二、Socket套接字

概念: Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。

分类: Socket套接字主要针对传输层协议划分为如下三类:

  1. 流套接字:使用传输层TCP协议

TCP,即(传输控制协议),传输层协议。

以下为TCP的特点:

  • 有连接
  • 可靠传输
  • 面向字节流
  • 有接收缓冲区,也有发送缓冲区
  • 大小不限

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。

  1. 数据报套接字:使用传输层UDP协议

UDP,即(用户数据报协议),传输层协议。

以下为UDP的特点:

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 有接收缓冲区,无发送缓冲区
  • 大小受限:一次最多传输64k

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据(假如100个字节),必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。

  1. 原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。

  • Socket编程注意事项
  1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机。
  2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程。
  3. Socket编程是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,也需要考虑。
  4. 关于端口被占用的问题
  5. 如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下:

三、UDP数据报套接字编程

API 介绍

  1. DatagramSocket : DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
  • DatagramSocket 构造方法:
方法签名 说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
  • DatagramSocket 方法:
方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字
  1. DatagramPacket : DatagramPacket 是UDP Socket发送和接收的数据报。
  • DatagramPacket 构造方法:
方法签名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket 以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个 DatagramPacket 以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号
  • DatagramPacket 方法:
方法签名 方法说明
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据
  1. 构造UDP发送的数据报时,需要传入 SocketAddress,该对象可以使用 InetSocketAddress 来创建。
  • InetSocketAddress:InetSocketAddressSocketAddress 的子类)构造方法:
方法签名 方法说明
InetSocketAddress(InetAddress addr, int port) 创建一个Socket地址,包含IP地址和端口号

代码示例:

  • UDP Echo Server
java 复制代码
//服务器
public class UdpEchoServer {

    //首先定义socket对象
    private DatagramSocket socket = null;

    /**
     *
     * @param port 端口号:区分一个主机上的不同的应用程序
     * @throws SocketException
     */
    public UdpEchoServer(int port) throws SocketException {
        socket= new DatagramSocket(port);
    }

    /**
     * 启动服务器
     */
    public void start() throws IOException {
        System.out.println("server start!");

        //死循环:编写服务器的一种常见做法,不断处理客服端发来的请求
        while (true){

            //1. 读取请求并解析
            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 responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);

            //打印日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }

    //根据请求计算响应
    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }


}
  • UDP Echo Client
java 复制代码
//客户端
public class UdpEchoClient {

    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    /**
     * 此处和服务器不一样,不用指定端口号
     * 意味着操作系统会自动分配一个空闲的端口号
     * @param serverIp : 服务器Ip
     * @param serverPort : 服务器端口号
     * @throws SocketException
     */
    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",9090);
        client.start();
    }
}
  • UDP Dict Server

编写一个英译汉的服务器,只需要重写 process 方法:

java 复制代码
/**
 * 英译汉服务器
 */
public class UdpDictServer extends UdpEchoServer{

    private Map<String,String> dict = new HashMap<String,String>();

    /**
     * @param port 端口号:区分一个主机上的不同的应用程序
     * @throws SocketException
     */
    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(9090);;
        server.start();
    }
}

四、TCP流套接字编程

和刚才UDP类似,实现一个简单的英译汉的功能。

API 介绍

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

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

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

代码示例

  • TCP Echo Server
java 复制代码
public class TcpEchoServer {
    ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("server start");

        while (true){
            // 首先接收客户端的连接,然后才能进行通信
            // 如果有客户端和服务器建立好了连接,accept 才能返回
            // 否则 accept 会阻塞
            Socket socket = serverSocket.accept();
            // 通过这个方法处理这个客服端整个的连接过程
            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. 读取请求并解析
                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] res: %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();
    }
}
  • TCP Echo Client
java 复制代码
public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket = new Socket(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 reponse = scannerNetwork.next();
                // 4. 把响应显示到控制台上
                System.out.println(reponse);
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}
4.1 服务器引入多线程

如果只是单个线程,无法同时响应多个客户端。此处给每个客户端都分配一个线程:

java 复制代码
// 启动服务器
public void start() throws IOException {
    System.out.println("服务器启动!");
    while (true) {
        Socket clientSocket = serverSocket.accept();
        Thread t = new Thread(() -> {
            processConnection(clientSocket);
        });
        t.start();
    }
}
4.2 服务器引入线程池

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

java 复制代码
// 启动服务器
public void start() throws IOException {
    System.out.println("服务器启动!");
    ExecutorService service = Executors.newCachedThreadPool();
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 使用线程池,来解决上述问题
        service.submit(new Runnable() {
            @Override
            public void run() {
                processConnection(clientSocket);
            }
        });
    }
}

长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

  • 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
  • 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

对比以上长短连接,两者区别如下:

  1. 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
  2. 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。

两者的使用场景有不同:

  • 短连接适用于客户端请求频率不高的场景,如浏览网页等。
  • 长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

扩展了解:

基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。

由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。

一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。

实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。


总结: Java 网络编程的核心是基于 Socket 套接字实现跨进程数据传输,核心概念包含发送 / 接收端、请求 / 响应、客户端 / 服务端。Socket 套接字主要分为 TCP 流套接字和 UDP 数据报套接字:TCP 具备有连接、可靠传输的特点,UDP 则无连接、传输高效但单次传输最大 64k。实操层面,UDP 通过 DatagramSocket 和 DatagramPacket 完成数据收发,TCP 依托 ServerSocket 和 Socket 实现通信,还可通过多线程、线程池优化多客户端并发处理。TCP 的长短连接适配不同场景,短连接适合低频请求,长连接适配高频通信,开发时需结合协议特性规避端口占用等问题,保障网络应用的稳定性与效率。

相关推荐
牧小七2 小时前
java14的新特性
java
努力努力再努力wz3 小时前
【Linux网络系列】:JSON+HTTP,用C++手搓一个web计算器服务器!
java·linux·运维·服务器·c语言·数据结构·c++
魂梦翩跹如雨3 小时前
死磕排序算法:手撕快速排序的四种姿势(Hoare、挖坑、前后指针 + 非递归)
java·数据结构·算法
带刺的坐椅10 小时前
Solon AI Skills 会是 Agent 的未来吗?
java·agent·langchain4j·solon-ai
jacGJ10 小时前
记录学习--文件读写
java·前端·学习
花间相见10 小时前
【JAVA开发】—— Nginx服务器
java·开发语言·nginx
扶苏-su11 小时前
Java---Properties 类
java·开发语言
cypking11 小时前
四、CRUD操作指南
java
2301_7806698611 小时前
文件字节流输出、文件复制、关闭流的方法
java