JAVA网络编程——socket套接字的介绍下(详细)

发布于:2025-05-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

前言

1.TCP 套接字编程 与 UDP 数据报套接字的区别

2.TCP流套接字编程 

API 介绍

TCP回显式服务器

Scanner 的多种使用方式

PrintWriter 的多种使用方式

 TCP客户端

3. TCP 服务器中引入多线程

结尾

前言

各位读者大家好,今天笔者继续更新socket套接字的下半部分,这部分承接上半篇的内容接着往下写

JAVA网络编程——socket套接字的介绍上(详细)-CSDN博客

上半篇博客笔者主要汇总了一些网络编程的基础知识,并介绍了UDP数据报套接字编程和代码案例

实现了一个简单的回显服务器和客户端

博客的主要内容如下:

1.TCP流套接字编程 与 UDP 数据报套接字的区别,并结合具体代码,说明两者在实现方式上的差异。

2.TCP流套接字 编程常用 API 和代码示例,并尝试引入多线程思路对服务端代码进行优化。

3.最后,笔者还将分享一些学习过程中遇到的问题和经验,希望能对读者有所启发。

本篇博客的内容会和上半篇博客有联系,但您依旧可以把他看作是独立的知识博客,无需太多阅读门槛,言尽于此,就让我们开始吧!

1.TCP 套接字编程 与 UDP 数据报套接字的区别

在实际编程之前,我们首先要了解:为什么 TCP 和 UDP 的套接字写法会有所不同?

 从传输层协议的角度来看, TCP和UDP是不同的协议

TCP 是一种面向连接的传输层协议,在通信前需要通过“三次握手”建立连接,通信过程中也会通过校验、序号、确认应答等机制保证数据的可靠传输。可以理解为,TCP 的通信就像是打电话,必须先拨号接通,双方确认后才能说话。

 而 UDP 是一种无连接的协议,发送数据前并不建立连接,也不会确认数据是否成功送达。UDP 更像是发快递,你把包裹投递出去就完事了,送达与否不做额外处理。这种机制导致 UDP 的通信过程更加简单高效,但也更容易出现数据丢失、乱序的问题。

也正因为如此,在编程中,两者的使用方式也大相径庭。

对于UDP套接字编程,正如前文提到的服务端通过 DatagramSocket 创建一个端口并监听,客户端发送数据时需要用 DatagramPacket 显式地指定目标 IP 和端口号。

  通俗来说 UDP就像发短信

  • 直接发送,不管对方是否收到
  • 发完就完了,不知道对方看没看到
  • 快速简单,但不保证送达

 相比之下,TCP 流套接字编程更加结构化。服务端使用 ServerSocket 来监听端口,当有客户端发起连接时,会通过 accept() 方法返回一个与客户端进行一对一通信的 Socket 对象。在这之后,服务端和客户端之间就建立起了一个稳定的通信通道,后续的通信就像操作输入输出流一样,不需要再关注 IP 和端口这些底层细节。

换句话说:TCP读取数据的基本单位是字节,TCP 协议本身是面向字节流(byte stream)的协议

 同样通俗的说 , TCP就像打电话

  • 拨号建立连接,确认对方接听后才开始说话
  • 说话过程中可以确认对方是否听到
  • 如果线路有问题会重新说一遍
  • 说完话要挂断电话结束通话

也就是说,UDP 更加“灵活”和“轻便”,每次通信都要说明来龙去脉(目标地址);而 TCP 则是“关系型”的,一旦连接建立,双方就可以安心地进行数据交换,程序的逻辑也更加清晰。

接下来笔者开始介绍API和代码示例,并介绍TCP套接字的代码是如何体现它和UDP的区别的. 

2.TCP流套接字编程 

 和UDP数据报套接字编程类似,TCP流套接字编程 同样有两个重要的API

API 介绍

1.ServerSocket
ServerSocket 这个类用于服务器端监听特定端口,等待客户端来连接。 它体现了 TCP 协议中"先建立连接,再通信”的特性。

 ServerSocket 构造方法

ServerSocket serverSocket = new ServerSocket(int port);

其中 int port 表示 监听的端口号 ,一旦绑定成功,port端口就会被占用,等待客户端连接

常用方法:

1.Socket accept()

    开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket建⽴与客⼾端的连接,否则阻塞等待
具体一点说:

accept() 方法执行以后:

