diff --git a/.gitignore b/.gitignore index 53d2375..237cb8c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /pitiupi.chat/nbproject/private/ /pitiupi.screenCast/build/ /pitiupi.screenCast/dist/ + +.idea/ diff --git a/pitiupi.chat/.idea/.gitignore b/pitiupi.chat/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/pitiupi.chat/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/pitiupi.chat/.idea/libraries/pitiupi_chat.xml b/pitiupi.chat/.idea/libraries/pitiupi_chat.xml new file mode 100644 index 0000000..d74edd9 --- /dev/null +++ b/pitiupi.chat/.idea/libraries/pitiupi_chat.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pitiupi.chat/.idea/misc.xml b/pitiupi.chat/.idea/misc.xml new file mode 100644 index 0000000..8547fc5 --- /dev/null +++ b/pitiupi.chat/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/pitiupi.chat/.idea/modules.xml b/pitiupi.chat/.idea/modules.xml new file mode 100644 index 0000000..0dec8d6 --- /dev/null +++ b/pitiupi.chat/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pitiupi.chat/.idea/vcs.xml b/pitiupi.chat/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/pitiupi.chat/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pitiupi.chat/build/built-jar.properties b/pitiupi.chat/build/built-jar.properties index 499f65e..5d16962 100644 --- a/pitiupi.chat/build/built-jar.properties +++ b/pitiupi.chat/build/built-jar.properties @@ -1,6 +1,6 @@ -#Thu, 16 Jul 2026 15:21:36 -0300 +#Tue, 29 Sep 2026 11:11:06 -0300 -/home/flavio/NetBeansProjects/pitiupi.chat= +C\:\\Users\\Gustavo\\OneDrive\\Documentos\\Faculdade\\2026_2\\Monitoria\\pitiupi\\pitiupi.chat= -/home/flavio/NetBeansProjects/pitiupi= +C\:\\Users\\Gustavo\\OneDrive\\Documentos\\Faculdade\\2026_2\\Monitoria\\pitiupi\\pitiupi= diff --git a/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow$1.class b/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow$1.class index 254570a..62a7d32 100644 Binary files a/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow$1.class and b/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow$1.class differ diff --git a/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow.class b/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow.class index 4d4bde2..23f514a 100644 Binary files a/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow.class and b/pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow.class differ diff --git a/pitiupi.chat/build/classes/pitiupi/chat/plugin/ChatPlugin.class b/pitiupi.chat/build/classes/pitiupi/chat/plugin/ChatPlugin.class index b5d0c5d..73c97c9 100644 Binary files a/pitiupi.chat/build/classes/pitiupi/chat/plugin/ChatPlugin.class and b/pitiupi.chat/build/classes/pitiupi/chat/plugin/ChatPlugin.class differ diff --git a/pitiupi.chat/dist/pitiupi.chat.jar b/pitiupi.chat/dist/pitiupi.chat.jar deleted file mode 100644 index 78438b7..0000000 Binary files a/pitiupi.chat/dist/pitiupi.chat.jar and /dev/null differ diff --git a/pitiupi.chat/pitiupi.chat.iml b/pitiupi.chat/pitiupi.chat.iml new file mode 100644 index 0000000..4e2cbb3 --- /dev/null +++ b/pitiupi.chat/pitiupi.chat.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pitiupi.chat/src/pitiupi.chat.iml b/pitiupi.chat/src/pitiupi.chat.iml new file mode 100644 index 0000000..937cc1c --- /dev/null +++ b/pitiupi.chat/src/pitiupi.chat.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pitiupi.chat/src/pitiupi/chat/GUI/ChatWindow.java b/pitiupi.chat/src/pitiupi/chat/GUI/ChatWindow.java index 3b707fa..b0cf6d8 100644 --- a/pitiupi.chat/src/pitiupi/chat/GUI/ChatWindow.java +++ b/pitiupi.chat/src/pitiupi/chat/GUI/ChatWindow.java @@ -12,6 +12,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import pitiupi.chat.net.ChatMessage; import pitiupi.GUI.MainWindow; +import pitiupi.control.ConnectionManager; import pitiupi.net.Message; /** @@ -29,10 +30,10 @@ public class ChatWindow extends javax.swing.JDialog { private javax.swing.JSplitPane jSplitPane1; private javax.swing.JTextArea jTextArea1; - private final MainWindow mainWindow; + private final ConnectionManager connectionManager; - public ChatWindow(MainWindow mainWindow) { - this.mainWindow = mainWindow; + public ChatWindow(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; initComponents(); } @@ -91,20 +92,16 @@ public class ChatWindow extends javax.swing.JDialog { } private void sendMessage() { - if (jTextArea1.getText().trim().length() == 0) { + if (jTextArea1.getText().trim().isEmpty()) { return; } - ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText()); + ChatMessage message = new ChatMessage(connectionManager.getUserName(), jTextArea1.getText()); this.sendChatMessage(message); jTextArea1.setText(""); } public void sendChatMessage(Message message) { - try { - mainWindow.getSocket().send(message.toByteArray()); - } catch (IOException ex) { - Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex); - } + connectionManager.sendUdpMulticast(message); } public void receiveChatMessage(ChatMessage message) { diff --git a/pitiupi.chat/src/pitiupi/chat/plugin/ChatPlugin.java b/pitiupi.chat/src/pitiupi/chat/plugin/ChatPlugin.java index 44f3911..7658b31 100644 --- a/pitiupi.chat/src/pitiupi/chat/plugin/ChatPlugin.java +++ b/pitiupi.chat/src/pitiupi/chat/plugin/ChatPlugin.java @@ -11,6 +11,8 @@ import pitiupi.GUI.MainWindow; import pitiupi.chat.GUI.ChatMenu; import pitiupi.chat.GUI.ChatWindow; import pitiupi.chat.net.ChatMessage; +import pitiupi.control.ConnectionManager; +import pitiupi.control.GuiManager; import pitiupi.net.Message; import pitiupi.plugin.Plugin; @@ -21,16 +23,14 @@ import pitiupi.plugin.Plugin; public class ChatPlugin implements Plugin { private ChatWindow chatWindow; - private MainWindow mainWindow; public ChatPlugin() { } @Override - public void createPlugin(MainWindow mainWindow) { - this.mainWindow = mainWindow; - this.chatWindow = new ChatWindow(mainWindow); - this.mainWindow.addMenu(new ChatMenu(chatWindow)); + public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) { + this.chatWindow = new ChatWindow(connectionManager); + guiManager.addMenu(new ChatMenu(chatWindow)); } @Override @@ -42,9 +42,7 @@ public class ChatPlugin implements Plugin { if (ois.readObject() instanceof ChatMessage msg) { this.chatWindow.receiveChatMessage(msg); } - } catch (IOException ex) { - System.out.println(ex.toString()); - } catch (ClassNotFoundException ex) { + } catch (IOException | ClassNotFoundException ex) { System.out.println(ex.toString()); } } @@ -64,6 +62,11 @@ public class ChatPlugin implements Plugin { return "0"; } + @Override + public String getIdentifier() { + return "Pitiupi.Chat.Example"; + } + @Override public Message getMessage() { return new ChatMessage(); diff --git a/pitiupi.mpChat/Makefile b/pitiupi.mpChat/Makefile new file mode 100644 index 0000000..e2be90f --- /dev/null +++ b/pitiupi.mpChat/Makefile @@ -0,0 +1,15 @@ +JAR = ../pitiupi/dist/pitiupi.jar +OUT_DIR = /out +META-INF_DIR = src/META-INF +DIST = dist/mpChat.jar + +SOURCES = $(shell find -name "*.java") + +all: + mkdir -p $(OUT_DIR) + javac -cp "$(JAR)" -d "$(OUT_DIR)" $(SOURCES) + cp -r "$(META-INF_DIR)" "$(OUT_DIR)" + jar cf "$(DIST)" -C "$(OUT_DIR)" . + +clean: + rm -rf "$(OUT_DIR)"/* \ No newline at end of file diff --git a/pitiupi.mpChat/out/GUI/ChatMenu.class b/pitiupi.mpChat/out/GUI/ChatMenu.class new file mode 100644 index 0000000..e8b9851 Binary files /dev/null and b/pitiupi.mpChat/out/GUI/ChatMenu.class differ diff --git a/pitiupi.mpChat/out/GUI/ChatWindow.class b/pitiupi.mpChat/out/GUI/ChatWindow.class new file mode 100644 index 0000000..97a91a4 Binary files /dev/null and b/pitiupi.mpChat/out/GUI/ChatWindow.class differ diff --git a/pitiupi.mpChat/out/GUI/PrivateChatWindow.class b/pitiupi.mpChat/out/GUI/PrivateChatWindow.class new file mode 100644 index 0000000..147cc53 Binary files /dev/null and b/pitiupi.mpChat/out/GUI/PrivateChatWindow.class differ diff --git a/pitiupi.mpChat/out/GUI/PublicChatWindow.class b/pitiupi.mpChat/out/GUI/PublicChatWindow.class new file mode 100644 index 0000000..605cc2f Binary files /dev/null and b/pitiupi.mpChat/out/GUI/PublicChatWindow.class differ diff --git a/pitiupi.mpChat/out/GUI/SocketTestMenu.class b/pitiupi.mpChat/out/GUI/SocketTestMenu.class new file mode 100644 index 0000000..724eae7 Binary files /dev/null and b/pitiupi.mpChat/out/GUI/SocketTestMenu.class differ diff --git a/pitiupi.mpChat/out/GUI/SocketTestWindow.class b/pitiupi.mpChat/out/GUI/SocketTestWindow.class new file mode 100644 index 0000000..b782ecd Binary files /dev/null and b/pitiupi.mpChat/out/GUI/SocketTestWindow.class differ diff --git a/pitiupi.mpChat/out/META-INF/services/pitiupi.plugin.Plugin b/pitiupi.mpChat/out/META-INF/services/pitiupi.plugin.Plugin new file mode 100644 index 0000000..3240a6f --- /dev/null +++ b/pitiupi.mpChat/out/META-INF/services/pitiupi.plugin.Plugin @@ -0,0 +1 @@ +plugin.MultiProtocolChat diff --git a/pitiupi.mpChat/out/control/ChatController.class b/pitiupi.mpChat/out/control/ChatController.class new file mode 100644 index 0000000..bbfa82b Binary files /dev/null and b/pitiupi.mpChat/out/control/ChatController.class differ diff --git a/pitiupi.mpChat/out/net/MpChatMessage.class b/pitiupi.mpChat/out/net/MpChatMessage.class new file mode 100644 index 0000000..2f15121 Binary files /dev/null and b/pitiupi.mpChat/out/net/MpChatMessage.class differ diff --git a/pitiupi.mpChat/out/net/TestChatMessage.class b/pitiupi.mpChat/out/net/TestChatMessage.class new file mode 100644 index 0000000..02c09f3 Binary files /dev/null and b/pitiupi.mpChat/out/net/TestChatMessage.class differ diff --git a/pitiupi.mpChat/out/net/TestMessage.class b/pitiupi.mpChat/out/net/TestMessage.class new file mode 100644 index 0000000..d472376 Binary files /dev/null and b/pitiupi.mpChat/out/net/TestMessage.class differ diff --git a/pitiupi.mpChat/out/plugin/MultiProtocolChat.class b/pitiupi.mpChat/out/plugin/MultiProtocolChat.class new file mode 100644 index 0000000..bbae454 Binary files /dev/null and b/pitiupi.mpChat/out/plugin/MultiProtocolChat.class differ diff --git a/pitiupi.mpChat/out/plugin/SocketTest.class b/pitiupi.mpChat/out/plugin/SocketTest.class new file mode 100644 index 0000000..549bbe9 Binary files /dev/null and b/pitiupi.mpChat/out/plugin/SocketTest.class differ diff --git a/pitiupi.mpChat/out/plugin/TestChat.class b/pitiupi.mpChat/out/plugin/TestChat.class new file mode 100644 index 0000000..29ca5c3 Binary files /dev/null and b/pitiupi.mpChat/out/plugin/TestChat.class differ diff --git a/pitiupi.mpChat/pitiupi.mpChat.iml b/pitiupi.mpChat/pitiupi.mpChat.iml new file mode 100644 index 0000000..4e2cbb3 --- /dev/null +++ b/pitiupi.mpChat/pitiupi.mpChat.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pitiupi.mpChat/src/GUI/ChatMenu.java b/pitiupi.mpChat/src/GUI/ChatMenu.java new file mode 100644 index 0000000..f336426 --- /dev/null +++ b/pitiupi.mpChat/src/GUI/ChatMenu.java @@ -0,0 +1,81 @@ +package GUI; + +import plugin.MultiProtocolChat; + +import javax.swing.*; +import java.awt.*; + +public class ChatMenu extends JMenu { + private final MultiProtocolChat multiProtocolChat; + + public ChatMenu(MultiProtocolChat multiProtocolChat) { + this.multiProtocolChat = multiProtocolChat; + this.setText("MP-Chat"); + + JMenuItem startPrivateChat = new JMenuItem("start Private Chat"); + startPrivateChat.addActionListener(_ -> showPrivateChatDialog()); + this.add(startPrivateChat); + } + + public void newMenuItem(JMenuItem newMenuItem, ChatWindow chatWindow) { + newMenuItem.addActionListener(_ -> chatWindow.setVisible(true)); + + this.add(newMenuItem); + } + + public void removeMenuItem(JMenuItem removeMenuItem) { + this.remove(removeMenuItem); + } + + private void showPrivateChatDialog() { + JDialog dialog = new JDialog( + SwingUtilities.getWindowAncestor(this), + "Novo chat privado", + Dialog.ModalityType.APPLICATION_MODAL + ); + + JTextField ipField = new JTextField(); + + JButton startButton = new JButton("Iniciar"); + JButton cancelButton = new JButton("Cancelar"); + + JPanel contentPanel = new JPanel(new BorderLayout(10, 10)); + contentPanel.setBorder( + BorderFactory.createEmptyBorder(10, 10, 10, 10) + ); + + JPanel ipPanel = new JPanel(new BorderLayout(5, 5)); + ipPanel.add(new JLabel("IP:"), BorderLayout.WEST); + ipPanel.add(ipField, BorderLayout.CENTER); + + JPanel buttonsPanel = new JPanel( + new FlowLayout(FlowLayout.RIGHT) + ); + + buttonsPanel.add(cancelButton); + buttonsPanel.add(startButton); + + JLabel errorLabel = new JLabel("Não foi possível estabelecer a conexão."); + errorLabel.setForeground(Color.RED); + errorLabel.setVisible(false); + + contentPanel.add(errorLabel, BorderLayout.NORTH); + contentPanel.add(ipPanel, BorderLayout.CENTER); + contentPanel.add(buttonsPanel, BorderLayout.SOUTH); + + cancelButton.addActionListener(e -> dialog.dispose()); + + startButton.addActionListener(e -> { + String ip = ipField.getText(); + + if (multiProtocolChat.openConnection(ip)) dialog.dispose(); + else errorLabel.setVisible(true); + + }); + + dialog.setContentPane(contentPanel); + dialog.pack(); + dialog.setLocationRelativeTo(SwingUtilities.getWindowAncestor(this)); + dialog.setVisible(true); + } +} diff --git a/pitiupi.mpChat/src/GUI/ChatWindow.java b/pitiupi.mpChat/src/GUI/ChatWindow.java new file mode 100644 index 0000000..bca8e1c --- /dev/null +++ b/pitiupi.mpChat/src/GUI/ChatWindow.java @@ -0,0 +1,94 @@ +package GUI; + +import net.MpChatMessage; +import pitiupi.control.ConnectionManager; + +import javax.swing.*; +import java.awt.*; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public abstract class ChatWindow extends JFrame { + + protected final ConnectionManager connectionManager; + + private JTextArea receivedMessagesArea; + protected JTextField messageField; + + public ChatWindow(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + initComponents(); + } + + private void initComponents() { + setTitle("Socket Test"); + setSize(700, 500); + setLocationRelativeTo(null); + setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + initReceivedMessagesPanel(mainPanel); + initBottomPanel(mainPanel); + + setContentPane(mainPanel); + } + + private void initReceivedMessagesPanel(JPanel mainPanel) { + receivedMessagesArea = new JTextArea(); + receivedMessagesArea.setEditable(false); + receivedMessagesArea.setLineWrap(true); + receivedMessagesArea.setWrapStyleWord(true); + + JScrollPane messagesScrollPane = new JScrollPane(receivedMessagesArea); + + messagesScrollPane.setBorder(BorderFactory.createTitledBorder("Mensagens recebidas")); + + mainPanel.add(messagesScrollPane, BorderLayout.CENTER); + } + + private void initBottomPanel(JPanel mainPanel) { + messageField = new JTextField(); + + JPanel bottomPanel = new JPanel(new BorderLayout(10, 10)); + + bottomPanel.add(messageField, BorderLayout.CENTER); + + initButtons(bottomPanel); + + mainPanel.add(bottomPanel, BorderLayout.SOUTH); + } + + protected abstract void initButtons(JPanel bottomPanel); + + protected MpChatMessage createMessage(String tipoMensagem) { + try { + String nome = connectionManager.getUserName(); + String ip = InetAddress.getLocalHost().getHostAddress(); + String message = messageField.getText(); + + return new MpChatMessage(nome, ip, tipoMensagem, message); + + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + public void receiveMessage(MpChatMessage msg) { + SwingUtilities.invokeLater(() -> { + String mensagem = String.format( + "[%s] %s (%s): %s%n", + msg.getTipoMensagem(), + msg.getNome(), + msg.getIp(), + msg.getMessage() + ); + + receivedMessagesArea.append(mensagem); + + receivedMessagesArea.setCaretPosition(receivedMessagesArea.getDocument().getLength()); + }); + } +} \ No newline at end of file diff --git a/pitiupi.mpChat/src/GUI/PrivateChatWindow.java b/pitiupi.mpChat/src/GUI/PrivateChatWindow.java new file mode 100644 index 0000000..56f111b --- /dev/null +++ b/pitiupi.mpChat/src/GUI/PrivateChatWindow.java @@ -0,0 +1,44 @@ +package GUI; + +import net.MpChatMessage; +import pitiupi.control.ConnectionManager; +import pitiupi.net.tcp.TcpConnection; + +import javax.swing.*; +import java.awt.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PrivateChatWindow extends ChatWindow { + + private final TcpConnection connection; + + public PrivateChatWindow(ConnectionManager ConnectionManager, TcpConnection connection) { + this.connection = connection; + + super(ConnectionManager); + } + + @Override + protected void initButtons(JPanel bottomPanel) { + JButton tcpButton = new JButton("TCP"); + + tcpButton.addActionListener(e -> sendTCP()); + + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); + + buttonsPanel.add(tcpButton); + + bottomPanel.add(buttonsPanel, BorderLayout.EAST); + } + + private void sendTCP() { + try { + MpChatMessage msg = createMessage("TCP Private"); + connection.send(msg.toByteArray()); + + } catch (Exception ex) { + Logger.getLogger(PrivateChatWindow.class.getName()).log(Level.SEVERE, null, ex); + } + } +} \ No newline at end of file diff --git a/pitiupi.mpChat/src/GUI/PublicChatWindow.java b/pitiupi.mpChat/src/GUI/PublicChatWindow.java new file mode 100644 index 0000000..ff6967e --- /dev/null +++ b/pitiupi.mpChat/src/GUI/PublicChatWindow.java @@ -0,0 +1,93 @@ +package GUI; + +import net.MpChatMessage; +import pitiupi.control.ConnectionManager; + +import javax.swing.*; +import java.awt.*; +import java.net.InetAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PublicChatWindow extends ChatWindow { + + private JTextField ipField; + + public PublicChatWindow(ConnectionManager connectionManager) { + super(connectionManager); + } + + @Override + protected void initButtons(JPanel bottomPanel) { + initIpField(bottomPanel); + initCommunicationButtons(bottomPanel); + } + + private void initIpField(JPanel bottomPanel) { + ipField = new JTextField(); + ipField.setPreferredSize(new Dimension(130, 30)); + + JPanel ipPanel = new JPanel(new BorderLayout(5, 5)); + + ipPanel.add(new JLabel("IP destino:"), BorderLayout.WEST); + + ipPanel.add(ipField, BorderLayout.CENTER); + + bottomPanel.add(ipPanel, BorderLayout.WEST); + } + + private void initCommunicationButtons(JPanel bottomPanel) { + JButton tcpButton = new JButton("TCP"); + JButton udpButton = new JButton("UDP"); + JButton multicastButton = new JButton("Multicast"); + + tcpButton.addActionListener(e -> sendTCP()); + udpButton.addActionListener(e -> sendUDP()); + multicastButton.addActionListener(e -> sendMulticast()); + + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); + + buttonsPanel.add(tcpButton); + buttonsPanel.add(udpButton); + buttonsPanel.add(multicastButton); + + bottomPanel.add(buttonsPanel, BorderLayout.EAST); + } + + private void sendTCP() { + try { + MpChatMessage msg = createMessage("TCP Unicast"); + + InetAddress ip = InetAddress.getByName(ipField.getText()); + + connectionManager.sendTcp(msg, ip); + + } catch (Exception ex) { + Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex); + } + } + + private void sendUDP() { + try { + MpChatMessage msg = createMessage("UDP Unicast"); + + InetAddress ip = InetAddress.getByName(ipField.getText()); + + connectionManager.sendUdpUnicast(msg, ip); + + } catch (Exception ex) { + Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex); + } + } + + private void sendMulticast() { + try { + MpChatMessage msg = createMessage("UDP Multicast"); + + connectionManager.sendUdpMulticast(msg); + + } catch (Exception ex) { + Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex); + } + } +} \ No newline at end of file diff --git a/pitiupi.mpChat/src/META-INF/services/pitiupi.plugin.Plugin b/pitiupi.mpChat/src/META-INF/services/pitiupi.plugin.Plugin new file mode 100644 index 0000000..3240a6f --- /dev/null +++ b/pitiupi.mpChat/src/META-INF/services/pitiupi.plugin.Plugin @@ -0,0 +1 @@ +plugin.MultiProtocolChat diff --git a/pitiupi.mpChat/src/control/ChatController.java b/pitiupi.mpChat/src/control/ChatController.java new file mode 100644 index 0000000..e437a4b --- /dev/null +++ b/pitiupi.mpChat/src/control/ChatController.java @@ -0,0 +1,61 @@ +package control; + +import GUI.ChatMenu; +import GUI.ChatWindow; +import GUI.PrivateChatWindow; +import GUI.PublicChatWindow; +import net.MpChatMessage; +import pitiupi.control.ConnectionManager; +import pitiupi.net.tcp.TcpConnection; + +import javax.swing.*; +import java.util.HashMap; + +public class ChatController { + ConnectionManager connectionManager; + ChatMenu chatMenu; + + PublicChatWindow publicChatWindow; + + private final HashMap chatMenus; + private final HashMap chatWindows; + + public ChatController(ConnectionManager connectionManager, ChatMenu chatMenu) { + this.connectionManager = connectionManager; + publicChatWindow = new PublicChatWindow(connectionManager); + + chatMenus = new HashMap<>(); + chatWindows = new HashMap<>(); + + this.chatMenu = chatMenu; + + this.chatMenu.newMenuItem(new JMenuItem("Show global chat"), publicChatWindow); + } + + public void addChatWindow(TcpConnection tcpConnection) { + if (chatWindows.containsKey(tcpConnection)) return; + + PrivateChatWindow newChat = new PrivateChatWindow(connectionManager, tcpConnection); + chatWindows.put(tcpConnection, newChat); + JMenuItem newMenuItem = new JMenuItem(); + chatMenus.put(tcpConnection, newMenuItem); + newMenuItem.setText(tcpConnection.getAddress().toString()); + chatMenu.newMenuItem(newMenuItem, newChat); + } + + public void removeChatWindow(TcpConnection tcpConnection) { + ChatWindow removedWindow = chatWindows.remove(tcpConnection); + JMenuItem removedMenu = chatMenus.remove(tcpConnection); + + chatMenu.removeMenuItem(removedMenu); + removedWindow.setVisible(false); + removedWindow.dispose(); + } + + public void receivePublic(MpChatMessage message) { + publicChatWindow.receiveMessage(message); + } + public void receivePrivate(TcpConnection tcpConnection, MpChatMessage message) { + chatWindows.get(tcpConnection).receiveMessage(message); + } +} diff --git a/pitiupi.mpChat/src/net/MpChatMessage.java b/pitiupi.mpChat/src/net/MpChatMessage.java new file mode 100644 index 0000000..731367b --- /dev/null +++ b/pitiupi.mpChat/src/net/MpChatMessage.java @@ -0,0 +1,35 @@ +package net; + +import pitiupi.net.Message; + +public class MpChatMessage extends Message { + private String nome; + private String ip; + private String tipoMensagem; + private String message; + + public MpChatMessage(String nome, String ip, String tipoMensagem, String message) { + this.nome = nome; + this.ip = ip; + this.tipoMensagem = tipoMensagem; + this.message = message; + } + + public MpChatMessage(){} + + public String getNome() { + return nome; + } + + public String getIp() { + return ip; + } + + public String getTipoMensagem() { + return tipoMensagem; + } + + public String getMessage() { + return message; + } +} diff --git a/pitiupi.mpChat/src/plugin/MultiProtocolChat.java b/pitiupi.mpChat/src/plugin/MultiProtocolChat.java new file mode 100644 index 0000000..089c582 --- /dev/null +++ b/pitiupi.mpChat/src/plugin/MultiProtocolChat.java @@ -0,0 +1,107 @@ +package plugin; + +import GUI.ChatMenu; +import control.ChatController; +import net.MpChatMessage; +import pitiupi.control.ConnectionManager; +import pitiupi.control.GuiManager; +import pitiupi.net.Message; +import pitiupi.net.tcp.TcpConnection; +import pitiupi.plugin.PrivateConnectionPlugin; + +import javax.swing.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetAddress; + +public class MultiProtocolChat implements PrivateConnectionPlugin{ + ChatController chatController; + ConnectionManager connectionManager; + GuiManager guiManager; + + @Override + public String getName() { + return "MultiProtocol chat"; + } + + @Override + public String getAuthor() { + return "Gustavo Henrique"; + } + + @Override + public String getVersion() { + return "0"; + } + + @Override + public String getIdentifier() { + return "Multi.Protocol.Chat.Pitiupi"; + } + + @Override + public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) { + this.connectionManager = connectionManager; + this.guiManager = guiManager; + + ChatMenu chatMenu = new ChatMenu(this); + chatController = new ChatController(connectionManager, chatMenu); + guiManager.addMenu(chatMenu); + } + + @Override + public Message getMessage() { + return new MpChatMessage(); + } + + @Override + public void receiveMessage(byte[] message) { + MpChatMessage mpChatMessage = parseMessage(message); + chatController.receivePublic(mpChatMessage); + } + + + @Override + public void receivePrivateConnection(TcpConnection tcpConnection) { + SwingUtilities.invokeLater(() -> { + chatController.addChatWindow(tcpConnection); + }); + } + + @Override + public void receiveBytes(TcpConnection connection, byte[] bytes) { + MpChatMessage mpChatMessage = parseMessage(bytes); + chatController.receivePrivate(connection, mpChatMessage); + } + + @Override + public void onSocketClosed(TcpConnection tcpConnection) { + chatController.removeChatWindow(tcpConnection); + } + + public boolean openConnection(String ip) { + try { + connectionManager.connectPluginTcp(InetAddress.getByName(ip), getIdentifier(), getIdentifier()); + return true; + } + catch (Exception e) { + System.out.println("Error opening connection: " + e.getMessage()); + } + return false; + } + + private MpChatMessage parseMessage(byte[] bytes) { + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream ois; + try { + ois = new ObjectInputStream(bis); + if (ois.readObject() instanceof MpChatMessage msg) { + return msg; + } + } catch (IOException | ClassNotFoundException ex) { + System.out.println(ex.toString()); + } + return null; + } +} diff --git a/pitiupi.screenCast/.idea/ant.xml b/pitiupi.screenCast/.idea/ant.xml new file mode 100644 index 0000000..a2a4769 --- /dev/null +++ b/pitiupi.screenCast/.idea/ant.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pitiupi.screenCast/.idea/misc.xml b/pitiupi.screenCast/.idea/misc.xml new file mode 100644 index 0000000..59ad060 --- /dev/null +++ b/pitiupi.screenCast/.idea/misc.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/pitiupi.screenCast/.idea/vcs.xml b/pitiupi.screenCast/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/pitiupi.screenCast/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pitiupi.screenCast/.idea/workspace.xml b/pitiupi.screenCast/.idea/workspace.xml new file mode 100644 index 0000000..0438088 --- /dev/null +++ b/pitiupi.screenCast/.idea/workspace.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + { + "associatedIndex": 7 +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1784745206049 + + + + + + \ No newline at end of file diff --git a/pitiupi.screenCast/build/built-jar.properties b/pitiupi.screenCast/build/built-jar.properties index 6898243..1f21a31 100644 --- a/pitiupi.screenCast/build/built-jar.properties +++ b/pitiupi.screenCast/build/built-jar.properties @@ -1,6 +1,6 @@ -#Thu, 16 Jul 2026 15:21:39 -0300 +#Tue, 29 Sep 2026 11:04:58 -0300 -/home/flavio/NetBeansProjects/pitiupi.screenCast= +C\:\\Users\\Gustavo\\OneDrive\\Documentos\\Faculdade\\2026_2\\Monitoria\\pitiupi\\pitiupi.screenCast= -/home/flavio/NetBeansProjects/pitiupi= +C\:\\Users\\Gustavo\\OneDrive\\Documentos\\Faculdade\\2026_2\\Monitoria\\pitiupi\\pitiupi= diff --git a/pitiupi.screenCast/build/classes/pitiupi/screenCast/GUI/ScreenCastWindow.class b/pitiupi.screenCast/build/classes/pitiupi/screenCast/GUI/ScreenCastWindow.class index 490ff3f..f8d140d 100644 Binary files a/pitiupi.screenCast/build/classes/pitiupi/screenCast/GUI/ScreenCastWindow.class and b/pitiupi.screenCast/build/classes/pitiupi/screenCast/GUI/ScreenCastWindow.class differ diff --git a/pitiupi.screenCast/build/classes/pitiupi/screenCast/plugin/ScreenCastPlugin.class b/pitiupi.screenCast/build/classes/pitiupi/screenCast/plugin/ScreenCastPlugin.class index 77a7e76..b26bee9 100644 Binary files a/pitiupi.screenCast/build/classes/pitiupi/screenCast/plugin/ScreenCastPlugin.class and b/pitiupi.screenCast/build/classes/pitiupi/screenCast/plugin/ScreenCastPlugin.class differ diff --git a/pitiupi.screenCast/dist/pitiupi.screenCast.jar b/pitiupi.screenCast/dist/pitiupi.screenCast.jar deleted file mode 100644 index 06682ea..0000000 Binary files a/pitiupi.screenCast/dist/pitiupi.screenCast.jar and /dev/null differ diff --git a/pitiupi.screenCast/src/pitiupi.screenCast.iml b/pitiupi.screenCast/src/pitiupi.screenCast.iml new file mode 100644 index 0000000..937cc1c --- /dev/null +++ b/pitiupi.screenCast/src/pitiupi.screenCast.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pitiupi.screenCast/src/pitiupi/screenCast/GUI/ScreenCastWindow.java b/pitiupi.screenCast/src/pitiupi/screenCast/GUI/ScreenCastWindow.java index f3ae1ca..cfd0022 100644 --- a/pitiupi.screenCast/src/pitiupi/screenCast/GUI/ScreenCastWindow.java +++ b/pitiupi.screenCast/src/pitiupi/screenCast/GUI/ScreenCastWindow.java @@ -5,6 +5,8 @@ */ package pitiupi.screenCast.GUI; +import pitiupi.control.ConnectionManager; +import pitiupi.control.GuiManager; import pitiupi.screenCast.control.AlarmClock; import pitiupi.screenCast.control.ImageTransformation; import pitiupi.screenCast.control.ScreenShot; @@ -38,10 +40,13 @@ public class ScreenCastWindow extends javax.swing.JFrame { public final static int TIME = 1000; private float compressRatio; - private final MainWindow mainWindow; + private final ConnectionManager connectionManager; + private final GuiManager guiManager; + + public ScreenCastWindow(ConnectionManager connectionManager, GuiManager guiManager) { + this.connectionManager = connectionManager; + this.guiManager = guiManager; - public ScreenCastWindow(MainWindow mainWindow) { - this.mainWindow = mainWindow; this.compressRatio = 0.5f; this.initComponents(); } @@ -91,19 +96,12 @@ public class ScreenCastWindow extends javax.swing.JFrame { BufferedImage image = ScreenShot.takeAShot(); BufferedImage rescaled = ImageTransformation.resize(image, 1024, 768); byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio); - ScreenCastMessage message = new ScreenCastMessage(this.mainWindow.getUserName(), byteImage); - msg = message.toByteArray(); + ScreenCastMessage message = new ScreenCastMessage(this.connectionManager.getUserName(), byteImage); + + if (!connectionManager.sendUdpMulticast(message)) this.compressRatio -= 0.1f; } catch (AWTException | IOException ex) { Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex); } - if (msg != null) { - try { - this.mainWindow.getSocket().send(msg); - } catch (IOException ex) { - System.out.println(ex.toString()); - this.compressRatio -= 0.1f; - } - } } public void receiveScreenCastMessage(ScreenCastMessage message) { diff --git a/pitiupi.screenCast/src/pitiupi/screenCast/plugin/ScreenCastPlugin.java b/pitiupi.screenCast/src/pitiupi/screenCast/plugin/ScreenCastPlugin.java index b8d593a..50641a6 100644 --- a/pitiupi.screenCast/src/pitiupi/screenCast/plugin/ScreenCastPlugin.java +++ b/pitiupi.screenCast/src/pitiupi/screenCast/plugin/ScreenCastPlugin.java @@ -8,6 +8,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import pitiupi.GUI.MainWindow; +import pitiupi.control.ConnectionManager; +import pitiupi.control.GuiManager; import pitiupi.net.Message; import pitiupi.plugin.Plugin; import pitiupi.screenCast.GUI.ScreenCastMenu; @@ -21,7 +23,6 @@ import pitiupi.screenCast.net.ScreenCastMessage; public class ScreenCastPlugin implements Plugin { private ScreenCastWindow screenCastWindow; - private MainWindow mainWindow; @Override public String getName() { @@ -39,10 +40,14 @@ public class ScreenCastPlugin implements Plugin { } @Override - public void createPlugin(MainWindow mainWindow) { - this.mainWindow = mainWindow; - this.screenCastWindow = new ScreenCastWindow(mainWindow); - this.mainWindow.addMenu(new ScreenCastMenu(screenCastWindow)); + public String getIdentifier() { + return "Pitiupi.ScreenCast.Example"; + } + + @Override + public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) { + this.screenCastWindow = new ScreenCastWindow(connectionManager, guiManager); + guiManager.addMenu(new ScreenCastMenu(screenCastWindow)); } @Override @@ -54,9 +59,7 @@ public class ScreenCastPlugin implements Plugin { if (ois.readObject() instanceof ScreenCastMessage msg) { this.screenCastWindow.receiveScreenCastMessage(msg); } - } catch (IOException ex) { - System.out.println(ex.toString()); - } catch (ClassNotFoundException ex) { + } catch (IOException | ClassNotFoundException ex) { System.out.println(ex.toString()); } } diff --git a/pitiupi/.gitignore b/pitiupi/.gitignore index 7530920..31f5f52 100644 --- a/pitiupi/.gitignore +++ b/pitiupi/.gitignore @@ -8,3 +8,5 @@ # JARs gerados *.jar + +.idea/ diff --git a/pitiupi/.idea/.gitignore b/pitiupi/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/pitiupi/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/pitiupi/.idea/inspectionProfiles/Project_Default.xml b/pitiupi/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..a05c602 --- /dev/null +++ b/pitiupi/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/pitiupi/.idea/libraries/pitiupi.xml b/pitiupi/.idea/libraries/pitiupi.xml new file mode 100644 index 0000000..95cf1ce --- /dev/null +++ b/pitiupi/.idea/libraries/pitiupi.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pitiupi/.idea/libraries/plugins.xml b/pitiupi/.idea/libraries/plugins.xml new file mode 100644 index 0000000..4c565c7 --- /dev/null +++ b/pitiupi/.idea/libraries/plugins.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/pitiupi/.idea/misc.xml b/pitiupi/.idea/misc.xml new file mode 100644 index 0000000..0ae1195 --- /dev/null +++ b/pitiupi/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pitiupi/.idea/modules.xml b/pitiupi/.idea/modules.xml new file mode 100644 index 0000000..4c6dc03 --- /dev/null +++ b/pitiupi/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pitiupi/.idea/vcs.xml b/pitiupi/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/pitiupi/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pitiupi/docs/Documentation.md b/pitiupi/docs/Documentation.md new file mode 100644 index 0000000..fb7024a --- /dev/null +++ b/pitiupi/docs/Documentation.md @@ -0,0 +1,547 @@ +# Visão geral + +O `Pitiupi` é uma plataforma P2P baseada em uma arquitetura de plugins, +desenvolvida para permitir a criação de aplicações distribuídas sobre uma +infraestrutura comum de comunicação em rede. + +A aplicação fornece mecanismos de descoberta de usuários, comunicação entre +peers, troca de mensagens serializadas e integração de extensões por meio de +plugins. A comunicação de rede utiliza dois protocolos de transporte: UDP, +empregado principalmente na descoberta e comunicação multicast entre as +instâncias, e TCP, utilizado para comunicação direta e persistente entre peers. + +Dessa forma, novos comportamentos podem ser adicionados por meio de plugins +sem modificar o núcleo da aplicação. + +O projeto foi desenvolvido como suporte às práticas da disciplina de Redes de +Computadores, servindo também como base para experimentação de aplicações +distribuídas e desenvolvimento de projetos acadêmicos. + +# Arquitetura geral + +A aplicação é dividida em três grupos principais: + +- **Núcleo da aplicação**: A `MainWindow` atua como ponto central da aplicação, + sendo responsável por inicializar e integrar esses componentes durante a + execução. +- **Sistema de plugins**: responsável por adicionar funcionalidades sem + alterar o núcleo da aplicação. +- **Comunicação de rede**: responsável pela troca de mensagens entre + instâncias da aplicação. + +## Inicialização da aplicação + +1. A MainWindow é criada. +2. O SocketUDP é inicializado, associa-se à porta configurada e ingressa no + grupo multicast. +3. O SocketTCP é inicializado e cria um ServerSocket na porta da aplicação + para aceitar conexões TCP. +4. Os mecanismos de comunicação iniciam seus respectivos processos de recepção. +5. O HeartbeatManager é criado e inicia o envio periódico de heartbeats. +6. O painel de usuários online é registrado como listener do HeartbeatManager. +7. O PluginLoader procura e carrega os plugins disponíveis. + +# Sistema de comunicação + +A comunicação entre as instâncias da aplicação é realizada por meio de dois +mecanismos complementares: + +- **UDP multicast/unicast**, implementado por `SocketUDP` +- **TCP**, implementado por `SocketTCP` e `TcpConnection` + +Ambos os mecanismos utilizam mensagens serializadas em `byte[]`, permitindo que +os plugins permaneçam independentes dos detalhes do protocolo de transporte. + +## Comunicação UDP + +A classe `SocketUDP` é responsável pela comunicação UDP da aplicação. + +Ela utiliza um `MulticastSocket` associado à porta configurada pela aplicação e +ingressa no grupo multicast `224.0.0.3`. Por ser baseada em UDP, a comunicação não +estabelece uma conexão permanente entre os peers. + +A classe estende `Thread`, mantendo um processo contínuo de recepção de datagramas +enquanto a aplicação está em execução. + +O SocketUDP suporta dois modos de envio: + +- **multicast**, destinado ao grupo de peers +- **unicast**, destinado diretamente ao endereço IP de um peer específico + +### Inicialização + +Durante sua inicialização, o `SocketUDP`: + +1. obtém o endereço do grupo multicast; +2. cria um `MulticastSocket` utilizando a porta configurada na `MainWindow`; +3. configura os buffers de envio e recepção; +4. habilita o reuso do endereço; +5. ingressa no grupo multicast. + +O tamanho dos buffers de comunicação é configurado para `256000` bytes. + +### Envio de mensagens UDP + +O `SocketUDP` suporta dois modos de envio de mensagens: multicast e unicast, ambos +baseados no mesmo mecanismo interno de criação e envio de DatagramPacket. + +O método `sendMulticast()` envia uma mensagem para o grupo multicast da aplicação. +Nesse caso, o `DatagramPacket` é direcionado ao endereço multicast configurado, +permitindo que todas as instâncias participantes do grupo recebam a mensagem +simultaneamente. + +Já o método `sendUnicast()` permite enviar uma mensagem diretamente para um peer +específico. Nesse caso, o `DatagramPacket` é direcionado ao endereço IP informado pelo +chamador, mantendo a mesma porta utilizada pela aplicação. + +Ambos os métodos são, na prática, abstrações de um único mecanismo interno de envio, +que utiliza o mesmo processo de criação e envio de `DatagramPacket` por baixo dos +panos. A diferença entre eles está apenas no endereço de destino: enquanto o multicast +utiliza o grupo compartilhado, o unicast direciona o pacote a um IP específico. + +### Recepção de mensagens + +O método `receive()` mantém um loop aguardando novos datagramas. + +Ao receber uma mensagem, o `SocketUDP` inicialmente tenta identificar se os dados +correspondem a uma `HeartbeatMessage`. + +O processamento segue o seguinte fluxo: + +1. O datagrama é recebido pelo `MulticastSocket`. +2. Os dados recebidos são analisados pelo método `tryParseHeartbeat()`. +3. Caso seja identificada uma `HeartbeatMessage`, ela é encaminhada ao + `HeartbeatManager`, juntamente com o endereço IP de origem. +4. Caso não seja um heartbeat, a mensagem é encaminhada para todos os plugins + carregados. +5. Cada plugin decide se a mensagem recebida pertence à sua funcionalidade. + +Essa separação mantém as mensagens de infraestrutura, como heartbeat, sob +responsabilidade do núcleo da aplicação, enquanto as mensagens específicas das +funcionalidades são processadas pelos plugins. + +### Identificação de mensagens de heartbeat + +O método `tryParseHeartbeat()` tenta desserializar os dados recebidos e verificar se o +objeto resultante é uma instância de `HeartbeatMessage`. + +Caso a mensagem não possa ser desserializada ou não corresponda ao tipo esperado, o +método retorna `null`. Nesse caso, a mensagem é tratada como uma mensagem destinada aos +plugins. + +### Encerramento + +O método `close()` interrompe a execução do processo de recepção, remove a aplicação +do grupo multicast e fecha o `MulticastSocket`. + +A variável `running` é utilizada para sinalizar que o processo de recepção deve ser +encerrado. + +## Comunicação TCP + +A comunicação TCP é implementada pela classe `SocketTCP`. + +Diferentemente do UDP, o TCP estabelece conexões entre dois peers. Essas conexões +são mantidas enquanto forem necessárias e podem ser reutilizadas para o envio de +múltiplas mensagens. + +A arquitetura utiliza uma conexão TCP persistente por endereço de peer. + +O SocketTCP possui um mapa de conexões: + +`InetAddress` -> `TcpConnection` + +Esse mapa permite localizar rapidamente uma conexão existente para determinado peer e +reutilizá-la durante novos envios. + +### SocketTCP + +A classe `SocketTCP` estende `Thread` e implementa `PeerListener`. + +Ao ser criada, ela inicializa um `ServerSocket` utilizando a porta configurada na +aplicação. + +O servidor TCP permanece aguardando novas conexões enquanto a aplicação estiver +em execução. + +### Envio de mensagens TCP + +O método `send()` recebe uma mensagem serializada e o endereço IP do peer destinatário. + +Antes de enviar a mensagem, o `SocketTCP` verifica se já existe uma conexão válida com +o peer. + +O comportamento é: + +1. Procurar uma conexão existente no mapa de conexões. +2. Verificar se a conexão está fechada. +3. Caso não exista uma conexão válida, criar um novo Socket TCP para o endereço do peer. +4. Encapsular o socket em uma `TcpConnection`. +5. Registrar a conexão no mapa. +6. Iniciar uma tarefa de recepção para essa conexão. +7. Enviar a mensagem pela conexão. + +Assim, uma conexão TCP existente pode ser reutilizada para várias mensagens, evitando +a criação de um novo socket para cada envio. + +### Recepção de conexões + +O método `receive()` permanece aguardando novas conexões através de +`ServerSocket.accept()`. + +Quando uma conexão é aceita: + +O endereço IP do peer é obtido. +Um objeto `TcpConnection` é criado para encapsular o socket. +A conexão é registrada no mapa de conexões. +Caso já exista uma conexão associada ao mesmo endereço, a conexão anterior é encerrada. +Uma tarefa independente de recepção é criada para a nova conexão. + +Esse modelo permite que o servidor aceite múltiplas conexões sem bloquear o +processamento das conexões já existentes. + +### Recepção concorrente + +A recepção das mensagens TCP é realizada de maneira concorrente por meio de um +`ExecutorService`. + +Cada conexão possui uma tarefa própria responsável por receber continuamente as +mensagens. + +O fluxo de recepção é: + +``` +SocketTCP +| ++-- Peer A → TcpConnection → tarefa de recepção +| ++-- Peer B → TcpConnection → tarefa de recepção +| ++-- Peer C → TcpConnection → tarefa de recepção +``` + +Dessa forma, uma conexão bloqueada ou aguardando dados não impede que outras conexões +continuem sendo processadas. + +Quando uma mensagem é recebida por uma `TcpConnection`, ela é encaminhada para todos os +plugins registrados na `MainWindow`. + +O plugin responsável pela funcionalidade deve identificar o tipo da mensagem e +realizar o processamento correspondente. + +### TcpConnection + +A classe TcpConnection encapsula uma conexão TCP individual entre dois peers. + +Ela mantém: + +o Socket utilizado na conexão; +um DataInputStream para recepção; +um DataOutputStream para envio. + +Além de encapsular esses recursos, a classe é responsável pelo framing das mensagens. + +### Framing das mensagens TCP + +A classe `TcpConnection` encapsula uma conexão TCP individual entre dois peers. + +Ela mantém: + +1. o `Socket` utilizado na conexão; +2. um `DataInputStream` para recepção; +3. um `DataOutputStream` para envio. + +Além de encapsular esses recursos, a classe é responsável pelo framing das mensagens. + +Framing das mensagens TCP + +O protocolo TCP fornece um fluxo contínuo de bytes e não preserva os limites das +mensagens enviadas pela aplicação. + +Por esse motivo, é adicionado o tamanho da mensagem antes do conteúdo. + +O formato utilizado é: + +``` ++----------------------+----------------------+ +| tamanho da mensagem | dados da mensagem | ++----------------------+----------------------+ +4 bytes N bytes +``` + +O tamanho é enviado como um `int`, seguido pelos bytes da mensagem. + +No envio, o método `TcpConnection.send()`: + +1. obtém o tamanho do vetor de bytes; +2. escreve o tamanho no `DataOutputStream`; +3. escreve os dados da mensagem; +4. realiza `flush()` no stream. + +O acesso ao stream de saída é sincronizado para evitar que envios concorrentes +misturem seus dados. + +Na recepção, o método `receive()`: + +1. lê o tamanho da mensagem; +2. verifica se o tamanho é válido; +3. lê a quantidade correspondente de bytes; +4. retorna o vetor de bytes completo. + +Esse mecanismo permite que várias mensagens sejam transmitidas pela mesma conexão +TCP sem que o receptor perca a delimitação entre elas. + +### Integração entre TCP e gerenciamento de peers + +O `SocketTCP` implementa a interface PeerListener para acompanhar as alterações na +lista de peers ativos. + +O `HeartbeatManager` mantém a lista de peers atualmente detectados na rede. Quando +essa lista é alterada, o SocketTCP recebe a notificação por meio do método +`onPeersChanged()`. + +O método obtém os endereços IP dos peers atualmente ativos e compara esses endereços +com as conexões TCP existentes. + +Quando uma conexão pertence a um peer que não está mais ativo: + +a conexão é removida do mapa; +a conexão TCP é encerrada; +os recursos associados são liberados. + +Esse mecanismo impede que conexões TCP permaneçam abertas indefinidamente para peers +que já deixaram de participar da rede. + +O fluxo de integração pode ser representado da seguinte forma: +``` +HeartbeatManager +| +| peers ativos alterados +| +v +SocketTCP.onPeersChanged() +| +| compara peers ativos +| +v ++---- peer ativo ------> mantém conexão +| ++---- peer inativo ----> fecha conexão +``` + +### Message + +A classe `Message` representa a estrutura base das mensagens trocadas pela aplicação. + +Todas as mensagens utilizadas pelo sistema devem herdar dessa classe. Ela implementa +`Serializable`, permitindo que objetos de mensagem sejam convertidos em vetores de +bytes para transmissão pela rede. + +A serialização é realizada pelo método `toByteArray()`, que transforma uma instância +da mensagem em uma representação binária enviada pelos mecanismos de comunicação. + +As mensagens específicas devem estender essa classe, adicionando os atributos e +comportamentos necessários para cada funcionalidade. + +A camada de transporte não depende do conteúdo específico das mensagens. Tanto o +UDP quanto o TCP recebem mensagens serializadas como `byte[]`. + +### Heartbeat + +O mecanismo de heartbeat é utilizado para identificar quais usuários estão ativos +na rede. + +A aplicação envia periodicamente uma `HeartbeatMessage` contendo informações do usuário, +um identificador único da instância da aplicação e um timestamp. + +Essas mensagens são transmitidas utilizando a comunicação UDP multicast. + +Quando outra instância recebe uma mensagem de heartbeat, o `SocketUDP` identifica +o tipo da mensagem e encaminha o objeto ao `HeartbeatManager`, juntamente com o endereço +IP de origem. + +O `HeartbeatManager` registra ou atualiza o peer correspondente, utilizando o +identificador da instância para diferenciá-lo das demais. + +Um peer é considerado inativo quando permanece mais de 15 segundos sem receber um novo +heartbeat. O `HeartbeatManager` então remove o peer da lista e notifica todos os +componentes registrados como listeners de mudanças de peers. + +Esses listeners podem incluir diferentes partes da aplicação, como componentes +da interface gráfica, mecanismos de comunicação (como o `SocketTCP`) e também plugins +que desejem reagir à presença ou ausência de usuários na rede. + +Dessa forma, o sistema permite que tanto o núcleo da aplicação quanto extensões +externas sejam notificados sobre alterações no estado da rede, mantendo a arquitetura +flexível e extensível. + +O fluxo completo é: + +``` +HeartbeatManager +| +| cria HeartbeatMessage +| +v +Message.toByteArray() +| +v +SocketUDP.sendMulticast() +| +v +UDP Multicast +| +v +SocketUDP.receive() +| +v +HeartbeatManager.receiveHeartbeat() +| +v +Lista de peers ativos +| ++----> Interface de usuários online +| ++----> SocketTCP (PeerListener) +| ++----> Plugins (PeerListener) +``` + +## Sistema de plugins + +O sistema de plugins é responsável por permitir a extensão das funcionalidades da +aplicação de forma modular, sem a necessidade de alterações no núcleo do sistema. +Ele define um contrato comum que deve ser seguido por todas as extensões, garantindo +que possam ser carregadas dinamicamente e integradas à comunicação e à interface da +aplicação. + +### Interface Plugin + +A interface `Plugin` define o contrato que deve ser implementado por qualquer +extensão da aplicação. + +Um plugin deve fornecer informações básicas de identificação (`name`, +`author` e `version`), inicializar seu estado através de `createPlugin()` e +participar do sistema de comunicação utilizando mensagens derivadas de +`Message`. + +Os principais métodos são: + +#### Identificação + +Os métodos: +- `getName()` +- `getAuthor()` +- `getVersion()` + +fornecem informações utilizadas pela aplicação para identificar o plugin. + +#### Inicialização + +`createPlugin(MainWindow window)` + +É chamado após o carregamento do plugin e permite que ele registre seus +componentes na aplicação, como menus ou elementos de interface. + +#### Comunicação + +`getMessage()` + +Deve retornar uma mensagem utilizada pelo plugin para comunicação com outras +instâncias da aplicação. + +`receiveMessage(byte[] message)` + +É chamado pela aplicação sempre que uma mensagem é recebida. Cada plugin deve +verificar se a mensagem pertence ao seu tipo e realizar o processamento +correspondente. + +### Descoberta de plugins + +Para serem reconhecidos, os plugins devem possuir os arquivos de configuração +do Java Service Provider Interface (SPI): + +``` +MeusPlugins +| ++---META-INF + | + +---services + | + +---pitiupi.plugin.Plugin +``` + +O arquivo `pitiupi.plugin.Plugin` deve conter o nome completo das classes que +implementam a interface Plugin: + +``` +MeusPlugins.plugins.NomeDaClasse +``` + +Para serem carregados, os plugins devem estar empacotados como arquivos JAR +e colocados no diretório plugins da aplicação. + +### Carregamento de plugins + +Ao iniciar a aplicação, o `PluginLoader` executa os seguintes passos: + +1. Procura arquivos `.jar` dentro do diretório `plugins`. +2. Cria um `URLClassLoader` contendo os JARs encontrados. +3. Utiliza `ServiceLoader` para localizar classes que implementam + `Plugin`. +4. Instancia cada plugin encontrado. +5. Chama o método `createPlugin(MainWindow)`. +6. Registra o plugin na lista de plugins ativos da aplicação. + +## Interface gráfica + +A interface gráfica da aplicação é baseada em Swing e é centralizada na +classe `MainWindow`. + +Durante a inicialização, cada plugin recebe uma referência para a +`MainWindow` através do método `createPlugin(MainWindow)`. + +Essa referência permite integrar componentes gráficos à aplicação, sendo a +principal forma de extensão a adição de menus à barra de menus: + +``` +JMenu menu = new JMenu("Meu Plugin"); +window.addMenu(menu); +``` + +Visão geral da comunicação + +A arquitetura de comunicação pode ser resumida da seguinte maneira: +``` + +------------------+ + | MainWindow | + +--------+---------+ + | + +--------------------+--------------------+ + | | | + v v v + HeartbeatManager SocketUDP SocketTCP + | | | + | | | + v v v + Heartbeat UDP Multicast TCP Connections + | | + | | + +---------+----------+ + | + v + Plugins + | + v + Message +``` + +O UDP multicast fornece a infraestrutura de descoberta e manutenção da presença dos +peers, além de permitir comunicação multicast e unicast. + +O TCP fornece canais diretos e persistentes entre peers, com uma conexão reutilizável +por endereço. + +Os dois mecanismos encaminham as mensagens destinadas às funcionalidades da aplicação +para os plugins, mantendo separadas as responsabilidades de transporte, infraestrutura +e lógica específica de cada extensão. \ No newline at end of file diff --git a/pitiupi/docs/PluginTutorial.md b/pitiupi/docs/PluginTutorial.md new file mode 100644 index 0000000..120dcfd --- /dev/null +++ b/pitiupi/docs/PluginTutorial.md @@ -0,0 +1,2 @@ +# Como criar plugins para o Share my Sheet + diff --git a/pitiupi/pitiupi.iml b/pitiupi/pitiupi.iml new file mode 100644 index 0000000..c90834f --- /dev/null +++ b/pitiupi/pitiupi.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pitiupi/plugins/pitiupi.chat.jar b/pitiupi/plugins/pitiupi.chat.jar deleted file mode 120000 index b2c6a32..0000000 --- a/pitiupi/plugins/pitiupi.chat.jar +++ /dev/null @@ -1 +0,0 @@ -../../pitiupi.chat/dist/pitiupi.chat.jar \ No newline at end of file diff --git a/pitiupi/plugins/pitiupi.screenCast.jar b/pitiupi/plugins/pitiupi.screenCast.jar deleted file mode 120000 index e6a0dc7..0000000 --- a/pitiupi/plugins/pitiupi.screenCast.jar +++ /dev/null @@ -1 +0,0 @@ -../../pitiupi.screenCast/dist/pitiupi.screenCast.jar \ No newline at end of file diff --git a/pitiupi/src/pitiupi/GUI/About.java b/pitiupi/src/pitiupi/GUI/About.java new file mode 100644 index 0000000..8e92516 --- /dev/null +++ b/pitiupi/src/pitiupi/GUI/About.java @@ -0,0 +1,72 @@ +/* + * 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.GUI; + +import javax.swing.*; + +/** + * Janela que exibe informações sobre a aplicação, incluindo versão, + * descrição e autores. + * + * @author gustavo + */ + +public class About extends javax.swing.JFrame { + public About () { + this.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE); + initComponents(); + } + + private void initComponents() { + this.setTitle("About"); + this.setBounds(new java.awt.Rectangle(0, 0, 800, 600)); + this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5)); + this.setPreferredSize(new java.awt.Dimension(800, 600)); + + JScrollPane scroll = getJScrollPane(); + + this.add(scroll); + } + + private static JScrollPane getJScrollPane() { + JEditorPane texto = new JEditorPane(); + texto.setContentType("text/html"); + texto.setEditable(false); + + texto.setText(""" + + +

