Как бы вы подошли к проектированию чат-сервера? Предоставьте информацию о компонентах внутренней системы (backend), классах и методах. Перечислите самые трудные задачи, которые необходимо решить.
14К открытий14К показов
Условие задачи
Как бы вы подошли к проектированию чат-сервера? Предоставьте информацию о компонентах внутренней системы (backend), классах и методах. Перечислите самые трудные задачи, которые необходимо решить.
Решение
Разработка чат-сервера — огромный проект, команды из многих людей проводят месяцы и годы, пытаясь создать хороший чат-сервер. Вы должны сосредоточиться на отдельном аспекте задачи. Ваше решение не обязано точно отражать реальность, но должно давать хорошее представление о фактической реализации.
Мы займёмся основными аспектами управления пользователями и организацией общения: добавлением пользователя, созданием беседы, обновлением статуса и т. д. Для экономии времени и места мы не будем углубляться в сетевые аспекты задачи и не будем рассматривать вопросы передачи данных между клиентами и сервером.
Предполагается, что «дружба» взаимна: вы дружите со мной только в том случае, если и я дружу с вами. Наша чат-система будет поддерживать как групповые, так и приватные беседы. Мы не будем рассматривать голосовой чат, видеочат и передачу файлов.
Какие конкретные операции должен поддерживать чат?
Мы приведем несколько примеров:
Вход и выход из чата;
Добавление запроса (отправка, приём и отклонение);
Обновление информации о статусе;
Создание приватных и групповых чатов;
Добавление новых сообщений в приватные и групповые чаты.
Это далеко не полный список. Если вам хватит времени, добавьте дополнительные действия.
Что можно узнать из этих требований?
Необходимо реализовать концепцию пользователей, статуса добавления запроса, статуса подключения и сообщений.
Какие базовые компоненты должны присутствовать в системе?
Система будет состоять из базы данных, клиентов и серверов. Мы не включаем эти части в объектно-ориентированный проект, но можем обсудить общее видение системы.
База данных будет использоваться для долгосрочного хранения данных, например списка пользователей и архивов чата. В большинстве случаев подойдет SQL-бaзa данных, но, если нам понадобится большая масштабируемость, можно использовать BigTable или другую аналогичную систему. Подробнее вы можете узнать в нашем разделе, посвящённом базам данных.
Для обмена данными между клиентами и серверами подойдет XML. Хотя его компактность оставляет желать лучшего, он наиболее удобен для восприятия как человеком, так и компьютером. Использование XML также упрощает отладку приложения, а это имеет большое значение.
Примечание переводчика Ещё лучше для передачи данных подойдёт формат JSON.
Сервер будет состоять из множества компьютеров. Данные будут распределены между машинами, что требует «переключения» с одного устройства на другое. Возможно, некоторые данные придется перераспределять между машинами. Чтобы сократить время поиска, нужно избегать узких мест. Например, если аутентификацией пользователей занимается только одна машина, то её выход из строя закроет доступ к системе миллионам пользователей.
Какие основные объекты и методы должны поддерживаться системой?
Ключевые объекты нашей системы — пользователи, беседы и сообщения о статусах. Всё это можно реализовать в классе UserManager. Если бы мы уделяли больше внимания сетевым аспектам задачи или другим компонентам, вероятно, нам пришлось бы создать для них дополнительные объекты.
/* UserManager — центральный класс для основных действий пользователя. */
public class UserManager {
private static UserManager instance;
/* Связывает идентификатор с пользователем */
private HashMap<Integer, User> usersById = new HashMap<Integer, User>();
/* Связывает имя учётной записи с пользователем */
private HashMap<String, User> usersByAccountName = new HashMap<String, User>();
/* Связывает идентификатор пользователя с подключённым пользователем */
private HashMap<Integer, User> onlineUsers = new HashMap<Integer, User>();
public static UserManager getInstance() {
if (instance == null) {
instance = new UserManager();
}
return instance;
}
public void addUser(User fromUser, String toAccountName) {
User toUser = usersByAccountName.get(toAccountName);
AddRequest req = new AddRequest(fromUser, toUser, new Date());
toUser.receivedAddRequest(req);
fromUser.sentAddRequest(req);
}
public void approveAddRequest(AddRequest req) {
req.status = RequestStatus.Accepted;
User from = req.getFromUser();
User to = req.getToUser();
from.addContact(to);
to.addContact(from);
}
public void rejectAddRequest(AddRequest req) {
req.status = RequestStatus.Rejected;
User from = req.getFromUser();
User to = req.getToUser();
from.removeAddRequest(req);
to.removeAddRequest(req);
}
public void userSignedOn(String accountName) {
User user = usersByAccountName.get(accountName);
if (user != null) {
user.setStatus(new UserStatus(UserStatusType.Available, ""));
onlineUsers.put(user.getId(), user);
}
}
public void userSignedOff(String accountName) {
User user = usersByAccountName.get(accountName);
if (user != null) {
user.setStatus(new UserStatus(UserStatusType.Offline, ""));
onlineUsers.remove(user.getId());
}
}
}
Метод receivedAddRequest класса User оповещает пользователя В о том, что пользователь А запросил добавление его в список контактов. Пользователь В соглашается или отклоняет запрос (при помощи UserManager.approveAddRequest или rejectAddRequest), а класс UserManager обеспечивает добавление пользователей в списки контактов двух пользователей.
Метод sentAddRequest класса User вызывается UserManager для добавления AddRequest в список запросов пользователя А. Поэтому последовательность операций должна выглядеть так:
Пользователь А нажимает кнопку «добавить пользователя» в клиентской программе, запрос отправляется на сервер.
Пользователь В вызывает requestAddUser(User В).
Этот метод вызывает UserManager.addUser.
UserManager вызывает методы UserA.sentAddRequest и UserB.receivedAddRequest.
Это всего лишь один из возможных способов проектирования подобных взаимодействий, но не единственный и даже не самый лучший.
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
public class User {
private int id;
private UserStatus status = null;
/* Связывает идентификатор пользователя другого участника с чатом */
private HashMap<Integer, PrivateChat> privateChats = new HashMap<Integer, PrivateChat>();
/* Список групповых чатов */
private ArrayList<GroupChat> groupChats = new ArrayList<GroupChat>();
/* Связывает идентификатор другого пользователя с полученным запросом на добавление */
private HashMap<Integer, AddRequest> receivedAddRequests = new HashMap<Integer, AddRequest>();
/* Связывает идентификатор другого пользователя с отправленным запросом на добавление*/
private HashMap<Integer, AddRequest> sentAddRequests = new HashMap<Integer, AddRequest>();
/* Связывает идентификатор пользователя с объектом пользователя */
private HashMap<Integer, User> contacts = new HashMap<Integer, User>();
private String accountName;
private String fullName;
public User(int id, String accountName, String fullName) {
this.accountName = accountName;
this.fullName = fullName;
this.id = id;
}
public boolean sendMessageToUser(User toUser, String content) {
PrivateChat chat = privateChats.get(toUser.getId());
if (chat == null) {
chat = new PrivateChat(this, toUser);
privateChats.put(toUser.getId(), chat);
}
Message message = new Message(content, new Date());
return chat.addMessage(message);
}
public boolean sendMessageToGroupChat(int groupId, String content) {
GroupChat chat = groupChats.get(groupId);
if (chat != null) {
Message message = new Message(content, new Date());
return chat.addMessage(message);
}
return false;
}
public void setStatus(UserStatus status) {
this.status = status;
}
public UserStatus getStatus() {
return status;
}
public boolean addContact(User user) {
if (contacts.containsKey(user.getId())) {
return false;
} else {
contacts.put(user.getId(), user);
return true;
}
}
public void receivedAddRequest(AddRequest req) {
int senderId = req.getFromUser().getId();
if (!receivedAddRequests.containsKey(senderId)) {
receivedAddRequests.put(senderId, req);
}
}
public void sentAddRequest(AddRequest req) {
int receiverId = req.getFromUser().getId();
if (!sentAddRequests.containsKey(receiverId)) {
sentAddRequests.put(receiverId, req);
}
}
public void removeAddRequest(AddRequest req) {
if (req.getToUser() == this) {
receivedAddRequests.remove(req);
} else if (req.getFromUser() == this) {
sentAddRequests.remove(req);
}
}
public void requestAddUser(String accountName) {
UserManager.getInstance().addUser(this, accountName);
}
public void addConversation(PrivateChat conversation) {
User otherUser = conversation.getOtherParticipant(this);
privateChats.put(otherUser.getId(), conversation);
}
public void addConversation(GroupChat conversation) {
groupChats.add(conversation);
}
public int getId() {
return id;
}
public String getAccountName() {
return accountName;
}
public String getFullName() {
return fullName;
}
}
Класс Conversation реализован как абстрактный, потому что все экземпляры Conversation должны относиться либо к классу GroupChat, либо к классу PrivateChat, а каждый из этих классов обладает собственной функциональностью.
import java.util.ArrayList;
import java.util.Date;
public abstract class Conversation {
protected ArrayList<User> participants = new ArrayList<User>();
protected int id;
protected ArrayList<Message> messages = new ArrayList<Message>();
public ArrayList getMessages() {
return messages;
}
public boolean addMessage(Message m) {
messages.add(m);
return true;
}
public int getId() {
return id;
}
}
public class GroupChat extends Conversation {
public void removeParticipant(User user) {
participants.remove(user);
}
public void addParticipant(User user) {
participants.add(user);
}
}
public class PrivateChat extends Conversation {
public PrivateChat(User user1, User user2) {
participants.add(user1);
participants.add(user2);
}
public User getOtherParticipant(User primary) {
if (participants.get(0) == primary) {
return participants.get(1);
} else if (participants.get(1) == primary) {
return participants.get(0);
}
return null;
}
}
public class Message {
private String content;
private Date date;
public Message(String content, Date date) {
this.content = content;
this.date = date;
}
public String getContent() {
return content;
}
public Date getDate() {
return date;
}
}
Классы AddRequest и UserStatus — простые классы с минимальной функциональностью. Их основное назначение — группировка данных, используемых другими классами.
import java.util.Date;
public class AddRequest {
private User fromUser;
private User toUser;
private Date date;
RequestStatus status;
public AddRequest(User from, User to, Date date) {
fromUser = from;
toUser = to;
this.date = date;
status = RequestStatus.Unread;
}
public RequestStatus getStatus() {
return status;
}
public User getFromUser() {
return fromUser;
}
public User getToUser() {
return toUser;
}
public Date getDate() {
return date;
}
}
public class UserStatus {
private String message;
private UserStatusType type;
public UserStatus(UserStatusType type, String message) {
this.type = type;
this.message = message;
}
public UserStatusType getStatusType() {
return type;
}
public String getMessage() {
return message;
}
}
public enum UserStatusType {
Offline, Away, Idle, Available, Busy
}
public enum RequestStatus {
Unread, Read, Accepted, Rejected
}
Какие проблемы окажутся самыми трудными (или интересными)?
Попробуйте ответить на следующие вопросы:
Как определить активных пользователей?
Подключённый пользователь — не всегда активный пользователь. Если сеанс не был корректно завершён, сессия может «зависнуть», расходуя ресурсы сервера и искажая реальное количество активных пользователей. Как с достаточно большой точностью определить активность пользователей?
Что делать с информационными конфликтами?
Часть информации должна храниться в оперативной памяти, а часть — в базе данных. Что случится, если синхронизация между ними будет нарушена? Какую информацию считать правильной?
Что делать с возрастающим количеством пользователей?
Как на этапе проектирования заложить многократное увеличение количества пользователей?
Как защититься от DоS-атак?
Клиенты могут передавать данные на сервер, но что, если сервер попытаются «завалить» потоком запросов? Как предотвратить атаку?