Chat System

项目已经放到 GitHub 了,想看看源码欢迎点个 star (> _ <)

项目地址

xiao-luo17/Mini-Whisper-Peer: JavaFX + Socket 基于P2P的即时聊天小工具(客户端)

xiao-luo17/Mini-Whisper-Server: JavaFX + Socket 基于P2P的即时聊天小工具(提供登录的Server) (github.com)

项目需求

项目分为服务端和客户端,服务端只有一个,为每个客户端提供一个共享的用户目录和在线用户列表,每一个客户对等方都需要接入服务器来表明自己的身份,其他用户才能根据服务器返回的IP进行连接请求。(其实更纯粹的P2P也可以每一个客户都是客户也是服务器,每一个终端都缓存已知的数据,当本地找不到就去请求其他对等方的列表来更新IP地址列表的缓存)

  • 对于服务端:

    • 实现注册功能,对收到的来自客户端注册的数据进行存储。例如姓名、密码。
    • 为每一个接入的客户端提供所有注册用户的IP地址列表,以便客户端实现P2P通信。
    • 要能够处理多个客户端的注册请求,为每一个客户端提供在线用户列表,使客户端能自由选择自己想要通信的对象
    • 建立用户名到密码的映射和登录校验,注册后的用户要通过密码和IP登录
  • 对于客户端:

    • 实现注册功能和注册界面,提取用户的输入并封装成请求发送给服务端,服务端要存储这部分的数据
    • 实现登录功能和登录界面,发给服务端校验来验证登录
    • 由于一个客户端要能和多个对等方通信,客户端要跑一个监听线程,接收到一个请求后要重新创建监听去准备下一个对等方的连接
    • 在登录之后要发出请求,接收到服务端发来的响应信息,之后写到Javafx界面的列表上。列表包括所有注册用户和他们的在线情况,点击在线用户要能对这个用户发出聊天请求。对等方收到聊天请求后有弹窗提醒,点击确定后建立连接。

项目基础

基于 P2P 和 socket 编程的一个即时聊天小工具,基本上是使用 Java 编写的。

用到的一些东西

  • 界面采用了 Javafx ,用起来还是比较方便的,可以用css去美化比较方便。有关这个库的详情请参见文档。快速访问==>JavaFX China
  • 一些多线程的思路,还有 socket 的使用,再加一些进程间通信的方法。

设计思路

  1. 首先需要一个客户端和公共的服务端,同时客户端要具备服务端的监听功能,在P2P的条件下,它既是客户端也是服务器。
  2. 服务端要能够处理多个客户端的请求,为每一个客户端提供在线用户列表,处理多个客户端的注册和登录,已经登录的在线用户可以通过服务端子线程保持和服务端的通信
  3. 由于客户端和服务端直接发送的消息种类很多,可以创建一个请求类和一个相应类去封装这些请求,在每个类的头部加一个自己定义的请求响应代码,可以标识发送数据的类型,以便两端进行解析。
  4. 一个客户端需要同时与其他多个客户端进行聊天,可以使用一个UDP线程去处理这些数据报,只要定义好数据报的内容和格式,也可以用一个端口去处理多个客户端的聊天。

服务端

主进程和服务器监听线程

服务端要能够处理多个客户端的请求,为每一个客户端提供在线用户列表,处理多个客户端的注册和登录,已经登录的在线用户可以通过服务端子线程保持和服务端的通信

P2PServer

P2PServer是服务器的主进程,也是程序的入口。运行后会进入一个while创建ServerSocket和ServerThread类的循环,ServerSocket监听客户端发来的连接,如果监听到了一条连接,就新建一个ServerThread子线程,专门处理来自这条连接的请求。之后ServerSocket重新进行监听,等待下一个请求来时创建子线程处理。

程序结束退出的时候还有个保存注册用户的操作,用doShutDownWork方法调用Runtime去监听,退出的时候就保存用户的名字和密码到本地,开始运行的时候读出来用作登录校验

