TCP流套接字编程

目录

前言

TCP协议

TCP和UDP的区别

TCP流套接字

ServerSocket类构造方法

ServerSocket类相关方法

Socket类构造方法

Socket类相关方法

  使用TCP套接字编程

服务端实现

创建ServerSocket服务器对象

实现一个start()方法,建立连接进行双方的通信

服务端业务逻辑实现

完整代码

客户端实现

创建Socket对象进行通信

实现TCP客户端服务主要逻辑

完整代码

解决程序中出现的三个bug

1.刷新缓冲区

2.释放clientSocket对象资源

3.多个客户端访问服务器

实现一个查字典的功能


前言

在上一篇中,我们已经讲解了什么是网络编程,为什么要使用网络编程,并讲解了TCP和UDP之间的区别,同时也讲解了在java中如何使用UDP数据报套接字编程,实现一个回显客户端-服务器程序,那么本篇我们就来讲解一下如何在java中使用TCP流套接字编程。

TCP协议

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。简单就是一种约定,使得不同的设备、CPU和操作系统之间能够通过网络进行通信,确保了不同计算机之间恩能够遵循相同的规则来进行数据交换。

TCP和UDP的区别

我们来简单回顾一下TCP和UDP之间的区别:

  • TCP是有连接的,UDP是无连接的;
  • TCP是可靠传输,UDP是不可靠传输;(可靠是因为TCP的重传机制)
  • TCP是面向字节流,UDP是面向数据报
  • TCP和UDP都是全双工。

我们接下来要实现的TCP流套接字编程和UDP数据报套接字编程之间的主要区别在于:

TCP是有连接的,UDP是无连接的;TCP是面向字节流,UDP是面向数据报。

TCP流套接字

首先我们先来了解一下在Java中要使用TCP流套接字的一些相关方法。

我们在实现网络通信的时候,需要调用系统相关的Socket API,但在java标准库中,已经将系统中的API封装成类,我们通过实现这些类,就能够实现网络通信。

UDP中,我们想要实现网络通信,需要使用DatagramSocket类进行绑定端口号,利用DatagramPacket类来进行数据交换。

而在TCP中,我们需要使用ServerSocket类来绑定端口号(用于服务器),客户端我们使用Socket类

ServerSocket类构造方法

构造方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定的端口

想要实现TCP通信,首先就需要通信双方建立连接,ServerSocket类底层类似于一个阻塞队列,当客户端和服务端建立连接之后,ServerSocket会通过系统内核将建立好的连接存储下来,当双方需要进行通信的时候服务器就会将这个连接从内核中取出来,和客户端进行通信。

ServerSocket只能用于创建服务器。

ServerSocket类相关方法

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

我们可以通过accept()方法与客户端建立连接,当连接成功的时候,会返回一个socket对象,反之,当没有获取到连接时,就会进入阻塞等待状态,直到接收到连接请求。

 而close()方法,当我们用完socket对象之后,我们需要进行手动关闭资源。

Socket类构造方法

构造方法说明
Socket(String host,int  port)创建一个客户端套接字Socket,并与对应IP的主机上,对应端的进程建立连接

我们在客户端中构造这个对象,就相当于在和服务器进行“打电话请求连接”。

所以在写TCP客户端的时候,我们创建socket对象时需要写入服务器的IP和端口号,才能与服务器进行连接。

如果socket对象构造成功,说明成功建立连接,反之,当连接建立失败,就会抛出异常。 

Socket类相关方法

方法说明
InetAddress  getInetAddress()返回套接字所连接的地址
InputStream  getInputStream()返回此套接字的输入流
OutputStream  getOutputStream()返回此套接字的输出流
  • getInetAddress():调用此方法可以获取到对端的IP和端口信息。
  • getInputStream():调用此方法,我们可以读取到端口中的数据。
  • getOutputSream():可以将响应的数据写到端口中。

  使用TCP套接字编程

