背景知识
所以并不是说,每一个客户端都兼具了服务端的功能,这里的服务端仅仅是一个中介,所有客户端的信息都要发给这个服务端,然后再转发给所有的客户端,对吗?
是的,你说得非常对。在这个应用场景下,服务端充当的是中介的角色,管理所有已连接的客户端之间的数据传输。所有客户端的信息都会先发给服务端,然后服务端再将其转发给其他已连接的客户端。
客户端之间不会直接进行数据交互,而是通过服务端来实现数据的传输和共享。客户端只负责将数据发给服务端,然后从服务端获取其他客户端发送过来的数据。服务端则负责将发送过来的数据广播到所有已连接的客户端。这种架构模式被称为客户端/服务端模式,常用于实现即时通讯、在线游戏等网络应用。
contect(): 这个函数负责建立一个与指定服务器的连接,并且启动两个线程分别用于发送和接收信息。其中,连接的IP和端口是由用户输入的,如果输入不合法,会有相应的提示。
send(): 这个函数负责发送聊天信息。它通过PrintWriter对象将信息发送出去。
ClientInputStreamThread: 这是一个线程类,用于接收聊天信息。它是一个无限循环,当接收到信息时,在聊天记录框中显示,并清空发送框。
需要注意的是,这仅仅是客户端部分的代码,还需要与服务端代码配合才能实现聊天功能。客户端发送信息时,
服务端需要接收并将其转发给其他客户端;而当其他客户端发送信息时,服务端也需要将其转发给当前客户端。
在网络编程中,数据的传输都是通过输入流和输出流进行的。在客户端给服务器发送数据时,需要将数据写入到Socket的输出流中,通过网络传输到服务端。
一、socket编程的含义
关于使用JavaSocket编程开发聊天室,我可以提供一些基本的建议和思路:
-
确定聊天室的功能和要求:在开始编写代码之前,需要考虑聊天室的基本功能,例如用户登录、发送消息、创建聊天室、加入聊天室等等。确定这些功能后,可以更容易地编写代码并且能够更好地测试你的应用程序。
-
使用Java Socket API:Java Socket API是Java标准库中的一部分,提供了一套用于网络编程的类和接口。你可以使用这些类和接口来创建一个网络连接并进行数据传输。对于聊天室开发,你可以使用ServerSocket类和Socket类来实现服务端和客户端之间的通信。
-
实现多线程:在聊天室中,每个用户都应该能够同时收发消息。因此,你需要使用多线程技术来处理多个用户之间的通信。你可以使用Java的线程机制来实现多线程。
-
使用GUI界面:为了更好地展示聊天室的功能,你可以使用GUI界面来构建一个更好的用户界面。Java Swing库提供了一套用于构建GUI界面的类和接口,你可以使用它来实现你的聊天室客户端界面。
-
遵循设计原则:在编写聊天室应用程序时,你需要遵循一些常见的设计原则,例如单一职责原则、开放封闭原则和依赖倒置原则。这些原则可以帮助你更好地组织你的代码并使其更易于维护和扩展。
希望这些建议可以帮助你开始使用Java Socket编程开发聊天室。
使用Java Socket可以轻松地开发聊天室应用程序。以下是一个简单的步骤:
-
创建一个ServerSocket对象来监听客户端的连接请求,指定服务器的端口号。
ServerSocket serverSocket = new ServerSocket(8888);
-
在服务器端,使用Socket对象接受来自客户端的连接请求。
Socket socket = serverSocket.accept();
-
在客户端,使用Socket对象连接服务器。
Socket socket = new Socket("localhost", 8888);
-
在服务器端,创建一个线程来处理来自客户端的消息。在该线程中,使用BufferedReader和PrintWriter对象读写数据流。该线程将接收来自客户端的消息,并将其广播给其他客户端。
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
-
在客户端,创建一个线程来处理来自服务器的消息。该线程将接收来自服务器的消息,并将其显示在客户端的聊天室窗口中。
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
在服务器端,将消息广播给所有客户端。可以使用一个集合来存储所有连接到服务器的客户端Socket对象。
for (Socket client : clients) { PrintWriter writer = new PrintWriter(client.getOutputStream(), true); writer.println(message); }
以上是简单的步骤,可以让你开始开发Java Socket聊天室应用程序。当然,还需要考虑到一些问题,如多线程同步、异常处理等。希望这可以帮助到你。
二、流程
1、connect函数
- 获取port
- 检查port是否合法
- 根据输入的IP以及port创建socket对象,同时tcp连接也被建立
- 新建线程池对象server,用lambda表达式重写函数,使得每个线程都是守护线程
- 开启客户端输入流监控
- 发送昵称到服务端
三、lambda表达式的运用
Executor server = Executors.newCachedThreadPool(r -> { Thread t = Executors.defaultThreadFactory().newThread(r); t.setDaemon(true); return t; }); //两部分 //外边 Executor server = Executors.newCachedThreadPool(); //里边 r -> { Thread t = Executors.defaultThreadFactory().newThread(r); t.setDaemon(true); ----定制的内容--->将该线程改为守护线程 return t; } //如果不用lambda Thread t = Executors.defaultThreadFactory().newThread(r);
当我们调用 Executors.newCachedThreadPool()
方法时,返回的对象实际上是一个 ThreadPoolExecutor
类型的线程池。这个线程池的默认的线程创建方式是由 Executors.defaultThreadFactory()
方法提供的。
在这个默认线程工厂创建出一个新的线程实例时,Java程序会将当前Runnable任务封装为一个Thread实例。具体来说,程序会调用Thread的构造函数来创建该线程实例,而该构造函数需要传入一个Runnable对象作为其构造参数。
可以看到,在这段lambda表达式中,我们重写了 Executors.newCachedThreadPool()
方法提供的默认线程创建方式,主要做了两件事:
-
根据传入的Runnable任务对象r,使用
Executors.defaultThreadFactory().newThread(r)
创建一个新的Thread对象。 -
将返回的Thread对象设置为守护线程,并返回该对象。
在第1步中,我们使用 Executors.defaultThreadFactory()
获取一个默认的线程工厂实例,然后调用其 newThread
方法来使用传入的Runnable对象创建一个新的Thread对象。
在第2步中,我们调用Thread对象的 setDaemon(true)
方法,将其设置为守护线程。这里需要注意,将一个线程设置为守护线程后,如果该线程是唯一的非守护线程,那么程序就会结束。通常情况下,我们会将后台任务或者与用户无关的任务设置为守护线程,以便在程序结束后自动结束线程。
最终,该lambda表达式会返回一个新创建的Thread对象,并加入到线程池中,由该线程执行传入的任务。我们在创建线程池时,将该lambda表达式作为参数传递给Executors.newCachedThreadPool()方法来替代默认的线程创建方式,从而实现了对线程创建过程的自定义定制化。
如果没有使用lambda表达式,该方法将只有以下内容:
Thread t = Executors.defaultThreadFactory().newThread(r);//非守护进程
在这个代码中,程序会使用默认的线程工厂Executors.defaultThreadFactory()
来创建一个新的线程工厂(在默认情况下,Java提供了一个线程工厂的实现Executors.defaultThreadFactory(),它会返回一个由系统设置的默认线程工厂实例。),该线程传递进入方法中的Runnable实例r
。然而,对于这个线程,程序没有做任何其他定制化的操作。
接下来,我们通过调用线程工厂的newThread(Runnable r)方法来创建一个新的线程。newThread(Runnable r)方法接收一个Runnable对象作为参数,并返回一个Thread类型的对象。这个Thread对象可以用来启动新线程。
在这种情况下,新创建的线程是一个普通的非守护线程,与任何其他使用该线程工厂创建的线程相同。线程不会自动退出,直到Runnable任务完成或者程序结束。
而使用了lambda表达式之后,程序对创建线程的过程进行了定制化的操作,将新线程设置为守护线程,以在程序退出时自动结束线程。因此,使用lambda表达式对线程创建进行了一些定制化,使其更符合具体的应用场景。
整个表达式 Executors.defaultThreadFactory().newThread(r) 创建了一个新线程,该线程包含了任务r,并具有线程工厂的默认属性。将其赋值给Thread类型的变量t,可以对线程进行操作和控制,例如启动、中断、等待等。
总之,这个代码片段创建了一个线程并将任务r作为参数传递给线程,并返回Thread对象t。通过这个对象,我们可以启动线程的运行并且进行控制。
四、守护线程的作用
在局域网中实现聊天功能时,有许多后台任务需要执行,例如监听网络连接、接受和发送消息等等。对于这些后台任务,我们可以创建守护线程来实现自动化的处理。
守护线程会在所有非守护线程都执行完毕后自动退出,因此我们不需要显示的关闭它们,这在实现聊天功能时很方便。例如,在接受和发送消息的线程中,我们可以设置这些线程为守护线程,以便在主线程退出后自动关闭这些线程,避免程序继续运行浪费资源。
同时,由于Java中的线程是共享内存的,多线程访问共享变量可能会出现线程安全问题。在聊天功能中,守护线程可以被设为对聊天记录的写入和读取的线程,避免多个线程同时访问聊天记录导致的线程安全问题。
在局域网聊天中,守护线程还可用于实现程序的一些后台任务,例如自动更新用户的在线状态、检测其他用户的状态等。这些任务可以在后台线程中调度和执行,不会影响主线程的执行,从而提高聊天系统的效率和稳定性。
综上所述,守护线程在局域网聊天中有很多作用。它们可以帮助我们实现自动化处理、避免线程安全问题、提高系统效率和稳定性,为我们打造一个出色的聊天系统提供了良好的支持。
五、server对象
在这段代码中,server
是一个Executor
类型的对象。Executor
是一个用于执行Runnable
任务的接口。在这里,Executor
对象是由Executors.newCachedThreadPool(...)
方法创建的,它返回一个可以动态调整线程数量的线程池。通过代码中的lambda表达式将线程工厂定义为每个线程都是守护线程(Daemon Thread)。因此,在使用该线程池执行任务时,所创建的线程都会是守护线程。
可以将server
看作是一个可以执行后台任务的线程池对象,使用execute
方法提交一个Runnable
的任务到线程池中执行。例如,可以通过以下方式执行一个任务:
server.execute(() -> { // 这里是任务要执行的代码 });
这段代码可以将一个新的Runnable
对象提交到server
线程池中去执行。线程池会从池中的空闲线程中选择一个线程来执行该任务,并在任务执行完毕后将线程返回到线程池中以便于下次使用。
六、重写run()
这行代码是 ClientInputStreamThread
类(之所以继承runnable接口,就是为了重写run方法,让他在运行的时候可以做点别的事情)中获取 Socket 输入流对象的代码。当客户端连接上服务器时,服务器需要获取该客户端的输入流对象,以便能够读取它发来的数据。
Socket
类中提供了一个 getInputStream()
方法,该方法返回一个输入流对象,可以用来读取数据。具体来说,该方法会返回一个 InputStream
类的对象,用于读取和接收来自此套接字的数据。
在 ClientInputStreamThread
类的这行代码中,首先声明了一个输入流对象 is
,并将其赋值为 socket.getInputStream()
的返回值,这样就获取到了客户端传来的数据的输入流。
这是 ClientInputStreamThread
类实现 Runnable
接口的代码实现。看到了这部分代码,我们可以明确这确实就是一个实现了输入流处理线程的类。
在 run()
方法中,做以下事情:
-
首先获取客户端 Socket 的 InetAddress 对象,通过
.getHostAddress()
方法获取其 IP 地址,这里将其存为字符串,以便于在后面使用。 -
获取 Socket 的输入流对象。
-
封装输入流为一个 InputStreamReader 对象,并创建长度为 1024 的 char 类型数组缓冲区(也可以是其他长度),读取输入流中的数据到缓冲区。
-
循环读取缓冲区中的内容,将其转为字符串,存入
txt_context
控件显示的文本框中。每次读取完成后,将txt_message
控件清空。 -
如果发生异常,则将其打印出来,并在
finally
块中关闭输入流。
循环读取是通过实现一个无限循环来完成的,因此这一线程将持续地不断读取输入流中的内容,直到网络连接被断开或者出现异常为止。另外,这段代码只处理了读取输入流中的内容,并没有向客户端发送数据。这部分处理应该在其他地方实现。
详细解析
-
首先,获取与客户端建立的连接
socket
的输入流对象is
。通过输入流对象可以获取客户端向服务端发送的数据。
is = socket.getInputStream();
-
创建一个
InputStreamReader
对象reader
,将is
输入流对象读取的字节流解码为字符流,方便后续的字符串处理。
InputStreamReader reader = new InputStreamReader(is);
-
定义一个字符数组
buffer
,准备存储读取到的字符。以 1024 个字符为一次读取的标准,循环读取客户端向服务端发送的数据。
char[] buffer = new char[1024]; int len = -1; while (true) { // 一直读取客户端发送的数据 // 读取输入流数据到 buffer 中,并返回实际读取到的字符数 if (-1 != (len = reader.read(buffer))) { String tmp = new String(buffer, 0 , len); // 将 buffer 中的字符转换为字符串 txt_context.setText(txt_context.getText() + "\r\n" + tmp); // 将读取到的数据展示在聊天室文本框中 txt_message.setText(""); // 清空输入文本框 } }
-
在循环中,可以通过
reader.read(buffer)
方法从客户端输入流中读取数据,如果读取到了数据,返回读取到字节数(或达到流末尾返回 -1)。当返回的字节数不为 -1 时,将读取到的字节转换为字符串tmp
。接着,将tmp
字符串显示在聊天室文本框中,以及清空客户端输入文本框txt_message
的内容。 -
由于代码中使用了死循环
while (true)
,因此程序会一直等待读取客户端发送的数据。如果客户端不主动关闭连接,该循环会一直运行,导致程序无法退出。因此,在聊天室客户端中,需要在退出聊天室时关闭客户端 socket 连接并中断本线程的执行。
总代码
public class ClientInputStreamThread implements Runnable{ @Override public void run() { { InputStream is = null; String ip = socket.getInetAddress().getHostAddress(); try { is = socket.getInputStream(); InputStreamReader reader = new InputStreamReader(is);//将输入流对象is封装,以便读取和处理字符数据 char[] buffer = new char[1024];//存放收到的消息 int len = -1; while (true) {//循环读取直到网络断开 if (-1 != (len = reader.read(buffer))) { //reader.read(buffer))返回字符串长度,如果不为-1 读取 String tmp = new String(buffer, 0 , len); txt_context.setText(txt_context.getText() + "\r\n" + tmp); txt_message.setText(""); } } } catch (IOException e) { e.printStackTrace(); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
七、区别write()与print()
write()
方法主要用于处理原始数据,例如字节数组、二进制格式数据等;
print()
方法主要用于向输出流中写入可读性强的文本数据,它可以将任何对象的字符串表示形式打印到输出流中,并且可以自动添加分隔符和换行符。
需要注意的是,在使用 write()
方法写入文本数据时,需要手动将文本编码成字节数组,再进行写入操作。而在使用 print()
方法时,则不用编码操作,可以直接使用字符串类型的参数直接输出。
八、详解server.execute(new ClientInputStreamThread());
这段代码中,server
是一个Executor
对象,可以是线程池中的一个实例对象,而ClientInputStreamThread
是一个实现了Runnable
接口的类的实例对象。
当执行server.execute(new ClientInputStreamThread())
时,将创建一个新的ClientInputStreamThread
线程对象,并将其封装为一个Runnable
接口的实例对象,以便将其提交给线程池server
中的线程来执行。
具体执行过程是这样的,在线程池中,会有一定数量的空闲线程,当调用execute
方法时,线程池会选择一个空闲线程,并将ClientInputStreamThread
类的实例对象封装为Runnable
来执行任务。执行过程是由线程池自动管理并执行的,执行完成后,该线程将返回线程池以便于下次使用。
当ClientInputStreamThread
类被执行时,它会执行run()
方法中定义的代码逻辑,完成相应的操作,并在操作完成后退出。
九、详解execute()
execute(Runnable command)
方法是用于在线程池中执行一个任务的方法,在java.util.concurrent.Executor
接口中定义。该方法接受一个Runnable
类型的任务作为参数,将其提交给线程池异步执行,并立即返回。线程池会从线程池的线程池中选取一个空闲的线程,将任务交给其执行。
当线程池中的某个线程执行任务时,它会执行Runnable
对象的run
方法。当run
方法执行完毕后,该线程会回到线程池中,准备执行下一个任务。
线程池中的线程是可以复用的,这就可以避免频繁创建和销毁线程所带来的开销和延迟。而且,使用线程池可以方便的控制并发线程的数量,避免阻塞系统资源。
在controller代码中,我们可以看到使用execute
方法创建了一个线程池,然后执行了两个任务,分别是一个接收消息的线程和一个发送昵称的线程。这样可以避免阻塞UI线程,从而提升程序的响应性能。
十、getInputStream与getOutputStream
客户端上的使用(client)
1.getInputStream方法可以得到一个输入流,客户端的Socket对象上的getInputStream方法得到输入流其实就是从服务器端发回的数据。
2.getOutputStream方法得到的是一个输出流,客户端的Socket对象上的getOutputStream方法得到的输出流其实就是发送给服务器端的数据。
服务器端上的使用(server)
1.getInputStream方法得到的是一个输入流,服务端的Socket对象上的getInputStream方法得到的输入流其实就是从客户端发送给服务器端的数据流。
2.getOutputStream方法得到的是一个输出流,服务端的Socket对象上的getOutputStream方法得到的输出流其实就是发送给客户端的数据。
PrintWriter(OutputStream out) 根据现有的 OutputStream 创建不带自动行刷新的新 PrintWriter。
这里是客户端使用,这个socket是 客户端对象 所以socket.getOutputStream()的意思就是 发送给服务器数据
十一、为什么要用PrintWriter
PrintWriter
是java.io.OutputStreamWriter的子类,而 OutputStreamWriter 继承了 java.io.Writer 类,它是用于字符输出流的抽象类,提供了多个写入字符的方法。其中,PrintWriter
子类具有将数据写入到文本输出流的能力,可以通过重载的 print()
和 println()
方法使用。虽然其输出流是由字符组成,但它们用于文本文件并可能采用任何字符编码,而非像 Writer
类中那样是用于纯文本文件,并总是使用平台默认字符编码。
因此,在 PrintWriter
对象包装后,可以通过 println()
方法直接写入字符串,底层实现会将字符串转换为字符流,并通过 socket 输出流发送给服务端。这样一来,我们就可以方便地向 socket 输出流中写入字符串,而不需要手动转换为字符数组或字节数组了。
当我们使用println将数据输入到聊天框中,点击发送时,会自动将字符串转换为字符流,这样我们就不用说:我想输一句话,我还得先转化为能传的过去的字符流编码,太费劲
controller代码
package tpc; ---------------------------------------------------------------------------------------------------- //connect是button的连接事件 public void contect() { int port = 8888; try { //从用户输入中获取port port = Integer.parseInt(txt_port.getText()); } catch (Exception e) { e.printStackTrace(); //给弹窗赋值 Alert alter = new Alert(Alert.AlertType.INFORMATION, "port输入错误"); //显示弹窗 alter.show(); return; } try { //根据IP地址和port新建一个客户端socket,用于与服务端建立网络连接 socket = new Socket(txt_ip.getText(), port); //新建一个线程池 Executor server = Executors.newCachedThreadPool(r -> { //新建线程工厂 Thread t = Executors.defaultThreadFactory().newThread(r); //给每一个线程都设置为守护线程 t.setDaemon(true); return t; }); //开启客户端输入流监控 //启动一个新的线程执行ClientInputStreamThread类的实例对象,用于监听客户端向服务端发送的数据。 server.execute(new ClientInputStreamThread()); //发送昵称给服务端——>我们现在是客户端 //启动一个新的线程,将客户端昵称发送给服务端。 //在该线程的 run() 方法中,获取客户端 socket 的输出流, //并使用 PrintWriter 对象将昵称以字符串的形式发送给服务端。 server.execute(() -> { try { //获取socket输出流,并包装为PrintWriter //对象包装成PrintWriter对象后,就可以通过println()方法直接写入字符串. //PrintWriter底层实现会将字符串转换为字符流 PrintWriter pw = new PrintWriter(socket.getOutputStream()); //通过调用write方法将昵称写入到PrintWriter输出流中,从而实现向服务端发送昵称的功能。 pw.write (txt_nickname.getText()); //将缓冲区中的数据刷入到底层接口中,以确保数据能够及时被传输到服务端 pw.flush(); } catch (IOException e) { e.printStackTrace(); } }); } catch (IOException e) { //连接失败弹出的信息 Alert alter = new Alert(Alert.AlertType.INFORMATION, "ip输入错误或服务端无效"); e.printStackTrace(); } } ---------------------------------------------------------------------------------------------------- // 服务端Socket对象 private Socket socket; // 向聊天室大厅发送消息 // 是button send的绑定事件 public void send() { // 初始化一个PrintWriter对象 PrintWriter osw = null; try { //将输入的信息写入到PrintWriter对象中 osw = new PrintWriter(socket.getOutputStream()); //通过write()方法将消息发送到服务端的输出流中,完成数据的发送 osw.write(txt_message.getText()); osw.flush(); } catch (IOException e) { e.printStackTrace(); } } ---------------------------------------------------------------------------------------------------- // 下面的功能:监控 接受消息 public class ClientInputStreamThread implements Runnable{ @Override public void run() { { InputStream is = null; //获取与当前 Socket 对象关联的远程 IP 地址的方法,返回一个字符串类型的 IP 地址。 //获取的就是服务端的 IP 地址,表示客户端向该 IP 地址上的服务端发起连接。 String ip = socket.getInetAddress().getHostAddress(); try { //新建输入流对象,用于从该Socket中读取数据 is = socket.getInputStream(); //将输入流对象封装 //下边这个类是从字节流到字符流的桥梁:它读取字节并使用指定的字符集将其解码为字符。 InputStreamReader reader = new InputStreamReader(is); //新建buffer数组用来存放读取的输入流数据 char[] buffer = new char[1024]; int len = -1; while (true) { // 读取输入流数据到 buffer 中,并返回实际读取到的字符数,赋值给len if (-1 != (len = reader.read(buffer))) { //将读取到的字节数据buffer转换成字符串tmp,tmp的长度为len。 String tmp = new String(buffer, 0 , len); //将txt_context(收发到的消息)当前的文本内容和tmp字符串使用"\r\n"分隔并拼接。 txt_context.setText(txt_context.getText() + "\r\n" + tmp); //将txt_message(聊天室的输入框)的内容设置为空字符串,以便下一次输入。 txt_message.setText(""); } } } catch (IOException e) { e.printStackTrace(); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }
start代码
详解
这段代码是一个 JavaFX 应用程序的入口方法,用于启动图形用户界面(GUI)。
-
main
方法是程序的入口方法,该方法会调用launch()
方法启动 JavaFX 应用程序。在调用该方法前,需要先确保 JavaFX 环境已经被正确地初始化。
public static void main(String[] args) { launch(args); // 启动 JavaFX 应用程序 }
-
start
方法是 JavaFX 应用程序的启动方法。在start
方法中,通过FXMLLoader.load()
加载 FXML 文件,并将其转化为一个对象。FXML 文件可以使用 Scene Builder 工具进行可视化设计,它保存了用户界面的结构和样式信息,可以在 JavaFX 应用程序中以代码方式进行呈现。
Parent root = FXMLLoader.load(this.getClass().getResource("/MyChatRoom.fxml"));
-
创建一个新的 Scene(场景) 对象并将 FXML 中加载的对象作为根节点,然后指定场景的宽度和高度。
Scene scene = new Scene(root, 400, 400);
-
将场景设置为主窗口的场景,并设置主窗口的标题,并通过
show()
方法显示主窗口。
primaryStage.setScene(scene); // 将场景设置为主窗口的场景 primaryStage.setTitle("世界首富交流群"); // 设置主窗口标题 primaryStage.show(); // 显示主窗口
总体来说,这段代码的作用就是启动 JavaFX 应用程序,并将 FXML 文件中的内容加载到 GUI 窗口上,以呈现出用户界面。首先加载 FXML 文件,并以 Parent
对象的形式返回所加载的文件。随后,将 Parent
对象作为根节点创建一个新的 Scene
对象。最后,将该 Scene
对象设置为主舞台场景,并通过 show()
方法显示窗口。
package tpc; public class Start extends Application { public static void main(String[] args) { //启动 JavaFX 应用程序 launch(args); } @Override public void start(Stage primaryStage) throws Exception { //FXMLLoader.load(url) 获取此文件路径 加载FXML文件 并将其转化为一个对象 //我们把加载的fxml文件作为父节点,可以以此对他的孩子们(文件里的按钮、文本框等)进行操作 Parent root = FXMLLoader.load(this.getClass().getResource("/MyChatRoom.fxml")); //创建一个新的 Scene(场景) 对象并将 FXML 中加载的对象作为根节点,然后指定聊天框的宽度和高度 Scene scene = new Scene(root, 400, 400); // 将场景设置为主窗口的场景 primaryStage.setScene(scene); // 设置主窗口标题 primaryStage.setTitle("世界首富交流群"); // 显示主窗口 primaryStage.show(); } }
server代码
详解
这段代码实现了一个简单的服务器程序,它通过 ServerSocket
类监听 8888 端口,并等待客户端连接。当客户端连接后,使用 Socket
类与客户端建立网络连接,并添加客户端的IP地址和socket对应的键值对到map
集合中,并添加socket至list
列表中。同时也新建立一个监听线程MyInputStreamListener
来处理当前连接的客户端发送的消息。
-
Server
类中定义了一个Map<String, Socket>
类型的map
集合和一个List<Socket>
类型的list
列表,分别用于存储客户端的 IP 地址及其对应的 socket 对象,以及所有已连接的客户端 socket 对象。
public static Map<String, Socket> map = new HashMap<>(); // 用于存储客户端的 IP 地址及其对应的 socket 对象 public static List<Socket> list = new ArrayList<>(); // 用于存储所有已连接的客户端 socket 对象,用于把收到的消息依次发送过去
map与list各自的作用
使用
Map
是为了更方便地查找某个客户端的socket
对象,而使用List
是为了记录所有已连接的客户端socket
对象,以便向它们广播消息。在
Server
类中,使用Map
存储客户端 IP 地址和对应的socket
对象,这样,当需要向特定客户端发送消息时,只需要根据 IP 地址在map
中查找对应的socket
对象,然后通过该对象发送消息即可。而
List
存储了所有已连接客户端的socket
对象,这样,在向所有已连接客户端广播消息时,只需要遍历list
列表中的所有socket
对象,依次将消息发送给每一个socket
对象即可。因此,在这个应用场景下,使用
Map
和List
是非常方便和有效的,它们分别用于不同的功能,共同协作实现客户端的消息交互。
-
在
main
方法中初始化一个ServerSocket
对象,并使用accept()
方法阻塞等待客户端连接。当有客户端连接过来时,获取客户端的socket
对象,并将其 IP 地址和socket
对象添加到map
集合和list
列表中,同时为当前客户端新建一个监听线程MyInputStreamListener
。MyInputStreamListener
类实现了客户端输入流的监听,从而实现客户端间的数据交流。Socket client = server.accept();是服务端等待接收客户端的连接对吗?
是的,
server.accept()
方法是一个阻塞方法,用于等待客户端的连接请求。当有客户端连接到服务端时,该方法会返回一个客户端Socket
对象,该对象包含了客户端的 IP 地址和端口号等信息,通过该对象可以与该客户端进行数据交互。服务端调用accept()
方法后会一直阻塞等待客户端连接,直到有客户端连接到服务端时才会返回,如果服务端一直没有接收到客户端的连接,则会一直阻塞在这里。一旦有新的客户端连接到服务端时,
server.accept()
方法会返回一个新的Socket
对象,该对象会在后面的代码中被用来表示当前连接的客户端。在这个应用场景下,服务端会不断调用accept()
方法,以处理不同客户端的连接请求,并为每个连接的客户端启动新的线程来处理客户端的消息。
ServerSocket server = new ServerSocket(8888); // 初始化一个 ServerSocket 对象,并指定监听端口号 while (true) { // 不断循环等待客户端连接 Socket client = server.accept(); // 阻塞等待客户端连接 String ip = client.getInetAddress().getHostAddress(); map.put(ip, client); // 将客户端的 IP 地址和 socket 对象添加到 map 集合中 list.add(client); // 将客户端的 socket 对象添加到 list 列表中 Thread t1 = new MyInputStreamListener(client); // 新建一个监听线程 MyInputStreamListener t1.start(); // 启动监听线程 }
-
MyInputStreamListener
类监听客户端输入流的数据,将从当前客户端输入流中读取的数据转化为字符串并添加客户端 IP 地址,形成完整的客户端消息。然后遍历list
列表中的所有已连接客户端socket
对象,依次将其写出的输出流中,以实现向所有客户端发送消息的功能。
class MyInputStreamListener extends Thread { public MyInputStreamListener(Socket client) { super(); this.client = client; } Socket client; @Override public void run() { try { String ip = client.getInetAddress().getHostAddress(); while (true) { InputStreamReader in = new InputStreamReader(client.getInputStream()); char[] buffer = new char[1024]; int len = -1; if ((len = in.read(buffer)) != -1) { String message = ip + "@say@:" + new String(buffer, 0, len); // 设置客户端发送的消息内容 for (int i = 0; i < Server.list.size(); i++) { Socket v = Server.list.get(i); try { OutputStreamWriter out = new OutputStreamWriter(v.getOutputStream()); out.write(message); // 向所有客户端发送客户端的消息 out.flush(); } catch (IOException e) { e.printStackTrace(); } } } } } catch (Exception e) { e.printStackTrace(); } } }
-
最后一部分是
MyInputStreamListener
的构造方法和实现了监听客户端输入流的run()
方法。该线程会不断地循环监听当前客户端输入流的数据,并将其发送给所有已连接的客户端。当监听到数据后,构造一个包含当前客户端 IP 地址的消息,并将其依次通过所有已连接的客户端
构造方法的目的
MyInputStreamListener
类的实例对象创建是为了开启一个线程,用于监听一个客户端输入流并将该客户端输入的消息转发给所有已连接的客户端。每当一个新的客户端连接到服务端时,都会创建一个相应的 MyInputStreamListener
的实例对象,用于监听该客户端的消息,并将其转发给其他客户端。
这里需要注意的是,该实例对象被创建时,传入了一个与该客户端对应的 Socket
对象(即创建该实例对象的客户端的 Socket
对象),这个 Socket
对象被存储在了 MyInputStreamListener
类的实例对象中。在 MyInputStreamListener
类中,Socket
对象被用于将客户端输入的数据转发给其他已连接的客户端。因此,在不同的 MyInputStreamListener
实例对象中,Socket
对象会指向不同的客户端。
综上所述,创建 MyInputStreamListener
类的实例对象是为了为每个已连接的客户端开启一个监听线程,能够独立地对该客户端输入流进行监听,并将该客户端的消息转发给其他已连接的客户端,以实现客户端之间的即时通讯。
package tpc; public class Server { // 用于存储客户端的 IP 地址及其对应的 socket ,为了更方便地查找某个客户端的 `socket` 对象 public static Map<String, Socket> map = new HashMap<>(); // 用于存储所有已连接的客户端 socket 对象,以便向它们广播消息。 public static List<Socket> list = new ArrayList<>(); public static void main(String[] args) { try { // 初始化一个 ServerSocket 对象,并指定监听端口号 ServerSocket server = new ServerSocket(8888); // 不断循环等待客户端连接 while (true) { // 阻塞等待客户端连接(是服务端等待客户端连接) // 当有客户端连接到服务端时,该方法会返回一个客户端 `Socket` 对象 // 该对象包含了客户端的 IP 地址和端口号等信息 Socket client = server.accept(); //获取客户端client socket IP地址 String ip = client.getInetAddress().getHostAddress(); // 将客户端的 IP 地址和 socket 对象添加到 map 集合中 map.put(ip, client); // 将客户端的 socket 对象添加到 list 列表中 list.add(client); // 新建一个监听线程 MyInputStreamListener Thread t1 = new MyInputStreamListener(client); //启动线程 t1.start(); } } catch (IOException e) { e.printStackTrace(); } } } class MyInputStreamListener extends Thread { // 为每一个连接的客户端都建立一个监听线程 public MyInputStreamListener(Socket client) { super(); this.client = client; } Socket client; @Override public void run() { try { String ip = client.getInetAddress().getHostAddress(); while (true) { // 获取当前客户端的IP地址 InputStreamReader in = new InputStreamReader(client.getInputStream()); char[] buffer = new char[1024]; int len = -1; // 接收输入流 if ((len = in.read(buffer)) != -1) { // 设置谁谁谁说什么什么 String message = ip + "@say@:" + new String(buffer, 0, len); // 将信息发送给所有与server连接的客户端 for (int i = 0; i < Server.list.size(); i++) { Socket v = Server.list.get(i); try { //把消息写入输出流,依次发送 OutputStreamWriter out = new OutputStreamWriter(v.getOutputStream()); out.write(message); out.flush(); } catch (IOException e) { e.printStackTrace(); } } } } } catch (Exception e) { e.printStackTrace(); } } }
package tpc; public void contect() { int port = 8888; try { port = Integer.parseInt(txt_port.getText()); } catch (Exception e) { e.printStackTrace(); Alert alter = new Alert(Alert.AlertType.INFORMATION, "port输入错误"); alter.show(); return; } try { socket = new Socket(txt_ip.getText(), port); Executor server = Executors.newCachedThreadPool(r -> { Thread t = Executors.defaultThreadFactory().newThread(r); t.setDaemon(true); return t; }); server.execute(new ClientInputStreamThread()); server.execute(() -> { try { PrintWriter pw = new PrintWriter(socket.getOutputStream()); pw.write (txt_nickname.getText()); pw.flush(); } catch (IOException e) { e.printStackTrace(); } }); } catch (IOException e) { Alert alter = new Alert(Alert.AlertType.INFORMATION, "ip输入错误或服务端无效"); e.printStackTrace(); } } private Socket socket; public void send() { PrintWriter osw = null; try { osw = new PrintWriter(socket.getOutputStream()); osw.write(txt_message.getText()); osw.flush(); } catch (IOException e) { e.printStackTrace(); } } public class ClientInputStreamThread implements Runnable{ @Override public void run() { { InputStream is = null; String ip = socket.getInetAddress().getHostAddress(); try { is = socket.getInputStream(); InputStreamReader reader = new InputStreamReader(is); char[] buffer = new char[1024]; int len = -1; while (true) { if (-1 != (len = reader.read(buffer))) { String tmp = new String(buffer, 0 , len); txt_context.setText(txt_context.getText() + "\r\n" + tmp); txt_message.setText(""); } } } catch (IOException e) { e.printStackTrace(); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } } } package tpc; public class Server { public static Map<String, Socket> map = new HashMap<>(); public static List<Socket> list = new ArrayList<>(); public static void main(String[] args) { try { ServerSocket server = new ServerSocket(8888); while (true) { Socket client = server.accept(); String ip = client.getInetAddress().getHostAddress(); map.put(ip, client); list.add(client); Thread t1 = new MyInputStreamListener(client); t1.start(); } } catch (IOException e) { e.printStackTrace(); } } } class MyInputStreamListener extends Thread { public MyInputStreamListener(Socket client) { super(); this.client = client; } Socket client; @Override public void run() { try { String ip = client.getInetAddress().getHostAddress(); while (true) { InputStreamReader in = new InputStreamReader(client.getInputStream()); char[] buffer = new char[1024]; int len = -1; if ((len = in.read(buffer)) != -1) { String message = ip + "@say@:" + new String(buffer, 0, len); for (int i = 0; i < Server.list.size(); i++) { Socket v = Server.list.get(i); try { OutputStreamWriter out = new OutputStreamWriter(v.getOutputStream()); out.write(message); out.flush(); } catch (IOException e) { e.printStackTrace(); } } } } } catch (Exception e) { e.printStackTrace(); } } } package tpc; public class Start extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { Parent root = FXMLLoader.load(this.getClass().getResource("/MyChatRoom.fxml")); Scene scene = new Scene(root, 600, 400); primaryStage.setScene(scene); primaryStage.setTitle("世界首富交流群"); primaryStage.show(); } }
总体概述
客户端点击connect按钮后,会根据用户所填的IP地址以及端口号与服务器进行连接,并发送自己的昵称给服务器,这里用的是直接写入输出流。
如果客户端要发送信息给服务器,点击send按钮,是调用的send函数,将相关数据以一定长度为单位写入输出流发送给服务端。同时服务端接受数据,把数据再转发给所有与之相连的客户端,在发送消息的客户端视角,我们要清空刚才输入数据的聊天框,并在聊天记录框中展示,由服务器转发而来的自己接收了的聊天信息。
若是服务器发送过来信息,我们要及时接收,所以就设置了监听程序。
输出数据:getoutstream()+write()
接收数据:getinputstream()+read()
controller
这段代码看起来是一个Java的网络通信程序,其中包含了一个contect()函数、一个send()函数和一个实现Runnable接口的内部类ClientInputStreamThread。
在contect()函数中,程序会尝试从文本输入框中获取端口号,并且尝试通过Socket连接到目标主机。如果连接成功,程序会创建一个线程池,其中包含一个作为输入流的ClientInputStreamThread线程和一个用于发送消息的线程。
根据代码中的描述,这个输入流是客户端和服务器之间进行通信所使用的输入流。客户端程序通过Socket连接到服务器,并将消息发送到服务器,而服务器收到消息后将其通过输出流返回给 与之相连接的所有客户端 ,客户端程序再通过输入流读取并显示这些消息。因此,这个输入流是服务器发给客户端的。
在send()函数中,程序通过PrintWriter向Socket中写入消息。ClientInputStreamThread线程则会在运行时读取Socket中的输入流,并将读取到的消息显示在文本区域中。
server
这段代码是一个简单的Java Socket编程的服务端代码。在main函数中,程序会创建一个ServerSocket对象并监听端口号8888。当有客户端连接到该端口后,程序会创建一个新的线程来监听该客户端的Socket输入流,读取客户端发送的消息。一旦收到消息,程序会将收到的消息添加上客户端IP信息,并将其广播给所有的客户端。同时,程序使用了map和list两个容器来管理所有连接到服务器的客户端。其中,map用来存储每个客户端的IP和Socket对象,list则用来存储所有连接的客户端的Socket对象。
在这段代码中,map的作用比较有限。map对象被定义为一个静态Map类型的变量,用于存储连接到服务器的客户端的IP地址和对应的Socket对象。在程序运行过程中,当有新的客户端连接到服务器时,程序会将其对应的Socket对象和IP地址添加到map集合中。然而,在本程序中,并没有使用该map集合来进行具体的业务逻辑处理,仅仅是在客户端连接时进行添加元素,并没有之后再进行相关处理。
因此,map作为一个辅助对象,在这段代码中起到的作用并不是很大,只是对连接上来的客户端进行了IP地址和Socket对象的存储。而实际的业务逻辑则是在
MyInputStreamListener
线程中进行监听和消息广播。扩展
可以利用Map实现一对一的聊天,具体实现方式如下:
定义一个Map对象用于存储用户名和对应的Socket对象:
Map<String, Socket> userSocketMap = new ConcurrentHashMap<>();
在用户登录时将用户名和对应的Socket对象添加到Map中:
userSocketMap.put(userName, clientSocket);
在向指定用户发送消息时,根据用户名从Map中获取对应的Socket对象,并通过该Socket对象向客户端发送消息。
Socket receiverSocket = userSocketMap.get(receiverName); if (receiverSocket != null) { OutputStream os = receiverSocket.getOutputStream(); os.write(msg.getBytes()); }通过以上步骤,即可实现一对一的聊天功能。每个客户端连接到服务器后,会根据其用户名将对应的Socket对象存储在Map中。当某个客户端想要向另一个客户端发送消息时,可以根据目标用户名从Map中获取对应的Socket对象,并通过该Socket对象将消息发送给目标客户端。
需要注意的是,在实际的应用中,可能还需要对Map进行扩展和优化,例如添加对Socket对象的管理和释放,处理异常情况等。
start
这段代码使用JavaFX框架编写的一个启动类,主要实现了启动JavaFX应用程序的功能。在启动类中,首先创建了一个Stage对象(窗口),然后读取一个fxml文件,作为JavaFX应用程序的用户界面,并通过Scene对象将fxml文件和窗口关联起来。最后通过show()方法展示出窗口。具体实现步骤如下:
-
创建一个Stage对象,作为JavaFX应用程序的窗口
Stage primaryStage = new Stage();
-
从fxml文件中读取用户界面的布局信息,并生成一个Parent对象
Parent root = FXMLLoader.load(this.getClass().getResource("/MyChatRoom.fxml"));
这里通过getResource()
方法获取fxml文件,然后通过FXMLLoader.load()
方法读取fxml文件并生成一个Parent对象。
-
创建一个Scene对象,并将Parent对象关联到该Scene中
Scene scene = new Scene(root, 600, 400);
这里使用Scene
类来创建一个场景,并将读取到的Parent
对象与该场景进行关联。
-
将Scene对象设置给Stage对象,并显示窗口。
primaryStage.setScene(scene); primaryStage.setTitle("世界首富交流群"); primaryStage.show();
将创建好的Scene
对象设置给Stage
对象,并设置窗口标题,然后通过show()
方法展示出窗口。这样就完成了启动JavaFX应用程序的过程。
总之,这段代码实现了JavaFX应用程序的基本启动流程,这里的fxml文件用来创建用户界面,可以通过JavaFX的FXML标签来定义布局和组件。通过这种方式,可以方便地实现用户友好的界面。