使用javaFx实现多线程的网络通信小程序——互联网程序设计基础(3)

上一篇博客,我们通过多线程,将主线程和接受信息的线程分开,实现了客户端和服务器的自由交流。本篇博客将分享文件传输以及服务端的多线程。

首先是文件传输,文件传输就讲解客户端怎么请求和下载文件。文件传输其实分为两个部分,一个是负责与服务器通信的8008端口,另外一个是负责接收文件内容的2020端口(具体端口多少其实都可以自己设置,只要不占用一些重要的端口就好)

先来说一下界面,界面如下图,其实和上一篇博客的界面差不多,就是多了一个下载按钮。按照之前的操作,我们先要建立一个文件传输的类,负责接收文件的流。

在这里插入图片描述

//这里定义了一个文件传输信息的类,负责构建文件流,该类中只实现了接收文件这一种方法
import java.io.*;
import java.net.Socket;

public class FileDataClient {
    //定义连接的套接字
    private Socket dataSocket;

    public FileDataClient(String ip, String port) throws IOException{
        //主动向服务器发起连接,实现TCP的三次握手过程
        //如果不成功,则抛出错误信息,其错误信息交由调用者处理
        dataSocket = new Socket(ip , Integer.parseInt(port));
    }

    public void getFile(File saveFile) throws IOException {

        if (dataSocket != null) { // dataSocket是Socket类型的成员变量


            FileOutputStream fileOut = new FileOutputStream(saveFile);//新建本地空文件
            byte[] buf = new byte[1024]; // 用来缓存接收的字节数据
            //网络字节输入流
            InputStream socketIn = dataSocket.getInputStream();
            //网络字节输出流
            OutputStream socketOut = dataSocket.getOutputStream();

            //(2)向服务器发送请求的文件名,字符串读写功能
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(socketOut, "utf-8"), true);
            pw.println(saveFile.getName());

            //(3)接收服务器的数据文件,字节读写功能
            //size为读取一次的字节数,当网络输入流为空时则socketIn.read(buf)的值为-1
            int size = 0;
                while ((size = socketIn.read(buf)) != -1) {//读一块到缓存,读取结束返回-1
                    fileOut.write(buf, 0, size); //写一块到文件
            }
            fileOut.flush();//关闭前将缓存的数据全部推出
            //文件传输完毕,关闭流
            fileOut.close();
            if (dataSocket != null) {
                dataSocket.close();
            }
        } else {
            System.err.println("连接ftp数据服务器失败");
        }
    }

}

这里其实接下来要用到很常见的FileChooser这个类,因此我这里就给大家总结一下一些常见的方法。

在这里插入图片描述

我们构造完这个文件传输的类后,就可以给保存这个按钮设置时间,然后实例化这个类来下载文件了。