接下来,我们就来使用上面的方法,来实现一个TCP回显客户端服务器程序。

服务端实现

创建ServerSocket服务器对象

在前面,我们已经讲解了在服务器中,我们需要使用ServerSocket类哎创建一个服务器对象,同时在创建对象的时候,我们还需要指定端口号。

/**
 * TCP服务器类,用于创建和管理TCP服务器
 */
public class TCPServer {
    // 声明一个服务端对象
    private ServerSocket serverSocket=null;

    /**
     * 构造方法,用于创建指定端口号的TCP服务器
     * 
     * @param port 服务器监听的端口号
     * @throws IOException 如果端口已被占用,则抛出异常
     */
    public TCPServer(int port) throws IOException {
        // 创建服务端对象,并指定端口号,若端口号被占用,则抛出异常
        serverSocket=new ServerSocket(port);
    }
}

实现一个start()方法,建立连接进行双方的通信

通过调用accept()方法来建立连接。同时用socket来接收返回的对象。

    /**
     * 启动服务器,使其开始监听客户端的连接请求
     * 
     * @throws IOException 如果在监听过程中发生I/O错误
     */
    public void start() throws IOException {
        // 启动服务器,监听客户端的连接请求
        System.out.println("服务器启动");

        // 循环监听客户端的连接请求
        while(true){
            // 调用accept方法,等待客户端的连接请求
            // 这里使用Socket对象接收客户端的连接,每个连接都封装在一个Socket对象中
            Socket clientSocket=serverSocket.accept();
        }
    }

服务端业务逻辑实现

此处我们选择实现一个方法processConnection()方法,在此方法中实现服务器业务处理的代码。

由于TCP基于字节流传输的,所以我们可以通过InputStream和OutputStream来读取和写入数据。此处我们使用try-with-resource语句课自动关闭两个流。

/**
     * 启动服务器,使其开始监听客户端的连接请求
     *
     * @throws IOException 如果在监听过程中发生I/O错误
     */
    public void start() throws IOException {
        // 启动服务器,监听客户端的连接请求
        System.out.println("服务器启动");

        // 循环监听客户端的连接请求
        while(true){
            // 调用accept方法,等待客户端的连接请求
            // 这里使用Socket对象接收客户端的连接,每个连接都封装在一个Socket对象中
            Socket clientSocket=serverSocket.accept();
            //实现一个processConnection方法,用于处理客户端的连接请求
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s : %d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //通过InputStream 读取客户端发送的数据,通过OutputStream 向客户端发送数据
        // 使用try-with-resources语句,自动关闭资源
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){

        }catch (IOException e){
            e.printStackTrace();
        }
    }

我们在读取字节数据的时候,如果使用read方法,可能会有点麻烦,所以我们可以使用Scanner来读取inputStream中的数据scanner.next()方法在读取到空白符(空格、回车、制表符)就会结束,这正好与我们客户端输入请求时以空白符结尾相应

 //用Scanner 读取客户端发送的数据
            Scanner scannerIn=new Scanner(inputStream);
            while(true) {
                if (!scannerIn.hasNext()) {
                    //如果scanner中没有数据,说明客户端已经断连了,服务器这边的数据读到“末尾”
                    break;
                }
                //创建一个String 对象,用于存储客户端发送的数据
                String request=scannerIn.next();
            }

既然我们已经实现了接收数据,并将数据转换为字符串的形式,那么我们就需要将请求进行处理,此处我们可以创建一个process()方法来处理请求。

//处理请求数据
String response=process(request);  
private String process(String request) {
        return request;
    }

当我们处理完请求之后,那么我们就需要将处理之后的数据写到服务器中,由于TCP是面向字节流传输的,所以我们需要依靠OutputStream类来传输数据给服务端,但如果我们使用write方法,会有点麻烦,所以我们可以使用PrintWriter类来进行数据传输。

