一、实验目的
学会服务器支持多用户并发访问的程序设计技术。多用户服务器是指服务器能同时支持多个用户并发访问服务器所提供的服务资源,如聊天服务、文件传输等。TCPServer是单用户版本,每次只能和一个用户对话,原因是第一个线程进入while循环,一直等待发来的消息,只有退出循环后才能执行新的线程。只有前一个用户退出后,后面的用户才能完成服务器连接。
二、修改运行配置
我的idea是2021.3.2,需要修改成允许多实例运行
三、多用户服务器程序设计
存在的问题:单用户版本的TCPServer.java程序不能同时服务多用户对话
1、线程池解决多用户对话问题
服务器可能面临很多客户的并发连接,因此,主线程负责监听客户请求和接受连接请求,用一个线程专门负责和一个客户对话。当一个客户请求成功后,创建一个新线程来专门负责该客户。对于服务器,一般是使用线程池来管理和复用线程。线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
2、在服务程序中支持群组聊天技术
本实验采用简单的“在线方式”记录客户套接字,即采用集合来保存用户登录的套接字信息,用于跟踪客户连接。因为每一个客户端的IP地址+端口组合不一样,用户套接字socket作为key来标识一个在线用户是比较方便的选择(可以结合泛型,将集合可存储的类型限制为Socket类型)。
四、实验过程
1、TCPClient.java
和之前代码一样
package chapter05.client;
import java.io.*;
import java.net.Socket;
public class TCPClient {
private Socket socket; //定义套接字
//定义字符输入流和输出流
private PrintWriter pw;
private BufferedReader br;
public TCPClient(String ip, String port) throws IOException {
//主动向服务器发起连接,实现TCP的三次握手过程
//如果不成功,则抛出错误信息,其错误信息交由调用者处理
socket = new Socket(ip, Integer.parseInt(port));
//得到网络输出字节流地址,并封装成网络输出字符流
OutputStream socketOut = socket.getOutputStream();
pw = new PrintWriter( // 设置最后一个参数为true,表示自动flush数据
new OutputStreamWriter(//设置utf-8编码
socketOut, "utf-8"), true);
//得到网络输入字节流地址,并封装成网络输入字符流
InputStream socketIn = socket.getInputStream();
br = new BufferedReader(
new InputStreamReader(socketIn, "utf-8"));
}
public void send(String msg) {
//输出字符流,由Socket调用系统底层函数,经网卡发送字节流
pw.println(msg);
}
public String receive() {
String msg = null;
try {
//从网络输入字符流中读信息,每次只能接受一行信息
//如果不够一行(无行结束符),则该语句阻塞,
// 直到条件满足,程序才往下运行
msg = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return msg;
}
public void close() {
try {
if (socket != null) {
//关闭socket连接及相关的输入输出流,实现四次握手断开
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//本机模块内测试与运行,需先运行TCPServer
public static void main(String[] args) throws IOException {
TCPClient tcpClient = new TCPClient("127.0.0.1", "8008");
tcpClient.send("hello");//发送一串字符
//接收服务器返回的字符串并显示
System.out.println(tcpClient.receive());
}
}
2、TCPClientThreadFX.java
注意需要修改IP地址为:202.116.195.71
端口号为:8008
package chapter05.client;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TCPClientThreadFX extends Application {
private Button btnExit = new Button("退出");
private Button btnSend = new Button("发送");
private TextField tfSend = new TextField();
private TextArea taDisplay = new TextArea();
private TextField tfIP = new TextField("202.116.195.71");
private TextField tfPort = new TextField("8008");
private Button btnConnect = new Button("连接");
private TCPClient tcpClient;
private Thread readThread;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
BorderPane mainPane = new BorderPane();
HBox connHbox = new HBox();
connHbox.setAlignment(Pos.CENTER);
connHbox.setSpacing(10);
connHbox.getChildren().addAll(new Label("IP地址:"), tfIP, new Label("端口:"), tfPort, btnConnect);
mainPane.setTop(connHbox);
VBox vBox = new VBox();
vBox.setSpacing(10);
vBox.setPadding(new Insets(10, 20, 10, 20));
// 设置发送信息的文本框
// 自动换行
taDisplay.setWrapText(true);
// 只读
taDisplay.setEditable(false);
vBox.getChildren().addAll(new Label("信息显示区: "), taDisplay, new Label("信息输入区:"), tfSend);
VBox.setVgrow(taDisplay, Priority.ALWAYS);
mainPane.setCenter(vBox);
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.setPadding(new Insets(10, 20, 10, 20));
hBox.setAlignment(Pos.CENTER_RIGHT);
// 按钮事件绑定
btnConnect.setOnAction(event -> {
String ip = tfIP.getText().trim();
String port = tfPort.getText().trim();
try {
//tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
tcpClient = new TCPClient(ip, port);
//成功连接服务器,接收服务器发来的第一条欢迎信息
String firstMsg = tcpClient.receive();
taDisplay.appendText(firstMsg + "\n");
// 启用发送按钮
btnSend.setDisable(false);
// 停用连接按钮
btnConnect.setDisable(true);
// 启用接收信息进程
readThread = new Thread(() -> {
String msg = null;
// 新增线程是否中断条件 解决退出时出现异常问题
while ((msg = tcpClient.receive()) != null) {
String msgTemp = msg;
Platform.runLater(() -> {
taDisplay.appendText(msgTemp + "\n");
});
}
Platform.runLater(() -> {
taDisplay.appendText("对话已关闭!\n");
// 连接断开后重新开放连接按钮
btnSend.setDisable(true);
btnConnect.setDisable(false);
});
});
readThread.start();
} catch (Exception e) {
taDisplay.appendText("服务器连接失败!" + e.getMessage() + "\n");
}
});
btnExit.setOnAction(event -> {
exit();
});
btnSend.setOnAction(event -> {
String sendMsg = tfSend.getText();
tcpClient.send(sendMsg);//向服务器发送一串字符
taDisplay.appendText("客户端发送:" + sendMsg + "\n");
tfSend.clear();
// 发送bye后重新启用连接按钮,禁用发送按钮
if (sendMsg.equals("bye")) {
btnConnect.setDisable(false);
btnSend.setDisable(true);
}
});
// 未连接时禁用发送按钮
btnSend.setDisable(true);
hBox.getChildren().addAll(btnSend, btnExit);
mainPane.setBottom(hBox);
Scene scene = new Scene(mainPane, 700, 400);
// 回车响应功能
scene.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.ENTER) {
sendText();
}
}
});
// 响应窗体关闭
primaryStage.setOnCloseRequest(event -> {
exit();
});
primaryStage.setScene(scene);
primaryStage.show();
}
public void exit() {
if (tcpClient != null) {
tcpClient.send("bye");
tcpClient.close();
}
// 系统退出时,单独的读线程没有结束,因此会出现异常。
// 解决方案:在这里通知线程中断,在线程循环中增加条件检测当前线程是否被中断。
// p.s. 此处使用的thread.stop()为deprecated的函数,应使用interrupt,正确写法见chapter03/TCPClientThreadFX
readThread.stop();
System.exit(0);
}
public void sendText() {
String sendMsg = tfSend.getText();
tcpClient.send(sendMsg);//向服务器发送一串字符
taDisplay.appendText("客户端发送:" + sendMsg + "\n");
tfSend.clear();
// 发送bye后重新启用连接按钮,禁用发送按钮
if (sendMsg.equals("bye")) {
btnConnect.setDisable(false);
btnSend.setDisable(true);
}
}
}
3、TCPThreadServer.java
在TCPThreadServer类中定义内部类Handler implements Runnable
Handler内部类
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());
try {
BufferedReader br = getReader(socket);//定义字符串输入流
PrintWriter pw = getWriter(socket);//定义字符串输出流
//客户端正常连接成功,则发送服务器欢迎信息,然后等待客户发送信息
pw.println("From 服务器:欢迎使用本服务!");
String msg = null;
//此处程序阻塞,每次从输入流中读入一行字符串
while ((msg = br.readLine()) != null) {
//如果客户发送的消息为"bye",就结束通信
if (msg.trim().equalsIgnoreCase("bye")) {
//向输出流中输出一行字符串,远程客户端可以读取该字符串
pw.println("From 服务器:服务器已断开连接,结束服务!");
System.out.println("客户端离开");
break;//跳出循环读取
}
if (msg.trim().equalsIgnoreCase("来自教师服务器的连接")){
pw.println("1");
}
else if(msg.trim().equalsIgnoreCase("教师服务器再次发送信息")) {
pw.println("2");
}
//向输出流中回传字符串,远程客户端可以读取该字符串
pw.println("From 服务器:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socket != null) {
socket.close(); //关闭socket连接及相关的输入输出流
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
与【互联网程序设计】网络对话程序设计结合即完整代码
4、GroupServer.java
在GroupServer类中添加核心的群组发送方法sendToAllMembers,给所有在线客服转发信息
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);
}![请添加图片描述](https://img-blog.csdnimg.cn/d9799bd46a394248a7dd0c6da182f529.png)
}
5、运行结果
上课来不及截图,这个是样例的图片
五、总结
- 在连接老师服务器前,需要开启自己的服务器。我的客户端连接老师服务器后,老师服务器会向我的服务器发送消息,我的服务器会给予老师服务器反馈。
if (msg.trim().equalsIgnoreCase("来自教师服务器的连接")){
pw.println("1");
}
else if(msg.trim().equalsIgnoreCase("教师服务器再次发送信息")) {
pw.println("2");
}
- 为什么要使用多用户并发访问?
单用户的程序中,每一次只能与一个客户建立通信连接,主线程在while中一直运行,无法进行多用户访问。
以下代码为单用户程序:
while (true) {
Socket socket = null;
try {
//服务器监听并等待客户发起连接,有连接请求就生成一个套接字。
socket = serverSocket.accept();
//本地服务器控制台显示客户端连接的用户信息
System.out.println("New connection accepted: " + socket.getInetAddress());
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("客户端离开");
break; //结束循环
}
//向输出流中输出一行字符串,远程客户端可以读取该字符串
// 正则表达式实现“人工智能”(扩展练习)
msg = msg.replaceAll("[吗?]", "") + "!";
pw.println("From服务器:" + msg);
}
当使用多用户服务器程序设计后,主程序负责接收消息,开启副线程,交给副线程处理。