【Java】学习路径58-TCP聊天-双向发送实现

Posted on 2022-05-15 12:28  罗芭Remoo  阅读(138)  评论(0编辑  收藏  举报

这一章内容比较复杂(乱)

重点在于解决利用TCP协议实现双向传输。

其余的细节(比如end)等,不需要太在意。

但是我也把折腾经历写出来了,如果大家和我遇到了类似的问题,下文可以提供一个参考。

 

 目标:

打算使用两个使用Runnable接口的线程类实现发送端、接收端。

其中发送端包含接收端的功能,接收端包含发送端的功能。并且包含请求关闭close时双方自动关闭。

但是后面我发现这样做非常麻烦,原因有很多,不限于只能使用大量try catch,不能抛出异常,内部线程类的作用域,线程的同步死锁,等待输入导致无法关闭线程等问题。


正文:

由于我们使用的是TCP协议,TCP协议的好处在于可以实现相互通信。

所以我们只需在发送端(用户端)创建一个Socket对象,

在接收端(服务器端)创建一个ServerSocket对象,一个Socket对象即可。

但是,同时我们需要实现循环接收、发送的需要,我们使用多线程。

我们可以创建一个新的线程类实现同时接收与发送的需求,但是我们可以直接创建一个线程内部类来实现。

 

 

 

这个是发送端的代码:

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class TCP_SendThread implements Runnable {
    private final int port;

    public TCP_SendThread(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        //TCP使用的是Socket

        Socket s = null;
        OutputStream ops = null;
        InputStream ips = null;

        try {
            s = new Socket("127.0.0.1", port);
            ops = s.getOutputStream();
            ips = s.getInputStream();

            InputStream finalIps = ips;
            new Thread() {//在客户端创建接收端
                @Override
                public void run() {
                    int length = -1;
                    byte[] buf = new byte[1024];
                    try{
                        while ((length = finalIps.read(buf)) > -1)
                            System.out.println(new String(buf,0,length));
                    }catch (Exception e){
                        e.printStackTrace();
                    }

                }
            }.start();


            Scanner sc = new Scanner(System.in);
            while (true) {
                String str = sc.next();
                if (str.equals("end"))
                    break;
                ops.write(str.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                ops.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
    }
}

这个是接收端的代码:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCP_ReceiveThread implements Runnable {

    private int port;
    public TCP_ReceiveThread(int port) {
        this.port=port;
    }
    @Override
    public void run() {
        ServerSocket ss = null;
        Socket clinet=null;
        try {
            ss = new ServerSocket(port);
            clinet = ss.accept();//会暂停,等待连接!

            InputStream input = clinet.getInputStream();
            OutputStream ops = clinet.getOutputStream();
            new Thread(){//在服务器端建立一个发送端(客户端)的线程
                //匿名内部线程
                @Override
                public void run() {
                    Scanner sc = new Scanner(System.in);
                    while(true){
                        String str = sc.next();
                        if(str.equals("end"))
                            break;
                        try {
                            ops.write(str.getBytes());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();


            byte[] buf = new byte[1024];
            int length;
            while ((length = input.read(buf)) >= 0) {
                System.out.println(new String(buf, 0, length));
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                clinet.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            //输入输出流,Socket会自动帮我们关闭
        }
    }
}

然后按顺序启动他们:

public class TCP_TestReceive {
    public static void main(String[] args) {
        TCP_ReceiveThread trt = new TCP_ReceiveThread(8989);
        Thread t1 = new Thread(trt,"服务器");
        t1.start();
    }
}
public class TCP_TestSend {
    public static void main(String[] args) {
        TCP_SendThread tst = new TCP_SendThread(8989);
        Thread t2 = new Thread(tst,"客户端");
        t2.start();
    }
}

解析:

但是有一个问题,内部线程类的使用涉及到内部类与外部类生命周期不同导致的变量使用问题。

如果我们要在内部类中使用外部类的变量,我们需要将外部类的变量设置为final;

如果我们要在内部类中使用外部类的对象,我们需要在内部类中复制一个副本。

  1. 上面的发送端、接收端代码中还分别创建了一个新的内部线程。
  2. 发送端的线程中创建了一个接收端的线程
  3. 接收端的线程中创建了一个发送端的线程
  4. 但TCP协议本身就支持双向传输,所以我们不需要创建新的Socket、ServerSocket对象。
  5. 由于run()方法中不能捕捉错误,所以我们只能使用try catch来捕捉异常
  6. 从内部类引用的本地变量必须是最终变量或实际上的最终变量,所以我们构造了一个内部类的fips,复制了ips变量

 

好了,目前总算是弄好了,实现了互相发送的功能。

但是当我们测试输入“end”关闭服务器端、客户端的时候,好像出现了一点问题。

  1. 在Receive服务器端输入end,并没有什么反应
  2. 在Send客户端输入end,直接就报异常了
  3. 错误代码定位到
  4. 原因是当我们在Send输入“end”关闭连接的时候,finalIps的read就无法正常调用了。

 所以我们将Send发送端中的内部线程类(接收端)代码修改为:

new Thread(() -> {
    int length = -1;
    byte[] buf = new byte[1024];
    try{
        while (! finalS.isClosed())
            if((length = ips.read(buf)) > -1)
                System.out.println(new String(buf,0,length));
        finalS.close();
        System.out.println("Send中的接收线程关闭了");
    }catch (Exception e){
        e.printStackTrace();
    }
}).start();

但是问题又来了,明明我在Send端输入了end,s已经关闭了,当执行上面代码时,还是会执行到read方法,这是为什么呢?

我们猜想:这是由于我们使用多线程的原因,所以我们需要使用线程同步解决问题。

对close方法和上面代码同步,对象使用Socket对象即可

synchronized (s) {
    try {
        while (!s.isClosed())
            if ((length = ips.read(buf)) > -1)
                System.out.println(new String(buf, 0, length));
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            s.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Send中的接收线程关闭了");
    }
}
synchronized (s) {
    System.out.println("Send中已发出end请求");
    s.close();
}

同步这两块代码即可。

经过实验,我们发现猜想错误

复盘时间,以下是我们的客户端与服务器端:

客户端:

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class TCP_SendThread implements Runnable {
    private final int port;

    public TCP_SendThread(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        //TCP使用的是Socket
        try {
            final Socket s = new Socket("127.0.0.1", port);//localhost;
            OutputStream ops = s.getOutputStream();
            InputStream ips = s.getInputStream();
            //在客户端创建接收线程(用的还是原来的Socket对象,TCP特性),使用lambda语句化简
            //Socket finalS = s;
            new Thread(() -> {
                int length;
                byte[] buf = new byte[1024];

                try {
                    while (!s.isClosed())
                        if ((length = ips.read(buf)) > -1)
                            System.out.println(new String(buf, 0, length));
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        s.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Send中的接收线程关闭了");
                }

            }).start();

            Scanner sc = new Scanner(System.in);
            while (true) {
                String str = sc.next();
                if (str.equals("end"))
                    break;
                ops.write(str.getBytes());
            }

            System.out.println("Send中已发出end请求");
            s.close();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服务器端:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCP_ReceiveThread implements Runnable {

    private int port;

    public TCP_ReceiveThread(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        try {
            ServerSocket ss = new ServerSocket(port);
            Socket clinet = ss.accept();//会暂停,等待连接!

            InputStream ips = clinet.getInputStream();
            OutputStream ops = clinet.getOutputStream();
            //在服务器端建立一个发送端(客户端)的线程
            //匿名内部线程
            new Thread(() -> {
                Scanner sc = new Scanner(System.in);
                while (true) {
                    String str = sc.next();
                    if (str.equals("end"))
                        break;
                    try {
                        ops.write(str.getBytes());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    clinet.close();
                    ss.close();

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

            byte[] buf = new byte[1024];
            int length;
            try {

                while (!ss.isClosed())
                    if ((length = ips.read(buf)) > -1)
                        System.out.println(new String(buf, 0, length));

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {

                    ss.close();
                    clinet.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("Send中的接收线程关闭了");
            }
            //输入输出流,Socket会自动帮我们关闭
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 实验流程:

  1. 在Send客户端中输入end,此时这一行代码会被执行:
        Scanner sc = new Scanner(System.in);
        while (true) {
            String str = sc.next();
            if (str.equals("end"))
                break;
            ops.write(str.getBytes());
        }
    
        System.out.println("Send中已发出end请求");
        s.close();
    
    } catch (Exception e) {
        e.printStackTrace();
    }
  2. 但是我们Send客户端中的匿名线程,此时正在等待输入,也就是停留在read方法中
    try {
        while (!s.isClosed())
            if ((length = ips.read(buf)) > -1)
                System.out.println(new String(buf, 0, length));
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            s.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Send中的接收线程关闭了");
    }

    但是正当s关闭的时候,read就会抛出异常。

    所以并不是我们的isClosed()代码没有检查出问题!于是我们修改回来,不用isClosed了。
  3. 所以说目前,从Send客户端、Receive服务器端提出的关闭,对于自己来说都是可以的。
  4. 但是另一方并没有自动关闭。
  5. 于是我们在Send端中的发送信息部分中的等待获取next()的后面再加一个isClosed判断吧,虽然效果可能不好,但是这也是没有办法的事情了
  6. 另外在Receive端中的发送线程直接设置为守护线程就好了。

但是在实际开发中,我们也不会这样处理。

主要是为了让大家熟悉一下TCP的发送与接收,以及各种以前的知识(内部类,final,和一些逻辑处理等等)。

 最终的代码:

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class TCP_SendThread implements Runnable {
    private final int port;

    public TCP_SendThread(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        //TCP使用的是Socket
        try {
            final Socket s = new Socket("127.0.0.1", port);//localhost;
            OutputStream ops = s.getOutputStream();
            InputStream ips = s.getInputStream();
            //在客户端创建接收线程(用的还是原来的Socket对象,TCP特性),使用lambda语句化简
            //Socket finalS = s;
            new Thread(() -> {
                int length;
                byte[] buf = new byte[1024];

                try {
                    while ((length = ips.read(buf)) > -1)
                        System.out.println(new String(buf, 0, length));
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        s.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Send中的接收线程关闭了");
                }

            }).start();

            Scanner sc = new Scanner(System.in);
            while (true) {
                String str = sc.next();
                if(s.isClosed())
                    break;
                if (str.equals("end"))
                    break;
                ops.write(str.getBytes());
            }

            System.out.println("Send中接收到Receive端的end请求");
            s.close();

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

 

 

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCP_ReceiveThread implements Runnable {

    private int port;

    public TCP_ReceiveThread(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        try {
            ServerSocket ss = new ServerSocket(port);
            Socket clinet = ss.accept();//会暂停,等待连接!

            InputStream ips = clinet.getInputStream();
            OutputStream ops = clinet.getOutputStream();
            //在服务器端建立一个发送端(客户端)的线程
            //匿名内部线程
            Thread t = new Thread(() -> {
                Scanner sc = new Scanner(System.in);
                while (true) {
                    String str = sc.next();
                    if (str.equals("end"))
                        break;
                    try {
                        ops.write(str.getBytes());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    clinet.close();
                    ss.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.setDaemon(true);
            t.start();

            byte[] buf = new byte[1024];
            int length;
            try {

                while ((length = ips.read(buf)) > -1)
                    System.out.println(new String(buf, 0, length));

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    ss.close();
                    clinet.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("Send中的接收线程关闭了");
            }
            //输入输出流,Socket会自动帮我们关闭
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

总结:

目前实现利用TCP协议相互通信;

并且利用守护线程实现了在客户端发送end指令,两端自动关闭。

 

但是暂时无法实现在服务器端发送指令,客户端自动关闭(需要手动发送一条消息,跳出Scanner的next()等待)