同时我们可以打印一下日志,以下是服务端的完整代码:

完整代码

   package net;

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;

/**
 * TCP服务器类,用于创建和管理TCP服务器
 */
public class TCPServer {
    // 声明一个服务端对象
    private ServerSocket serverSocket=null;

    /**
     * 构造方法,用于创建指定端口号的TCP服务器
     *
     * @param port 服务器监听的端口号
     * @throws IOException 如果端口已被占用,则抛出异常
     */
    public TCPServer(int port) throws IOException {
        // 创建服务端对象,并指定端口号,若端口号被占用,则抛出异常
        serverSocket=new ServerSocket(port);
    }

    /**
     * 启动服务器,使其开始监听客户端的连接请求
     *
     * @throws IOException 如果在监听过程中发生I/O错误
     */
    public void start() throws IOException {
        // 启动服务器,监听客户端的连接请求
        System.out.println("服务器启动");

        // 循环监听客户端的连接请求
        while(true){
            // 调用accept方法,等待客户端的连接请求
            // 这里使用Socket对象接收客户端的连接,每个连接都封装在一个Socket对象中
            Socket clientSocket=serverSocket.accept();
            //实现一个processConnection方法,用于处理客户端的连接请求
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s : %d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //通过InputStream 读取客户端发送的数据,通过OutputStream 向客户端发送数据
        // 使用try-with-resources语句,自动关闭资源
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){

            //用Scanner 读取客户端发送的数据
            Scanner scannerIn=new Scanner(inputStream);
            //用PrintWriter 向客户端发送数据
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true) {
                if (!scannerIn.hasNext()) {
                    //如果scanner中没有数据,说明客户端已经断连了,服务器这边的数据读到“末尾”
                    break;
                }
                //创建一个String 对象,用于存储客户端发送的数据
                String request=scannerIn.next();
                //处理请求数据
                String response=process(request);
                //向客户端发送响应数据
                printWriter.println(response);
                //打印日志
                System.out.printf("[%s : %d]  req:%s  resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);

            }
            System.out.printf("[%s : %d]客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        }catch (IOException e){
            e.printStackTrace();
        }
    }

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

    public static void main(String[] args) throws IOException {
        TCPServer tcpServer=new TCPServer(8808);
        tcpServer.start();
    }
}

当然,此时代码中其实存在着三个bug,等我们实现完TCP客户端再来讲解。

客户端实现

对于客户端代码的实现,其实与前面讲解的UDP类似。

创建Socket对象进行通信

此处我们需要创建一份Socket对象并指定服务器的IP和端口号。

import java.io.IOException;
import java.net.Socket;

public class TCPclient {
    // 声明一个客户端套接字对象
    Socket socket=null;
    // 构造方法,用于创建客户端套接字对象,并指定服务器地址和端口号
    public TCPclient(String serverIp,int serverPort) throws IOException {
        // 创建客户端套接字对象,并指定服务器地址和端口号,若创建失败则抛出异常
        socket=new Socket(serverIp,serverPort);
    }
}

实现TCP客户端服务主要逻辑

此处我们同样创建一个start()方法,在此方法中实现客户端的主要逻辑:

  • 写入请求
  • 读取响应

此处我们同样需要借助InputStream和Outputstream来读取和写入数据。

同样的,我们借助Scanner和PrintWriter来读取数据和写入数据。

 /**
     * 实现一个start方法,用于启动客户端以及实现客户端与服务器之间的通信
     */
    public void start(){
        // 启动客户端,向服务器发送数据
        System.out.println("客户端启动");
        // 创建一个Scanner对象,用于从键盘读取数据
        Scanner scanner=new Scanner(System.in);
        //利用try-with-resource语句,创建输入输出流对象
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            //利用Scanner来获取请求数据
            Scanner scannerIn = new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true) {
                // 提示用户输入
                System.out.println("->  ");
                // 获取用户输入的数据
                String request = scanner.next();
                //借助PrintWriter对象向服务器发送数据
                printWriter.println(request);
                //借助Scanner对象获取服务器返回的数据
                if (!scannerIn.hasNext()) {
                    //如果scanner中没有数据,说明服务器已经断连了,客户端这边的数据读到“末尾”
                    break;
                }
                //创建一个String 对象,用于存储服务器返回的数据
                String response = scannerIn.next();
                //显示服务器返回的数据
                System.out.println(response);
            }
        }catch (IOException e){
            //处理IO异常
            e.printStackTrace();
        }
    }

