【JavaEE】网络编程套接字2: TCP流 套接字编程

发布于:2025-09-03 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、基于TCP的Socket API

首先要明确 TCP 协议和 UDP 协议的很重要的区别 :
TCP 协议是有链接, 面向字节流传输。
主要体现在 : 发送方和接收方在网络通信之间要先建立连接, 并且传输的数据的基本单位是字节。

1.1 基于 TCP 协议的 Socket API 中, 要分清楚以下两个类

类名 解释
ServerSocket 创建TCP服务端Socket的API
Socket Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept 方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

(1)ServerSocket 类:

  • 创建一个这样的对象,就相当于打开了一个 socket 文件。
  • 这个 socket 对象是给服务器专门使用的。
  • 这个类本身不负责发送、接收,主要负责“建立连接”。

(2)Socket 类:

  • 创建这样一个对象,也就相当于打开了一个 socket 文件。
  • 这个类,服务器和客户端都会使用。
  • 这个类,负责发送、接收数据。

(3)TCP 是字节流的,读写的时候,都以字节 byte 为基本单位。而文件也是字节流的,读写 TCP 的代码,本质上就是和读写文件的代码是一致的,都是通过 InputStream / OutputStream 展开的。

1.2 ServerSocket 类 和 Socket 类 的构造方法和成员方法

1️⃣ServerSocket 类

1)ServerSocket 类的构造方法 :
方法签名 方法说明
ServerSocket(int port) 创建⼀个服务端流套接字Socket,并绑定到指定端⼝
2)ServerSocket 类的成员方法 :
方法签名 方法说明
Socket accept() 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket 建⽴与客⼾端的连接,否则阻塞等待。
void close() 关闭此套接字

2️⃣Socket 类

1)Socket 类的构造方法 :
方法签名 方法说明
ServerSocket(int port) 建⼀个客户端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接
2)Socket 类的成员方法 :
方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
IInputStream getInputStream() 返回此套接字的输⼊流
OutputStream getOutputStream() 返回此套接字的输出流

二、TCP流 套接字编程

2.1 服务器

package network.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;

//TCP的回显服务器
public class EchoServer {
    private ServerSocket serverSocket;

    public EchoServer(int port) throws IOException{
        //服务器启动,就会绑定到 port 这个端口上面
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException{
        System.out.println("服务器启动!");
        while(true){
            Socket socket = serverSocket.accept();
            processConnection(socket);
        }
    }

    //处理一个客户端/一个连接的逻辑
    private void processConnection(Socket socket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n",socket.getInetAddress().toString(),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()) {
                    //针对客户端下线逻辑的处理。如果客户端打开连接了(比如客户端结束了)
                    //此时 hasNext 就会返回 false
                    //如果使用 read方法,就会出现返回-1的情况,也可以用来判断客户端断开连接
                    System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress(),socket.getPort());
                    break;
                }
               String request = scanner.next();
                /*byte[] buffer = new byte[1024];
                inputStream.read(buffer);*/

                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回到客户端
                writer.println(response);

                System.out.printf("[%s:%d] req: %s; resq: %s\n",socket.getInetAddress(),socket.getPort(),
                                request,response);
            }

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

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

    public static void main(String[] args) throws IOException {
        EchoServer echoServer = new EchoServer(9090);//虽然前面的UDP和这里的TCP,端口都是9090
        //但是二者不会冲突,因为协议不同
        echoServer.start();
    }
}

在这里插入图片描述
在这里插入图片描述

2.2 客户端

package network.TCP;

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的回显客户端
public class EchoClient {
    private Socket socket;

    public EchoClient(String serverIp,int serverPort) throws IOException {
        //在 new 这个对象的时候,就涉及到“建立连接操作”
        //由于连接建立好了之后,服务器的信息就在操作系统中被 TCP 协议记录了。我们在应用程序层面上就不需要保存 IP 和 端口
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("启动客户端!");
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                //1.从控制台读取用户的输入
                System.out.println("> ");
                String request = scanner.next();
                //2.构造请求发送给服务器
                writer.println(request);
                //3.读取服务器的响应
                if(!scannerNet.hasNext()){
                    System.out.println("服务器断开了连接");
                    break;
                }
                String response = scannerNet.next();
                //4.把响应显示到控制台上
                System.out.println(response);

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

    public static void main(String[] args) throws IOException {
        EchoClient echoClient = new EchoClient("127.0.0.1",9090);
        echoClient.start();
    }
}

2.3 上述代码 存在的问题

运行网络程序,一定是先启动服务器,后启动客户端~

1️⃣ 问题1:通信不通(客户端的2,直接调用 println只是把数据写入缓冲区,并没有真正进入网卡)

1)现象
客户端发了请求之后,没有收到响应。
(是客户端没把数据发出去?还是服务器收到了没有正确处理?)

先启动服务器,输出“服务器启动”,显示“客户端上线!”; Ctrl+F2结束客户端的运行,显示“客户端下线!”; 重新运行服务器,键盘输入hello,什么都不显示。

在这里插入图片描述

在这里插入图片描述

2)解决方案:加上刷新缓冲区 writer.flush()
① 客户端

package network.TCP;

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的回显客户端
public class EchoClient {
    private Socket socket;