Share My Sheet

+ \s +

+ 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 +

+ \s +

+ 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 List plugins; - - + /** + * Cria a janela principal da aplicação. + * + * @param userName nome utilizado para identificação na rede. + * @param port porta utilizada na comunicação multicast. + */ public MainWindow(String userName, int port) { - this.userName = userName; - this.port = port; - this.menuBar = new MenuBar(this); - this.plugins = new ArrayList<>(); - this.initComponents(); + initMenu(); + + pluginManager = new PluginManager(this); + + connectionManager = new ConnectionManager(this, userName, pluginManager, port); + guiManager = new GuiManager(this); + + initUserPanel(); + initStatusPanel(); + + loadPlugins(); + + this.pack(); + + this.setSize(new java.awt.Dimension(1024, 768)); } - private void initComponents() { + private void initMenu() { + menuBar = new MenuBar(this); + this.setJMenuBar(menuBar); this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); this.setTitle("Share My Sheet"); - jScrollPane1 = new javax.swing.JScrollPane(); - tabbedPane = new javax.swing.JTabbedPane(); - statusPanel = new javax.swing.JPanel(); - status = new javax.swing.JLabel(); + javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane(); + javax.swing.JTabbedPane tabbedPane = new javax.swing.JTabbedPane(); jScrollPane1.setViewportView(tabbedPane); - statusPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT)); - status.setText("Connected on port " + this.port + " name " + this.userName); - statusPanel.add(status); this.getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER); - this.getContentPane().add(statusPanel, java.awt.BorderLayout.PAGE_END); - - this.pack(); - - this.socket = new Socket(this); - this.socket.start(); - - this.heartbeatManager = new HeartbeatManager(this); - this.onlineUsersPanel = new OnlineUsersPanel(); - this.heartbeatManager.addPeerListener(this.onlineUsersPanel); - this.getContentPane().add(this.onlineUsersPanel, java.awt.BorderLayout.LINE_END); - this.heartbeatManager.start(); - - this.setSize(new java.awt.Dimension(1024, 768)); - this.pluginManager = new PluginLoader(this); - this.pluginManager.loadPlugins(); } + private void initUserPanel() { + OnlineUsersPanel onlineUsersPanel = new OnlineUsersPanel(); + this.connectionManager.addPeerListener(onlineUsersPanel); + this.getContentPane().add(onlineUsersPanel, java.awt.BorderLayout.LINE_END); + } + + private void initStatusPanel() { + javax.swing.JPanel statusPanel = new javax.swing.JPanel(); + status = new javax.swing.JLabel(); + + statusPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT)); + updateStatus(); + statusPanel.add(status); + + this.getContentPane().add(statusPanel, java.awt.BorderLayout.PAGE_END); + } public void exit() { this.setVisible(false); @@ -88,51 +99,24 @@ public class MainWindow extends javax.swing.JFrame { System.exit(0); } - public HeartbeatManager getHeartbeatManager() { - return heartbeatManager; + public ConnectionManager getConnectionManager() { + return connectionManager; } - public String getUserName(){ - return this.userName; + public void loadPlugins() { + pluginManager.loadPlugins(connectionManager, guiManager); } - + public void addMenu(JMenu menu) { this.menuBar.addPlugin(menu); } - - public int getPort() { - return this.port; - } - - public Socket getSocket() { - return this.socket; - } - - public List getPlugins() { - return plugins; - } - public void setUserName(String userName) { - this.userName = userName; - updateStatus(); - } - - public void setPort(int port) { - this.port = port; - reconnect(); + connectionManager.setUserName(userName); updateStatus(); } private void updateStatus() { - this.status.setText("Connected on port " + this.port + " name " + this.userName); - } - - private void reconnect() { - if (this.socket != null) { - this.socket.close(); - } - this.socket = new Socket(this); - this.socket.start(); + this.status.setText("Connected on port " + connectionManager.getUserPort() + " name " + connectionManager.getUserName()); } } diff --git a/pitiupi/src/pitiupi/GUI/MenuBar.java b/pitiupi/src/pitiupi/GUI/MenuBar.java index 64fc6c7..4e517a8 100644 --- a/pitiupi/src/pitiupi/GUI/MenuBar.java +++ b/pitiupi/src/pitiupi/GUI/MenuBar.java @@ -9,20 +9,31 @@ import javax.swing.JMenu; import javax.swing.JMenuBar; /** + * Barra de menus principal da aplicação. + * + *

Gerencia 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(List peers) { SwingUtilities.invokeLater(() -> { diff --git a/pitiupi/src/pitiupi/Main.java b/pitiupi/src/pitiupi/Main.java index deb38d1..9e07887 100644 --- a/pitiupi/src/pitiupi/Main.java +++ b/pitiupi/src/pitiupi/Main.java @@ -15,7 +15,6 @@ import java.util.logging.Logger; * @author tony */ public class Main { - /** * @param args the command line arguments */ diff --git a/pitiupi/src/pitiupi/control/ConnectionManager.java b/pitiupi/src/pitiupi/control/ConnectionManager.java new file mode 100644 index 0000000..8fc360b --- /dev/null +++ b/pitiupi/src/pitiupi/control/ConnectionManager.java @@ -0,0 +1,112 @@ +package pitiupi.control; + +import pitiupi.GUI.MainWindow; +import pitiupi.net.HeartbeatMessage; +import pitiupi.net.Message; +import pitiupi.net.tcp.SocketTCP; +import pitiupi.net.tcp.TcpConnection; +import pitiupi.net.udp.SocketUDP; + +import java.net.InetAddress; + +public class ConnectionManager { + private final String pitiupiIdentifier; + + private final PluginManager pluginManager; + + private final HeartbeatManager heartbeatManager; + + private SocketTCP socketTCP; + private SocketUDP socketUDP; + private String username; + private int port; + + public ConnectionManager(MainWindow main, String username, PluginManager pluginManager, int port) { + this.pluginManager = pluginManager; + + this.username = username; + this.port = port; + + pitiupiIdentifier = "Pitiupi.Alice.2026"; + + reconnect(); + + heartbeatManager = new HeartbeatManager(this); + heartbeatManager.start(); + } + + public TcpConnection connectPluginTcp(InetAddress destinationAdress, String senderId, String receiverId) { + return socketTCP.createPrivateConnection(destinationAdress, senderId, receiverId); + } + + public boolean sendTcp(Message message, InetAddress destinationAddress) { + return socketTCP.send(message, destinationAddress); + } + + public boolean sendUdpUnicast(Message message, InetAddress destinationAddress) { + try { + socketUDP.sendUnicast(message, destinationAddress); + return true; + } catch (Exception ex) { + return false; + } + } + + public boolean sendUdpMulticast(Message message) { + try { + socketUDP.sendMulticast(message); + return true; + } catch (Exception _) { + System.err.println("Error sending UDP Multicast"); + } + return false; + } + + public void broadcastLocal(byte[] message) { + pluginManager.handlePublicMessage(message); + } + + public void addPeerListener(PeerListener peerListener) { + heartbeatManager.addPeerListener(peerListener); + } + + public void receiveHeartbeat(HeartbeatMessage message, InetAddress destinationAddress) { + heartbeatManager.receiveHeartbeat(message, destinationAddress); + } + + public void setPort(int port) { + this.port = port; + reconnect(); + } + + public int getPort() { + return port; + } + + public String getIdentifier(){ + return this.pitiupiIdentifier; + } + + private void reconnect() { + if (this.socketUDP != null) { + this.socketUDP.close(); + } + if (this.socketTCP != null) { + this.socketTCP.close(); + } + + this.socketUDP = new SocketUDP(this, pluginManager); + this.socketTCP = new SocketTCP(this, pluginManager); + + this.socketUDP.start(); + this.socketTCP.start(); + } + + public String getUserName() { + return this.username; + } + + public void setUserName(String userName) { + this.username = userName; + } +} diff --git a/pitiupi/src/pitiupi/control/GuiManager.java b/pitiupi/src/pitiupi/control/GuiManager.java new file mode 100644 index 0000000..210451a --- /dev/null +++ b/pitiupi/src/pitiupi/control/GuiManager.java @@ -0,0 +1,17 @@ +package pitiupi.control; + +import pitiupi.GUI.MainWindow; + +import javax.swing.*; + +public class GuiManager { + private MainWindow mainWindow; + + public GuiManager(MainWindow mainWindow) { + this.mainWindow = mainWindow; + } + + public void addMenu(JMenu menu) { + mainWindow.addMenu(menu); + } +} diff --git a/pitiupi/src/pitiupi/control/HeartbeatManager.java b/pitiupi/src/pitiupi/control/HeartbeatManager.java index 77466c6..7bc566e 100644 --- a/pitiupi/src/pitiupi/control/HeartbeatManager.java +++ b/pitiupi/src/pitiupi/control/HeartbeatManager.java @@ -16,6 +16,11 @@ import pitiupi.GUI.MainWindow; import pitiupi.net.HeartbeatMessage; import java.util.UUID; /** + * Gerencia o mecanismo de heartbeat da aplicação. + * + *

Responsá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 Map peers = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); private ScheduledExecutorService scheduler; private final String instanceId = UUID.randomUUID().toString(); - public HeartbeatManager(MainWindow mainWindow) { - this.mainWindow = mainWindow; + /** + * Cria um gerenciador de heartbeat associado à janela principal da aplicação. + * + */ + public HeartbeatManager(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; } + /** + * Inicia o mecanismo de heartbeat. + */ public void start() { scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(this::announce, 0, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS); scheduler.scheduleAtFixedRate(this::checkTimeouts, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS); } + /** + * Para o mecanismo de heartbeat e encerra as tarefas agendadas. + */ public void stop() { if (scheduler != null) { scheduler.shutdownNow(); @@ -47,15 +61,23 @@ public class HeartbeatManager { } private void announce() { - try { - HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName(), instanceId); - mainWindow.getSocket().send(msg.toByteArray()); - } catch (IOException ex) { - Logger.getLogger(HeartbeatManager.class.getName()).log(Level.WARNING, "Falha ao enviar heartbeat", ex); - } + HeartbeatMessage msg = new HeartbeatMessage(connectionManager.getUserName(), instanceId); + connectionManager.sendUdpMulticast(msg); } - + // Chamado pelo Socket quando um pacote é identificado como heartbeat + /** + * Processa uma mensagem de heartbeat recebida de outro usuário. + * + *

Atualiza 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 List getPeers() { return new ArrayList<>(peers.values()); } diff --git a/pitiupi/src/pitiupi/control/PeerInfo.java b/pitiupi/src/pitiupi/control/PeerInfo.java index 04e6bdd..b798120 100644 --- a/pitiupi/src/pitiupi/control/PeerInfo.java +++ b/pitiupi/src/pitiupi/control/PeerInfo.java @@ -2,6 +2,11 @@ package pitiupi.control; import java.net.InetAddress; /** + * Representa um usuário identificado na rede e as informações necessárias + * para acompanhar sua presença através de mensagens de heartbeat. + * + *

Armazena 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(List peers); } diff --git a/pitiupi/src/pitiupi/control/PluginLoader.java b/pitiupi/src/pitiupi/control/PluginLoader.java index 386724a..96915eb 100644 --- a/pitiupi/src/pitiupi/control/PluginLoader.java +++ b/pitiupi/src/pitiupi/control/PluginLoader.java @@ -4,20 +4,23 @@ */ package pitiupi.control; -import java.awt.List; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.ServiceLoader; -import java.util.Vector; +import java.util.*; + import pitiupi.GUI.MainWindow; import pitiupi.Main; import pitiupi.plugin.Plugin; /** + * Responsável por localizar, carregar e registrar os plugins + * presentes no diretório {@code plugins}. * + *

Os 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 plugins, HashSet loadedJars, ConnectionManager connectionManager, GuiManager guiManager) { File pluginDir = new File("plugins"); File[] jarFiles = pluginDir.listFiles((dir, name) -> name.endsWith(".jar")); @@ -37,33 +40,34 @@ public class PluginLoader { return; } - try { - // 2. Convert jar file paths into URLs - ArrayList urls = new ArrayList(); - for (File file : jarFiles) { - urls.add(file.toURI().toURL()); - System.out.println("Discovered plugin package: " + file.getName()); + for (File file : jarFiles) { + + if (loadedJars.contains(file.getName())) { + continue; } - // 3. Create a class loader isolated from the application's default loader - URLClassLoader pluginClassLoader = new URLClassLoader( - (URL[])urls.toArray(new URL[0]), - Main.class.getClassLoader() - ); - System.out.println(pluginClassLoader); + System.out.println("Discovered plugin package: " + file.getName()); - // 4. Locate and instantiate all classes implementing the Plugin interface - ServiceLoader serviceLoader = ServiceLoader.load(Plugin.class, pluginClassLoader); + try { + URLClassLoader classLoader = new URLClassLoader( + new URL[]{file.toURI().toURL()}, + Main.class.getClassLoader() + ); - System.out.println("\n--- Executing Discovered Plugins ---" + serviceLoader.findFirst()); - for (Plugin plugin : serviceLoader) { - System.out.println("Loading: " + plugin.getName()); - plugin.createPlugin(this.mainWindow); - this.mainWindow.getPlugins().add(plugin); + ServiceLoader serviceLoader = ServiceLoader.load(Plugin.class, classLoader); + + for (Plugin plugin : serviceLoader) { + System.out.println("Loading: " + plugin.getName()); + + plugin.createPlugin(connectionManager, guiManager); + plugins.add(plugin); + loadedJars.add(file.getName()); + } + + } catch (Exception e) { + System.out.println("Failed to load plugin " + file.getName()); + e.printStackTrace(); } - - } catch (MalformedURLException e) { - System.out.println(e.toString()); } } } diff --git a/pitiupi/src/pitiupi/control/PluginManager.java b/pitiupi/src/pitiupi/control/PluginManager.java new file mode 100644 index 0000000..1a11f60 --- /dev/null +++ b/pitiupi/src/pitiupi/control/PluginManager.java @@ -0,0 +1,68 @@ +package pitiupi.control; + +import pitiupi.GUI.MainWindow; +import pitiupi.net.tcp.TcpConnection; +import pitiupi.plugin.Plugin; +import pitiupi.plugin.PrivateConnectionPlugin; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +public class PluginManager { + private final PluginLoader pluginLoader; + private final List plugins; + private final HashSet loadedJars; + + public PluginManager(MainWindow main) { + this.pluginLoader = new PluginLoader(main); + this.plugins = new ArrayList<>(); + this.loadedJars = new HashSet<>(); + } + + public void loadPlugins(ConnectionManager connectionManager, GuiManager guiManager) { + pluginLoader.loadPlugins(plugins, loadedJars, connectionManager, guiManager); + } + + public void handlePublicMessage(byte[] msg) { + for (Plugin plugin : plugins) { + plugin.receiveMessage(msg); + } + } + + private Plugin getPlugin(String pluginId) { + for (Plugin plugin : plugins) { + if (plugin.getIdentifier().equals(pluginId)) return plugin; + } + return null; + } + + public void handlePrivateMessage(TcpConnection connection, byte[] msg) { + Plugin plugin = getPlugin(connection.getReceiver()); + if (plugin == null) return; + + if (plugin instanceof PrivateConnectionPlugin privatePlugin) { + privatePlugin.receiveBytes(connection, msg); + } + + } + + public boolean handleConnection(TcpConnection connection, String receiverId) { + Plugin plugin = getPlugin(receiverId); + if (plugin == null) return false; + + if (plugin instanceof PrivateConnectionPlugin privatePlugin) { + privatePlugin.receivePrivateConnection(connection); + return true; + } + + return false; + } + + public void handleDisconnection(TcpConnection connection, String receiverId) { + Plugin plugin = getPlugin(receiverId); + if (plugin instanceof PrivateConnectionPlugin privatePlugin) { + privatePlugin.onSocketClosed(connection); + } + } +} diff --git a/pitiupi/src/pitiupi/net/HeartbeatMessage.java b/pitiupi/src/pitiupi/net/HeartbeatMessage.java index b42f306..5476e0e 100644 --- a/pitiupi/src/pitiupi/net/HeartbeatMessage.java +++ b/pitiupi/src/pitiupi/net/HeartbeatMessage.java @@ -1,5 +1,10 @@ package pitiupi.net; + /** + * Mensagem utilizada para sinalizar a presença de um usuário na rede. + * + *

É 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 Map publicConnections; + private final Map> privateConnections; + + /** + * Cria um gerenciador de conexões TCP vazio. + */ + public TcpConnectionManager() { + publicConnections = new ConcurrentHashMap<>(); + privateConnections = new ConcurrentHashMap<>(); + + logger = Logger.getLogger(getClass().getName()); + } + + /** + * Obtém a conexão pública registrada para um determinado peer. + * + * @param address endereço IP do peer. + * @return a conexão pública registrada para o endereço ou {@code null} + * caso não exista uma conexão pública registrada. + */ + + public TcpConnection getPublicConnection(InetAddress address) { + return publicConnections.get(address); + } + + /** + * Registra uma conexão privada associada a um peer. + * + *

Um 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) { + List connections = + privateConnections.computeIfAbsent(connection.getAddress(), _ -> new ArrayList<>()); + + synchronized (connections) { + connections.add(connection); + } + } + + /** + * Registra uma conexão pública associada a um peer. + * + *

Cada 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()) { + List connections = privateConnections.get(connection.getAddress()); + + if (connections != null) { + synchronized (connections) { + connections.remove(connection); + + if (connections.isEmpty()) privateConnections.remove(connection.getAddress(), connections); + } + } + } + else publicConnections.remove(connection.getAddress(), connection); + + try { + if (!connection.isClosed()) { + connection.close(); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, "Error closing TCP connection", ex); + } + } + + /** + * Envia uma mensagem de encerramento para todas as conexões de um peer + * e, em seguida, fecha essas conexões. + * + * @param address endereço IP do peer cujas conexões serão encerradas. + * @param reason motivo do encerramento informado ao peer. + */ + public void closeByAdress(InetAddress address, String reason) { + List connections = privateConnections.get(address); + for (TcpConnection connection : connections) { + SysInfoMessage.sendSystemInfo(connection, SysInfoType.CONNECTION_CLOSED, reason); + } + closeByAdress(address); + } + + /** + * Fecha e remove todas as conexões associadas a um determinado endereço. + * + *

Tanto 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); + } + + } + + List connections = privateConnections.remove(address); + + if (connections != null) { + synchronized (connections) { + for (TcpConnection connection : connections) { + try { + connection.close(); + } + catch (IOException ex) { + logger.log(Level.SEVERE, "Error closing TCP connection", ex); + } + } + connections.clear(); + } + } + } + + /** + * Envia uma mensagem de encerramento para todas as conexões privadas + * gerenciadas e, em seguida, fecha todas elas. + * + * @param reason motivo do encerramento informado aos peers. + */ + public void closeAllConnections(String reason) { + for (List connections : privateConnections.values()) { + for (TcpConnection connection : connections) { + SysInfoMessage.sendSystemInfo(connection, SysInfoType.CONNECTION_CLOSED, reason); + } + } + closeAllConnections(); + } + + /** + * Fecha e remove todas as conexões públicas e privadas gerenciadas. + * + *

Apó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 (List connectionsList : privateConnections.values()) { + for (TcpConnection connection : connectionsList) { + closeConnection(connection); + } + privateConnections.clear(); + } + } + } + + /** + * Atualiza as conexões de acordo com os peers atualmente ativos. + * + *

Os 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(List peers) { + Set activePeers = peers.stream().map(PeerInfo::getAddress).collect(Collectors.toSet()); + + for (InetAddress address : publicConnections.keySet()) { + if (!activePeers.contains(address)) { + closeByAdress(address); + } + } + for (InetAddress address : privateConnections.keySet()) { + if (!activePeers.contains(address)) { + closeByAdress(address); + } + } + } +} diff --git a/pitiupi/src/pitiupi/net/udp/SocketUDP.java b/pitiupi/src/pitiupi/net/udp/SocketUDP.java new file mode 100644 index 0000000..fbb6708 --- /dev/null +++ b/pitiupi/src/pitiupi/net/udp/SocketUDP.java @@ -0,0 +1,195 @@ +/* + * 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.udp; + +import pitiupi.GUI.MainWindow; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.net.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import pitiupi.control.ConnectionManager; +import pitiupi.control.PluginManager; +import pitiupi.net.HeartbeatMessage; +import pitiupi.net.Message; +import pitiupi.plugin.Plugin; + +/** + * Gerencia a comunicação UDP multicast e unicast da aplicação. + * + *

Manté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); +}