保存数据就是目录里有写的那个SaveData类,保存到data.txt里

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
public class P2PServer {
public static final int PORT = 8000;
public static final int MAX_QUEUE_LENGTH = 100;

public void start() {
try {
ServerSocket serverSocket = new ServerSocket(PORT, MAX_QUEUE_LENGTH);
System.out.println("*******服务器已经启动*******");
while (true) {
Socket socket = serverSocket.accept();
System.out.println("[系统消息] 已接收到客户:" + socket.getInetAddress());
ServerThread serverThread = new ServerThread(socket);

serverThread.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}

//进程结束进行数据存储
private void doShutDownWork() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
MapToTxT(registerPassword);
}));
}

public static void main(String[] args) {
P2PServer ms = new P2PServer();
ms.doShutDownWork();
//进行数据导入
registerPassword = TxTToMap();
ms.start();
}
}

ServerThread

ServerThread是服务端主进程创建的子线程,用来一对一地处理客户端的请求(因为客户端那边有刷新在线用户列表的事件,所以还得连着刷新)。继承Runnable接口重写run方法,run里面就循环跑请求监听的一些方法:接收、解析、响应。这里不知道什么是线程的可以先学习下。

线程start方法初始化线程类里的一些属性,进入准备阶段

因为要用Socket连接,所以这个类的构造函数要接收一个客户端的套接字作为参数

线程run方法循环监听请求和发送响应

  • receiveRequest()方法就负责接收消息,也可以和解析写在一起。
  • parseRequest()方法解析请求request对象,先是看看请求类型,然后对每一种类型的请求做不同的操作,然后把要返回的数据封装成response对象。
  • 请求结束了自然就是响应,sendResponse()方法发送response对象到客户端,response对象已经在上一个解析的方法里封装好了。
  • 线程run方法里的循环通过一个keepListening属性来控制,当parseRequest()方法解析到客户端的退出登录请求时调用stop()方法,同时将keepListening置为false,stop关闭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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
