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

重点在于解决利用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. 【题解】Codeforces Round #798 (Div. 2)

    本篇为 Codeforces Round #798 (Div. 2) 也就是 CF1689 的题解,因本人水平比较菜,所以只有前四题 A.Lex String 题目描述 原题面 给定两个字符串 \(a ...

  2. 我的第一个springboot starter

      在springboot中有很多starter,很多是官方开发的,也有是个人或开源组织开发的.这些starter是用来做什么的呐? 一.认识starter   所谓的starter,在springb ...

  3. Mac安装Brew包管理系统

    Mac安装Brew包管理系统 前言 为什么需要安装brew 作为一个开发人员, 习惯了使用centos的yum和ubuntu的apt, 在mac中有没有这两个工具的平替? 有, 就是Brew. Bre ...

  4. 编程技巧│提高 Javascript 代码效率的技巧

    目录 一.变量声明 二.三元运算符 三.解构赋值 四.解构交换 五.箭头函数 六.字符串模版 七.多值匹配 八.ES6对象简写 九.字符串转数字 十.次方相乘 十一.数组合并 十二.查找数组最大值最小 ...

  5. BluePrism手把手教程2.0 创建流程

    2.0.1 创建流程 2.0.2 设置流程名称 2.0.3 添加流程说明 2.0.4 添加流程成功 2.0.4 打开新建的流程 RPA行业微信交流群,欢迎大家扫码加入一起交流,此群用于RPA行业技术. ...

  6. 全新升级的AOP框架Dora.Interception[汇总,共6篇]

    多年之前利用IL Emit写了一个名为Dora.Interception(github地址,觉得不错不妨给一颗星)的AOP框架.前几天利用Roslyn的Source Generator对自己为公司写的 ...

  7. Linux for CentOS 下的 nginx 绿色安装-超省心安装

    1.我这里是nginx-1.13.0-1.x86_64 .rpm(点击下载)版本的. 2.安装nginx的相应环境.有些环境可能不必须,但是安装了,确保以防万一,多多益善 yum install gd ...

  8. 集合-Collection工具类

    一.概念 二.常用方法 1.Collection和Collections的区别 Collection:是创建集合的接口,Collections是一个操作Collection工具类 2.常用方法 点击查 ...

  9. idea java 打包的方法

    方法1: 在pom.xml 里面加上maven打包的配置 <plugin> <groupId>org.springframework.cloud</groupId> ...

  10. APISpace 未来7天生活指数API接口 免费好用

    随着经济的发展,我们的生活水平在不断的提高,生活指数在我们的生活中也越来越受到关注,根据当天的生活指数,我们就可以知道在今天我们可以干什么比较好.   未来7天生活指数API,支持国内3400+个城市 ...