    public EchoClient(String serverIp,int serverPort) throws IOException {
        //在 new 这个对象的时候,就涉及到“建立连接操作”
        //由于连接建立好了之后,服务器的信息就在操作系统中被 TCP 协议记录了。我们在应用程序层面上就不需要保存 IP 和 端口
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("启动客户端!");
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                //1.从控制台读取用户的输入
                System.out.println("> ");
                String request = scanner.next();
                //2.构造请求发送给服务器
                writer.println(request);//此处的 println 是执行到了
                //但是 println只是把数据先写到缓冲区,没有真正得写入网卡,也就没有真正得发送
                //缓冲区:buffer,就是一个"内存空间"。假设要频繁得写入网卡,就需要把多次的数据攒一起,一次性写入网卡。

                writer.flush();//刷新缓冲区



                //3.读取服务器的响应
                if(!scannerNet.hasNext()){
                    System.out.println("服务器断开了连接");
                    break;
                }
                String response = scannerNet.next();
                //4.把响应显示到控制台上
                System.out.println(response);

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

    public static void main(String[] args) throws IOException {
        EchoClient echoClient = new EchoClient("127.0.0.1",9090);
        echoClient.start();
    }
}

② 服务器

package network.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;

//TCP的回显服务器
public class EchoServer {
    private ServerSocket serverSocket;

    public EchoServer(int port) throws IOException{
        //服务器启动,就会绑定到 port 这个端口上面
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException{
        System.out.println("服务器启动!");
        while(true){
            Socket socket = serverSocket.accept();
            processConnection(socket);
        }
    }

    //处理一个客户端/一个连接的逻辑
    private void processConnection(Socket socket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n",socket.getInetAddress().toString(),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()) {
                    //针对客户端下线逻辑的处理。如果客户端打开连接了(比如客户端结束了)
                    //此时 hasNext 就会返回 false
                    //如果使用 read方法,就会出现返回-1的情况,也可以用来判断客户端断开连接
                    System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress(),socket.getPort());
                    break;
                }
                //没有执行到这个打印,说明上面的 hasNext 没有解除阻塞,大概率就是客户端没发来数据
                //并且没可能丢包:因为是自己给自己发送 (客户端和服务器在一台主机)
                System.out.println("服务器收到数据了!");
               String request = scanner.next();
                /*byte[] buffer = new byte[1024];
                inputStream.read(buffer);*/

                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回到客户端
                writer.println(response);
                writer.flush();

                System.out.printf("[%s:%d] req: %s; resq: %s\n",socket.getInetAddress(),socket.getPort(),
                                request,response);
            }

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

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

    public static void main(String[] args) throws IOException {
        EchoServer echoServer = new EchoServer(9090);//虽然前面的UDP和这里的TCP,端口都是9090
        //但是二者不会冲突,因为协议不同
        echoServer.start();
    }
}

③ 输出结果

在这里插入图片描述

在这里插入图片描述

2️⃣问题2:当前的程序,存在“文件资源泄露”问题

① 问题出现的原因
在这里插入图片描述

② 解决方法

在这里插入图片描述

③ 为什么只有TCP的socket 需要关闭?

在这里插入图片描述

④ 为啥 inputScream 和 outputScream 不需要关闭?
在这里插入图片描述

3️⃣问题3:存在“多个客户端”的情况(与程序运行效果有关)

① 现象
首先,先打开两个客户端:
在这里插入图片描述
在这里插入图片描述

然后观察发现:第二个开启的客户端并没有和服务器成功通信, 这是因为, 我们的服务器处理多个连接时, 是在一个while循环中, 如果第一个连接的客户端没有下线, 就不会接收第二个客户端的连接。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

退出第一个客户端,第二个客户端就可以正常使用。
在这里插入图片描述

在这里插入图片描述

② 出现问题的原因
在这里插入图片描述

③解决方法1:多线程
在这里插入图片描述

观察发现:多个客户端可以正常通信。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


解决方法2:线程池

在这里插入图片描述
在这里插入图片描述


③ 拓展:IO多路复用
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

三、TCP回显服务器执行流程

在这里插入图片描述

四、完整代码

3.1 客户端

import 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 {
        socket = new Socket(serverIP, serverPort);
    }

    public void start() {
        Scanner in = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            // 把字节流转换成字符流
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner inFromSocket = new Scanner(inputStream);

            // 发送多个请求
            while (true) {
                // 1,从控制台输入字符串
                String requestString = in.next();

                // 2,写入请求
                printWriter.println(requestString);
                printWriter.flush();

                // 3,读取请求
                String responseString = inFromSocket.next();

                // 控制台 打印请求字符串 + 响应字符串
                System.out.println(requestString + " + " + responseString);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
        tcpEchoClient.start();
    }
}

3.2 服务器

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

public class TCPEchoServer {
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        while (true) {
            // 建立连接 返回一个 Socket 对象
            Socket socket = serverSocket.accept();

            // 处理连接到的这个客户端
            Thread thread = new Thread( () -> {
                try {
                    processConnection(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            // 别忘了调用 start() 启动线程
            thread.start();
        }
    }

    private void processConnection(Socket socket) throws IOException {
        System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream() ) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner inFromSocket = new Scanner(inputStream);

            // 处理多个请求
            while(true) {
                if (!inFromSocket.hasNext()) {
                    System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端下线");
                    break;
                }

                // 1,读取请求
                String requestString = inFromSocket.next();

                // 2,处理请求
                String responseString = process(requestString);

                // 3,写入响应
                printWriter.println(responseString);
                printWriter.flush();

                // 控制台打印 客户端IP地址 + 客户端端口号 + 请求字符串 + 响应字符串
                System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + requestString + " + " + responseString);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }

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

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

五、总结

在这里插入图片描述


网站公告

今日签到

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