在上一讲中我们讲到了服务器端与客户端的双向通信的例子,显然这种双向通信不是非常好,因为服务端与客户端建立连接之后,服务端一定要等待客户端的输入然后自己再返回一个输出,这在实际的开发过程显然是没有任何意义的,因为客户端要进行与服务端的双向通信,那么服务器端与客户端谁先发送消息,谁后发送消息,这写都是无法预料的,而且服务也不一定是先发送消息之后还要等待客户端回消息,有可能服务端发送消息之后还要继续发送消息,这些都是现实应用场景中很常见的一种情况。这种情况下必须而且只能通过线程来进行解决。
这一讲我们就来讲解一下如何利用线程来进行服务端与客户端之间的双向通信,如下图所示:
1) 服务端与客户端进行连接,一旦连接建立好之后,就可以通过输入流和输出流进行通信了。
本质上输入和输出是互不干扰的,也就是说处理输出的不管输入,处理输入的不管输出。
2) 在这个连接上服务端开启两个线程
Thread1:专门用来处理服务器端的读,也就是客户端的写
Thread2:专门用来处理服务器端的写,也就是客户端的读
3) 在这个连接上服务端也开启两个线程
Thread3: 专门用来处理客户端的写,也就是服务器端的读
Thread4: 专门用来处理客户端的读,也就是服务器端的写
既然是两个线程,则他们之间的处理就是独立的,也就是把输入和输出的处理分开了。
4) 服务端在连接之后就可以获得socket对象,通过socket就可以获得InputStream和OutputStream了,把这两个流分配给这两个线程即可。或者直接将socket对象传给这两个线程。
下面我们开始实现上面的过程,输入和输出以命令行的方式呈现出来。
服务器端
package com.ahuier.socket; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /* * 服务端 */ public class MainServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(4000); //这边定义为死循环,目的是监听是否有新的线程接入,如果有请求,就建立一个连接。 while (true) { // 等待线程的连接 Socket socket = serverSocket.accept(); // 启动读写线程 new ServerInputThread(socket).start(); new ServerOutputThread(socket).start(); } } }
服务器端的线程package com.ahuier.socket; import java.io.IOException; import java.io.InputStream; import java.net.Socket; /* * 服务器端的读线程 * 服务器端输入流 */ public class ServerInputThread extends Thread { // 读的流是来源于Socket对象,所以这边使用构造方式的方式来传递这个对象 /* * 使用这种方式就可以实现了socket从MainServer中获取传递到线程中。 * 然后通过socket的InputStream和OutputStream进行处理了 */ private Socket socket; public ServerInputThread(Socket socket) { this.socket = socket; } @Override public void run() { try { /* * 由于我们是通过命令行的方式来进行的,不想图形用户界面有一个关闭按钮来执行流的关闭,所以在这种情况下关闭流就不处理了 * 当然有一种情况是发送特定的字符来告诉执行者这个流将要关闭。这个程序中我们就不关闭流了,让其一直连接着 */ InputStream is = socket.getInputStream(); /* * 这边使用死循环,因为获取的流之后,不应该是读一次的,它是一直在读取数据的过程,客户端有多少数据进来,他就读多少数据 * 从这边也可以看出这种情况用while(true)来解决。 */ while (true) { // 获得到流之后就可以读客户端传过来的数据了 byte[] buffer = new byte[1024]; int length = is.read(buffer); //这边也是一个受阻塞的情景,如果客户端没有给他发数据,它也是在等待客户端给他发送 String str = new String(buffer, 0, length); System.out.println(str); } } catch (IOException e) { e.printStackTrace(); } } }
package com.ahuier.socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; /* * 服务器段写线程 * 服务器段输出流 */ public class ServerOutputThread extends Thread { private Socket socket; public ServerOutputThread(Socket socket) { this.socket = socket; } @Override public void run() { try { OutputStream os = socket.getOutputStream(); while(true){ // 通过命令行的方式来写,用system.in方式 // 从命令行方式写出来的信息我们要用bufferReader包装一下,一次只读一行,然后将字符串拿到后转换成字节信息返回给输出流,这样信息就发送出去了 BufferedReader reader = new BufferedReader(new InputStreamReader( System.in)); String line = reader.readLine(); os.write(line.getBytes()); } } catch (IOException e) { e.printStackTrace(); } } }
客户段package com.ahuier.socket; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; /* * 客户端 */ public class MainClient { public static void main(String[] args) throws Exception, IOException { Socket socket = new Socket("127.0.0.1", 4000); new ClientInputStreamThread(socket).start(); new ClientOutputStreamThread(socket).start(); } }
客户段的两个线程,客户端的两个线程的写法与服务端类似,可以仿造服务端写。package com.ahuier.socket; import java.io.IOException; import java.io.InputStream; import java.net.Socket; /* * 客户端端的读线程 * 客户端端输入流,对于服务端的输出流 */ public class ClientInputStreamThread extends Thread{ private Socket socket; public ClientInputStreamThread(Socket socket) { this.socket = socket; } @Override public void run() { try { InputStream is = socket.getInputStream(); while (true) { // 获得到流之后就可以读服务端传过来的数据了 byte[] buffer = new byte[1024]; int length = is.read(buffer); //这边也是一个受阻塞的情景,如果服务端没有给他发数据,它也是在等待服务端给他发送 String str = new String(buffer, 0, length); System.out.println(str); } } catch (IOException e) { e.printStackTrace(); } } }
package com.ahuier.socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; /* * 客户端端的写线程 * 客户端端输出流,对于服务端的输入流 */ public class ClientOutputStreamThread extends Thread{ private Socket socket; public ClientOutputStreamThread(Socket socket) { this.socket = socket; } @Override public void run() { try { OutputStream os = socket.getOutputStream(); while(true){ BufferedReader reader = new BufferedReader(new InputStreamReader( System.in)); String line = reader.readLine(); os.write(line.getBytes()); } } catch (IOException e) { e.printStackTrace(); } } }
【说明】:将以上两个文件进行编译,可以在命令行模式下进行编译,编译后,通过开启命令行模式,一个执行服务端MainServer,一个执行客户端MainClient,然后再这两个命令行模式下进行输入输出操作,则成功就进行互相通信,如下图所示:
上面两个命令一个作为服务端,一个作为客户端,他们之间进行双向的通信。
这个程序虽然是命令行模式,但是对接下来的多人聊天项目是非常有用的,具有一个里程碑的意义,好的作品是不断完善的结果。
但是这个程序还引出另外一个问题,它只针对服务端与客户端进行通信,如果再调出一个客户端,则这个客户端就能很好的与服务端进行双向通信,两个客户端之间更不能很好的进行通信,这都是需要解决的问题。
要解决这个问题,我们或许可以采取这种方式:有一个服务器连接多个客户端,服务器端有专门客户端输入的线程,它受到请求之后遍历所有的客户端,服务端就可以获取到所有客户端的socket,就可以管理这些socket[比如可以利用集合来管理],然后进行广播发送。如果客户端与客户端进行通信,则客户端通过服务器把已经登陆到服务器的信息发送给客户端的方式。以上是一个大概的解决方法。