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

重点在于解决利用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. generatorConfig.xml自动生成实体类,dao和xml

    <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfiguration ...

  2. Redis 切片集群的数据倾斜分析

    Redis 中如何应对数据倾斜 什么是数据倾斜 数据量倾斜 bigkey导致倾斜 Slot分配不均衡导致倾斜 Hash Tag导致倾斜 数据访问倾斜 如何发现 Hot Key Hot Key 如何解决 ...

  3. SAP BDC 调用中 金额格式转换

    在BDC调用中,由于用户设置不同,导致金额.日期等字段的输入格式不正确.此处给出 自创 金额转换FM 并配有 调用方式. function zgm_conver_cuur. *"------ ...

  4. ASP.NET MVC的核心-Controller(控制器)

    "每一个请求都必须通过Controller处理,然而其中有些请求是不需要模型和视图的" MVC框架规定带Controller后缀的类称为所谓的"控制器",在xx ...

  5. 我大抵是卷上瘾了,横竖睡不着!竟让一个Bug,搞我两次!

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言:一个Bug 没想到一个Bug,竟然搞我两次! 我大抵是卷上瘾了,横竖都睡不着,坐起来 ...

  6. 疫情在校学生之——用python对某校园热水服务app进行测试,实现自动免费用水(仅供参考)

    写在前面的过场话: 本文只是对某校园热水服务app做个测试,其实本人并没有做大坏事,并未传播相关技术,文章以下内容的敏感部分会打码,并且相关厂商已经正在进行漏洞修复,大家看看就好.文章后会提供&quo ...

  7. CF141E Clearing Up 题解

    思路分析 自认为是一道很好的思维题. 直接看上去的想法是: 跑一个生成树,每一次加的边颜色交替进行,直到拉出生成树. 仔细想想,发现可能无法保证最后是一棵树而不是森林,也是说输出都是 \(-1\) . ...

  8. 『现学现忘』Git后悔药 — 31、reset版本回退命令总结

    目录 1.--soft回退说明 2.--mixed回退说明 3.--hard回退说明 4.总结 在Git中进行版本回退需要使用git reset命令. 以前面文章中的示例为例,当我准备在V4版本,回退 ...

  9. Codeforces Round #796 (Div. 2)(A~E题题解)

    文章目录 原题链接: A.Cirno's Perfect Bitmasks Classroom 思路 代码 B.Patchouli's Magical Talisman 思路 代码 C.Manipul ...

  10. 关于标准IO缓冲区的问题

    关于标准IO缓冲区的问题 按照标准IO缓冲区可以分为三类: 不缓存类型: 一旦有数据,直接将数据写入到文件 行缓冲类型: 同全缓冲类型 遇到\n时,将数据写入文件 全缓冲类型: 当程序结束,将数据冲洗 ...