服务器端和客户端之间就建立起了一条可靠的 TCP 连接,双方都会在系统内核中保存关于对方的关键信息,如:

  • 客户端的 IP 地址

  • 客户端的端口号

  • 服务器的 IP 地址

  • 服务器的端口号

这 4 个信息,加上传输协议(TCP),就构成了所谓的 “五元组”,唯一标识一条 TCP 连接。这也体现了 TCP协议和UDP协议之间的区别

2.Socket close()

    关闭此套接字

2. Socket

Socket 是客户端/服务端之间的通信通道

客户端和服务端之间每一次连接,都会被封装成一个 Socket 对象,负责双方的输入输出数据流。

Socket 构造方法

 构造方法(客户端使用):

Socket socket = new Socket(String host, int port);

其中 host 表示 IP 地址,port表示端口

常用方法:

1.InputStream getInputStream()


获取输入流,用于读取从对端发送过来的字节数据。

2.OutputStream getOutputStream()


获取输出流,用于向对端发送字节数据。

3.close()


关闭当前连接。

主要的API介绍完了,接下来我们通过代码示例来展示他们如何使用,笔者还是先展示一个回显服务器+客户端

TCP回显式服务器

根据上面对 ServerSocket 的介绍,我们可以知道:

在编写一个 TCP 回显式服务器时,首先需要创建一个 ServerSocket 实例并指定监听的端口号。这个操作会在当前机器上开启一个服务器进程,等待客户端的连接请求

一旦有客户端连接到这个端口,服务器就会通过 accept() 方法接收这个连接请求,并返回一个新的 Socket 对象。这个 Socket 对象就代表了服务器与某个客户端之间的一条通信通道

public class TcpEchoServer {
    ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        // 绑定端口号
//      和 UDP 不同,TCP 在通信前必须先建立连接(就是三次握手)。
// 所以服务器要做的第一件事:不是接收数据,而是 等待客户端来连我。
    }
}

然后我们启动服务器:

    public void start() throws IOException {
        System.out.println("服务器启动了");

        Socket socket = serverSocket.accept(); // 建立连接
        processwork(socket);
    }

 其中 processwork () 方法内来实现服务器的功能

而我们的服务器主要实现的功能为:

1.读取请求

2.响应

3.返回请求

请看代码:

    private  void processwork(Socket socket) throws IOException {
        // 1.读取请求
        // 2.响应
        // 3.返回响应
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream())
        {
            // 包装一下
            Scanner scanner = new Scanner(inputStream);//填入什么参数,就代表了什么输入方式
            PrintWriter printWriter = new PrintWriter(outputStream);
      while(true) {


          String request = scanner.nextLine();// 读取请求

          String response = process(request); // 回显

          printWriter.println(response); // 返回响应
          printWriter.flush(); // 刷新缓冲区,确保已经返回

//            4.打印日志
          System.out.printf("[%s:%d] request: %s, response: %s\n",
                  socket.getInetAddress().toString(),
                  socket.getPort(),
                  request,

                  response);
            }
        }
        finally {
            socket.close();
        }
    }

   InputStream 表示从客户端读入数据的字节流,OutputStream 表示写数据给客户端的字节流

使用了 try-with-resources 语法,能确保在代码执行完毕后,自动关闭资源(这里是输入输出流)。这种写法简洁、安全,避免了流忘记关闭导致的资源泄露问题。

 使用 Scanner 包装输入流,能够将字节数据按照「行文本」的形式解析,方便我们通过 scanner.nextLine() 读取一整行字符串。相比直接用 inputStream.read() 读取字节数组,这种方式更直观、适合处理文本请求。

我们都知道,我们常用 Scanner scanner = new Scanner(System.in); 这代表控制台输入,这是我们初学时学会的,但实际上 Scanner 会根据你传入参数的不同表示不同的输入方法

Scanner 的多种使用方式

1. 控制台输入:

Scanner scanner = new Scanner(System.in);

从键盘读取用户输入,适合交互式程序。


2. 从文件中读取:

Scanner scanner = new Scanner(new File("input.txt"));

读取文件内容,适用于处理数据文件或配置文件等场景。


3. 从字符串中读取:

Scanner scanner = new Scanner("Hello World\n123");

可以将字符串当作输入源,非常适合字符串解析。


4. 从网络输入流中读取(Socket 网络通信中):

