【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获取文件名称的 ...
随机推荐
- Java JavaMail通过SMPT发送邮件
概述 本讲讲述如何使用JavaMail工具包,通过SMPT协议,在Java代码中发送邮件. 一.JavaMail简介 JavaMail API提供了一个独立于平台且与协议无关的框架来构建邮件和消息传递 ...
- cool-admin vite-vue3 打包部署 nginx代理设置
location /api {rewrite ^/api/(.*)$ /$1 break;proxy_pass http://xxx.com;}location /socket.io {rewrite ...
- 在海思芯片上使用GDB远程调试
1 前言 使用海思平台上(编译工具链:arm-himix200-linux)交叉编译 GDB 工具(使用版本8.2,之前用过10.2的版本,在编译 gdbserver 遇到编译出错的问题,因为关联了其 ...
- 分享自己平时使用的socket多客户端通信的代码技术点和软件使用
前言 说到linux下多进程通信,有好几种,之前也在喵哥的公众号回复过,这里再拿出来,重新写一遍:多进程通信有管道,而管道分为匿名和命名管道 ,后者比前者优势在于可以进行无亲缘进程通信:此外信号也是进 ...
- python爬虫之protobuf协议介绍
前言 在你学习爬虫的知识过程中是否遇到下面的类型.如果有兴趣学习一下或者了解相关知识的,且不嫌在下才疏学浅,可以参考一下.欢迎各位网友的指正. 首先叙述一下问题的会出现的式样. 你可能会在请求参数中看 ...
- python采集一下美团外卖数据~~
所需知识点(https://jq.qq.com/?_wv=1027&k=Ap5XvyNN) 1.动态数据抓包演示2.json数据解析3.requests模块的使用4.保存csv 安装命令:re ...
- 初识Java GUI
1. 使用Java Swing 显示的窗口如下 在原有代码基础上添加代码实现对窗口大小 标题等信息
- Collection子接口:List接口
1. 存储的数据特点:存储序的.可重复的数据. 2. 常用方法:(记住)增:add(Object obj)删:remove(int index) / remove(Object obj)改:set(i ...
- javaScript去重的11种方法
前言 去重是开发和面试中经常遇到的问题,下面是总结的 11 种去重方法 方法 示例数组 var arr = [1, 2, 4, 5, 5, 2, 1, 1, 4, 6] set + 解构赋值 这种方法 ...
- k8s+crio+podman搭建集群
前言 在传统的k8s集群中,我们都是使用docker engine做为底层的容器管理软件的,而docker engine因为不是k8s亲生的解决方案,所以实际使用中会有更多的分层.之前我们也讲过,k8 ...