17.3.4 加入多线程
前面Server
和Client
只是进行了简单的通信操作:服务器端接收到客户端连接之后,服务器端向客户端输出一个字符串,而客户端也只是读取服务器端的字符串后就退出了。实际应用中的客户端则可能需要和服务器端保持长时间通信,即
- 服务器端需要不断地读取客户端数据,并向客户端写入数据;
- 客户端也需要不断地读取服务器端数据,并向服务器端写入数据。
在使用传统BufferedReader
的readLine()
方法读取数据时,在该方法成功返回之前,线程被阻塞,程序无法继续执行。考虑到这个原因,
服务器端应该为每个Socket
单独启动一个线程,每个线程负责与一个客户端进行通信。
客户端读取服务器端数据的线程同样会被阻塞,所以系统应该单独启动一个线程,该线程专门负责读取服务器端数据
现在考虑实现一个命令行界面的C/S
聊天室应用,服务器端应该包含多个线程,每个Socket
对应一个线程,该线程负责读取Socket
对应输入流的数据(从客户端发送过来的数据),并将读到的数据向每个Socket
输出流发送一次(将一个客户端发送的数据“广播”给其他客户端),因此需要在服务器端使用List
来保存所有的Socket
。
程序示例
下面是服务器端的实现代码,程序为服务器端提供了两个类,一个是创建ServerSocket
监听的主类个是负责处理每通信的线程类。
服务器端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import java.net.*; import java.io.*; import java.util.*;
public class MyServer { public static List<Socket> socketList = Collections.synchronizedList(new ArrayList<>());
public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(30000); while (true) { Socket s = ss.accept(); socketList.add(s); new Thread(new ServerThread(s)).start(); } } }
|
上面程序实现了服务器端只负责接收客户端Socket
的连接请求,每当客户端Socket
连接到该ServerSocket
之后,程序将对应Socket
加入socketList
集合中保存,并为该Socket
启动一个线程,该线程负责处理该Socket
所有的通信任务,如程序中4行粗体字代码所示。
服务器端线程类
服务器端线程类的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import java.io.*; import java.net.*;
public class ServerThread implements Runnable { Socket s = null; BufferedReader br = null;
public ServerThread(Socket s) throws IOException { this.s = s; br = new BufferedReader(new InputStreamReader(s.getInputStream())); }
public void run() { try { String content = null; while ((content = readFromClient()) != null) { for (Socket s : MyServer.socketList) { PrintStream ps = new PrintStream(s.getOutputStream()); ps.println(content); } } } catch (IOException e) { e.printStackTrace(); } }
private String readFromClient() { try { return br.readLine(); } catch (IOException e) { MyServer.socketList.remove(s); } return null; } }
|
上面的服务器端线程类不断地读取客户端数据,程序使用readFromClient()
方法来读取客户端数据,如果读取数据过程中捕获到IOException
异常,则表明该Socket
对应的客户端Socket
出现了问题(到底什么问题不用深究,反正不正常),程序就将该Socket
从socketlist
集合中删除,如readFromClient()
方法中①号代码所示
当服务器端线程读到客户端数据之后,程序遍历socketList
集合,并将该数据向socketList
集合中的每个Socket
发送一次,也就是该服务器端线程把从Socket
中读到的数据向socketList
集合中的每个Socket
转发一次。
客户端
毎个客户端应该包含两个线程,
- 一个负责读取用户的键盘输入,并将用户输入的数据写入
Socket
对应的输出流中;
- 一个负责读取
Socket
对应输入流中的数据(从服务器端发送过来的数据),并将这些数据打印输出。
其中负责读取用户键盘输入的线程由MyClient
负责,也就是由程序的主线程负责。客户端主程序代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import java.io.*; import java.net.*;
public class MyClient { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1", 30000); new Thread(new ClientThread(s)).start(); PrintStream ps = new PrintStream(s.getOutputStream()); String line = null; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); while ((line = br.readLine()) != null) { ps.println(line); } } }
|
当该线程读到用户键盘输入的内容后,将用户键盘输入的内容写入该Socket
对应的输出流
除此之外,当主线程使用Socket
连接到服务器之后,启动了ClientThread
来处理该线程的Socket
通信,如程序中①号代码所示。ClientThread
线程负责读取Socket
输入流中的内容,并将这些内容在控制台打印出来.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import java.io.*; import java.net.*;
public class ClientThread implements Runnable { private Socket s; BufferedReader br = null;
public ClientThread(Socket s) throws IOException { this.s = s; br = new BufferedReader(new InputStreamReader(s.getInputStream())); }
public void run() { try { String content = null; while ((content = br.readLine()) != null) { System.out.println(content); } } catch (Exception e) { e.printStackTrace(); } } }
|
上面线程的功能也非常简单,它只是不断地获取Socket
输入流中的内容,当获取到Socket
输入流中的内容后,直接将这些内容打印在控制台
先运行上面程序中的MyServer
类,该类运行后只是作为服务器,看不到任何输出。再运行多个相当于启动多个聊天室客户端登录该服务器,然后可以在任何一个客户端通过键盘输入些内容后按回车键,即可在所有客户端(包括自己)的控制台上收到刚刚输入的内容,这就粗略地实现了一个**C/S
结构聊天室**的功能。