public class ServerThread implements Runnable {
private Socket socket;
private ObjectInputStream ois;
private ObjectOutputStream oos;
private Thread serverChild;
/**
* 这里是用户名到ip的映射,同时也是在线用户名册
*/
private static Hashtable<String, InetSocketAddress> registerMap = new Hashtable<>();
/**
* 这里是用户名到密码的映射,同时是所有用户名册
*/
public static Hashtable<String, String> registerPassword = new Hashtable<>();
private Request request;
private Response response;
private boolean keepListening = true;

public ServerThread(Socket socket) {
this.socket = socket;
}

public synchronized void start() {
if (serverChild == null) {
try {
ois = new ObjectInputStream(socket.getInputStream());
oos = new ObjectOutputStream(socket.getOutputStream());
serverChild = new Thread(this);
serverChild.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public synchronized void stop() {
if (serverChild != null) {
try {
serverChild.interrupt();
serverChild = null;
ois.close();
oos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void run() {
try {
while (keepListening) {
receiveRequest();
parseRequest();
sendResponse();
}
stop();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
stop();
System.err.println("[系统消息] 客户端登录异常... 客户端中断连接...");
}
}

private void receiveRequest() throws IOException, ClassNotFoundException {
request = (Request) ois.readObject();
}

private void parseRequest() {
if (request == null)
return;
response = null;
int requestType = request.getRequestType();
String registerName = request.getRegisterName();
if (requestType != 1 && !isRegister(registerName)) {
response = new Response(1, registerName + "你还未注册");
return;
}
switch (requestType) {//测试请求类型
case REGISTER_EXIT:
if (isRegister(registerName)) {
response = new Response(STRING_TYPE, "|" + registerName + "|" + "已被其他人使用,请使用其他名字注册");
break;
}
//这里是注册map表
registerPassword.put(registerName, request.getPassword());
response = new Response(STRING_TYPE, registerName + ",你已经注册成功");
System.out.println("[系统消息] |" + registerName + "| 注册成功...");
break;
case SIGN_IN:
if(registerMap.containsKey(registerName)){
response = new Response(STRING_TYPE, registerName + ",该账号已经在线");
System.out.println("[系统消息] |" + registerName + "| 该在线账号试图重复登录...");
break;
}
if (request.getPassword().equals(registerPassword.get(registerName))) {
registerMap.put(registerName, new InetSocketAddress(socket.getInetAddress(), request.getUDPPort()));
response = new Response(STRING_TYPE, registerName + ",你已经登录成功");
System.out.println("[系统消息] |" + registerName + "| 登录成功...");
} else {
response = new Response(STRING_TYPE, registerName + ",你的密码错误");
System.out.println("[系统消息] |" + registerName + "| 登录密码错误...");
break;
}
break;
case GET_REGISTER_MAP:
Vector<String> registerList = new Vector<>();
for (String key : registerMap.keySet()) {
registerList.addElement(key);
}
//这里是已经注册的用户
Vector<String> registerAll = new Vector<>();
for (String key : registerPassword.keySet()) {
registerAll.addElement(key);
}
response = new Response(VECTOR_TYPE, registerList, registerAll);
System.out.println("[系统消息] |" + registerName + "| 成功请求用户列表...");
break;
case GET_OTHER_ADDRESS:
String chatRegisterName = request.getChatRegisterName();
InetSocketAddress chatP2PEndAddress = registerMap.get(chatRegisterName);
response = new Response(IP_ADDRESS_TYPE, chatP2PEndAddress);
System.out.println("[系统消息] |" + registerName + "| 请求 |" + chatRegisterName + "| 的IP和UDP端口号");
break;
case EXIT:
registerMap.remove(registerName);
response = new Response(STRING_TYPE, registerName + ",你已经从服务器退出!");
keepListening = false;
System.out.println("[系统消息] |" + registerName + "| 从在线列表退出...");
}
}

/**
* 检查name是否已经被注册,true为已经被注册
*/
private boolean isRegister(String name) {
return name != null && ServerThread.registerPassword.get(name) != null;
}

/**
* 向output输出流发送返回消息
*/
private void sendResponse() throws IOException {
if (response != null) {
oos.writeObject(response);
}
}

}

请求类和响应类

由于客户端和服务端直接发送的消息种类很多,可以创建一个请求类和一个相应类去封装这些请求,在每个类的头部加一个自己定义的请求响应代码,可以标识发送数据的类型,以便两端进行解析。这样就不必再去管该发送什么,另一边又要怎么收了,直接一种request发过去就没事了。

请求类封装了每次请求方需要的数据类型和数据内容,响应类封装了响应方传输的数据类型
请求类和响应类都有一个 int 型的参数标识请求响应类型,方便两端解析报文内容

  • 请求类
Request 请求报文
requestType 请求类型
registerName 注册名
password 密码
port 用户端端口号
chatRegisterName 请求另一个在线用户的名字
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
public class Request implements Serializable {
private int requestType;
private String registerName;
private String password;
private int UDPPort;
private String chatRegisterName;

public Request(int requestType,String registerName){
this.requestType=requestType;
this.registerName=registerName;
}
public Request(int requestType,String registerName, String password, int UDPPort){
this(requestType,registerName);
this.UDPPort=UDPPort;
}
public Request(int requestType,String registerName, String chatRegisterName){
this(requestType,registerName);
this.chatRegisterName=chatRegisterName;
}

public int getRequestType() {
return requestType;
}

public String getRegisterName() {
return registerName;
}

public int getUDPPort() {
return UDPPort;
}

public String getChatRegisterName() {
return chatRegisterName;
}

public String getPassword() {
return password;
}
}
  • 响应类
Respons 响应报文
responseType 响应类型
message 响应String消息
allRegisterOnline 响应Vector消息
allRegisterDone 响应Vector消息
chatP2PEndAddress 响应IPAddress消息
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
public class Response implements Serializable {
private int responseType;
private String message;
private Vector<String> allRegisterOnline;
private Vector<String> allRegisterDone;
private InetSocketAddress chatP2PEndAddress;
public Response(int responseType){
this.responseType=responseType;
}
public Response(int responseType, String message) {
this.responseType = responseType;
this.message = message;
}

public Response(int responseType, Vector<String> allNameOfRegister, Vector<String> allRegisterDone) {
this(responseType);
this.allRegisterOnline = allNameOfRegister;
this.allRegisterDone = allRegisterDone;
}

public Response(int responseType, InetSocketAddress chatP2PEndAddress) {
this(responseType);
this.chatP2PEndAddress = chatP2PEndAddress;
}

public int getResponseType() {
return responseType;
}

public String getMessage() {
return message;
}

public Vector<String> getAllRegisterOnline() {
return allRegisterOnline;
}

public InetSocketAddress getChatP2PEndAddress() {
return chatP2PEndAddress;
}

public Vector<String> getAllRegisterDone() {
return allRegisterDone;
}
}

其他的简单类

DataSave 保存文件到 txt,数据比较简单就不接数据库了,json 还要包,感觉不如 txt(悲

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class DataSave {
//这里进行registerPassword数据的持久化存储,保存用户名到Password的映射
//可以用json或者xml格式存储,因为不想导依赖包就直接自定义格式化和解析方法
/**
* 将map数据格式化并保存到txt文件中
*/
public static void MapToTxT(Map<String,String> map){

String fpname="./data.txt";
String vcontent;
BufferedWriter bufferedWriter=null;
try {
//自动创建目录
File file = new File(fpname);
File file_dir = new File(file.getParent());
if(!file_dir.exists()){
file_dir.mkdirs();
}
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter osr = new OutputStreamWriter(fos,"UTF-8");//避免中文乱码
bufferedWriter = new BufferedWriter(osr);

for (Map.Entry<String,String> entry : map.entrySet()) {
// 用户名:密码|
vcontent="";
vcontent+= entry.getKey();
vcontent+=":";
vcontent+=entry.getValue();
bufferedWriter.write(vcontent);
bufferedWriter.newLine();
}
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bufferedWriter!=null){
bufferedWriter.close();
}
}catch (Exception e) {
e.printStackTrace();
}
}

}

/**
* 将txt数据解析并保存到静态的registerMap中
*/
public static Hashtable<String,String> TxTToMap(){
Hashtable<String,String> hashtable = new Hashtable<>();
//--------------读取文本-------------//
String fpath="./data.txt";
BufferedReader bufferedReader=null;
try {
File file = new File(fpath);

FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");//避免中文乱码
bufferedReader = new BufferedReader(isr);

String str_line="";
String[] temp;
//逐行读取文本
while ((str_line=bufferedReader.readLine())!=null){
temp = str_line.split(":");
hashtable.put(temp[0],temp[1]);
str_line="";
}
//读取文件并执行业务
//.... list
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bufferedReader!=null){
bufferedReader.close();
}
}catch (Exception e) {
e.printStackTrace();
}
}
return hashtable;
}

客户端

客户端界面使用了JavaFX制作,可以看看相关的文档啥的,因为JavaFX可以用css去美化,所以比较方便就用了。

客户端主要有两个部分

  • 一个是作为客户端去请求服务器存储的注册用户列表和在线用户列表
  • 另一个是作为对等方与另一个用户聊天

总体过程

  1. 启动程序,进入注册登录界面

    主进程是客户端界面JavaFX的进程,当主进程开启时,会开启与服务端进行通信的TCP连接线程,同时弹出注册登录界面,客户端通过这个TCP线程与服务器连接,完成登录和注册的相关逻辑。

  2. 用户列表

    登录完成后,主进程进入到用户列表界面,这时主进程开启另一个UDP监听线程。此时有两个线程在运行

    • 一个是TCP连接,从服务器获取在线用户列表和注册用户列表。
    • 一个是UDP连接,负责监听其他客户端发起的聊天请求。
  3. 打开聊天窗口

    选择聊天对象后会打开一个聊天窗口,每一个窗口都会分配一个对应的Controller类。这个Controller通过与UDP线程的交互,获取其他用户的聊天消息显示在对应的窗口上。

  4. 登出

    关闭聊天窗口后登出到最开始的注册登录界面,此时会关闭UDP监听线程。通过TCP连接线程告知服务器从在线列表中退出。

  5. 关闭程序

    关闭TCP线程,关闭JavaFX界面,程序退出。

主进程

主进程就是JavaFX的界面进程,负责新建窗口和窗口跳转。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
public class ViewAlter extends Application {

private Stage stage;

public static void main(String[] args) {
launch(args);
}

@Override
public void start(Stage primaryStage) throws Exception {
//开启用户通信进程
peerThread = new PeerThread();
//开启用户界面进程
stage = primaryStage;
stage.setOnCloseRequest(event -> System.exit(0));
stage.initStyle(StageStyle.TRANSPARENT);
stage.getIcons().add(new Image("/static/icon.jpg"));
stage.setTitle("Chat System");
gotoRegister();
stage.show();
}

/**
* 跳转到登录界面
*/
public void gotoRegister() {
try {
RegisterController register = (RegisterController) replaceSceneContent(REGISTER_PATH,822,500);
register.setTopImg();
register.setViewAlter(this);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 跳转到主界面
*/
public void gotoChatList(DatagramSocket socket) {
try {
ChatListController chatList = (ChatListController) replaceSceneContent(CHATLIST_PATH,1200,700);
chatList.setViewAlter(this);
chatList.setSocket(socket);
chatList.setTopImg();
//从列表程序获取到inetSocketAddress,转为inetAddress和port。开启用户聊天进程打开数据包和UPD端口
chatThread = new ChatThread(socket, this);
chatThread.start();
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 弹出聊天窗口
*/
public void createChatWindow(DatagramSocket socket, InetSocketAddress inetSocketAddress, String stageName) {
//开启用户聊天窗口,这个得在继承了application接口的类里进行,同时新将经过RegisterController中新建的ChatThread UDP监听线程加入
try {
Stage secondStage = new Stage();

FXMLLoader loader = new FXMLLoader();
InputStream inputStream = ViewAlter.class.getResourceAsStream(CHATWINDOW_PATH);
loader.setBuilderFactory(new JavaFXBuilderFactory());
loader.setLocation(ViewAlter.class.getResource(CHATWINDOW_PATH));

AnchorPane secondPane = loader.load(inputStream);
Scene secondScene = new Scene(secondPane, 822, 500);
secondStage.setScene(secondScene);
secondStage.show();
inputStream.close();
//加入队列
STAGE.put(stageName, secondStage);

ChatWindowController chatWindow = loader.getController();
chatWindow.setChatOtherName(stageName);
chatWindow.setSocket(socket);
chatWindow.setInetAddress(inetSocketAddress.getAddress());
chatWindow.setPort(inetSocketAddress.getPort());
chatWindow.setViewAlter(this);
//加入队列
CONTROLLER.put(stageName, chatWindow);

} catch (IOException e) {
e.printStackTrace();
}

}

/**
* 弹出询问窗口
*/
public void createAlertWindow(String alertMessage) {
//开启询问窗口,这个得在继承了application接口的类里进行
try {
Stage secondStage = new Stage();
secondStage.initStyle(StageStyle.TRANSPARENT);

FXMLLoader loader = new FXMLLoader();
InputStream inputStream = ViewAlter.class.getResourceAsStream(ALERTWINDOW_PATH);
loader.setBuilderFactory(new JavaFXBuilderFactory());
loader.setLocation(ViewAlter.class.getResource(ALERTWINDOW_PATH));

AnchorPane pane = loader.load(inputStream);
Scene secondScene = new Scene(pane, 300, 150, Color.TRANSPARENT);
secondStage.setScene(secondScene);
secondStage.show();

inputStream.close();

AlertWindowController alertWindow = loader.getController();
alertWindow.setAlertMessage(alertMessage);
alertWindow.setViewAlter(this);

} catch (IOException e) {
e.printStackTrace();
}

}

/**
* 替换场景
*/
private Initializable replaceSceneContent(String fxmlURL,int width,int height) throws Exception {

FXMLLoader loader = new FXMLLoader();
InputStream inputStream = ViewAlter.class.getResourceAsStream(fxmlURL);
loader.setBuilderFactory(new JavaFXBuilderFactory());
loader.setLocation(ViewAlter.class.getResource(fxmlURL));
try {
AnchorPane page = loader.load(inputStream);
page.setOnMousePressed(event -> {
xOffset = event.getSceneX();
yOffset = event.getSceneY();
});
page.setOnMouseDragged(event -> {
stage.setX(event.getScreenX() - xOffset);
stage.setY(event.getScreenY() - yOffset);
});
Scene scene = new Scene(page, width, height, Color.TRANSPARENT);
stage.setScene(scene);
stage.sizeToScene();
} catch (Exception e) {
e.printStackTrace();
} finally {
inputStream.close();
}
return loader.getController();
}

public void WindowCloseEvent(boolean isConnect) {
if(isConnect){
peerThread.keepCommunicating = false;
peerThread.notifyPeerThread();
peerThread.interrupt();
peerThread.close();
peerThread = null;
}
stage.close();
}

public Object getControllerByName(String name) {
return CONTROLLER.get(name);
}
}

TCP线程

TCP线程由主进程新建第一个窗口时开启,负责处理和服务器的通信。而JavaFX的界面才能获取输入,所以TCP线程和主进程通过管道通信来传输消息。

注册登录界面

Register包含了注册登录界面和注册登录通信逻辑,包含3个输入框和两个按钮

  • UserName 用户注册名
  • IPAddress 登录使用的IP地址
  • PassWord 注册用的密码

两个按钮分别为注册和登录

  • 注册登录逻辑
    • 如果这三个空有没填的,都不能注册或登录
    • 如果选择了注册
      • 如果是新的用户名,那么把这个用户名和对应的密码存到服务器
      • 如果这个用户名已经存在,则提示重新注册或输密码登录
    • 如果选择了登录
      • 如果是已经存在的用户名
        • 如果密码正确,登录然后跳转
        • 如果密码错误,提示登录失败密码错误
      • 如果是不存在的用户名,则提示先注册再登录

在TCP线程中,用户界面的Controller获取注册名、自己的IP、密码打包成一个request请求类

  • 注册按钮

    注册按钮会给这个请求类添加一个请求的代码,表示了这个请求是一个注册的请求,服务器会对这个消息执行注册相关的逻辑。

  • 登录按钮

    登录按钮同样是为中国请求类添加一个请求代码,表示这个请求是一个登录的请求,服务器进行密码校验和一些相关的逻辑。

当按钮的事件没有触发时,TCP线程处于等待状态,等待主进程获取到输入之后,通过管道通信将这个请求的内容发送到TCP线程。TCP线程接收到这些信息后,主进程唤醒TCP线程,TCP线程将这个请求发送到服务端,然后接收服务端的响应,循环后又进入等待直到下次唤醒。

用户列表界面

主要是从服务端获取所需的注册用户列表和在线用户列表

与上一个界面一样,主进程都是通过TCP线程的等待去通信,然后TCP线程标识请求代码之类的一些东西,把请求发给服务端,再接收响应传回主进程。

UDP线程

UDP线程由主进程完成登录后进入聊天列表的时候开启,负责监听和处理来自其他客户端的数据报。

当点击前面用户列表界面中的在线用户时,会弹出与这个用户聊天的窗口,通过窗口发送聊天请求,而UDP进程就负责接收和解析这些数据报。

UDP数据报Data部分

发送方名字 register register register
发送方包类型 连接【1】 断开【2】 聊天【3】
包内容 registerName/chat registerName/exit registerName/聊天内容

JavaFX的Controller

JavaFX程序中每一个fxml文件都绑定一个Controller类,属于主进程。可以给这个窗口绑定按钮事件之类的一些操作,所以一些与服务端通信和与客户端通信的打包就放在这些Controller里