【JavaEE】——TCP回显服务器(万字长文超详细)

简介: ServerSocket类,Socket类,PrintWriter缓冲区问题,Socket文件释放问题,多线程问题

  image.gif 编辑

阿华代码,不是逆风,就是我疯

你们的点赞收藏是我前进最大的动力!!

希望本文内容能够帮助到你!!

目录

一:TCP的API

1:SevereScoket类

(1)构造方法

(2)方法

2:Socket类

(1)构造方法

(2)方法

二:基本代码实现

1:服务端

(1)有注释版

(2)无注释版

2:客户端

(1)有注释版

(2)无注释版

三:细节问题

优化点①

优化点②

四: 过程梳理

五:PrintWriter缓冲区问题

1:问题引入

2:解决方式

六:Socket文件释放问题

1:问题引入

编辑

2:解决方式

七:多个客户端连接问题

1:问题引入

(1)如何运行多个同一程序

(2)实际效果

2:解决方式

八:线程池优化多线程问题

1:问题引入

2:解决方式

九:写代码易错的地方

1:服务器

2:客户端

十:完整代码

服务器

客户端


本文代码建议敲打至少3遍

一:TCP的API

1:SevereScoket类

(1)构造方法

image.gif 编辑

(2)方法

image.gif 编辑

注:accept可以接收多个客户端的请求连接,有阻塞功能

image.gif 编辑

2:Socket类

Socket是客户端Socket,或者服务端收到客户端accept的请求后返回的服务端Socket,不管是客户端还是服务端,都是建立连接以后,保存对端的信息,以及用来与对方接收和发送数据的

(1)构造方法

image.gif 编辑

(2)方法

image.gif 编辑

二:基本代码实现

☆注:此处代码非完整版本,是一个最基本的框架,代码本身还有三个很重要的问题,需要解决,完整代码文章最后有上传

1:服务端

(1)有注释版

package InternetTcp;
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;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-10-11
 * Time: 17:01
 */
