很快我将完成一个单客户端聊天系统作为一个娱乐个人项目,我想知道......将这样的东西转换为具有多个客户端是否简单,或者是否需要大量重写?
无论哪种情况,你会怎么做?请描述一下! :D 谢谢!
另外,如何通过套接字传输文件?
如果您正在考虑编写 IM 系统,那么我建议您使用更高级别的 协议和 API,例如 Jabber/XMPP 。 这是通过套接字传输文件的示例。
将单个客户端转换为多个客户端可以很简单。这取决于您如何撰写申请。 由于您正在重写一个以前已经编写过多次的应用程序,因此我认为您并不害怕重写代码,并且您将其作为一种学习练习。
最简单的改变是,现在你accept()一个客户端,你应该使用一个循环来接受多个客户端。 (并将该客户端的处理传递给另一个线程)。
要通过套接字传输文件,您需要能够像现在一样连接到服务器,读取文件并将内容发送到服务器。您无需将文本聊天消息传递给接收者,而是传递包含文件内容的文件消息。
Group chat sever/clients communication
public class ChatClient {
// Instance level fields.
private Socket client;
// I/O streams for communicating with client.
private ObjectInputStream in;
private ObjectOutputStream out;
// Using a thread-safe queue to handle multiple threads adding to the same
// queue (potentially) and a single thread de-queueing and sending messages
// across the network/internet.
private BlockingQueue<Message> outgoingMessages = new LinkedBlockingDeque<>();
// Reads messages from this specific client.
private ReadThread readThread;
// Writes messages to this specific client.
private WriteThread writeThread;
// Details about the current connection's client.
public int clientNum;
public String handle = "";
public String groupName = "";
public ChatClient(Socket client, int clientNum) {
this.client = client;
this.clientNum = clientNum;
// Start read loop thread.
readThread = new ReadThread();
readThread.start();
// Add client to lobby.
Groups.join("lobby", ChatClient.this);
// Queue group list to be sent to client.
send(new GroupsListed());
}
/**
* Sending a message involves adding it to a queue of messages to be
* sent. The WriteThread will send ACTUALLY send the message on the connection
* to the client when it gets a turn to run.
*
* Queueing messages prevents wierd things happening if multiple clients happen
* to be wanting to send to the SAME client at the SAME time and communication is
* not completed before the other starts sending. This can lead to garbled data
* being sent, due to race conditions.
*
* Additionally, if there WERE a GUI, none of the writing on the output stream
* would happen on the UI thread. All that would be done on the UI thread is adding
* a message to a queue object.
* @param message The message to be sent to this chat client.
*/
public void send(Message message) {
try {
outgoingMessages.put(message);
} catch (InterruptedException e) {
System.out.println(clientNum + ": Read.Exception: " + e.getMessage());
e.printStackTrace();
}
}
/**
* This thread is responsible for setting up the I/O streams, then goes into
* a read loop in which messages are read from the client (a blocking operation),
* then processed. When shutting down the read thread, the write thread is
* interrupted to stop it as well.
*/
private class ReadThread extends Thread {
@Override
public void run() {
try {
System.out.println(clientNum + ": Read thread started.");
// Obtain I/O streams. Gotcha for object streams is to make sure
// that both sides do not set up the input stream first. One must
// set up the output stream and flush it, else the other side will
// wait for its input stream to be initialised (which it never will).
// Remember this side's output is other side's input.
out = new ObjectOutputStream(client.getOutputStream());
out.flush();
in = new ObjectInputStream(client.getInputStream());
System.out.println(clientNum + ": Obtained I/O streams.");
// Start write loop thread. Start write thread here to ensure
// that the I/O streams have been initialised correctly BEFORE
// starting to read and write messages.
writeThread = new WriteThread();
writeThread.start();
// Read messages from client.
System.out.println(clientNum + ": Started Read Loop...");
Message msg;
do {
// Read next message (blocking operation).
msg = (Message) in.readObject();
System.out.println(clientNum + " --> " + msg);
// Process the message.
msg.apply(ChatClient.this);
} while (msg.getClass() != Quit.class);
// Close the connection.
client.close();
} catch (Exception e) {
System.out.println(clientNum + ": Read.Exception: " + e.getMessage());
e.printStackTrace();
} finally {
System.out.println(clientNum + ": Leaving groups...");
Groups.leave(ChatClient.this);
System.out.println(clientNum + ": Stopping Write thread...");
writeThread.interrupt();
System.out.println(clientNum + ": Read thread finished.");
}
}
}
/**
* This thread is responsible for dequeueing messages (blocking operation) and
* then sending them to the client.
*/
private class WriteThread extends Thread {
@Override
public void run() {
System.out.println(clientNum + ": Started Write Loop thread...");
// Remember this thread.
writeThread = this;
try {
// Check outgoing messages and send.
while (!isInterrupted()) {
Message msg = outgoingMessages.take();
out.writeObject(msg);
out.flush();
System.out.println(msg + " --> " + clientNum);
}
} catch (Exception e) {
System.out.println(clientNum + ": Write.Exception = " + e.getMessage());
e.printStackTrace();
} finally {
writeThread = null;
System.out.println(clientNum + ": Write thread finished.");
}
}
}
public class ChatServer {
public static void main(String[] args) throws Exception {
new ChatServer();
}
// The server socket that listens to port 5050 for connection requests.
private ServerSocket server;
private int clientNum = 0;
public ChatServer() throws Exception {
// Start new server socket on port 5050.
server = new ServerSocket(5050);
System.out.printf("Chat server started on: %s:5050\n",
InetAddress.getLocalHost().getHostAddress());
// Create the initial group to which all clients belong.
Groups.addGroup("lobby");
while(true) {
// Accept connection requests.
Socket client = server.accept();
System.out.printf("Connection request received: %s\n", client.getInetAddress().getHostAddress());
// Increment number of clients encountered.
clientNum++;
// Create new client connection object to manage.
ChatClient chatClient = new ChatClient(client, clientNum);
}
}
}
/**
* This class is responsible for managing chat groups and sending messages to
* clients in specific groups.
*/
public class Groups {
// Lock to prevent multiple threads manipulating the groups data
// structure while busy with an operation.
private static final ReentrantLock lock = new ReentrantLock();
// [Group Name] -> {clients}
public static final Map<String, Set<ChatClient>> groups = new HashMap<>();
/**
* Join a new group. Before joining a new group, the client will leave
* any groups it is currently in. All other clients in the group are
* notified of the client joining.
* @param groupName The new group's name.
* @param client The client joining.
*/
public static void join(String groupName, ChatClient client) {
// If already in a group, leave it.
leave(client);
// If no such group, create it.
if(!groups.containsKey(groupName))
addGroup(groupName);
// Now join the new group.
lock.lock();
// Add client to group.
groups.get(groupName).add(client);
client.groupName = groupName;
// Tell all clients that client joined group.
groups.get(groupName)
.forEach(chatClient -> chatClient.send(new Joined(groupName, client.handle)));
lock.unlock();
}
/**
* The client leaves all groups currently a member of. All other clients
* are sent a notification of this fact.
* @param client The client leaving.
*/
public static void leave(ChatClient client) {
lock.lock();
// Get groups to which client belongs.
List<String> groupsIn = groups.entrySet()
.stream()
.filter(entry -> entry.getValue().contains(client))
.map(entry -> entry.getKey())
.collect(Collectors.toList());
// Remove client from these groups and notify other clients of this.
groupsIn.forEach(groupName -> {
// Get group.
Set<ChatClient> group = groups.get(groupName);
// Remove client from group.
group.remove(client);
client.groupName = "";
// Send message to other clients in group.
Left msg = new Left(groupName, client.handle);
group.forEach(chatClient -> chatClient.send(msg));
});
lock.unlock();
}
/**
* A new (empty) group is added.
* @param groupName The new group's name.
*/
public static void addGroup(String groupName) {
lock.lock();
groups.put(groupName, new HashSet<>());
lock.unlock();
}
/**
* A message is sent to all clients in the named group.
* @param groupName The group's name.
* @param message The message to be sent.
*/
public static void send(String groupName, Message message) {
lock.lock();
// Is there a group with the given name? If not, exit.
if(!groups.containsKey(groupName)) return;
// Get clients in group.
Set<ChatClient> clients = groups.get(groupName);
// Send message to each client.
for(ChatClient client : clients)
client.send(message);
lock.unlock();
}
/**
* Send a message to ALL clients, regardless of the group
* they're in.
* @param message The message being sent.
*/
public static void sendAll(Message message) {
lock.lock();
// groups.values().stream() returns a collection of SETS of
// chat clients, i.e. a stream of (sets of chat clients).
// The flatmap method flattens this into a stream of chat clients.
groups.values()
.stream()
.flatMap(Collection::stream)
.distinct()
.forEach(chatClient -> {
chatClient.send(message);
System.out.println("sendAll: " + chatClient.handle + ", " + message);
});
lock.unlock();
}
}
/**
* The message received when a client wishes to create a new
* chat group.
*/
public class AddGroup extends Message<ChatClient> {
private static final long serialVersionUID = 7L;
// The name of the group to create.
public String groupName;
public AddGroup(String groupName) {
this.groupName = groupName;
}
@Override
public String toString() {
return String.format("AddGroup('%s')", groupName);
}
@Override
public void apply(ChatClient chatClient) {
// Add new chat group.
Groups.addGroup(groupName);
// Return the list of group names to all clients.
Groups.sendAll(new GroupsListed());
}
}
/**
* The message received when a client wishes to join a different
* chat group.
*/
public class Join extends Message<ChatClient> {
private static final long serialVersionUID = 1L;
public String groupName;
public Join(String groupName) {
this.groupName = groupName;
}
@Override
public String toString() {
return String.format("Join('%s')", groupName);
}
@Override
public void apply(ChatClient chatClient) {
Groups.join(groupName, chatClient);
}
}
/**
* The message received when a client wishes to leave the
* current chat group that they are in.
*/
public class Leave extends Message<ChatClient> {
private static final long serialVersionUID = 2L;
@Override
public String toString() {
return "Leave()";
}
@Override
public void apply(ChatClient chatClient) {
// Client leaves the group they are currently in.
Groups.leave(chatClient);
}
}
**
* The message received from a client when they wish to send a text
* message to all the clients in the same group as them.
*/
public class SendChatMessage extends Message<ChatClient> {
private static final long serialVersionUID = 5L;
// The text message to be sent to all clients in the same group.
public String chatMessage;
public SendChatMessage(String chatMessage) {
this.chatMessage = chatMessage;
}
@Override
public String toString() {
return String.format("SendChatMessage('%s')", chatMessage);
}
@Override
public void apply(ChatClient chatClient) {
// Get group name of client.
String groupName = chatClient.groupName;
// If not in a group, don't proceed.
if(groupName.length() == 0) return;
// Send message to all clients in same group as client.
Groups.send(groupName,
new ChatMessageReceived(groupName, chatClient.handle, chatMessage));
}
}
/**
* The message received from a client, typically once logged on initially,
* to set the handle that will be used when communicating.
*/
public class SetHandle extends Message<ChatClient> {
private static final long serialVersionUID = 6L;
public String handle;
public SetHandle(String handle) {
this.handle = handle;
}
@Override
public String toString() {
return String.format("SetHandle('%s')", handle);
}
@Override
public void apply(ChatClient chatClient) {
// Check if the handle is already being used. If is, append client number.
long count = Groups.groups.values()
.stream()
.flatMap(Collection::stream)
.distinct()
.filter(client -> client.handle.equalsIgnoreCase(handle))
.count();
if(count > 0) handle = String.format("%s#%d", handle, chatClient.clientNum);
// Set the handle.
chatClient.handle = handle;
// Tell the client about the handle that was decided upon.
chatClient.send(new HandleSet(handle));
}
}
/**
* Message sent from server to clients in a particular group in response
* to a client in the group sending a SendChatMessage.
*/
public class ChatMessageReceived extends Message {
private static final long serialVersionUID = 100L;
public Date timeStamp;
public String groupName;
public String handle;
public String chatMessage;
public ChatMessageReceived(String groupName, String handle, String chatMessage) {
this.groupName = groupName;
this.handle = handle;
this.chatMessage = chatMessage;
timeStamp = new Date();
}
@Override
public String toString() {
return String.format("ChatMessageReceived(%s, '%s', '%s', '%s')",
timeStamp, groupName, handle, chatMessage);
}
}
/**
* Message sent to all clients in a group when a new client joins
* a group.
*/
public class Joined extends Message {
private static final long serialVersionUID = 102L;
public String groupName;
public String handle;
public Joined(String groupName, String handle) {
this.groupName = groupName;
this.handle = handle;
}
@Override
public String toString() {
return String.format("Joined(%s, %s)", groupName, handle);
}
}
/**
* A message that will be sent OR received on Object streams to and
* from clients. Because want to use Object streams, needs to implement
* the Serializable interface.
*
* The apply method is provided as a way for messages from a client to
* be processed (applied to some context). The type of the context is
* a generic and in these examples will be a ChatClient.
*
* Note: the exact definition of the classes in the Java server and the
* Android client will differ slightly (in terms of methods, not fields).
* In order for the slightly different classes to match when reading/writing
* each class will have a <code>private static final long serialVersionUID</code>
* that will be used to perform the matching.
* @param <C> The type of the context.
*/
public abstract class Message<C> implements Serializable {
private static final long serialVersionUID = 999L;
/**
* Apply this message's logic to a specific context. This will only
* be used for messages received from a client.
* @param context The context to apply the message logic too.
*/
public void apply(C context) {}
}