btnDownload.setOnAction(event -> {
            if(tfSend.getText().equals("")) //没有输入文件名则返回
                return;
            //从输入中得到文件名
            String fName = tfSend.getText().trim();
            tfSend.clear();
            //在电脑中选择或创建对应的文件
            FileChooser fileChooser = new FileChooser();
            //设置好保存文件的初始名就是下载的文件名
            fileChooser.setInitialFileName(fName);
            //不显示一个新的“保存文件选择”对话框
            File saveFile = fileChooser.showSaveDialog(null);
            if (saveFile == null) {
                return;//用户放弃操作则返回
            }
            try {
                //数据端口是2020
                String ip = tfIP.getText().trim();
                new FileDataClient(ip, "2020").getFile(saveFile);
                //Java FX 使用 Alert 类创建提示对话框,提示用户文件下载完成
                Alert alert = new Alert(Alert.AlertType.INFORMATION);
                alert.setContentText(saveFile.getName() + " 下载完毕!");
                alert.showAndWait();
                //通知服务器已经完成了下载动作,不发送的话,服务器不能提供有效反馈信息
                fileDialogClient.send("客户端开启下载");
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

上述其实已经实现了文件传输的基本内容。这里给大家一个有意思的功能,我们选择对话框的内容,就可以将内容直接出现在发送框。

		//信息显示区鼠标拖动高亮文字直接复制到信息输入框,方便选择文件名
        //taDispaly为信息选择区的TextArea,tfSend为信息输入区的TextField
        //为taDisplay的选择范围属性添加监听器,当该属性值变化(选择文字时),会触发监听器中的代码
        taDisplay.selectionProperty().addListener((observable, oldValue, newValue) -> {
            //只有当鼠标拖动选中了文字才复制内容
            if(!taDisplay.getSelectedText().equals(""))
                tfSend.setText(taDisplay.getSelectedText());
            else if (taDisplay.getSelectedText().equals(""))
                tfSend.clear();
        });

其实我们可以发现文件传输和网络对话都有非常相似的地方,比如要构建网络的输入输出流,不过不同之处在于,网络对话中接收信息我们使用的是用BufferReader封装好的输入流中的readline方法进行读取服务器发送过来的信息,而文件传输则直接对底层的网络流inputstream进行操作,通过循环读取字节进行缓存和写入。

接下来是服务器的多线程,之前版本我们实现的是服务器的多线程,服务器一次只能跟一个用户进行连接,原因就是因为我们服务器把主线程作为网络交流,而其中有readline()这个阻塞语句,因此再不结束对话的前提下,会一直阻塞主线程。根据我们多线程客户端的经验,我们只要把跟客户端对话的这个readline()语句放在多线程中,就不会阻塞主线程,但由于连接的用户数量不固定,因此我们要设置一个静态或者动态的线程池来对新连接的用户进行线程分配。

package chapter05;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class GroupServer {
    private int port = 8008; //服务器监听端口
    private ServerSocket serverSocket; //定义服务器套接字
    //创建一个动态线程池,适合小并发量
    private ExecutorService executorService = Executors. newCachedThreadPool();
    //
    private static Set<Socket> members = new CopyOnWriteArraySet<Socket>();

    public GroupServer() throws IOException {
        serverSocket = new ServerSocket(8008);//初始化类时候,服务器套接字选定8008端口
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

    //服务器发送数据给其他连接的主机的方法
    private PrintWriter getWriter(Socket socket) throws IOException {
        //获得输出流缓冲区的地址,这里socket应该是缓冲区地址,从缓冲区中直接取出数据,数据类型是二进制
        OutputStream socketOut = socket.getOutputStream();
        //网络流写出需要使用flush,这里在PrintWriter构造方法中直接设置为自动flush
        return new PrintWriter( //OutputStreamWrite是字符写流,将socketOut的字节进行编码
                new OutputStreamWriter(socketOut, "utf-8"), true);
    }

    //服务器收取连接主机发送数据的方法
    private BufferedReader getReader(Socket socket) throws IOException {
        //获得输入流缓冲区的地址
        InputStream socketIn = socket.getInputStream();
        return new BufferedReader(
                new InputStreamReader(socketIn, "utf-8"));
    }

	//由于该内部类有线程的接口,因此要重写run方法
    class Handler implements Runnable{
        private Socket socket;
        public Handler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            //本地服务器控制台显示客户端连接的用户信息
            System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
            try {
                BufferedReader br = getReader(socket);//定义字符串输入流
                PrintWriter pw = getWriter(socket);//定义字符串输出流

                //客户端正常连接成功,则发送服务器欢迎信息,然后等待客户发送信息
                pw.println("From 服务器:欢迎使用本服务!");

                String msg = null;
                //此处程序阻塞,每次从输入流中读入一行字符串
                while ((msg = br.readLine()) != null) {
                    //如果客户发送的消息为"bye",就结束通信
                    if (msg.equals("bye")) {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From服务器:服务器断开连接,结束服务!");
                        System.out.println(socket.getInetAddress().getHostAddress() + "客户端离开");
                        members.remove(socket);
                        break; //结束循环
                    }
                    sendToAllMembers(msg, socket.getInetAddress().getHostAddress());

                }
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    if(socket != null)
                        socket.close(); //关闭socket连接及相关的输入输出流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //单客户版本,即每一次只能与一个客户建立通信连接
    public void Service() throws IOException{
        while (true) {
            Socket socket = null;
            //此处程序阻塞等待,监听并等待客户发起连接,有连接请求就生成一个套接字。
            socket = serverSocket.accept();
            //将新的在线成员加入到哈希表中
            members.add(socket);
            //将服务器和客户端的通信交给线程池处理,将执行handle中的run代码
            Handler handler = new Handler(socket);
            executorService.execute(handler);
        }
    }

    private void sendToAllMembers(String msg,String hostAddress) throws IOException{
        PrintWriter pw;
        OutputStream out;

        for (Socket tempSocket : members) {
            out = tempSocket.getOutputStream();
            pw = new PrintWriter(
                    new OutputStreamWriter(out, "utf-8"), true);
            pw.println(hostAddress + " 发言:" + msg );
        }
    }

    public static void main(String[] args) throws IOException{
        new GroupServer().Service();
    }
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值