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

重点在于解决利用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()等待)

【Java】学习路径58-TCP聊天-双向发送实现的更多相关文章

  1. Java学习路径及练手项目合集

    Java 在编程语言排行榜中一直位列前排,可知 Java 语言的受欢迎程度了. 实验楼上的[Java 学习路径]中将首先完成 Java基础.JDK.JDBC.正则表达式等基础实验,然后进阶到 J2SE ...

  2. Java学习路径(抛光砖)

    这就是我刚刚在五孔问答中找到的Java学习路线图抛光砖价格.我个人认为,这条Java学习路线是可以的.它是2018年相对较新的Java学习路线,更符合企业就业标准. Java学习路径的第一阶段:Jav ...

  3. Java学习路径:不走弯路,这是一条捷径

    1.如何学习编程? JAVA是一种平台.也是一种程序设计语言,怎样学好程序设计不只适用于JAVA,对C++等其它程序设计语言也一样管用.有编程高手觉得,JAVA也好C也好没什么分别,拿来就用.为什么他 ...

  4. Java学习路径

    -------第一部分:基础语法-------- 1.输出语句 1.1 hello world 1.2 拼接输出.换行和不换行输出 1.3 拼接变量输出 2.输入语句: 2.1 定义变量,赋值(整数. ...

  5. 【JDBC】学习路径1-JDBC背景知识

    学习完本系列JDBC课程后,你就可以愉快使用Java操作我们的MySQL数据库了. 各种数据分析都不在话下了. 第一章:废话 JDBC编程,就是写Java的时候,调用了数据库. Java Databa ...

  6. Java进阶:基于TCP通信的网络实时聊天室

    目录 开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 ...

  7. Java学习---TCP Socket的学习

    基础知识 1. TCP协议 TCP是一种面向连接的.可靠的.基于字节流的运输层(Transport layer)通信协议.在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,UDP是同一层 ...

  8. Java学习-043-获取文件在目录中的路径

    我们在日常的电脑使用中,经常需要在当前目录或当期目录及其子目录中查找文件,并获取相应的文件路径名.在我们的自动化测试中,也经常需要确认文件在目录中是否成功生成或已存在,因而我写了一个小方法来实现. 获 ...

  9. Java学习-009-文件名称及路径获取实例及源代码

    此文源码主要为应用 Java 获取文件名称及文件目录的源码及其测试源码.若有不足之处,敬请大神指正,不胜感激!源代码测试通过日期为:2015-2-3 00:02:27,请知悉. Java获取文件名称的 ...

随机推荐

  1. ExtJS 布局-Fit布局(Fit Layout)

    更新记录: 2022年5月31日 第一稿. 1.说明 Fit布局只会显示一个子组件,子项组件的尺寸会拉伸到容器的尺寸.当容器进行调整大小(resized),子组件会自动调整去拉伸到付容器的大小. 注意 ...

  2. 开发工具-Typora编辑器下载地址

    更新记录 2022年6月10日 完善标题. 比较好用的Markdown编辑器了,哈哈. https://typoraio.cn/

  3. C#中常用的目录|文件|路径信息操作

    更新记录 本文迁移自Panda666原博客,原发布时间:2021年5月16日. 说明 .NET的类库API设计的非常优秀,再加上文档docs.com写的非常优秀,写代码给人一种十分优雅的感觉. 获得当 ...

  4. 经典漏洞-后台备份数据库getshell

    由于接触安全时间不多,一些老的getshell方法不是很清楚.这次碰到了个老站,正好学习了一下. 首先这边是用户名可以猜测出来的,因为输入错误的用户名显示用户名不存在,如果存在的话会显示密码错误. 爆 ...

  5. WPF开发随笔收录-报警闪烁效果实现

    一.前言 工作中目前经手的项目是医疗相关的监护软件,所以会涉及到一些报警效果的实现,今天在这里就简单分享一下实现方式 二.正文 1.实现的方式比较的简单,就是通过一个Border控件,然后搭配Data ...

  6. JDBC: ThreadLocal 类

    1.ThreadLocal ThreadLocal用于保存某个线程共享变量.在Java中,每个线程对象都有一个ThreadLocal<ThreadLocal,Object>,其中key就是 ...

  7. esp8266模拟输入(ADC)检测问题

    今天使用esp12f读取A0数据时一直出现错误; Serial.println(analogRead(A0));读取值一直为1024 因为前段时间一直用的是开发板,读取电压值正常 而从昨天换为了esp ...

  8. 使用OnPush和immutable.js来提升angular的性能

    angular里面变化检测是非常频繁的发生的,如果你像下面这样写代码 <div> {{hello()}} </div> 则每次变化检测都会执行hello函数,如果hello函数 ...

  9. YII自定义第三方扩展

    cat.php <?php /** * Created by PhpStorm. * Date: 2016/5/25 * Time: 15:23 */ namespace vendor\anim ...

  10. Win10系统下安装编辑器之神(The God of Editor)Vim并且构建Python生态开发环境(2020年最新攻略)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_160 众神殿内,依次坐着Editplus.Atom.Sublime.Vscode.JetBrains家族.Comodo等等一众编辑 ...