InputStream inputStream = socket.getInputStream();
 Scanner scanner = new Scanner(inputStream);

这是网络编程中常见的用法,此时 Scanner 会从网络连接中读取对方(比如客户端)发送来的数据。比如 TCP 编程中,服务器端使用 socket.getInputStream() 获取输入流,从客户端接收数据。

同理

PrintWriter 是对输出流的包装,能够方便地以「文本行」的形式输出数据,比如我们可以用 printWriter.println(response) 将一行响应发出去。注意它默认有缓冲机制,发送数据前必须手动调用 flush() 刷新缓冲区,确保数据立即写出。 

同理,我在这里汇总一下

PrintWriter 的多种使用方式

1. 输出到控制台

PrintWriter writer = new PrintWriter(System.out);
writer.println("Hello, Console!");
writer.flush();

2. 输出到文件

PrintWriter writer = new PrintWriter(new File("output.txt")); 
writer.println("Write this to file."); writer.flush(); writer.close();
  • 输出目标:磁盘文件

  • 适用场景:日志记录、写入结果、持久化数据

3. 输出到网络(如 TCP 编程)

OutputStream outputStream = socket.getOutputStream(); 
PrintWriter writer = new PrintWriter(outputStream); 
writer.println("Hello, client!");
writer.flush();
  • 输出目标:通过 TCP 连接的对方客户端(或服务器)

  • 适用场景:TCP 套接字通信中,服务器/客户端发送响应或数据

4. 输出到内存字符串缓冲区

StringWriter stringWriter = new StringWriter();
 PrintWriter writer = new PrintWriter(stringWriter);
 writer.println("Write to memory."); 
writer.flush(); 
String result = stringWriter.toString(); // 获取最终字符串
  • 输出目标:内存中的字符串

  • 适用场景:构建复杂字符串、模板处理、动态拼接内容

最后,无论通信过程中是否抛出异常,最终都要关闭 socket。这是网络编程中很重要的一步,必须手动释放网络资源,避免连接堆积导致服务器崩溃。

那么为什么之前的UDP不用?

本质原因:UDP 是无连接的

TCP 是一种面向连接的协议,通信前需要建立连接,通信后要关闭连接,连接的生命周期是明确的,因此必须显式关闭以释放资源。

而 UDP 是无连接的协议DatagramSocket 并不会维持一个长时间的连接状态。它只是一个发送/接收数据报的工具,就像一个“邮筒”或“信箱”,你发完信或者收完信就可以不管它了,系统会随着进程结束自动回收资源。

 因此完整的服务器代码如下:

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 {
    ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        // 绑定端口号
//      和 UDP 不同,TCP 在通信前必须先建立连接(就是三次握手)。
// 所以服务器要做的第一件事:不是接收数据,而是 等待客户端来连我。
    }
    public void start() throws IOException {
        System.out.println("服务器启动了");

        Socket socket = serverSocket.accept(); // 建立连接
        processwork(socket);
    }

    private  void processwork(Socket socket) throws IOException {
        // 1.读取请求
        // 2.响应
        // 3.返回响应
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream())
        {
            // 包装一下
            Scanner scanner = new Scanner(inputStream);//填入什么参数,就代表了什么输入方式
            PrintWriter printWriter = new PrintWriter(outputStream);
      while(true) {


          String request = scanner.nextLine();// 读取请求

          String response = process(request); // 回显

          printWriter.println(response); // 返回响应
          printWriter.flush(); // 刷新缓冲区,确保已经返回

//            4.打印日志
          System.out.printf("[%s:%d] request: %s, response: %s\n",
                  socket.getInetAddress().toString(),
                  socket.getPort(),
                  request,

                  response);
            }
        }
        finally {
            socket.close();
        }
    }

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

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

 TCP客户端

 接下来是客户端,他大概有两个功能

1.构造请求,并发送给服务器

2.读取服务器发送回来的请求

我们首先创建一个 Socket 对象,并且绑定好IP地址和端口号

public class TcpEchoClient{
    private Socket socket = null;


    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 连接服务器
        socket = new Socket(serverIp, serverPort);
    }
}

 然后实现功能:

