17.3.4 加入多线程

17.3.4 加入多线程

前面ServerClient只是进行了简单的通信操作:服务器端接收到客户端连接之后,服务器端向客户端输出一个字符串,而客户端也只是读取服务器端的字符串后就退出了。实际应用中的客户端则可能需要和服务器端保持长时间通信,即

  • 服务器端需要不断地读取客户端数据,并向客户端写入数据;
  • 客户端也需要不断地读取服务器端数据,并向服务器端写入数据。

在使用传统BufferedReaderreadLine()方法读取数据时,在该方法成功返回之前,线程被阻塞,程序无法继续执行。考虑到这个原因,
服务器端应该为每个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 {
// 定义保存所有Socket的ArrayList,并将其包装为线程安全的
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);
// 每当客户端连接后启动一条ServerThread线程为该客户端服务
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
Socket s = null;
// 该线程所处理的Socket所对应的输入流
BufferedReader br = null;

public ServerThread(Socket s) throws IOException {
this.s = s;
// 初始化该Socket对应的输入流
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}

public void run() {
try {
String content = null;
// 采用循环不断从Socket中读取客户端发送过来的数据
while ((content = readFromClient()) != null) {
// 遍历socketList中的每个Socket,
// 将读到的内容向每个Socket发送一次
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();
}
// 如果捕捉到异常,表明该Socket对应的客户端已经关闭
catch (IOException e) {
// 删除该Socket。
MyServer.socketList.remove(s); // ①
}
return null;
}
}

上面的服务器端线程类不断地读取客户端数据,程序使用readFromClient()方法来读取客户端数据,如果读取数据过程中捕获到IOException异常,则表明该Socket对应的客户端Socket出现了问题(到底什么问题不用深究,反正不正常),程序就将该Socketsocketlist集合中删除,如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);
// 客户端启动ClientThread线程不断读取来自服务器的数据
new Thread(new ClientThread(s)).start(); // ①
// 获取该Socket对应的输出流
PrintStream ps = new PrintStream(s.getOutputStream());
String line = null;
// 不断读取键盘输入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while ((line = br.readLine()) != null) {
// 将用户的键盘输入内容写入Socket对应的输出流
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 {
// 该线程负责处理的Socket
private Socket s;
// 该线程所处理的Socket所对应的输入流
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;
// 不断读取Socket输入流中的内容,并将这些内容打印输出
while ((content = br.readLine()) != null) {
System.out.println(content);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

上面线程的功能也非常简单,它只是不断地获取Socket输入流中的内容,当获取到Socket输入流中的内容后,直接将这些内容打印在控制台
先运行上面程序中的MyServer类,该类运行后只是作为服务器,看不到任何输出。再运行多个相当于启动多个聊天室客户端登录该服务器,然后可以在任何一个客户端通过键盘输入些内容后按回车键,即可在所有客户端(包括自己)的控制台上收到刚刚输入的内容,这就粗略地实现了一个**C/S结构聊天室**的功能。