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

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

  • [一、基于TCP的Socket API](#一、基于TCP的Socket API)
    • [1.1 基于 TCP 协议的 Socket API 中, 要分清楚以下两个类](#1.1 基于 TCP 协议的 Socket API 中, 要分清楚以下两个类)
    • [1.2 ServerSocket 类 和 Socket 类 的构造方法和成员方法](#1.2 ServerSocket 类 和 Socket 类 的构造方法和成员方法)
      • [1️⃣ServerSocket 类](#1️⃣ServerSocket 类)
        • [1)ServerSocket 类的构造方法 :](#1)ServerSocket 类的构造方法 :)
        • [2)ServerSocket 类的成员方法 :](#2)ServerSocket 类的成员方法 :)
      • [2️⃣Socket 类](#2️⃣Socket 类)
        • [1)Socket 类的构造方法 :](#1)Socket 类的构造方法 :)
        • [2)Socket 类的成员方法 :](#2)Socket 类的成员方法 :)
  • [二、TCP流 套接字编程](#二、TCP流 套接字编程)
  • 三、TCP回显服务器执行流程
  • 四、完整代码
    • [3.1 客户端](#3.1 客户端)
    • [3.2 服务器](#3.2 服务器)
  • 五、总结

一、基于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 服务器

java 复制代码
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 客户端

java 复制代码
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()

① 客户端

java 复制代码
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();
    }
}

② 服务器

java 复制代码
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 客户端

java 复制代码
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 服务器

java 复制代码
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();
    }
}

五、总结

相关推荐
白帽黑客沐瑶4 小时前
【网络安全入门基础教程】网络安全行业,未来两年就业和再就业都会很难
网络·人工智能·计算机网络·安全·web安全·网络安全就业
winkel_wang5 小时前
电脑接入企业中的网线,为啥网卡上面显示AD域名
网络·windows
岛屿旅人7 小时前
涉私数据安全与可控匿名化利用机制研究(上)
网络·人工智能·安全·web安全
wanhengidc7 小时前
使用云手机进行游戏搬砖划算吗?
运维·服务器·网络·安全·游戏·智能手机
岛屿旅人8 小时前
涉私数据安全与可控匿名化利用机制研究(下)
网络·人工智能·安全·web安全·生成对抗网络
悲伤小伞9 小时前
Linux_网络基础
linux·服务器·c语言·网络
晚风(●•σ )9 小时前
【华为 ICT & HCIA & eNSP 习题汇总】——题目集23
网络·计算机网络·华为·ensp·交换机
博睿谷IT99_9 小时前
华为HCIE数通含金量所剩无几?考试难度加大?
网络·华为·华为认证·hcie·it·数据通信·含金量
KingRumn10 小时前
Linux ARP老化机制/探测机制/ip neigh使用
linux·网络·tcp/ip