完整代码

package net;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * TCP客户端类,用于与TCP服务器进行通信
 */
public class TCPclient {
    // 声明一个客户端套接字对象
    Socket socket=null;

    /**
     * 构造方法,用于创建客户端套接字对象,并指定服务器地址和端口号
     * @param serverIp 服务器的IP地址
     * @param serverPort 服务器的端口号
     * @throws IOException 如果创建套接字时发生IO错误
     */
    public TCPclient(String serverIp,int serverPort) throws IOException {
        // 创建客户端套接字对象,并指定服务器地址和端口号,若创建失败则抛出异常
        socket=new Socket(serverIp,serverPort);
    }

    /**
     * 实现一个start方法,用于启动客户端以及实现客户端与服务器之间的通信
     */
    public void start(){
        // 启动客户端,向服务器发送数据
        System.out.println("客户端启动");
        // 创建一个Scanner对象,用于从键盘读取数据
        Scanner scanner=new Scanner(System.in);
        //利用try-with-resource语句,创建输入输出流对象
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            //利用Scanner来获取请求数据
            Scanner scannerIn = new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true) {
                // 提示用户输入
                System.out.println("->  ");
                // 获取用户输入的数据
                String request = scanner.next();
                //借助PrintWriter对象向服务器发送数据
                printWriter.println(request);
                //借助Scanner对象获取服务器返回的数据
                if (!scannerIn.hasNext()) {
                    //如果scanner中没有数据,说明服务器已经断连了,客户端这边的数据读到“末尾”
                    break;
                }
                //创建一个String 对象,用于存储服务器返回的数据
                String response = scannerIn.next();
                //显示服务器返回的数据
                System.out.println(response);
            }
        }catch (IOException e){
            //处理IO异常
            e.printStackTrace();
        }
    }
    /**
 * 程序入口点
 * 创建并启动一个TCP客户端,连接到本地服务器进行通信
 * 
 * @param args 命令行参数,本程序不使用此参数
 * @throws IOException 如果无法启动客户端或与服务器建立连接,则抛出此异常
 */
public static void main(String[] args) throws IOException {
    // 创建TCP客户端实例,指定服务器地址和端口
    TCPclient tcpClient=new TCPclient("127.0.0.1",8808);
    // 启动TCP客户端,开始与服务器通信
    tcpClient.start();
}

}

到这里,我们已经把TCP回显C/S程序实现完毕,我们可以来运行测试一下。

解决程序中出现的三个bug

我们运行之后,可以看到:

看起来是不是没有问题?

那么接下来我们往客户端中输入请求:

是不是感觉很疑惑?为什么这里客户端和服务器都没有输出数据呢?

1.刷新缓冲区

其实这里的问题就在于PrintWriter类,为什么呢?像PrintWriter这样的类,以及很多IO流中的类,都是自带“缓冲区”的。当我们调用IO流中的类的时候,首先会把请求放到内存的缓冲区中,当攒到一定的数据量才会进行发送。但由于我们这里的数据比较少,达不到发送的要求,所以数据就会一直存在于缓冲区中,从而导致数据发送不出去。

那么什么方法可以解决这个问题呢?

既然我们不想每次数据达到一定才发送,那么我们就可以调用flush()方法进行刷新操作,不让数据堆积在内存缓冲区中。