public class TcpEchoServer {
    //不同于Udp的DatagramSocket文件,ServerSocket文件负责揽活
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        //1:建立socket文件,并构造
        serverSocket = new ServerSocket(port);//导包抛异常
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //2:再建立一个Socket文件来进行连接通信(负责接待客户端),可阻塞
            Socket clientSocket = serverSocket.accept();
            //3:写一个方法来处理这次连接,包括了收到请求和发出响应
            processConnection(clientSocket);
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
        //4:可以通过Socket文件获得客户端的ip和端口号
        System.out.printf("[%s , %d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //5:循环读取请求和返回响应
        //Tcp是面向字节流,单位为字节,Socket本质就是文件,所以可以进行流操作
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //6:while循环不断读取此次连接的请求并返回响应
            while(true){
                //7:读取操作
                /*
                byte[] buffer = new byte[1024];
                inputStream.read(buffer);//把读取到的请求放到数组里
                因为一会还要把这个请求转化为字符串,我们还有一个更简单拿到方法
                */
                //7:不要去放System.in输入,用(Socket对象)inputStream来帮助Scanner进行构造
                //   Scanner不仅可以从操作系统中读,也可以从文件,网络中读,Scanner放在while循环外也可以
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    System.out.printf("[%s , %d],客户端断开连接\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //8:根据请求计算响应
                String request = scanner.next();
                String response = process(request);
                //9:把响应返回给客户端(封装一下),进行写入
                /*
                直接用write方法写回响应,不方便添加换行符\n,因为客户端在读取响应的时候使用next,结束标志为\n、空格、tab
                outputStream.write(response.getBytes(),0,response.getBytes().length);
                */
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                System.out.printf("[%s,%d] , request: %s , response : %s\n " ,clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } 
    }
    public String process(String response){
        return response;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

image.gif

(2)无注释版

package repeat2;
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;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-10-12
 * Time: 9:57
 */
public class TcpEchoServer {
    //socket文件来读写
    private ServerSocket serverSocket = null;//拉客
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动\n");
        while(true){
            //一次连接
            Socket socketClient = serverSocket.accept();//拉完客了给接待
            System.out.printf("[%s %d]客户端已上线\n",socketClient.getInetAddress(),socketClient.getPort());
            processConnection(socketClient);
        }
    }
    public void processConnection(Socket socketClient){
        //面向字节流
        try(InputStream inputStream = socketClient.getInputStream();
            OutputStream outputStream = socketClient.getOutputStream()){
            while(true){
                /*
                byte[] buffer = new byte[1024];
                inputStream.read(buffer);//数组再转字符串
                */
                //接收请求
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    System.out.printf("[%s %d]客户端断开连接\n", socketClient.getInetAddress(), socketClient.getPort());
                    break;
                }
                String request = scanner.next();
                String response = process(request);
                //怎么发送呢?write不建议,封装outStream
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                System.out.printf("[%s %d] , req: %s resp: %s\n",socketClient.getInetAddress(),socketClient.getPort(),request,response);
            }
        } 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(9091);
        server.start();
    }
}

image.gif

2:客户端

(1)有注释版

package InternetTcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-10-11
 * Time: 17:01
 */
public class TcpEchoClient {
    //1:创建一个Scoket对象
    private Socket socket = null;
    //2:构造方法因为 TCP是有连接的 所以Scoket对象中包含服务器的IP和端口,实例化对象就会与服务器创立连接,服务器中的accept就会进行呼应
    public TcpEchoClient(String serverIp , int serverPort) throws IOException {
        socket = new Socket(serverIp , serverPort);
    }
    public void start(){
        System.out.println("客户端启动");
        //一:获取控制台输入
        //3:基于socket文件创建输入输出流,并实例化两个扫描器
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerConsole = new Scanner(System.in);//控制台扫描器
            Scanner scannerNetWork = new Scanner(inputStream);
            //5:调用PrintWriter中的write方法发送请求,用outputStream帮助构造
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                System.out.print("->");
                if (!scannerConsole.hasNext()){
                    break;
                }
                //二:构造请求,并发送
                String request = scannerConsole.next();
                writer.println(request);//呼应server中的获取响应scanner.next()//问题
                //三:接收响应
                String response = scannerNetWork.next();
                //四:在显示器上进行打印
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

image.gif

(2)无注释版

package repeat2;
import jdk.internal.util.xml.impl.Input;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-10-12
 * Time: 9:57
 */
public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIp , int serverport){
        socket = new Socket();
    }
    public void start(){
        System.out.println("客户端启动");
        //从读取控制台的请求
        //构造请求并发送
        //接收响应
        //把相应打印显示出来
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerConsole = new Scanner(System.in);
            Scanner scannerNetWork = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                System.out.print("->");
                String request = scannerConsole.next();
                //怎么发送呢?outputStream(带封装)
                writer.println(request);
                //怎么接收响应?
                String response = scannerNetWork.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        InternetTcp.TcpEchoClient client = new InternetTcp.TcpEchoClient("127.0.0.1",9091);
        client.start();
    }
}

image.gif

三:细节问题

优化点①

(读/接受request)用Scanner替代数组转化为字符串,因为有文件流操作,用inputStream来帮助Scanner进行构造

image.gif 编辑

优化点②

直接用outputStream的write方法不方便写换行符\n,所以给outputStream“封装一下”,用outputStream帮助PrintWriter进行构造,再使用println方法实现自动换行

image.gif 编辑

构造方法

image.gif 编辑

四: 过程梳理

image.gif 编辑

image.gif 编辑

image.gif 编辑

image.gif 编辑

五:PrintWriter缓冲区问题

1:问题引入

客户端发出请求没有收到响应

image.gif 编辑

image.gif 编辑

2:解决方式

引入PrintWriter中的flush()方法

之所以会出现上述问题是因为,PrintWriter的内置缓存区在发力,因为文件IO是比较低效的操作,所以操作系统会进行优化,尽可能的让这种操作少一点,就引入了缓存区(内存),把要写入网卡的数据放到内存缓冲区中,等攒一波大的,在统一发送;

换个说法就是如果发送的数据太少,数据就会先滞留在内存缓冲区中,没有被真正的发送出去。

image.gif 编辑

image.gif 编辑

六:Socket文件释放问题

1:问题引入

image.gif 编辑

因为服务器每连接一个客户端都要建立一个Socket文件(名字叫clientSocket),这个文件是会占用文件描述符表的,连接的客户端数量多的话,只建立文件不释放文件的话就会造成——文件描述符表被占满

2:解决方式

每次服务器执行完客户端的请求后(即processConnection方法执行完毕)就释放掉Socket文件

image.gif 编辑

七:多个客户端连接问题

1:问题引入

(1)如何运行多个同一程序

创建两个客户端,让服务器同时对两个服务端进行服务,最后再点运行就会出现两个Client了

image.gif 编辑

image.gif 编辑

image.gif 编辑

(2)实际效果

image.gif 编辑

image.gif 编辑

image.gif 编辑

本质原因:

accept使用了一次while循环,processClient方法中又嵌套了一层while循环

导致在服务器在处理客户端A的请求时,一直在processClient方法中出不来,就执行不了第二次客户端B的accept

(内核中:客户端B和服务器已经建立了TCP连接,但是用户态应用程序这里无法处理到,也就是拿不到)

(理解成:别人给你打电话,电话响了,但是你不接)

重点:虽然处理不了客户端B的数据请求,但是B的数据请求会暂时放在Socket缓冲区中(缓存区不能无限放数据,有大小限制的,所以可能会存在数据丢失的情况),等A端下线后,B端的请求会被瞬间处理掉

2:解决方式

引入多线程——把嵌套的两个while循环给拆分开来

image.gif 编辑

image.gif 编辑

image.gif 编辑

八:线程池优化多线程问题

1:问题引入

上述八中引入多线程解决了多个客户端的上线问题,但是频繁的创建和销毁进程,会带来很大的资源消耗问题,我们给出了方案——引入线程池

2:解决方式

image.gif 编辑

3:其他方案(了解即可)

(1)协程

轻量级线程,本质上还是一个线程,用户态可以通过手动调度的方式让这个线程“并发”的做多个任务

(2)IO多路复用

系统内核级别的机制,本质上是让一个线程同时去负责处理多个Socket(这虽然有多个socket数据,但是同一时刻活跃的socket只是少数,大部分socket都是在等)在Java中也有对应的封装了的API

九:写代码易错的地方

1:服务器

image.gif 编辑

image.gif 编辑

image.gif 编辑

image.gif 编辑

2:客户端

image.gif 编辑

image.gif 编辑

十:完整代码

服务器

package InternetTcp;
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;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-10-11
 * Time: 17:01
 */
public class TcpEchoServer {
    //不同于Udp的DatagramSocket文件,ServerSocket文件负责揽活
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        //1:建立socket文件,并构造
        serverSocket = new ServerSocket(port);//导包抛异常
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //2:再建立一个Socket文件来进行连接通信(负责接待客户端),可阻塞
            Socket clientSocket = serverSocket.accept();
            //3:写一个方法来处理这次连接,包括了收到请求和发出响应
            ExecutorService pool = Executors.newCachedThreadPool();
            pool.submit(new Runnable(){
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            /*
            Thread thread = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();
            */
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
        //4:可以通过Socket文件获得客户端的ip和端口号
        System.out.printf("[%s , %d]客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //5:循环读取请求和返回响应
        //Tcp是面向字节流,单位为字节,Socket本质就是文件,所以可以进行流操作
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //6:while循环不断读取此次连接的请求并返回响应
            while(true){
                //7:读取操作
                /*
                byte[] buffer = new byte[1024];
                inputStream.read(buffer);//把读取到的请求放到数组里
                因为一会还要把这个请求转化为字符串,我们还有一个更简单拿到方法
                */
                //7:不要去放System.in输入,用(Socket对象)inputStream来帮助Scanner进行构造
                //   Scanner不仅可以从操作系统中读,也可以从文件,网络中读,Scanner放在while循环外也可以
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    System.out.printf("[%s , %d],客户端断开连接\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //8:根据请求计算响应
                String request = scanner.next();
                String response = process(request);
                //9:把响应返回给客户端(封装一下),进行写入
                /*
                直接用write方法写回响应,不方便添加换行符\n,因为客户端在读取响应的时候使用next,结束标志为\n、空格、tab
                outputStream.write(response.getBytes(),0,response.getBytes().length);
                */
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s,%d] , request: %s , response : %s\n " ,clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }
    }
    public String process(String response){
        return response;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

image.gif

客户端

package InternetTcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-10-11
 * Time: 17:01
 */
public class TcpEchoClient {
    //1:创建一个Scoket对象
    private Socket socket = null;
    //2:构造方法因为 TCP是有连接的 所以Scoket对象中包含服务器的IP和端口,实例化对象就会与服务器创立连接,服务器中的accept就会进行呼应
    public TcpEchoClient(String serverIp , int serverPort) throws IOException {
        socket = new Socket(serverIp , serverPort);
    }
    public void start(){
        System.out.println("客户端启动");
        //一:获取控制台输入
        //3:基于socket文件创建输入输出流,并实例化两个扫描器
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerConsole = new Scanner(System.in);//控制台扫描器
            Scanner scannerNetWork = new Scanner(inputStream);
            //5:调用PrintWriter中的write方法发送请求,用outputStream帮助构造
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                System.out.print("->");
                if (!scannerConsole.hasNext()){
                    break;
                }
                //二:构造请求,并发送
                String request = scannerConsole.next();
                writer.println(request);//呼应server中的获取响应scanner.next()//问题
                writer.flush();
                //三:接收响应
                String response = scannerNetWork.next();
                //四:在显示器上进行打印
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

image.gif


相关文章
|
7月前
|
API
回显服务器(基于UDP)
回显服务器(基于UDP)
97 0
|
12小时前
|
网络协议 Java API
【JavaEE】——Udp翻译器的实现(回显服务器)
网络编程,DatagramSocket 和 DatagramPacket类,回显服务器,服务器实现,客户端实现,
|
2月前
|
存储 网络协议 Java
【网络】UDP和TCP之间的差别和回显服务器
【网络】UDP和TCP之间的差别和回显服务器
77 1
|
2月前
|
网络协议 Java API
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
72 2
|
缓存 网络协议 安全
TCP首部格式【TCP原理(笔记五)】
TCP首部格式【TCP原理(笔记五)】
345 0
TCP首部格式【TCP原理(笔记五)】
|
7月前
|
网络协议 安全 Java
【JavaEE初阶】 TCP协议详细解析
【JavaEE初阶】 TCP协议详细解析
|
7月前
|
网络协议 Java API
【JavaEE初阶】 TCP服务器与客户端的搭建
【JavaEE初阶】 TCP服务器与客户端的搭建
|
存储 网络协议 Java
简单实现基于UDP与TCP的回显服务器(一)
简单实现基于UDP与TCP的回显服务器
111 0
|
网络协议 Java API
简单实现基于UDP与TCP的回显服务器(二)
简单实现基于UDP与TCP的回显服务器
81 0
|
网络协议 Java 数据安全/隐私保护
使用Socket实现UDP版的回显服务器
使用Socket实现UDP版的回显服务器

热门文章

最新文章