【Java】学习路径58-TCP聊天-双向发送实现
这一章内容比较复杂(乱)
重点在于解决利用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;
如果我们要在内部类中使用外部类的对象,我们需要在内部类中复制一个副本。
- 上面的发送端、接收端代码中还分别创建了一个新的内部线程。
- 发送端的线程中创建了一个接收端的线程
- 接收端的线程中创建了一个发送端的线程
- 但TCP协议本身就支持双向传输,所以我们不需要创建新的Socket、ServerSocket对象。
- 由于run()方法中不能捕捉错误,所以我们只能使用try catch来捕捉异常
- 从内部类引用的本地变量必须是最终变量或实际上的最终变量,所以我们构造了一个内部类的fips,复制了ips变量
好了,目前总算是弄好了,实现了互相发送的功能。
但是当我们测试输入“end”关闭服务器端、客户端的时候,好像出现了一点问题。
- 在Receive服务器端输入end,并没有什么反应
- 在Send客户端输入end,直接就报异常了
- 错误代码定位到
- 原因是当我们在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();
}
}
}
实验流程:
- 在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();
} - 但是我们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了。
- 所以说目前,从Send客户端、Receive服务器端提出的关闭,对于自己来说都是可以的。
- 但是另一方并没有自动关闭。
- 于是我们在Send端中的发送信息部分中的等待获取next()的后面再加一个isClosed判断吧,虽然效果可能不好,但是这也是没有办法的事情了
- 另外在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聊天-双向发送实现的更多相关文章
- Java学习路径及练手项目合集
Java 在编程语言排行榜中一直位列前排,可知 Java 语言的受欢迎程度了. 实验楼上的[Java 学习路径]中将首先完成 Java基础.JDK.JDBC.正则表达式等基础实验,然后进阶到 J2SE ...
- Java学习路径(抛光砖)
这就是我刚刚在五孔问答中找到的Java学习路线图抛光砖价格.我个人认为,这条Java学习路线是可以的.它是2018年相对较新的Java学习路线,更符合企业就业标准. Java学习路径的第一阶段:Jav ...
- Java学习路径:不走弯路,这是一条捷径
1.如何学习编程? JAVA是一种平台.也是一种程序设计语言,怎样学好程序设计不只适用于JAVA,对C++等其它程序设计语言也一样管用.有编程高手觉得,JAVA也好C也好没什么分别,拿来就用.为什么他 ...
- Java学习路径
-------第一部分:基础语法-------- 1.输出语句 1.1 hello world 1.2 拼接输出.换行和不换行输出 1.3 拼接变量输出 2.输入语句: 2.1 定义变量,赋值(整数. ...
- 【JDBC】学习路径1-JDBC背景知识
学习完本系列JDBC课程后,你就可以愉快使用Java操作我们的MySQL数据库了. 各种数据分析都不在话下了. 第一章:废话 JDBC编程,就是写Java的时候,调用了数据库. Java Databa ...
- Java进阶:基于TCP通信的网络实时聊天室
目录 开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 ...
- Java学习---TCP Socket的学习
基础知识 1. TCP协议 TCP是一种面向连接的.可靠的.基于字节流的运输层(Transport layer)通信协议.在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,UDP是同一层 ...
- Java学习-043-获取文件在目录中的路径
我们在日常的电脑使用中,经常需要在当前目录或当期目录及其子目录中查找文件,并获取相应的文件路径名.在我们的自动化测试中,也经常需要确认文件在目录中是否成功生成或已存在,因而我写了一个小方法来实现. 获 ...
- Java学习-009-文件名称及路径获取实例及源代码
此文源码主要为应用 Java 获取文件名称及文件目录的源码及其测试源码.若有不足之处,敬请大神指正,不胜感激!源代码测试通过日期为:2015-2-3 00:02:27,请知悉. Java获取文件名称的 ...
随机推荐
- Camunda定时器事件示例Demo(Timer Events)
Camunda定时器事件(Timer Events)是由定义的计时器触发的事件.它们可以用作启动事件.中间事件或边界事件.边界事件可以中断,也可以不中断. Camunda定时器事件包括:Timer ...
- C#中的枚举器
更新记录 本文迁移自Panda666原博客,原发布时间:2021年6月28日. 一.先从可枚举类型讲起 1.1 什么是可枚举类型? 可枚举类型,可以简单的理解为: 有一个类,类中有挺多的数据,用一种统 ...
- 基于MybatisPlus代码生成器(2.0新版本)
一.模块简介 1.功能亮点 实时读取库表结构元数据信息,比如表名.字段名.字段类型.注释等,选中修改后的表,点击一键生成,代码成即可提现出表结构的变化. 单表快速转化restful风格的API接口并对 ...
- nginx https证书配置
1. Nginx配置 server { listen 443; #指定ssl监听端口 server_name www.example.com; ssl on; #开启ssl支持 ssl_certifi ...
- VScode运行总是显示running状态
一.每次点击运行都显示code is already running,而且键盘也没有办法输入 二.解决办法 注意:记得重新启动VScode
- gslb(global server load balance)技术的一点理解
gslb(global server load balance)技术的一点理解 前言 对于比较大的互联网公司来说,用户可能遍及海内外,此时,为了提升用户体验,公司一般会在离用户较近的地方建立机房,来服 ...
- CSS基本知识点——带你走进CSS的新世界
CSS基本知识点 我们在学习HTML之后,前端三件套第二件便是CSS,但CSS内容较多,我们分几部分讲解: (如果没有学习HTML,请参考之前文章:HTML知识点概括--一篇文章带你完全掌握HTML& ...
- Tapdata PDK 生态共建计划启动!Doris、OceanBase、PolarDB、SequoiaDB 等十余家厂商首批加入
2022年4月7日,Tapdata 正式启动 PDK 插件生态共建计划,致力于全面连接数据孤岛,加速构建更加开放的数据生态,以期让各行各业的使用者都能释放数据的价值,随时获取新鲜的数据.截至目前, ...
- 聊一聊 C# 后台GC 到底是怎么回事?
一:背景 写这一篇的目的主要是因为.NET领域内几本关于阐述GC方面的书,都是纯理论,所以懂得人自然懂,不懂得人也没法亲自验证,这一篇我就用 windbg + 源码 让大家眼见为实. 二:为什么要引入 ...
- 【填坑】树莓派4B上运行Bullseye版本系统,不能登录xrdp的问题~~
以前使用 buster,安装xrdp后 pi用户xrdp登录正常, 可自从使用了 bullseye系统,pi登录xrdp后,出现黑屏不能登录现象. 网上搜寻解决方案,一种方法是: 登录树莓派后,打开这 ...