-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChatServer.java
More file actions
495 lines (466 loc) · 32.3 KB
/
ChatServer.java
File metadata and controls
495 lines (466 loc) · 32.3 KB
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
import java.io.*;
import java.net.*;
import java.util.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
public class ChatServer{
//list of users is a public static variable so that it can be accessed inside threads
public static List<ChatUser> userList = Collections.synchronizedList(new ArrayList<ChatUser>());
//actual active users int, since the way this is set up requires some nonsense
public static int userNum = 0;
//date formatting variable, useful for printouts
public static DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//list of rooms, stored in public static variable so that it can be accessed in threads
public static List<ChatRoom> rooms = Collections.synchronizedList(new ArrayList<ChatRoom>());
//method to parse number of commands so i don't have to
//actually while i was writing this it occurred to me that i probably could have used split to make parsing a lot of this easier
//and more readable for that matter
//but im not going to lie to you i'm really proud that all this string manipulation worked as well as it did first try
//also im still a little gunshy about running out of heap space since i ran into that error a while back. so. yknow.
public static int numArgs(String command){
return command.split(" ").length;
}
//new type of thread that handles timer management
private static class HeartbeatThread extends Thread{
public void run(){
//declare system message variable to be used if a timer fails
String sysmsg;
while(true){
//go through each user in the userList
for(int i = 0; i<userList.size(); i++){
//if the user is valid, run the timer update
if(userList.get(i).nickname().equals("")!=true){
userList.get(i).timerUpdate();
//check if user's pingvalue is greater than 30000
if(userList.get(i).pingValue()>30000){
//system logging
System.out.println(LocalDateTime.now().format(timeFormat) + " :: " + userList.get(i).nickname() + ": disconnected.");
//create the system message
sysmsg = "type:system,message:" + userList.get(i).nickname() + ": disconnected.,timestamp:" + LocalDateTime.now().format(timeFormat);
//for every single user that isn't the timed out user, if the user is valid, output a system message for the disconnect
for(int j = 0; j<userList.size(); j++){
try {
if(i!=j&&!userList.get(j).nickname().equals("")){
userList.get(j).outputToUser.writeInt(sysmsg.getBytes().length);
userList.get(j).outputToUser.write(sysmsg.getBytes());
}
//then close the timed out user's connection
userList.get(i).connectionSocket.close();
}catch (IOException e){}
}
//mark timed out user as null
userList.get(i).name("");
//also decrement the actual user number
userNum--;
}// end if statement
}
}//end for loop
//since the heartbeat thread will only ever be capable of blocking during the process of timing out a user, im also using it to check
// whether or not the userList needs to be cleaned
//if the userList has only invalid users, clear the whole thing
if(userNum<=0&&userList.size()!=0){
userList.clear();
}
}//end while loop
}//end run method
}//end thread class
//new type of thread that takes an int so that it knows which user to listen for
private static class SocketThread extends Thread {
int index;
public SocketThread(int i){
this.index = i;
} //end constructor
//method for sending a message selectively to every person in a room
public void messageSend(String room, int index, byte[] messageBytes){
//get the index of every user in the room
int[] roomUsers = rooms.get(rooms.indexOf(new ChatRoom(room))).users();
try {
//output messages to each user in the list of users in the room
for (int i : roomUsers) {
//check to make sure that you dont send the message back to the user
if(i!=index&&userList.get(i).nickname().equals("")!=true){
//send size for message framing, then send actual bytes
userList.get(i).outputToUser.writeInt(messageBytes.length);
userList.get(i).outputToUser.write(messageBytes);
}
}
} catch (EOFException f) {
return;
} catch (IOException e){
return;
} // end try catch block
rooms.get(rooms.indexOf(new ChatRoom(room))).historyAdd(new String(messageBytes));
String usersInRoom = "";
//get user indexes from current room
int[] roomUserIndex = rooms.get(rooms.indexOf(new ChatRoom(userList.get(this.index).room()))).users();
//add names of all users
for(int i : roomUserIndex){
if(i!=this.index){
usersInRoom += userList.get(i).nickname() + ", ";
}
}
if(usersInRoom.equals("")){
//if list is empty, add system logging special case
usersInRoom = "(none)";
} else {
//otherwise remove the last comma from the list
usersInRoom = usersInRoom.substring(0,usersInRoom.length()-2);
}
System.out.println("Delivered(Room="+room+"): " + usersInRoom);
}
public void run(){
//declare string to use for parsing later
String msg = "";
String type;
String date;
String payload = "";
String room;
int n = 0;
byte[] messageBytes = null;
//listen for messages from the given user
while(true){
try {
//read in size of message
n = userList.get(this.index).userInput.readInt();
//create array to store message
messageBytes = new byte[n];
//write rest of message to array, convert array to string
userList.get(this.index).userInput.read(messageBytes, 0, n);
msg = new String(messageBytes);
} catch (EOFException f) {
//set name to null and display disconnect message on socket closure
//system logging
System.out.println(LocalDateTime.now().format(timeFormat) + " :: " + userList.get(this.index).nickname() + ": disconnected.");
userList.get(this.index).name("");
//exit loop
break;
} catch (SocketException s){
//set name to null and display disconnect message on socket closure
//system logging
System.out.println(LocalDateTime.now().format(timeFormat) + " :: " + userList.get(this.index).nickname() + ": disconnected.");
userList.get(this.index).name("");
//exit loop if socket gets closed
break;
} catch (IOException e){
break;
} // end try catch block
//since we have now recieved a message from the user, we will reset the user's ping timer
userList.get(this.index).timerReset();
//get the type and the date since those are the ones that are always importantand always in the same spot
date = msg.substring(msg.lastIndexOf(",timestamp:")+11);
//the message always starts with "type:" so we just skip to index 5
type = msg.substring(5,msg.indexOf(','));
//if i wasnt specifically asked to use "a series of text fields" for the meta data i probably would not have
// put the type indicator there at all tbh
//but im already playing kinda fast and loose by just making the whole thing one string that i parse for 10 years
//so whatever
//in my defense figuring out how to reconstruct a json file from bytes when its probably just going to involve
// sending the json as a string and then constructing a new json when it arrives
// sounds really annoying and we were pretty explicitly told to handle this however
//depending on the type of message, do different stuff
switch (type) {
//if the message is text, get the payload
case "text":
//uses "lastindexof" for timestamp so that the phrase ",timestamp:" in a message won't beef the whole program
//also adds an extra line of white space so that the upcoming switch case doesn't brick itself on single argument commands
//that's not an ellegant solution but the alternative was throwing a ternary operator into the switch case statement
//which i think would put me on a list somewhere
payload = msg.substring(msg.indexOf(",text:")+6, msg.lastIndexOf(",timestamp:")) + " ";
//serverlogging
//this is the worst println command ive ever run
System.out.println("Recieved: IP:" + userList.get(this.index).connectionSocket.getInetAddress().toString() +", Port:" + String.valueOf(userList.get(this.index).connectionSocket.getPort()) + ", Client-Nickname:" + userList.get(this.index).nickname() + ", ClientID:" + msg.substring(msg.indexOf(",userID:")+8,msg.lastIndexOf(",timestamp:"))+", Room:" + userList.get(this.index).room() + ", Date/Time:" +date +", Msg-Size:" + String.valueOf(n));
//check if the message is a command
if(payload.charAt(0)=='/'){
//if it is a command, do different stuff depending on the command
switch (payload.substring(1, payload.indexOf(' '))){
case "join":
if(numArgs(payload)!=2){
type="error";
payload = "Error: Invalid arguments!";
break;
} else {
//break the room argument off on its own
String roomTarget = payload.substring(6,payload.indexOf(' ',6));
//check if the room argument is valid
if(roomTarget.length()>0&&roomTarget.length()<=20){
//if it is, check if the room already exists
if(rooms.contains(new ChatRoom(roomTarget))){
//if it does, move user to that room
//remove user from previous room
rooms.get(rooms.indexOf(new ChatRoom(userList.get(this.index).room()))).remove(this.index);
//add user to new room
rooms.get(rooms.indexOf(new ChatRoom(roomTarget))).add(this.index);
} else {
//if it doesn't, make the room and move the user to it
rooms.add(new ChatRoom(roomTarget));
rooms.get(rooms.indexOf(new ChatRoom(userList.get(this.index).room()))).remove(this.index);
//add user to new room
rooms.get(rooms.indexOf(new ChatRoom(roomTarget))).add(this.index);
}
//then send a system message to the user
String userMoveMsg = "type:system,message:room" + roomTarget + ",timestamp:" + LocalDateTime.now().format(timeFormat);
try {
userList.get(this.index).outputToUser.writeInt(userMoveMsg.getBytes().length);
userList.get(this.index).outputToUser.write(userMoveMsg.getBytes());
} catch (IOException e) { break;
}
//then send a system message to the people in the user's old room
String otherMoveMsg = "type:system,message:" + userList.get(this.index).nickname() + ": joined room " + roomTarget + ".,timestamp:" + LocalDateTime.now().format(timeFormat);
messageSend(userList.get(this.index).room(),this.index,otherMoveMsg.getBytes());
//then send a system message to the people in the user's new room
messageSend(roomTarget,this.index,otherMoveMsg.getBytes());
//then update the room on the server side user list
userList.get(this.index).newRoom(roomTarget);
//if i were building this from the ground up to my own specifications i would probably make this look cleaner
//as it stands the need to match both the specifications and make it work on what i've already build is kinda killing this
//thats fine though i guess
//anyway now you do the history
//get the history from the room
String historyString = rooms.get(rooms.indexOf(new ChatRoom(roomTarget))).historyGet() + ",timestamp:" + LocalDateTime.now().format(timeFormat);
try {
//frame and send the history
userList.get(this.index).outputToUser.writeInt(historyString.getBytes().length);
userList.get(this.index).outputToUser.write(historyString.getBytes());
} catch (IOException e) { break;}
//system logging
System.out.println("HistoryDelivered: Room:" + roomTarget + ", To:" + userList.get(this.index).nickname() + ", Count:" + String.valueOf(rooms.get(rooms.indexOf(new ChatRoom(roomTarget))).historySize()));
} else {
type="error";
payload = "Error: Invalid arguments!";
break;
} //end if statement
System.out.println(date + " :: " + userList.get(this.index).nickname() + ": joined room " + roomTarget + ".");
} //end else
break;
case "msg":
//check if arguments are legal
if(numArgs(payload)<3){ //<-- this line has a heart in it! yay
//if arguments are not legal, get mad at the user
type="error";
payload = "Error: Invalid arguments!";
break;
}
//get person user is trying to dm
String dmTarget = payload.substring(5,payload.indexOf(' ',5));
//check if user is real
if(userList.contains(new ChatUser(dmTarget))){
//replace message with pm metadata and payload
msg = "type:pm,from:" + userList.get(this.index).nickname() + ",text:[PM from " + userList.get(this.index).nickname() +"] " + payload.substring(payload.indexOf(dmTarget)+dmTarget.length()) + ",timestamp:" + date;
//get new message size
int pmSize = msg.getBytes().length;
try {
//send dm to target w/ message framing
userList.get(userList.indexOf(new ChatUser(dmTarget))).outputToUser.writeInt(pmSize);
userList.get(userList.indexOf(new ChatUser(dmTarget))).outputToUser.write(msg.getBytes(), 0, pmSize);
} catch (IOException e){ break; } // end try catch block
//system logging
System.out.println("PrivateDelivered: From:" +userList.get(this.index).nickname()+", To:" + dmTarget +", Date/Time:" + date +", Msg-Size:" + pmSize);
} else { //if user isnt real send back error ping
type = "error";
payload = "Error: User " + dmTarget + " does not exist!";
}
break;
case "leave":
//remove user from previous room
rooms.get(rooms.indexOf(new ChatRoom(userList.get(this.index).room()))).remove(this.index);
//add user to new room
rooms.get(rooms.indexOf(new ChatRoom("lobby"))).add(this.index);
//then send a system message to the user
String userMoveMsg = "type:system,message:roomlobby,timestamp:" + LocalDateTime.now().format(timeFormat);
try {
userList.get(this.index).outputToUser.writeInt(userMoveMsg.getBytes().length);
userList.get(this.index).outputToUser.write(userMoveMsg.getBytes());
} catch (IOException e) { break; }
//then send a system message to the people in the user's old room
String otherMoveMsg = "type:system,message:" + userList.get(this.index).nickname() + " joined room lobby.,timestamp:" + LocalDateTime.now().format(timeFormat);
messageSend(userList.get(this.index).room(),this.index,otherMoveMsg.getBytes());
//then send a system message to the people in the user's new room
messageSend("lobby",this.index,otherMoveMsg.getBytes());
//then update the room on the server side user list
userList.get(this.index).newRoom("lobby");
System.out.println("leave");
break;
case "rooms":
//declare list of all rooms, start with meta data
String roomList = "type:system,message:";
//add the name of every room to the message
for(int i = 0; i<rooms.size(); i++){
roomList += rooms.get(i).name() + ", ";
}
//remove the last comma from the list
roomList = roomList.substring(0,roomList.length()-2);
//add final meta data
roomList += ",timestamp:" + LocalDateTime.now().format(timeFormat);
//frame and send message
try {
userList.get(this.index).outputToUser.writeInt(roomList.getBytes().length);
userList.get(this.index).outputToUser.write(roomList.getBytes());
} catch (IOException e) { break;
}
System.out.println("rooms");
break;
case "who":
//make string with metadata
String roomUsers = "type:system,message:";
//get user indexes from current room
int[] roomUserIndex = rooms.get(rooms.indexOf(new ChatRoom(userList.get(this.index).room()))).users();
//add names of all users
for(int i : roomUserIndex){
roomUsers += userList.get(i).nickname() + ", ";
}
//remove the last comma from the list
roomUsers = roomUsers.substring(0,roomUsers.length()-2);
//add final meta data
roomUsers += ",timestamp:" + LocalDateTime.now().format(timeFormat);
//frame and send message
try {
userList.get(this.index).outputToUser.writeInt(roomUsers.getBytes().length);
userList.get(this.index).outputToUser.write(roomUsers.getBytes());
} catch (IOException e) { break;
}
System.out.println("who");
break;
case "nick":
if(numArgs(payload)<2){
type="error";
payload = "Error: Invalid arguments!";
break;
}
//in the case of the nick command, check if the given nickname is already in the list
if(userList.contains(new ChatUser(payload.substring(payload.indexOf(' '))))){
//return an error message if it is
type = "error";
payload = "Error: Username already registered";
} else {
//if it isnt, update the nickname
System.out.println(date + " :: " + userList.get(this.index).nickname() + ": changed nickname to " + payload.substring(payload.indexOf(' ')+1,payload.length()-1) + ".");
userList.get(this.index).name(payload.substring(payload.indexOf(' ')+1,payload.length()-1));
msg = "type:system,message:nick" + userList.get(this.index).nickname() + ",timestamp:" + LocalDateTime.now().format(timeFormat);
try {
userList.get(this.index).outputToUser.writeInt(msg.getBytes().length);
userList.get(this.index).outputToUser.write(msg.getBytes());
} catch (IOException e){
break;
}
}
break;
default:
payload = "Error: Command not recognized.";
type = "error";
break;
} //end command switch case statement
} //end command handling if-statement
//else statement for if its just, like, a normal text message
else {
//get user's room from message
room = msg.substring(msg.indexOf(",room:")+6,msg.lastIndexOf(",nickname:"));
//send message to every user in the room who isnt you
messageSend(room, this.index, messageBytes);
}
break;
case "ping":
break;
case "disconnect":
//on disconnect message, send system message to all users in the room
//system logging
System.out.println(date + " :: " + userList.get(this.index).nickname() + ": disconnected.");
//construct exit message
String exitMessage = "type:system,message:" + userList.get(this.index).nickname() + " disconnected.,timestamp:"+ LocalDateTime.now().format(timeFormat);
//send the message
messageSend(userList.get(this.index).room(), n, exitMessage.getBytes());
//also remove the user from their room
rooms.get(rooms.indexOf(new ChatRoom(userList.get(this.index).room()))).remove(this.index);
//also close the socket and mark userlist slot for reuse
try {
userList.get(this.index).connectionSocket.close();
} catch (IOException e){}
userList.get(index).name("");
//also decrement the actual user number
userNum--;
break;
case "register":
//if the type is a register ping, check to see if the new nickname is already in the list
if(userList.contains(new ChatUser(msg.substring(msg.indexOf(",nickname:")+10, msg.lastIndexOf(",userID:"))))){
//if it is, return an error message
userNum--;
type = "error";
payload = "Error: Username already registered";
userList.get(this.index).nickname = "";
} else {
//otherwise, set the new nickname
userList.get(this.index).name(msg.substring(msg.indexOf(",nickname:")+10, msg.lastIndexOf(",userID:")));
System.out.println(date + " :: " + userList.get(this.index).nickname() + ": connected. (ClientID=" + msg.substring(msg.indexOf(",userID:")+8, msg.lastIndexOf(",timestamp:"))+")");
//then add user to lobby
rooms.get(0).add(this.index);
//then send ok message
msg = "type:ok,message:registered,room:lobby,timestamp:" + date;
//message framing, yada yada, this does the same thing all the other try-catch blocks do, you get it by now
try {
userList.get(this.index).outputToUser.writeInt(msg.getBytes().length);
userList.get(this.index).outputToUser.write(msg.getBytes());
} catch (IOException e){
}
}
break;
default:
break;
} //end message type switch-case statement
//if any of the commands or pings returned an error, return an error message to the user
if(type.equals("error")){
System.out.println(payload);
msg = "type:error,message:" + payload + ",timestamp:" + LocalDateTime.now().format(timeFormat);
try {
userList.get(this.index).outputToUser.writeInt(msg.getBytes().length);
userList.get(this.index).outputToUser.write(msg.getBytes());
} catch (IOException e){}
//set the thread to end if the error nulled the user
if(userList.get(this.index).nickname().equals("")){
type = "disconnect";
}
}
//end while loop if type is disconnect, and in so doing end the thread
if(type.equals("disconnect")){
break;
}
}//end while loop
}//end run method
}//end private subclass
@SuppressWarnings({ "resource" })
public static void main(String argv[]) throws Exception {
//get port from arguments
int port;
if(Integer.parseInt(argv[0])<=11000&&Integer.parseInt(argv[0])>=10000){
port = Integer.parseInt(argv[0]);
} else {
System.out.println("ERR - arg 1");
return;
}
//set up welcome socket
ServerSocket welcome = new ServerSocket(port);
System.out.println("ChatServer started with server IP: " + welcome.getInetAddress().toString() + ", port: " + String.valueOf(port) + ", Date/Time: " + LocalDateTime.now().format(timeFormat));
//set up thread to maintain pings and kick dc'd users
HeartbeatThread pings = new HeartbeatThread();
pings.start();
//create lobby room
rooms.add(new ChatRoom("lobby"));
//welcome new connections forever
while(true){
Socket newConnectionOne = welcome.accept();
SocketThread newUser;
//check if existing ChatUser object can be repurposed
if(userList.contains(new ChatUser(""))){
//if so, make a new thread and repurpose the old userList spot
userNum++;
newUser = new SocketThread(userList.indexOf(new ChatUser("")));
userList.get(userList.indexOf(new ChatUser(""))).reUseUser(newConnectionOne, String.valueOf(userList.size()), "lobby");
} else {
//if not, add a new user and then start a brand new thread
userNum++;
userList.add(new ChatUser(newConnectionOne, String.valueOf(userList.size()), "lobby"));
newUser = new SocketThread(userList.size()-1);
}
//add new user to list and start new thread
newUser.start();
} // end while loop
} //end main
}// end class