代码和服务器原理一致,笔者就不过多细说了

    public void start() throws IOException
    {
        System.out.println("客户端启动,已连接服务器");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream())
        {
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true)
            {
                // 1.读取内容
                System.out.println("输入内容");
                String request = scanner.nextLine();
               //  2.发送给服务器
                 printWriter.println(request);
                 // 加上ln, 暗中约定一个请求以\n作为结尾
                 // 刷新缓冲区,保证数据能正确发出去
                printWriter.flush();

                // 3.读取服务器返回的响应
                String response = scannerNet.nextLine();

                //4.打印返回的数据
                System.out.println(response);


            }
        }
        catch (IOException e)
        {
         throw  new RuntimeException(e);
        }

    }

 最后效果如下:

3. TCP 服务器中引入多线程

接下来我们谈一下 多线程的问题

在最初的 TCP 服务器代码中,我们使用如下方式来接收客户端连接:

Socket socket = serverSocket.accept();
processwork(socket);

这种写法是单线程、串行处理,意味着服务器一次只能处理一个客户端的请求。当一个客户端连接上来,服务器就会一直处理这个客户端的请求,直到处理完毕后,才能继续 accept() 等待下一个客户端。这带来两个致命问题:

  1. 其他客户端无法同时连接
    如果第一个客户端一直占用服务器,那么第二个客户端连接时,服务器就无法 accept(),连接会阻塞甚至超时。

  2. 服务器响应效率极低
    在网络通信中,尤其是长连接的场景下,一个客户端可能长时间保持连接,服务器如果不使用并发机制,就会严重浪费资源和降低效率。

TCP 的连接机制导致必须并发处理

TCP 是面向连接的协议,通信前需要完成三次握手,通信过程中保持连接状态。每个 Socket 实例都代表一次独立的连接。一旦 accept() 接收了连接,这个 Socket 的处理过程就独占了当前线程。如果不多线程处理,服务器根本无法同时应对多个 Socket 连接。

因此我们可以引入多线程的写法, 那么,我们怎么引入呢?

 笔者选的的是线程池,这样方便管理

来看具体代码:

ExecutorService executorService = Executors.newCachedThreadPool(); // 创建线程池

while (true) {
    Socket socket = serverSocket.accept(); // 接受连接
    executorService.submit(() -> {         // 把任务提交给线程池
        try {
            processwork(socket);           // 多线程处理该连接
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    });
}
  • newCachedThreadPool()这是一个可自动扩容的线程池,适合连接数不固定、通信时长不一致的场景;

  • submit(() -> {...})使用 Lambda 表达式将每个客户端的任务包装成线程任务提交;

  • 每个客户端的 Socket 都被交给线程池中的一个线程独立处理,互不干扰。

 完整代码:


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 {
    ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        // 绑定端口号
//      和 UDP 不同,TCP 在通信前必须先建立连接(就是三次握手)。
// 所以服务器要做的第一件事:不是接收数据,而是 等待客户端来连我。
    }
    public void start() throws IOException {
        System.out.println("服务器启动了");
        ExecutorService executorService = Executors.newCachedThreadPool();//动态增长的线程池
        while (true) {
            Socket socket = serverSocket.accept(); // 建立连接
            executorService.submit(() ->{
                try {
                    processwork(socket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
//        Socket socket = serverSocket.accept(); // 建立连接
//        processwork(socket);
    }

    private  void processwork(Socket socket) throws IOException {
        // 1.读取请求
        // 2.响应
        // 3.返回响应
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream())
        {
            // 包装一下
            Scanner scanner = new Scanner(inputStream);//填入什么参数,就代表了什么输入方式
            PrintWriter printWriter = new PrintWriter(outputStream);
      while(true) {


          String request = scanner.nextLine();// 读取请求

          String response = process(request); // 回显

          printWriter.println(response); // 返回响应
          printWriter.flush(); // 刷新缓冲区,确保已经返回

//            4.打印日志
          System.out.printf("[%s:%d] request: %s, response: %s\n",
                  socket.getInetAddress().toString(),
                  socket.getPort(),
                  request,

                  response);
            }
        }
        finally {
            socket.close();
        }
    }

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

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

效果如下:

由此可见,可以同步的和服务器进行网络通信

如果多线程依旧无法满足要求,那么还可以考虑多路复用,这个的话以后有时间笔者完全理解了,就写博客分享一下

结尾

这一篇的文本量也达到了1w字往上,希望对于读者们有帮助

后面笔者还想开博客介绍一下 IO操作,HTTPS和HTTP协议,希望能多多支持