+ Versão: 1.0 +
+ \s ++ Plataforma P2P baseada em plugins para o desenvolvimento de aplicações + distribuídas. Criada para apoiar as práticas da disciplina de Redes de + Computadores e servir como base para projetos acadêmicos e TCCs. + \s +
+ \s +
+ Autores:
+ Flávio
+ Tony
+ Gustavo Henrique
+
+ Universidade Federal de São João del-Rei (UFSJ) +
+ \s + + + \s"""); + + return new JScrollPane(texto); + } +} \ No newline at end of file diff --git a/pitiupi/src/pitiupi/GUI/Login.java b/pitiupi/src/pitiupi/GUI/Login.java index 2fde02c..893161d 100644 --- a/pitiupi/src/pitiupi/GUI/Login.java +++ b/pitiupi/src/pitiupi/GUI/Login.java @@ -16,6 +16,10 @@ import javax.swing.JSpinner; import javax.swing.JTextField; /** + * Janela de preferencias da aplicação. + * + *Permite ao usuário informar o nome utilizado na rede e a porta + * multicast que será utilizada pela comunicação.
* * @author flavio * @author tony @@ -44,7 +48,6 @@ public final class Login extends JDialog { public void initComponents() { this.setTitle("Login"); - String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name")); int portaSalva = prefs.getInt(PREF_PORTA, 8000); diff --git a/pitiupi/src/pitiupi/GUI/MainWindow.java b/pitiupi/src/pitiupi/GUI/MainWindow.java index 73cb955..139f697 100644 --- a/pitiupi/src/pitiupi/GUI/MainWindow.java +++ b/pitiupi/src/pitiupi/GUI/MainWindow.java @@ -5,82 +5,93 @@ */ package pitiupi.GUI; -import pitiupi.net.Socket; -import java.util.ArrayList; -import java.util.List; +import pitiupi.control.ConnectionManager; +import pitiupi.control.GuiManager; +import pitiupi.control.PluginManager; + import javax.swing.JMenu; -import pitiupi.control.PluginLoader; -import pitiupi.plugin.Plugin; + import pitiupi.control.HeartbeatManager; /** + * Janela principal da aplicação. + * + *Responsável por inicializar a interface gráfica e coordenar os + * principais componentes da aplicação, incluindo comunicação de rede, + * gerenciamento de heartbeat e carregamento de plugins.
+ * + *A instância desta classe é compartilhada com os plugins para permitir + * integração com a aplicação principal.
* * @author flavio * @author tony + * @author Gustavo */ public class MainWindow extends javax.swing.JFrame { - private final MenuBar menuBar; - private javax.swing.JScrollPane jScrollPane1; + private MenuBar menuBar; private javax.swing.JLabel status; - private javax.swing.JPanel statusPanel; - private javax.swing.JTabbedPane tabbedPane; - private HeartbeatManager heartbeatManager; - private OnlineUsersPanel onlineUsersPanel; + private final ConnectionManager connectionManager; + private final PluginManager pluginManager; + private final GuiManager guiManager; - private Socket socket; - - private String userName; - private int port; - - private PluginLoader pluginManager; - private final ListGerencia os menus padrões da interface e disponibiliza um espaço + * para que plugins adicionem seus próprios menus.
* * @author flavio + * @author Gustavo * @author tony - * + * */ public class MenuBar extends JMenuBar { private javax.swing.JMenuItem setupMenu; + private javax.swing.JMenuItem aboutMenu; private javax.swing.JMenu fileMenu; private javax.swing.JMenu helpMenu; private javax.swing.JMenu pluginMenu; private javax.swing.JMenuItem exitMenu; private final MainWindow mainWindow; + /** + * Cria a barra de menus principal da aplicação. + * + * @param mainWindow mainWindow janela principal da aplicação utilizada para executar + */ public MenuBar(MainWindow mainWindow) { this.mainWindow = mainWindow; this.initComponents(); @@ -34,6 +45,7 @@ public class MenuBar extends JMenuBar { helpMenu = new javax.swing.JMenu(); pluginMenu = new javax.swing.JMenu(); setupMenu = new javax.swing.JMenuItem(); + aboutMenu = new javax.swing.JMenuItem(); fileMenu.setText("File"); exitMenu.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F4, java.awt.event.InputEvent.ALT_MASK)); @@ -46,29 +58,44 @@ public class MenuBar extends JMenuBar { this.add(pluginMenu); helpMenu.setText("Help"); + setupMenu.setText("Preferences"); setupMenu.addActionListener(this::setupMenuActionPerformed); helpMenu.add(setupMenu); + + aboutMenu.setText("About"); + aboutMenu.addActionListener(this::aboutMenuActionPerformed); + helpMenu.add(aboutMenu); + this.add(helpMenu); } - - private void exitMenuActionPerformed(java.awt.event.ActionEvent evt) { mainWindow.exit(); } - + private void setupMenuActionPerformed(java.awt.event.ActionEvent evt) { Login login = new Login(); login.setVisible(true); mainWindow.setUserName(login.getName()); - mainWindow.setPort(login.getPort()); + mainWindow.getConnectionManager().setPort(login.getPort()); login.dispose(); } - + + private void aboutMenuActionPerformed(java.awt.event.ActionEvent evt) { + About about = new About(); + about.setVisible(true); + } + + /** + * Adiciona um menu fornecido por um plugin à seção de plugins da barra + * de menus. + * + * @param menu menu criado pelo plugin. + */ public void addPlugin(JMenu menu){ this.pluginMenu.add(menu); } diff --git a/pitiupi/src/pitiupi/GUI/OnlineUsersPanel.java b/pitiupi/src/pitiupi/GUI/OnlineUsersPanel.java index 5821bbd..7d3231e 100644 --- a/pitiupi/src/pitiupi/GUI/OnlineUsersPanel.java +++ b/pitiupi/src/pitiupi/GUI/OnlineUsersPanel.java @@ -11,7 +11,10 @@ import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import pitiupi.control.PeerInfo; import pitiupi.control.PeerListener; + /** + * Painel da interface gráfica responsável por exibir os usuários ativos + * na rede. * * @author tony */ @@ -30,6 +33,11 @@ public class OnlineUsersPanel extends JPanel implements PeerListener { add(new JScrollPane(userList), BorderLayout.CENTER); } + /** + * Atualiza a lista de usuários exibida no painel. + * + * @param peers lista atualizada de usuários ativos na rede. + */ @Override public void onPeersChanged(ListResponsável por anunciar a presença da aplicação na rede, receber + * heartbeats de outros usuários, controlar o tempo desde a última atividade + * dos peers e notificar listeners quando a lista de usuários ativos muda.
* * @author tony */ @@ -23,23 +28,32 @@ public class HeartbeatManager { private static final long HEARTBEAT_INTERVAL_MS = 5000; private static final long PEER_TIMEOUT_MS = 15000; // ~3 batimentos perdidos - - private final MainWindow mainWindow; + private final ConnectionManager connectionManager; private final MapAtualiza o momento da última atividade do peer correspondente e notifica os listeners + * caso outra instância da aplicação seja identificada.
+ * + *Mensagens de heartbeat enviadas pela própria aplicação são ignoradas.
+ * + * @param msg mensagem de heartbeat recebida. + * @param from endereço de rede do usuário que enviou a mensagem. + */ + public void receiveHeartbeat(HeartbeatMessage msg, InetAddress from) { // ignora o próprio heartbeat if (msg.getInstanceId().equals(instanceId) ) { @@ -88,15 +110,33 @@ public class HeartbeatManager { } } + /** + * Adiciona um listener para receber atualizações sobre os peers conhecidos. + * + *O listener recebe imediatamente o estado atual dos peers ao ser + * registrado.
+ * + * @param l listener que será notificado sobre alterações. + */ public void addPeerListener(PeerListener l) { listeners.add(l); l.onPeersChanged(new ArrayList<>(peers.values())); } + /** + * Remove um listener previamente registrado. + * + * @param l listener que deixará de receber atualizações. + */ public void removePeerListener(PeerListener l) { listeners.remove(l); } + /** + * Retorna uma cópia da lista atual de peers conhecidos. + * + * @return lista contendo os peers atualmente conhecidos. + */ public ListArmazena o nome do usuário, seu endereço de rede e o instante da + * última mensagem recebida.
* * @author tony */ @@ -29,6 +34,11 @@ public class PeerInfo { return lastSeen; } + /** + * Atualiza o instante da última atividade do usuário. + * + * @param timestamp novo instante de atividade. + */ public void touch(long timestamp) { this.lastSeen = timestamp; } diff --git a/pitiupi/src/pitiupi/control/PeerListener.java b/pitiupi/src/pitiupi/control/PeerListener.java index 1b97f8b..679395c 100644 --- a/pitiupi/src/pitiupi/control/PeerListener.java +++ b/pitiupi/src/pitiupi/control/PeerListener.java @@ -1,10 +1,21 @@ package pitiupi.control; import java.util.List; + /** + * Interface para receber notificações sobre alterações nos usuários + * conhecidos na rede. + * + *Implementações desta interface são notificadas sempre que a lista + * de usuários ativos é modificada.
* * @author tony */ public interface PeerListener { + /** + * Chamado quando a lista de usuários conhecidos na rede é alterada. + * + * @param peers lista atualizada de usuários ativos. + */ void onPeersChanged(ListOs plugins são descobertos utilizando {@link java.util.ServiceLoader} + * e devem estar empacotados como arquivos JAR contendo a configuração + * {@code META-INF/services} correspondente.
* @author flavio */ public class PluginLoader { @@ -28,7 +31,7 @@ public class PluginLoader { this.mainWindow = main; } - public void loadPlugins() { + public void loadPlugins(ListÉ enviada periodicamente para permitir que a aplicação identifique + * usuários ativos e mantenha o controle dos participantes conectados.
* * @author tony */ diff --git a/pitiupi/src/pitiupi/net/Message.java b/pitiupi/src/pitiupi/net/Message.java index 3b5966b..db6be65 100644 --- a/pitiupi/src/pitiupi/net/Message.java +++ b/pitiupi/src/pitiupi/net/Message.java @@ -12,11 +12,25 @@ import java.io.ObjectOutputStream; import java.io.Serializable; /** + * Classe base para as mensagens trocadas entre a aplicação e os plugins. + * + *As mensagens devem estender esta classe para utilizar o sistema de + * comunicação da aplicação. Subclasses podem adicionar os dados e a lógica + * necessários para representar diferentes tipos de mensagens.
+ * + *Por implementar {@link Serializable}, objetos derivados desta classe + * podem ser convertidos em bytes para transmissão pela rede.
* * @author flavio */ public abstract class Message implements Serializable{ + /** + * Serializa a mensagem para um vetor de bytes. + * + * @return representação serializada da mensagem. + * @throws IOException caso ocorra um erro durante a serialização. + */ public byte[] toByteArray() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(baos); diff --git a/pitiupi/src/pitiupi/net/Socket.java b/pitiupi/src/pitiupi/net/Socket.java deleted file mode 100644 index ad26059..0000000 --- a/pitiupi/src/pitiupi/net/Socket.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package pitiupi.net; - -import pitiupi.GUI.MainWindow; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.ObjectInput; -import java.io.ObjectInputStream; -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.MulticastSocket; -import java.net.UnknownHostException; -import java.util.logging.Level; -import java.util.logging.Logger; -import pitiupi.plugin.Plugin; -import pitiupi.net.HeartbeatMessage; - -/** - * - * @author flavio - * @author tony - */ -public class Socket extends Thread { - - private MulticastSocket multicastSocket; - private InetAddress address; - private MainWindow main; - private volatile boolean running = true; - public final static String INET_ADDR = "224.0.0.3"; - - public Socket(MainWindow main) { - this.main = main; - try { - this.address = InetAddress.getByName(Socket.INET_ADDR); - } catch (UnknownHostException ex) { - System.getLogger(Socket.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex); - } - try { - multicastSocket = new MulticastSocket(this.main.getPort()); - multicastSocket.setSendBufferSize(256000); - multicastSocket.setReceiveBufferSize(256000); - multicastSocket.setReuseAddress(true); - multicastSocket.joinGroup(address); - } catch (IOException ex) { - System.out.println("There is no socket connection. Sorry."); - System.out.println(ex.toString()); - } - } - - public void send(byte[] msg) throws IOException { - DatagramPacket msgPacket; - msgPacket = new DatagramPacket(msg, msg.length, this.address, this.main.getPort()); - multicastSocket.send(msgPacket); - } - - public void close() { - running = false; - if (multicastSocket != null) { - try { - multicastSocket.leaveGroup(address); - } catch (IOException ex) { - - } - multicastSocket.close(); - } - } - - public void receive() throws UnknownHostException, IOException, ClassNotFoundException { - byte[] buf = new byte[256000]; - while (running) { - DatagramPacket msgPacket = new DatagramPacket(buf, buf.length); - try { - multicastSocket.receive(msgPacket); - } catch (IOException ex) { - if (!running) { - break; - } - throw ex; - } - HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData()); - if (heartbeat != null) { - main.getHeartbeatManager().receiveHeartbeat(heartbeat, msgPacket.getAddress()); - continue; - } - for (Plugin plugin : main.getPlugins()) { - System.out.println(msgPacket.getAddress().toString()); - plugin.receiveMessage(msgPacket.getData()); - } - } - } - - - - private HeartbeatMessage tryParseHeartbeat(byte[] data) { - try { - ByteArrayInputStream bais = new ByteArrayInputStream(data); - ObjectInput in = new ObjectInputStream(bais); - Object obj = in.readObject(); - return (obj instanceof HeartbeatMessage) ? (HeartbeatMessage) obj : null; - } catch (IOException | ClassNotFoundException | ClassCastException ex) { - return null; - } - } - - @Override - public void run() { - try { - this.receive(); - } catch (IOException | ClassNotFoundException ex) { - Logger.getLogger(Socket.class.getName()).log(Level.SEVERE, null, ex); - } - } -} diff --git a/pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java b/pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java new file mode 100644 index 0000000..954a56e --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/ConnectionFactory.java @@ -0,0 +1,227 @@ +package pitiupi.net.tcp; + +import pitiupi.net.Message; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Fábrica responsável pela criação e configuração de conexões TCP. + * + *Esta classe centraliza o estabelecimento de conexões de saída e a + * configuração de conexões recebidas. Antes de uma conexão ser considerada + * válida, é realizado um handshake entre os peers para identificar o remetente, + * o destinatário e se a conexão é privada.
+ * + *Durante o handshake é utilizado um timeout menor, definido por + * {@link #HANDSHAKE_TIMEOUT}. Após a validação bem-sucedida, a conexão passa + * a utilizar o timeout padrão definido por {@link #STANDARD_TIMEOUT}.
+ */ +public class ConnectionFactory { + private static final int HANDSHAKE_TIMEOUT = 1000; + private static final int STANDARD_TIMEOUT = 300_000; + + /** + * Cria uma conexão TCP de saída com o peer de destino e realiza o handshake + * necessário para validá-la. + * + *A conexão é configurada com os identificadores do remetente e do + * destinatário e com o tipo indicado pelo parâmetro {@code isPrivate}. + * Após o estabelecimento do socket, o método envia um + * {@link HandshakeMessage} e aguarda uma resposta válida.
+ * + *Se o handshake for validado com sucesso, o timeout da conexão é + * alterado de {@link #HANDSHAKE_TIMEOUT} para {@link #STANDARD_TIMEOUT} + * e a conexão é retornada. Caso contrário, {@code null} é retornado.
+ * + * @param address endereço IP do peer de destino. + * @param port porta TCP na qual o peer de destino está aguardando conexões. + * @param sender identificador do remetente da conexão. + * @param receiver identificador do destinatário da conexão. + * @param isPrivate {@code true} se a conexão deve ser privada; + * {@code false} se deve ser pública. + * @return a conexão TCP estabelecida e validada ou {@code null} caso + * não seja possível estabelecer ou validar a conexão. + */ + public static TcpConnection createConnection(InetAddress address, int port, String sender, String receiver, boolean isPrivate) { + try { + TcpConnection connection; + + Socket socket = new Socket(address, port); + connection = new TcpConnection(socket); + connection.setSender(sender); + connection.setReceiver(receiver); + connection.setPrivate(isPrivate); + + HandshakeMessage handshake = new HandshakeMessage(isPrivate, sender, receiver); + + connection.send(handshake.toByteArray()); + HandshakeMessage handshakeResponse = validateConnection(connection); + + if (handshakeResponse == null) { + return null; + } + + connection.setTimeout(STANDARD_TIMEOUT); + + return connection; + } + catch (IOException ex) { + Logger.getLogger(ConnectionFactory.class.getName()).log(Level.SEVERE, null, ex); + } + return null; + } + + /** + * Configura uma conexão TCP recebida e valida o handshake enviado pelo + * peer remoto. + * + *O handshake recebido é utilizado para obter e configurar os + * identificadores do remetente e do destinatário, bem como o tipo da + * conexão. Após a validação bem-sucedida, o timeout da conexão é alterado + * para {@link #STANDARD_TIMEOUT}.
+ * + *O {@link Socket} recebido deve representar uma conexão TCP já aceita + * pelo servidor.
+ * + * @param socket socket TCP correspondente à conexão recebida. + * @return a conexão TCP configurada e validada ou {@code null} caso + * o handshake seja inválido ou não possa ser processado. + */ + public static TcpConnection receiveConnection(Socket socket) { + try { + TcpConnection connection; + + connection = new TcpConnection(socket); + + HandshakeMessage handshake = validateConnection(connection); + + if (handshake == null) return null; + + connection.send(handshake.createResponse().toByteArray()); + + connection.setSender(handshake.getSender()); + connection.setReceiver(handshake.getReceiver()); + connection.setPrivate(handshake.isPrivate()); + + connection.setTimeout(STANDARD_TIMEOUT); + + return connection; + } + catch (IOException ex) { + Logger.getLogger(ConnectionFactory.class.getName()).log(Level.SEVERE, null, ex); + } + return null; + } + + /** + * Aguarda e valida a mensagem de handshake de uma conexão TCP. + * + *Durante a validação, a conexão utiliza {@link #HANDSHAKE_TIMEOUT} + * como limite para o recebimento do handshake. A mensagem recebida deve + * ser uma instância de {@link Message} e, especificamente, de + * {@link HandshakeMessage}. Mensagens de sistema ou mensagens de outros + * tipos são rejeitadas.
+ * + *Quando o handshake é recebido, seus campos de remetente e destinatário + * são validados por {@link #validateHandshake(HandshakeMessage, TcpConnection)}. + * Caso a validação seja bem-sucedida, o próprio handshake é retornado.
+ * + *Em caso de timeout, mensagem inválida ou erro durante o processamento, + * uma mensagem de sistema apropriada pode ser enviada ao peer e + * {@code null} é retornado.
+ * + * @param connection conexão TCP da qual o handshake será recebido. + * @return o {@link HandshakeMessage} validado ou {@code null} caso o + * handshake não seja recebido ou seja inválido. + */ + private static HandshakeMessage validateConnection(TcpConnection connection) { + try { + connection.setTimeout(HANDSHAKE_TIMEOUT); + byte[] bytes = connection.receive(); + + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bis); + + Object object = ois.readObject(); + + if (object instanceof Message message) { + SysInfoMessage sysInfo = SysInfoMessage.parseSystemInfo(bytes); + if (sysInfo != null) return null; + + + if (message instanceof HandshakeMessage handshake) { + return (validateHandshake(handshake, connection)) ? handshake : null; + } + } + + SysInfoMessage.sendSystemInfo( + connection, + SysInfoType.CONNECTION_REJECTED, + "The message was not from an allowed type." + ); + } + catch (SocketTimeoutException ex) { + SysInfoMessage.sendSystemInfo( + connection, + SysInfoType.CONNECTION_REJECTED, + "No handshake was received within the allowed time." + ); + } + catch (ClassNotFoundException | IOException ex) { + Logger.getLogger(ConnectionFactory.class.getName()).log(Level.SEVERE, null, ex); + + SysInfoMessage.sendSystemInfo( + connection, + SysInfoType.INTERNAL_ERROR, + "The server could not process the handshake." + ); + } + + return null; + } + + /** + * Valida os identificadores presentes em uma mensagem de handshake. + * + *O remetente e o destinatário devem estar presentes e não podem ser + * vazios ou conter apenas espaços em branco. Quando um dos identificadores + * é inválido, uma mensagem de rejeição é enviada ao peer e a validação + * falha.
+ * + * @param handshake mensagem de handshake a ser validada. + * @param connection conexão associada ao handshake, utilizada para enviar + * uma mensagem de rejeição quando necessário. + * @return {@code true} se os identificadores do remetente e do destinatário + * forem válidos; {@code false} caso contrário. + */ + private static boolean validateHandshake(HandshakeMessage handshake, TcpConnection connection) { + String sender = handshake.getSender(); + String receiver = handshake.getReceiver(); + + if (sender == null || sender.isBlank()) { + SysInfoMessage.sendSystemInfo( + connection, + SysInfoType.CONNECTION_REJECTED, + "Invalid Sender: " + sender + "." + ); + return false; + } + if (receiver == null || receiver.isBlank()) { + SysInfoMessage.sendSystemInfo( + connection, + SysInfoType.CONNECTION_REJECTED, + "Invalid Receiver: " + receiver + "." + ); + return false; + } + + return true; + } +} diff --git a/pitiupi/src/pitiupi/net/tcp/HandshakeMessage.java b/pitiupi/src/pitiupi/net/tcp/HandshakeMessage.java new file mode 100644 index 0000000..d18b593 --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/HandshakeMessage.java @@ -0,0 +1,30 @@ +package pitiupi.net.tcp; + +import pitiupi.net.Message; + +class HandshakeMessage extends Message { + private final boolean isPrivate; + private final String sender; + private final String receiver; + + + public HandshakeMessage(boolean isPrivate, String sender, String receiver) { + this.isPrivate = isPrivate; + this.sender = sender; + this.receiver = receiver; + } + + public boolean isPrivate() { + return isPrivate; + } + public String getSender() { + return sender; + } + public String getReceiver() { + return receiver; + } + + public HandshakeMessage createResponse() { + return new HandshakeMessage(isPrivate, receiver, sender); + } +} diff --git a/pitiupi/src/pitiupi/net/tcp/SocketTCP.java b/pitiupi/src/pitiupi/net/tcp/SocketTCP.java new file mode 100644 index 0000000..a08e309 --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/SocketTCP.java @@ -0,0 +1,455 @@ +package pitiupi.net.tcp; + +import pitiupi.control.ConnectionManager; +import pitiupi.control.PluginManager; +import pitiupi.net.Message; +import pitiupi.plugin.PrivateConnectionPlugin; + +import java.io.*; +import java.net.*; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Gerencia a comunicação TCP da aplicação. + * + *Mantém conexões persistentes com os peers e reutiliza essas conexões + * para o envio de mensagens. As mensagens recebidas são encaminhadas aos + * plugins, que são responsáveis por interpretá-las.
+ * + *Novas conexões são aceitas pelo servidor e cada conexão possui uma + * tarefa própria para receber mensagens, permitindo que múltiplas conexões + * sejam mantidas simultaneamente.
+ * + *As conexões são associadas aos endereços dos peers e podem ser + * encerradas quando um peer deixa de estar ativo.
+ * + * @author Gustavo + */ +public class SocketTCP extends Thread { + private final Logger logger; + + private final ExecutorService connectionExecutor = Executors.newCachedThreadPool(); + + private final ConnectionManager connectionManager; + + private final PluginManager pluginManager; + + private final TcpConnectionManager tcpConnectionManager; + + private ServerSocket serverSocket; + private InetAddress address; + + private volatile boolean running = true; + + /** + * Cria o servidor TCP da aplicação. + * + *O servidor é criado na porta definida pela aplicação. As conexões + * com os peers são estabelecidas sob demanda durante o envio ou aceitas + * quando iniciadas por outros peers.
+ * + * @param connectionManager interface de conexão da aplicação, usada para obter a porta, + * o identificador local e os plugins disponíveis. + */ + public SocketTCP(ConnectionManager connectionManager, PluginManager pluginManager) { + this.connectionManager = connectionManager; + this.pluginManager = pluginManager; + + logger = Logger.getLogger(getClass().getName()); + tcpConnectionManager = new TcpConnectionManager(); + + try { + serverSocket = new ServerSocket(connectionManager.getPort()); + address = InetAddress.getByName(InetAddress.getLocalHost().getHostAddress()); + } catch (IOException ex) { + System.out.println("There is no socket connection. Sorry."); + System.out.println(ex.toString()); + } + } + + /** + * Envia uma mensagem por meio de uma conexão TCP pública com o peer de destino. + * + *A conexão pública existente para o endereço informado é reutilizada. + * Caso não exista uma conexão válida, uma nova conexão é estabelecida antes + * do envio. Falhas durante o estabelecimento ou envio são registradas na + * saída padrão e não são propagadas ao chamador.
+ * + * @param msg mensagem que será enviada. + * @param destinationAddress endereço IP do peer que receberá a mensagem. + */ + public boolean send(Message msg, InetAddress destinationAddress) { + try { + TcpConnection connection = getPublicConnection( + destinationAddress, + connectionManager.getIdentifier(), + connectionManager.getIdentifier() + ); + + if (connection == null) { + System.out.println("Could not establish TCP connection."); + return false; + } + connection.send(msg.toByteArray()); + } catch (Exception ex) { + System.out.println("Could not send TCP message."); + System.out.println(ex.toString()); + return false; + } + return true; + } + + /** + * Estabelece uma conexão TCP privada com um plugin específico do peer de destino. + * + *A conexão é criada como privada e passa a ser gerenciada pelo + * {@link TcpConnectionManager}. O método também inicia a tarefa responsável + * por receber os dados dessa conexão.
+ * + * @param destinationAddress endereço IP do peer de destino. + * @param senderId identificador do remetente da conexão. + * @param receiverId identificador do plugin que receberá os dados. + * @return a conexão privada estabelecida ou {@code null} quando a criação + * da conexão falhar. + * @throws IllegalArgumentException se o endereço de destino for o endereço + * local desta instância. + */ + public TcpConnection createPrivateConnection(InetAddress destinationAddress, String senderId, String receiverId) { + return establishOutgoingConnection(destinationAddress, senderId, receiverId, true); + } + + /** + * Obtém uma conexão TCP pública existente com o peer de destino ou estabelece + * uma nova caso não exista uma conexão válida. + * + *As conexões públicas são gerenciadas pelo {@link TcpConnectionManager}. + * O método primeiro verifica se já existe uma conexão pública registrada + * para o endereço de destino e, caso ela exista e não esteja fechada, + * reutiliza-a. Caso contrário, uma nova conexão pública é estabelecida, + * registrada no {@link TcpConnectionManager} e disponibilizada para uso.
+ * + *O acesso ao método é sincronizado para evitar que múltiplas threads + * estabeleçam simultaneamente conexões públicas para o mesmo peer.
+ * + * @param destinationAddress endereço IP do peer de destino. + * @param senderId identificador usado como remetente no estabelecimento da conexão. + * @param receiverId identificador usado como destinatário no estabelecimento da conexão. + * @return uma conexão pública válida ou {@code null} caso não seja possível + * estabelecê-la. + */ + private synchronized TcpConnection getPublicConnection(InetAddress destinationAddress, String senderId, String receiverId) { + TcpConnection connection; + + connection = tcpConnectionManager.getPublicConnection(destinationAddress); + if (connection != null && !connection.isClosed()) return connection; + + connection = establishOutgoingConnection(destinationAddress, senderId, receiverId, false); + + return connection; + } + + /** + * Aguarda e aceita novas conexões TCP. + * + *Cada socket aceito é encaminhado para processamento assíncrono, + * interpretado como uma {@link TcpConnection}, registrado pelo + * {@link TcpConnectionManager} e associado ao receptor adequado.
+ */ + private void receive() { + while (running) { + + try { + Socket socket = serverSocket.accept(); + acceptIncomingConnection(socket); + + } catch (IOException ex) { + if (!running) { + break; + } + + System.out.println("Error accepting TCP connection."); + System.out.println(ex.toString()); + } + } + } + + /** + * Estabelece uma conexão TCP de saída com outro peer. + * + *O método impede conexões com o próprio endereço local, cria a conexão + * por meio da {@link ConnectionFactory}, registra-a no gerenciador de + * conexões e inicia uma tarefa para receber dados continuamente.
+ * + * @param destinationAddress endereço IP do peer de destino. + * @param sender identificador do remetente. + * @param receiver identificador do destinatário. + * @param isPrivate {@code true} para uma conexão destinada a um plugin + * específico; {@code false} para uma conexão pública. + * @return a conexão estabelecida ou {@code null} se a + * {@link ConnectionFactory} não conseguir criá-la. + * @throws IllegalArgumentException se o destino for o endereço desta máquina. + */ + private TcpConnection establishOutgoingConnection(InetAddress destinationAddress, String sender, String receiver, boolean isPrivate) { + if (address.equals(destinationAddress)) { + System.out.println("Failed to get socket."); + throw new IllegalArgumentException("Connections to the local host are forbidden."); + } + + TcpConnection connection = ConnectionFactory.createConnection(destinationAddress, connectionManager.getPort(), sender, receiver, isPrivate); + if (connection == null) return null; + + saveConnection(connection); + + connectionExecutor.submit(() -> { + startReceiver(connection); + }); + + return connection; + } + + /** + * Agenda o processamento de uma conexão TCP recebida. + * + *A conexão é interpretada pela {@link ConnectionFactory}, registrada + * no gerenciador e associada a uma tarefa de recepção. O processamento é + * executado de forma assíncrona pelo executor compartilhado.
+ * + * @param socket socket aceito pelo {@link ServerSocket}. + */ + private void acceptIncomingConnection(Socket socket) { + connectionExecutor.submit(() -> { + TcpConnection connection = ConnectionFactory.receiveConnection(socket); + if (connection == null) return; + + saveConnection(connection); + startReceiver(connection); + }); + } + + /** + * Registra uma conexão no gerenciador de acordo com seu tipo. + * + *Conexões privadas são armazenadas na coleção de conexões privadas; + * as demais são armazenadas na coleção de conexões públicas.
+ * + * @param connection conexão que será registrada. + */ + private void saveConnection(TcpConnection connection) { + if (connection.isPrivate()) { + tcpConnectionManager.addPrivateConnection(connection); + } + else { + tcpConnectionManager.addPublicConnection(connection); + } + } + + /** + * Seleciona e inicia o receptor adequado para uma conexão. + * + *Conexões públicas são tratadas pelo receptor de mensagens públicas. + * Conexões privadas destinadas ao próprio sistema são tratadas + * separadamente; as demais são encaminhadas ao plugin identificado como + * destinatário. Quando solicitado, o plugin também é notificado sobre o + * estabelecimento da conexão.
+ * + * @param connection conexão cujo fluxo de entrada será processado. + */ + private void startReceiver(TcpConnection connection) { + + if (!connection.isPrivate()) { + publicConnectionReceiver(connection); + return; + } + + if (connection.getReceiver().equals(connectionManager.getIdentifier())) { + systemConnectionReceiver(connection); + return; + } + + if (pluginManager.handleConnection(connection, connection.getReceiver())) + privateConnectionReceiver(connection); + else { + SysInfoMessage.sendSystemInfo( + connection, + SysInfoType.RECEIVER_NOT_FOUND, + "This peer does not have the plugin: " + connection.getReceiver() + ); + } + + } + + /** + * Processa continuamente as mensagens recebidas em uma conexão pública. + * + *As mensagens são desserializadas e, quando são instâncias de + * {@link Message}, encaminhadas a todos os plugins da aplicação. Mensagens + * de controle do sistema podem encerrar a conexão ou registrar uma + * condição de erro.
+ * + *O método também trata timeouts e erros de I/O, removendo a conexão + * do {@link TcpConnectionManager} quando ela deixa de ser utilizável.
+ * + * @param connection conexão pública da qual as mensagens serão recebidas. + */ + private void publicConnectionReceiver(TcpConnection connection) { + while (running) { + try { + byte[] bytes = connection.receive(); + + SysInfoMessage sysMessage = SysInfoMessage.parseSystemInfo(bytes); + + if (sysMessage == null) { + pluginManager.handlePublicMessage(bytes); + continue; + } + + switch (sysMessage.getType()) { + case CONNECTION_CLOSED, INTERNAL_ERROR -> { + tcpConnectionManager.closeConnection(connection); + return; + } + case MESSAGE_REJECTED -> { + logger.log(Level.SEVERE, "Somehow sent something that was not an Message"); + } + default -> {} + } + } + catch (SocketTimeoutException ex) { + if (connection.hasTimedOut()) { + tcpConnectionManager.closeConnection(connection, "Connection timeout"); + return; + } + } + catch (IOException ex) { + tcpConnectionManager.closeConnection(connection); + } + } + } + + /** + * Processa os dados recebidos em uma conexão privada e os encaminha + * ao plugin destinatário. + * + *Dados que não representam mensagens de controle são entregues + * diretamente ao {@link PrivateConnectionPlugin}. Mensagens de controle + * relacionadas ao encerramento ou a erros internos provocam a notificação + * do plugin e o fechamento da conexão.
+ * + * @param connection conexão privada da qual os dados serão recebidos. + */ + private void privateConnectionReceiver(TcpConnection connection) { + while (running) { + try { + byte[] message = connection.receive(); + SysInfoMessage sysMessage = SysInfoMessage.parseSystemInfo(message); + + if (sysMessage == null) { + pluginManager.handlePrivateMessage(connection, message); + continue; + } + + switch (sysMessage.getType()) { + case CONNECTION_CLOSED, INTERNAL_ERROR -> { + pluginManager.handleDisconnection(connection, connection.getReceiver()); + tcpConnectionManager.closeConnection(connection); + } + default -> { + continue; + } + } + } + catch (SocketTimeoutException ex) { + if (connection.hasTimedOut()) { + tcpConnectionManager.closeConnection(connection, "Connection timeout"); + return; + } + } + catch (IOException ex) { + tcpConnectionManager.closeConnection(connection); + } + } + } + + /** + * Processa mensagens recebidas em uma conexão destinada ao próprio sistema. + * + *Atualmente, mensagens destinadas ao sistema são apenas verificadas + * quanto ao seu formato de controle. Não há processamento adicional de + * mensagens de sistema neste método.
+ * + * @param connection conexão destinada ao identificador da própria aplicação. + */ + private void systemConnectionReceiver(TcpConnection connection) { + while (running) { + try { + byte[] message = connection.receive(); + try (ByteArrayInputStream bis = new ByteArrayInputStream(message); + ObjectInputStream ois = new ObjectInputStream(bis)) { + + SysInfoMessage sysMessage = SysInfoMessage.parseSystemInfo(message); + if (sysMessage != null) continue; + + //TODO: Ainda não existem mensagens trocadas entre o sistema. + } + + } + catch (SocketTimeoutException ex) { + if (connection.hasTimedOut()) return; + } + catch (IOException ex) { + tcpConnectionManager.closeConnection(connection); + } + } + } + + /** + * Encerra uma conexão TCP e registra o motivo informado. + * + *Quando nenhuma descrição é fornecida, é utilizado um motivo padrão. + * O fechamento efetivo da conexão é delegado ao {@link TcpConnectionManager}.
+ * + * @param connection conexão que será encerrada. + * @param description motivo do encerramento; pode ser {@code null}. + */ + public void disconnect(TcpConnection connection, String description) { + tcpConnectionManager.closeConnection( + connection, + Objects.requireNonNullElse(description, "No reason provided") + ); + } + + @Override + public void run() { + receive(); + } + + /** + * Encerra o servidor TCP e libera os recursos associados. + * + *Interrompe o laço de aceitação de conexões, fecha o + * {@link ServerSocket}, encerra todas as conexões gerenciadas e interrompe + * as tarefas do executor.
+ * + *Após a chamada, esta instância não deve ser reutilizada para aceitar + * novas conexões.
+ */ + public void close() { + running = false; + + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException ex) { + logger.log(Level.SEVERE, null, ex); + } + } + + tcpConnectionManager.closeAllConnections("The peer must have closed the application or changed ports"); + + connectionExecutor.shutdownNow(); + } +} \ No newline at end of file diff --git a/pitiupi/src/pitiupi/net/tcp/SysInfoMessage.java b/pitiupi/src/pitiupi/net/tcp/SysInfoMessage.java new file mode 100644 index 0000000..9e149eb --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/SysInfoMessage.java @@ -0,0 +1,51 @@ +package pitiupi.net.tcp; + +import pitiupi.net.Message; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SysInfoMessage extends Message { + + private final SysInfoType type; + private final String message; + + public SysInfoMessage(String message, SysInfoType type) { + this.message = message; + this.type = type; + } + + public String getMessage() { + return message; + } + + public SysInfoType getType() { + return type; + } + + public static void sendSystemInfo(TcpConnection connection, SysInfoType type, String description) { + try { + SysInfoMessage error = new SysInfoMessage(description, type); + connection.send(error.toByteArray()); + } catch (IOException ex) { + Logger.getLogger(SysInfoMessage.class.getName()).log(Level.WARNING, null, ex); + } + } + + public static SysInfoMessage parseSystemInfo(byte[] bytes) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bis)) { + + Object object = ois.readObject(); + if (object instanceof SysInfoMessage) { + return (SysInfoMessage) object; + } + } catch (IOException | ClassNotFoundException ex) { + Logger.getLogger(SysInfoMessage.class.getName()).log(Level.WARNING, null, ex); + } + return null; + } +} diff --git a/pitiupi/src/pitiupi/net/tcp/SysInfoType.java b/pitiupi/src/pitiupi/net/tcp/SysInfoType.java new file mode 100644 index 0000000..c5dfe44 --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/SysInfoType.java @@ -0,0 +1,9 @@ +package pitiupi.net.tcp; + +public enum SysInfoType { + CONNECTION_REJECTED, + RECEIVER_NOT_FOUND, + MESSAGE_REJECTED, + INTERNAL_ERROR, + CONNECTION_CLOSED, +} diff --git a/pitiupi/src/pitiupi/net/tcp/TcpConnection.java b/pitiupi/src/pitiupi/net/tcp/TcpConnection.java new file mode 100644 index 0000000..be7c0e4 --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/TcpConnection.java @@ -0,0 +1,171 @@ +package pitiupi.net.tcp; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; + +/** + * Representa uma conexão TCP ativa com um peer remoto. + * + *A conexão permite o envio e recebimento de mensagens, além de manter + * informações sobre o endereço remoto e metadados da conexão, como remetente, + * destinatário e se a conexão é privada.
+ * + * @author Gustavo + */ +public class TcpConnection { + + private final Socket socket; + private final DataInputStream input; + private final DataOutputStream output; + private final InetAddress address; + private String sender; + private String receiver; + private boolean isPrivate; + private volatile long lastActivity; + private long timeout; + + /** + * Cria uma conexão TCP a partir de um socket existente. + * + * @param socket socket utilizado para a comunicação + * @throws IOException se não for possível obter os fluxos de entrada ou saída + * do socket + */ + TcpConnection(Socket socket) throws IOException { + this.socket = socket; + this.input = new DataInputStream(socket.getInputStream()); + this.output = new DataOutputStream(socket.getOutputStream()); + this.address = socket.getInetAddress(); + this.lastActivity = System.currentTimeMillis(); + this.timeout = 300_000; // 5 minutos + } + + /** + * Envia uma mensagem pela conexão. + * + *A mensagem é enviada precedida pelo seu tamanho, permitindo que o + * receptor determine quantos bytes devem ser lidos.
+ * + * @param message mensagem a ser enviada + * @throws IOException se ocorrer um erro durante o envio + */ + public synchronized void send(byte[] message) throws IOException { + output.writeInt(message.length); + output.write(message); + output.flush(); + updateLastActivity(); + } + + /** + * Recebe uma mensagem da conexão. + * + *Primeiro lê o tamanho da mensagem e, em seguida, lê aquantidade + * correspondente de bytes.
+ * + * @return mensagem recebida + * @throws IOException se o tamanho da mensagem for inválido ou se a conexão + * for encerrada antes que a mensagem seja completamente recebida + */ + byte[] receive() throws IOException { + int length = input.readInt(); + + if (length < 0) throw new IOException("Invalid message length."); + + byte[] message = input.readNBytes(length); + + if (message.length != length) { + throw new IOException("Connection closed before receiving the complete message."); + } + + updateLastActivity(); + + return message; + } + + /** + * Verifica se o socket da conexão está fechado. + * + * @return {@code true} se o socket estiver fechado; + * {@code false} caso contrário + */ + public boolean isClosed() { + return socket.isClosed(); + } + + /** + * Fecha o socket da conexão. + * + * @throws IOException se ocorrer um erro ao fechar o socket + */ + void close() throws IOException { + socket.close(); + } + + /** + * Retorna o endereço do peer remoto conectado. + * + * @return endereço IP do peer remoto + */ + public InetAddress getAddress() { + return address; + } + + /** + * Atualiza o instante da última atividade da conexão. + */ + private void updateLastActivity() { + lastActivity = System.currentTimeMillis(); + } + + /** + * Define o tempo limite de leitura do socket. + * + * @param timeout tempo limite, em milissegundos + * @throws SocketException se ocorrer um erro ao definir o tempo limite + */ + void setTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + this.timeout = timeout; + } + + /** + * Verifica se a conexão ficou inativa pelo tempo limite configurado. + * + *A verificação é baseada no instante da última atividade registrada + * durante o envio ou recebimento de uma mensagem.
+ * + * @return {@code true} se a conexão estiver inativa há pelo menos 5 minutos; + * {@code false} caso contrário + */ + boolean hasTimedOut() { + return System.currentTimeMillis() - lastActivity >= timeout; + } + + public String getReceiver() { + return receiver; + } + + public String getSender() { + return sender; + } + + public boolean isPrivate() { + return isPrivate; + } + + void setSender(String sender) { + this.sender = sender; + } + + void setReceiver(String receiver) { + this.receiver = receiver; + } + + void setPrivate(boolean aPrivate) { + isPrivate = aPrivate; + } +} diff --git a/pitiupi/src/pitiupi/net/tcp/TcpConnectionManager.java b/pitiupi/src/pitiupi/net/tcp/TcpConnectionManager.java new file mode 100644 index 0000000..a8df7db --- /dev/null +++ b/pitiupi/src/pitiupi/net/tcp/TcpConnectionManager.java @@ -0,0 +1,262 @@ +package pitiupi.net.tcp; + +import pitiupi.control.PeerInfo; +import pitiupi.control.PeerListener; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Gerencia as conexões TCP estabelecidas com os peers da aplicação. + * + *As conexões públicas são armazenadas individualmente por endereço IP, + * enquanto as conexões privadas são agrupadas por endereço, permitindo que + * um mesmo peer mantenha múltiplas conexões privadas simultaneamente.
+ * + *Além de manter o registro das conexões ativas, esta classe é responsável + * por encerrá-las quando necessário e por remover do gerenciamento as + * conexões que forem fechadas. Também acompanha as alterações na lista de + * peers ativos para encerrar conexões associadas a peers que deixaram de + * estar disponíveis.
+ */ +class TcpConnectionManager implements PeerListener { + private final Logger logger; + private final MapUm mesmo peer pode possuir múltiplas conexões privadas. Por isso, + * as conexões são armazenadas em uma lista associada ao endereço do + * peer.
+ * + * @param connection conexão privada a ser registrada. + */ + public void addPrivateConnection(TcpConnection connection) { + ListCada peer pode possuir apenas uma conexão pública registrada. + * Quando já existe uma conexão pública para o endereço da nova conexão, + * a conexão anterior é encerrada e substituída pela nova.
+ * + * @param connection nova conexão pública a ser registrada. + */ + + public void addPublicConnection(TcpConnection connection) { + TcpConnection oldConnection = publicConnections.put(connection.getAddress(), connection); + if (oldConnection != null && oldConnection != connection) { + closeConnection(oldConnection); + } + } + + /** + * Envia uma mensagem de encerramento ao peer e fecha a conexão. + * + *A mensagem enviada informa o motivo do encerramento antes que a + * conexão seja removida do gerenciamento e fechada.
+ * + * @param connection conexão TCP a ser encerrada. + * @param reason motivo do encerramento que será informado ao peer. + */ + public void closeConnection(TcpConnection connection, String reason) { + SysInfoMessage.sendSystemInfo(connection, SysInfoType.CONNECTION_CLOSED, reason); + closeConnection(connection); + } + + /** + * Remove uma conexão do gerenciamento e encerra seu socket. + * + *Para conexões privadas, a conexão é removida da lista associada ao + * endereço do peer. Caso essa seja a última conexão privada do endereço, + * a lista também é removida do mapa.
+ * + *Para conexões públicas, a conexão é removida do mapa somente se + * ainda for a conexão registrada para seu endereço.
+ * + *Se a conexão já estiver fechada ou for {@code null}, nenhuma ação + * adicional de fechamento é realizada.
+ * + * @param connection conexão TCP a ser removida e encerrada. + */ + + public void closeConnection(TcpConnection connection) { + if (connection == null) return; + if (connection.isPrivate()) { + ListTanto a conexão pública quanto todas as conexões privadas associadas + * ao endereço são encerradas e removidas do gerenciamento.
+ * + * @param address endereço IP do peer cujas conexões serão encerradas. + */ + public void closeByAdress(InetAddress address) { + TcpConnection publicConnection = publicConnections.remove(address); + + if (publicConnection != null) { + try { + publicConnection.close(); + } + catch (IOException ex) { + logger.log(Level.SEVERE, "Error closing TCP connection", ex); + } + + } + + ListApós a execução, não devem permanecer conexões registradas nos + * mapas de conexões deste gerenciador.
+ */ + + public void closeAllConnections() { + for (TcpConnection connection : publicConnections.values()) { + closeConnection(connection); + } + + synchronized (privateConnections) { + for (ListOs endereços presentes na lista de peers são utilizados como + * referência para determinar quais conexões ainda devem permanecer + * ativas. Conexões públicas e privadas associadas a endereços que não + * estão mais presentes na lista de peers são encerradas e removidas.
+ * + * @param peers lista atualizada de peers ativos. + */ + + @Override + public void onPeersChanged(ListMantém a conexão com o grupo multicast da aplicação e permite o envio + * de mensagens tanto para o grupo quanto diretamente para um peer.
+ * + *As mensagens recebidas são verificadas para identificar mensagens de + * heartbeat. As demais mensagens são encaminhadas aos plugins, que são + * responsáveis por interpretá-las.
+ * + * @author flavio + * @author tony + * @author Gustavo + */ +public class SocketUDP extends Thread { + + private MulticastSocket multicastSocket; + + private InetAddress address; + + private final ConnectionManager connectionManager; + private final PluginManager pluginManager; + + private volatile boolean running = true; + public final static String INET_ADDR = "224.0.0.3"; + + /** + * Cria e inicializa o socket UDP utilizado pela aplicação. + * + *O socket é associado à porta da aplicação e ingressa no grupo + * multicast configurado.
+ * + * @param connectionManager interface que gerencia toda conexão da aplicação. + * @param pluginManager interface que gerencia os plugins da conexão. + */ + public SocketUDP(ConnectionManager connectionManager, PluginManager pluginManager) { + this.connectionManager = connectionManager; + this.pluginManager = pluginManager; + + try { + this.address = InetAddress.getByName(SocketUDP.INET_ADDR); + + multicastSocket = new MulticastSocket(connectionManager.getPort()); + multicastSocket.setSendBufferSize(256000); + multicastSocket.setReceiveBufferSize(256000); + multicastSocket.setReuseAddress(true); + multicastSocket.joinGroup(address); + } + catch (UnknownHostException ex) { + System.getLogger(SocketUDP.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex); + } + catch (IOException ex) { + System.out.println("There is no socket connection. Sorry."); + System.out.println(ex); + } + } + + /** + * Envia uma mensagem para o grupo multicast da aplicação. + * + * @param msg mensagem a ser enviada. + * @throws IOException caso ocorra um erro durante o envio. + */ + public void sendMulticast(Message msg) throws IOException { + send(msg.toByteArray(), this.address); + } + + /** + * Envia uma mensagem diretamente para um peer utilizando UDP. + * + * @param msg mensagem a ser enviada. + * @param destinationAddress endereço IP do peer destinatário. + * @throws IOException caso ocorra um erro durante o envio. + */ + public void sendUnicast(Message msg, InetAddress destinationAddress) throws IOException { + send(msg.toByteArray(), destinationAddress); + } + + /** + * Envia uma mensagem UDP para o endereço especificado. + * + * @param msg mensagem serializada a ser enviada. + * @param destinationAddress endereço de destino. + * @throws IOException caso ocorra um erro durante o envio. + */ + private void send(byte[] msg, InetAddress destinationAddress) throws IOException { + DatagramPacket msgPacket; + msgPacket = new DatagramPacket(msg, msg.length, destinationAddress, connectionManager.getPort()); + multicastSocket.send(msgPacket); + } + + /** + * Encerra a comunicação multicast e interrompe a recepção de mensagens. + */ + public void close() { + running = false; + if (multicastSocket != null) { + try { + multicastSocket.leaveGroup(address); + } catch (IOException ex) { + System.out.println(ex); + } + multicastSocket.close(); + } + } + + + /** + * Recebe e processa mensagens UDP. + * + *Mensagens de heartbeat são encaminhadas ao gerenciador de heartbeat. + * As demais mensagens são encaminhadas aos plugins carregados pela + * aplicação.
+ * + * @throws UnknownHostException caso não seja possível resolver o endereço + * utilizado pela comunicação. + * @throws IOException caso ocorra um erro durante a recepção. + * @throws ClassNotFoundException caso ocorra um erro ao verificar uma + * mensagem de heartbeat. + */ + public void receive() throws UnknownHostException, IOException, ClassNotFoundException { + byte[] buf = new byte[256000]; + while (running) { + DatagramPacket msgPacket = new DatagramPacket(buf, buf.length); + try { + multicastSocket.receive(msgPacket); + } catch (IOException ex) { + if (!running) { + break; + } + throw ex; + } + + HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData()); + if (heartbeat != null) { + connectionManager.receiveHeartbeat(heartbeat, msgPacket.getAddress()); + continue; + } + + pluginManager.handlePublicMessage(msgPacket.getData()); + } + } + + /** + * Tenta identificar os dados recebidos como uma mensagem de heartbeat. + * + * @param data dados serializados recebidos. + * @return a mensagem de heartbeat caso os dados correspondam a uma; + * {@code null} caso contrário. + */ + private HeartbeatMessage tryParseHeartbeat(byte[] data) { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(data); + ObjectInput in = new ObjectInputStream(bais); + Object obj = in.readObject(); + return (obj instanceof HeartbeatMessage) ? (HeartbeatMessage) obj : null; + } catch (IOException | ClassNotFoundException | ClassCastException ex) { + return null; + } + } + + /** + * Inicia a recepção de mensagens UDP. + */ + @Override + public void run() { + try { + this.receive(); + } catch (IOException | ClassNotFoundException ex) { + Logger.getLogger(SocketUDP.class.getName()).log(Level.SEVERE, null, ex); + } + } +} diff --git a/pitiupi/src/pitiupi/plugin/Plugin.java b/pitiupi/src/pitiupi/plugin/Plugin.java index 7a6b711..321335b 100644 --- a/pitiupi/src/pitiupi/plugin/Plugin.java +++ b/pitiupi/src/pitiupi/plugin/Plugin.java @@ -4,11 +4,17 @@ */ package pitiupi.plugin; -import javax.swing.JMenu; import pitiupi.GUI.MainWindow; +import pitiupi.control.ConnectionManager; +import pitiupi.control.GuiManager; import pitiupi.net.Message; +import pitiupi.net.tcp.TcpConnection; /** + * Interface base para plugins da aplicação. + * + *Cada plugin deve implementar esta interface para ser carregado + * pela aplicação e participar do sistema de comunicação por mensagens.
* * @author flavio */ @@ -17,7 +23,32 @@ public interface Plugin { public String getName(); public String getAuthor(); public String getVersion(); - public void createPlugin(MainWindow window); + public String getIdentifier(); + + /** + * Inicializa o plugin. + * + *Este método é chamado pela aplicação após o carregamento do plugin.
+ * + * @param window janela principal da aplicação. + */ + public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager); + + /** + * Retorna uma mensagem utilizada pelo plugin. + * + * @return mensagem criada pelo plugin. + */ public Message getMessage(); + + /** + * Recebe uma mensagem enviada pela aplicação. + * + *A mensagem é recebida em formato serializado. O plugin deve verificar + * se a mensagem pertence ao seu tipo e realizar o processamento necessário.
+ * + * @param message mensagem serializada recebida. + */ public void receiveMessage(byte[] message); + } \ No newline at end of file diff --git a/pitiupi/src/pitiupi/plugin/PrivateConnectionPlugin.java b/pitiupi/src/pitiupi/plugin/PrivateConnectionPlugin.java new file mode 100644 index 0000000..7da4368 --- /dev/null +++ b/pitiupi/src/pitiupi/plugin/PrivateConnectionPlugin.java @@ -0,0 +1,9 @@ +package pitiupi.plugin; + +import pitiupi.net.tcp.TcpConnection; + +public interface PrivateConnectionPlugin extends Plugin { + public void receivePrivateConnection(TcpConnection connection); + public void receiveBytes(TcpConnection connection, byte[] bytes); + public void onSocketClosed(TcpConnection connection); +}