既然如此,那么我们就在客户端和服务器中,在写入数据之后,调用flush()方法来刷新一下缓冲区~

//刷新缓冲区,确保数据被发送出去
printWriter.flush();

客户端:

 服务器:

当我们添加完刷新操作的代码后,我们再重新运行代码写入数据试试:

 服务器:

客户端:

 我们可以看到,当我们加入刷新缓冲区的操作,客户端和服务器之间的交互就能够正常进行了。

那么上面这样的代码真的就没有问题了吗?

2.释放clientSocket对象资源

这里有个小细节点:对于ServerSocket、DatagramSocket这样的类,它们的生命周期是跟随整个进程的。

我们此处的clientSocket是“连接级别”的数据,即随着客户端的断开,这个clientSocket对象就不再使用(即使是同一个客户端没在断开连接之后重新连接,也是用一个新的Socket,与旧的Socket不是同一个)

所以我们需要在每次客户端断开连接之后,同时释放掉创建的Socket对象占用的资源。否则,不释放掉socket占用的资源,就会导致文件资源泄露。

但是可能有时候我们会忘记再使用完后关闭掉socket,所以我们可以在服务器中使用try-catch-finally语句,在finally中调用close方法来关闭资源。

finally {
            clientSocket.close();
        }

以上对于一对一的情况下,其实就没有什么问题了。

3.多个客户端访问服务器

 但是,在实际的服务器中,一个服务器其实不是只接受一个客户端的请求,而是会接收多个客户端的请求,那么对于这种情况,以上的代码还能够解决吗?我们可以来实验一下:

既然我们想要创建多个客户端,首先我们需要更改一下在客户端中代码的一些配置:

1.首先我们在客户端中点击右键,我们可以看到下面有个Modify run ...什么的,我们点击一下

2.点击Modify options 

3.勾选第一个

再点击确认,这样我们就能利用一个TCP客户端代码创建多个客户端进程咯。

这里我们想要实现的效果是:在任意一个客户端输入后,服务器能过及时接收到请求并返回响应,但对于上面的代码,实际情况真的如我们所愿?可以来测试一下:

我们在三个客户端中输入“你好”的数据,但是实际情况下,只有一个客户端能够和够及时和服务器进行交互。

而当我们把能和服务器进行交互的客户端关闭,就会看到后面的客户端能够连接上,并且打印出前面输入的请求:

这是为什么?

我们可以观察服务端的代码,其实是有两个while循环的,第一个循环是用来和客户端建立连接的,但这里为什么同时连接不了多个客户端呢?原因就在于我们的第二个while中,当我们建立完连接,会进入processConnection()方法中进行交互,当第一个建立连接的客户端没有断开,那么后面的客户端就无法跟服务器建立连接。那么怎么解决这个问题呢?

这里就需要我们用到多线程的知识,创建多个线程进行processConnection()方法,这样就可以让想要建立连接的客户端及时和服务器建立连接。

