Задача по проектированию чат-сервера

чат

Условие задачи

Как бы вы подошли к проектированию чат-сервера? Предоставьте информацию о компонентах внутренней системы (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 в список запросов пользователя А. Поэтому последовательность операций должна выглядеть так:

  1. Пользователь А нажимает кнопку «добавить пользователя» в клиентской программе, запрос отправляется на сервер.
  2. Пользователь В вызывает requestAddUser(User В).
  3. Этот метод вызывает UserManager.addUser.
  4. 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-атак?

Клиенты могут передавать данные на сервер, но что, если сервер попытаются «завалить» по­током запросов? Как предотвратить атаку?

Источник: Карьера программиста

Подобрали два теста для вас:
— А здесь можно применить блокчейн?
Серверы для котиков: выберите лучшее решение для проекта и проверьте себя.