我们在服务端的start()方法加上创建多线程的代码

    /**
     * 启动服务器,使其开始监听客户端的连接请求
     *
     * @throws IOException 如果在监听过程中发生I/O错误
     */
    public void start() throws IOException {
        // 启动服务器,监听客户端的连接请求
        System.out.println("服务器启动");

        // 循环监听客户端的连接请求
        while(true){
            // 调用accept方法,等待客户端的连接请求
            // 这里使用Socket对象接收客户端的连接,每个连接都封装在一个Socket对象中
            Socket clientSocket=serverSocket.accept();
            // 为每个连接创建一个新的线程,以异步处理连接
            Thread thread=new Thread(()->{
                //实现一个processConnection方法,用于处理客户端的连接请求
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            // 启动线程
            thread.start();
        }
    }

 

不过这里也有个缺点,就是频繁废创建和销毁线程,也会消耗比较多的资源,所以我们其实可以用线程来进行优化一些。

/**
     * 启动服务器,使其开始监听客户端的连接请求
     *
     * @throws IOException 如果在监听过程中发生I/O错误
     */
    public void start() throws IOException {
        // 输出服务器启动信息
        System.out.println("服务器启动");
        
        // 创建一个可缓存的线程池,用于处理客户端连接请求
        ExecutorService pool= Executors.newCachedThreadPool();
        
        // 无限循环以持续监听客户端的连接请求
        while(true){
            // 调用accept方法,阻塞等待直到一个客户端连接请求到达
            // 返回代表客户端连接的Socket对象
            Socket clientSocket=serverSocket.accept();
            
            // 使用线程池提交一个Runnable任务,用于处理客户端连接
            pool.submit(()->{
                try {
                    // 调用processConnection方法处理客户端连接,此处省略具体实现
                    processConnection(clientSocket);
                } catch (IOException e) {
                    // 如果在处理连接时发生IO异常,则打印异常信息
                    e.printStackTrace();
                }
            });
        }
    }

 但其实优化的也不是很多。但其实也有一个更好的处理方法:IO多路复用/IO多路连接

IO多路复用是一种同步IO模型,允许在单个进程/线程内同时处理多个IO请求。也就是说,一个进程/线程可以监视多个文件句柄,一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作。如果没有文件权柄,应用程序就会被阻塞,并让出CPU。

这种模型允许多个请求共享同一个进程/线程,使得在处理大量请求时,可以有效地利用系统资源。如果每个请求都使用独立的进程/线程来处理,那么系统就需要创建和销毁大量的进程/线程,就将消耗大量的系统资源。而使用IO多路复用技术,可以复用一个或者多个线程来处理多个连接,从而大大减少了系统开销。

IO多路复用技术的出现主要是为了解决阻塞IO的问题,在操作系统中,最开始其实只有BIO模型,就是阻塞IO,当一个请求被处理时,该请求需要等待IO操作完成(例如读写操作等),则该进程/线程将被阻塞,直到IO操作完成,这会导致系统资源浪费,尤其是在需要处理大量请求的情况下。

IO多路复用技术广泛应用于网络编程中,特别是服务器端编程。由于服务器需要同时处理来自大量客户的请求,所有我们可以使用多路复用技术来提高服务器的性能和响应速度。

这里就不做过多介绍了,感兴趣的可以去查~

实现一个查字典的功能

服务器端:

package net;

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.HashMap;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * TCP服务器类,用于创建和管理TCP服务器
 */
public class TCPServer {
    HashMap<String,String> map=new HashMap<>();
    // 声明一个服务端对象
    private ServerSocket serverSocket=null;

    /**
     * 构造方法,用于创建指定端口号的TCP服务器
     *
     * @param port 服务器监听的端口号
     * @throws IOException 如果端口已被占用,则抛出异常
     */
    public TCPServer(int port) throws IOException {
        // 创建服务端对象,并指定端口号,若端口号被占用,则抛出异常
        serverSocket=new ServerSocket(port);
        map.put("cat","小猫");
        map.put("dog","小狗");
        map.put("mouse","老鼠");
        map.put("tiger","老虎");
        map.put("lion","狮子");
    }

    /**
     * 启动服务器,使其开始监听客户端的连接请求
     *
     * @throws IOException 如果在监听过程中发生I/O错误
     */
    public void start() throws IOException {
        // 输出服务器启动信息
        System.out.println("服务器启动");

        // 创建一个可缓存的线程池,用于处理客户端连接请求
        ExecutorService pool= Executors.newCachedThreadPool();

        // 无限循环以持续监听客户端的连接请求
        while(true){
            // 调用accept方法,阻塞等待直到一个客户端连接请求到达
            // 返回代表客户端连接的Socket对象
            Socket clientSocket=serverSocket.accept();

            // 使用线程池提交一个Runnable任务,用于处理客户端连接
            pool.submit(()->{
                try {
                    // 调用processConnection方法处理客户端连接,此处省略具体实现
                    processConnection(clientSocket);
                } catch (IOException e) {
                    // 如果在处理连接时发生IO异常,则打印异常信息
                    e.printStackTrace();
                }
            });
        }
    }




    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s : %d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //通过InputStream 读取客户端发送的数据,通过OutputStream 向客户端发送数据
        // 使用try-with-resources语句,自动关闭资源
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){

            //用Scanner 读取客户端发送的数据
            Scanner scannerIn=new Scanner(inputStream);
            //用PrintWriter 向客户端发送数据
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true) {
                if (!scannerIn.hasNext()) {
                    //如果scanner中没有数据,说明客户端已经断连了,服务器这边的数据读到“末尾”
                    break;
                }
                //创建一个String 对象,用于存储客户端发送的数据
                String request=scannerIn.next();
                //处理请求数据
                String response=process(request);
                //向客户端发送响应数据
                printWriter.println(response);
                //刷新缓冲区,确保数据被发送出去
                printWriter.flush();
                System.out.printf("[%s : %d]  req:%s  resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }
            System.out.printf("[%s : %d]客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }

    private String process(String request) {
        return map.getOrDefault(request,"未知单词");
    }

    public static void main(String[] args) throws IOException {
        TCPServer tcpServer=new TCPServer(8808);
        tcpServer.start();
    }
}

客户端:

package net;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * TCP客户端类,用于与TCP服务器进行通信
 */
public class TCPclient {
    // 声明一个客户端套接字对象
    Socket socket=null;

    /**
     * 构造方法,用于创建客户端套接字对象,并指定服务器地址和端口号
     * @param serverIp 服务器的IP地址
     * @param serverPort 服务器的端口号
     * @throws IOException 如果创建套接字时发生IO错误
     */
    public TCPclient(String serverIp,int serverPort) throws IOException {
        // 创建客户端套接字对象,并指定服务器地址和端口号,若创建失败则抛出异常
        socket=new Socket(serverIp,serverPort);
    }

    /**
     * 实现一个start方法,用于启动客户端以及实现客户端与服务器之间的通信
     */
    public void start(){
        // 启动客户端,向服务器发送数据
        System.out.println("客户端启动");
        // 创建一个Scanner对象,用于从键盘读取数据
        Scanner scanner=new Scanner(System.in);
        //利用try-with-resource语句,创建输入输出流对象
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            //利用Scanner来获取请求数据
            Scanner scannerIn = new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            System.out.println("请输入要翻译的单词:");
            while(true) {
                // 提示用户输入
                System.out.print("->  ");
                // 获取用户输入的数据
                String request = scanner.next();
                //借助PrintWriter对象向服务器发送数据
                printWriter.println(request);
                //刷新缓冲区,确保数据发送出去
                printWriter.flush();
                //借助Scanner对象获取服务器返回的数据
                if (!scannerIn.hasNext()) {
                    //如果scanner中没有数据,说明服务器已经断连了,客户端这边的数据读到“末尾”
                    break;
                }
                //创建一个String 对象,用于存储服务器返回的数据
                String response = scannerIn.next();
                //显示服务器返回的数据
                System.out.println(response);
            }
        }catch (IOException e){
            //处理IO异常
            e.printStackTrace();
        }
    }

    /**
     * 程序入口点
     * 创建并启动一个TCP客户端,连接到本地服务器进行通信
     *
     * @param args 命令行参数,本程序不使用此参数
     * @throws IOException 如果无法启动客户端或与服务器建立连接,则抛出此异常
     */
    public static void main(String[] args) throws IOException {
        // 创建TCP客户端实例,指定服务器地址和端口
        TCPclient tcpClient=new TCPclient("127.0.0.1",8808);
        // 启动TCP客户端,开始与服务器通信
        tcpClient.start();
    }
}


以上就是本篇所有内容咯,若有不足,欢迎指正~ 

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值