Author SHA1 Message Date
GustavoHMDS 23559dcc06 WIP bug fix 3 2026-09-29 01:20:48 -03:00
GustavoHMDS fa4ee046b1 WIP BugFix 3 2026-09-29 01:13:42 -03:00
GustavoHMDS 0c71a85cba WIP bug fix 3 2026-09-29 00:21:04 -03:00
GustavoHMDS e7c8983775 WIP TCP bug fix 2 2026-09-28 22:05:14 -03:00
GustavoHMDS 0ec3129a22 WIP TCP bug fix 2026-09-28 21:43:17 -03:00
GustavoHMDS ba2f41daf7 WIP bug fix 2026-09-28 18:59:20 -03:00
GustavoHMDS 57059c3ef0 WIP bugfix 2026-09-28 12:42:49 -03:00
GustavoHMDS b5bbd1df2d atualizar javadoc dos das classes SocketTCP, ConnectionManager, ConnectionFactory e TcpConnection 2026-09-09 00:14:15 -03:00
GustavoHMDS 93ac292f72 bugfix: SocketUDP converte para Mensagem antes de enviar ao plugin 2026-09-08 01:37:03 -03:00
GustavoHMDS b610ec20e1 Esqueci de adicionar nos commits anteriores 2026-09-08 01:30:23 -03:00
GustavoHMDS 4461772ebf modificar o socketTCP para servir de interface com o restante da aplicação, enquanto usa a factory e o manager para criar conexões conforme necessário 2026-09-08 01:29:23 -03:00
GustavoHMDS 0d5a4dbe75 adicionar um gerenciador de conexões que mantem um registro das conexões ativas e cuida do seu encerramento 2026-09-08 01:25:52 -03:00
GustavoHMDS bbb1ceea23 adicionar suporte para criação de conexões privadas, com uma interface para plugins que queiram usa-los e uma fabrica que faz a validação das conexões 2026-09-08 01:24:35 -03:00
GustavoHMDS 7d9ca4ca71 adicionar enum utilizado para o commit anterior 2026-09-08 01:21:36 -03:00
GustavoHMDS 1fcc03d9be adicionar classe para envio de mensagens do sistema 2026-09-08 01:20:34 -03:00
GustavoHMDS 3efc59444f impedindo TCP para si mesmo 2026-08-18 12:53:30 -03:00
GustavoHMDS 947aaf00b7 Adicionando o plugin corretamente 2026-08-18 10:06:47 -03:00
GustavoHMDS e9ce17abb3 Plugin TesteSocket para testar as mudanças 2026-08-18 09:51:22 -03:00
GustavoHMDS 45e3bf6876 Unicast implementado, mas falta testes 2026-08-17 14:11:09 -03:00
GustavoHMDS 81c37225fa atualizando o arquivo documentation.md 2026-08-11 14:35:51 -03:00
GustavoHMDS 5c69189293 atualizando documentacao depois do merge 2026-08-11 14:30:17 -03:00
GustavoHMDS 543073e84e resolvendo conflitos 2026-08-11 10:12:04 -03:00
tonyhcj 7f21e89a0a fix: salvando nome e porta 2026-07-29 15:00:44 -03:00
tonyhcj 239a10e79f fix: Corrige bug que exibia frame0 como nome do usuário 2026-07-28 22:11:37 -03:00
tonyhcj a7ba307daf fix: usar instanceId único em vez de userName para identificar heartbeats 2026-07-24 19:31:39 -03:00
tonyhcj 668b2d417b Corrige Preferences: name/port do Login agora atualizam MainWindow
- Login.commitEdit() no JSpinner para capturar porta digitada manualmente
- Main.java agora exibe o Login antes de criar o MainWindow
- MenuBar aplica os valores do Login no MainWindow via setUserName/setPort
- MainWindow: campos userName/port deixam de ser final; setPort() reconecta o socket
- Socket.java ganha close() para permitir reconexão limpa em nova porta
2026-07-22 18:53:11 -03:00
GustavoHMDS cf43f62793 Documentação de funções e arquivo Documentação.md 2026-07-21 20:33:28 -03:00
GustavoHMDS 5d650df8eb Merge branch 'feature/heartbeat-online-users' of https://git.alice.ufsj.edu.br/fls/pitiupi into feature/help-and-documentation 2026-07-21 11:09:23 -03:00
GustavoHMDS 3beadca0c1 comeco da documentacao 2026-07-21 11:06:23 -03:00
76 changed files with 3141 additions and 140 deletions

No files matched your search

+2
View File
@@ -5,3 +5,5 @@
/pitiupi.chat/nbproject/private/ /pitiupi.chat/nbproject/private/
/pitiupi.screenCast/build/ /pitiupi.screenCast/build/
/pitiupi.screenCast/dist/ /pitiupi.screenCast/dist/
.idea/
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+9
View File
@@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="pitiupi.chat">
<CLASSES>
<root url="jar://$PROJECT_DIR$/dist/pitiupi.chat.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
+5
View File
@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.chat.iml" filepath="$PROJECT_DIR$/pitiupi.chat.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+3 -3
View File
@@ -1,6 +1,6 @@
#Thu, 16 Jul 2026 15:21:36 -0300 #Mon, 17 Aug 2026 14:26:26 -0300
/home/flavio/NetBeansProjects/pitiupi.chat= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.chat=
/home/flavio/NetBeansProjects/pitiupi= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>
@@ -91,17 +91,17 @@ public class ChatWindow extends javax.swing.JDialog {
} }
private void sendMessage() { private void sendMessage() {
if (jTextArea1.getText().trim().length() == 0) { if (jTextArea1.getText().trim().isEmpty()) {
return; return;
} }
ChatMessage message = new ChatMessage(mainWindow.getName(), jTextArea1.getText()); ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText());
this.sendChatMessage(message); this.sendChatMessage(message);
jTextArea1.setText(""); jTextArea1.setText("");
} }
public void sendChatMessage(Message message) { public void sendChatMessage(Message message) {
try { try {
mainWindow.getSocket().send(message.toByteArray()); mainWindow.getSocketUDP().sendMulticast(message.toByteArray());
} catch (IOException ex) { } catch (IOException ex) {
Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
} }
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AntConfiguration">
<buildFile url="file://$PROJECT_DIR$/build.xml" />
</component>
</project>
+3
View File
@@ -0,0 +1,3 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK" />
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/../.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../.idea/vcs.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi.chat/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi.chat/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi.chat/build/built-jar.properties" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi.chat/build/built-jar.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow.class" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi.chat/build/classes/pitiupi/chat/GUI/ChatWindow.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi.chat/dist/pitiupi.chat.jar" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi.chat/pitiupi.chat.iml" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi.chat/pitiupi.chat.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi.chat/src/pitiupi/chat/GUI/ChatWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi.chat/src/pitiupi/chat/GUI/ChatWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build/built-jar.properties" beforeDir="false" afterPath="$PROJECT_DIR$/build/built-jar.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build/classes/pitiupi/screenCast/GUI/ScreenCastWindow.class" beforeDir="false" afterPath="$PROJECT_DIR$/build/classes/pitiupi/screenCast/GUI/ScreenCastWindow.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/dist/pitiupi.screenCast.jar" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/pitiupi/screenCast/GUI/ScreenCastWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/pitiupi/screenCast/GUI/ScreenCastWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi/plugins/pitiupi.chat.jar" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/plugins/pitiupi.chat.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi/plugins/pitiupi.screenCast.jar" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/plugins/pitiupi.screenCast.jar" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi/src/pitiupi/GUI/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/src/pitiupi/GUI/MainWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../pitiupi/src/pitiupi/control/HeartbeatManager.java" beforeDir="false" afterPath="$PROJECT_DIR$/../pitiupi/src/pitiupi/control/HeartbeatManager.java" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 7
}</component>
<component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "feature/unicast",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi/pitiupi.screenCast",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.15",
"project.structure.side.proportion": "0.2",
"settings.editor.selected.configurable": "preferences.keymap"
}
}]]></component>
<component name="RunManager">
<configuration default="true" type="JetRunConfigurationType">
<module name="pitiupi.screenCast" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<module name="pitiupi.screenCast" />
<option name="filePath" />
<method v="2" />
</configuration>
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
<module name="pitiupi.screenCast" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration default="true" type="Tox" factoryName="Tox">
<module name="pitiupi.screenCast" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-bf35d07a577b-intellij.indexing.shared.core-IU-252.28539.54" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="" />
<created>1784745206049</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1784745206049</updated>
</task>
<servers />
</component>
</project>
@@ -1,6 +1,6 @@
#Thu, 16 Jul 2026 15:21:39 -0300 #Mon, 17 Aug 2026 14:35:40 -0300
/home/flavio/NetBeansProjects/pitiupi.screenCast= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.screenCast=
/home/flavio/NetBeansProjects/pitiupi= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=
Binary file not shown.
@@ -98,7 +98,7 @@ public class ScreenCastWindow extends javax.swing.JFrame {
} }
if (msg != null) { if (msg != null) {
try { try {
this.mainWindow.getSocket().send(msg); this.mainWindow.getSocketUDP().sendMulticast(msg);
} catch (IOException ex) { } catch (IOException ex) {
System.out.println(ex.toString()); System.out.println(ex.toString());
this.compressRatio -= 0.1f; this.compressRatio -= 0.1f;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
plugin.TestChat
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>
+84
View File
@@ -0,0 +1,84 @@
package GUI;
import plugin.TestChat;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
public class ChatMenu extends JMenu {
private final TestChat testChat;
public ChatMenu(TestChat testChat) {
this.testChat = testChat;
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 (testChat.openConnection(ip)) dialog.dispose();
else errorLabel.setVisible(true);
});
dialog.setContentPane(contentPanel);
dialog.pack();
dialog.setLocationRelativeTo(SwingUtilities.getWindowAncestor(this));
dialog.setVisible(true);
}
}
@@ -0,0 +1,94 @@
package GUI;
import net.TestChatMessage;
import pitiupi.GUI.MainWindow;
import javax.swing.*;
import java.awt.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
public abstract class ChatWindow extends JFrame {
protected final MainWindow mainWindow;
private JTextArea receivedMessagesArea;
protected JTextField messageField;
public ChatWindow(MainWindow mainWindow) {
this.mainWindow = mainWindow;
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 TestChatMessage createMessage(String tipoMensagem) {
try {
String nome = mainWindow.getUserName();
String ip = InetAddress.getLocalHost().getHostAddress();
String message = messageField.getText();
return new TestChatMessage(nome, ip, tipoMensagem, message);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
public void receiveMessage(TestChatMessage 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());
});
}
}
@@ -0,0 +1,46 @@
package GUI;
import net.TestChatMessage;
import pitiupi.GUI.MainWindow;
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(MainWindow mainWindow, TcpConnection connection) {
this.connection = connection;
super(mainWindow);
}
@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 {
TestChatMessage msg = createMessage("TCP Private");
connection.send(msg.toByteArray());
} catch (Exception ex) {
Logger.getLogger(
PrivateChatWindow.class.getName()
).log(Level.SEVERE, null, ex);
}
}
}
@@ -0,0 +1,93 @@
package GUI;
import net.TestChatMessage;
import pitiupi.GUI.MainWindow;
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(MainWindow mainWindow) {
super(mainWindow);
}
@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 {
TestChatMessage msg = createMessage("TCP Unicast");
InetAddress ip = InetAddress.getByName(ipField.getText());
mainWindow.getSocketTCP().send(msg, ip);
} catch (Exception ex) {
Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void sendUDP() {
try {
TestChatMessage msg = createMessage("UDP Unicast");
InetAddress ip = InetAddress.getByName(ipField.getText());
mainWindow.getSocketUDP().sendUnicast(msg, ip);
} catch (Exception ex) {
Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void sendMulticast() {
try {
TestChatMessage msg = createMessage("UDP Multicast");
mainWindow.getSocketUDP().sendMulticast(msg);
} catch (Exception ex) {
Logger.getLogger(PublicChatWindow.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@@ -0,0 +1 @@
plugin.TestChat
@@ -0,0 +1,61 @@
package control;
import GUI.ChatMenu;
import GUI.ChatWindow;
import GUI.PrivateChatWindow;
import GUI.PublicChatWindow;
import net.TestChatMessage;
import pitiupi.GUI.MainWindow;
import pitiupi.net.tcp.TcpConnection;
import javax.swing.*;
import java.util.HashMap;
public class ChatController {
MainWindow mainWindow;
ChatMenu chatMenu;
PublicChatWindow publicChatWindow;
private final HashMap<TcpConnection, JMenuItem> chatMenus;
private final HashMap<TcpConnection, ChatWindow> chatWindows;
public ChatController(MainWindow mainWindow, ChatMenu chatMenu) {
this.mainWindow = mainWindow;
publicChatWindow = new PublicChatWindow(mainWindow);
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(mainWindow, 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(TestChatMessage message) {
publicChatWindow.receiveMessage(message);
}
public void receivePrivate(TcpConnection tcpConnection, TestChatMessage message) {
chatWindows.get(tcpConnection).receiveMessage(message);
}
}
@@ -0,0 +1,35 @@
package net;
import pitiupi.net.Message;
public class TestChatMessage extends Message {
private String nome;
private String ip;
private String tipoMensagem;
private String message;
public TestChatMessage(String nome, String ip, String tipoMensagem, String message) {
this.nome = nome;
this.ip = ip;
this.tipoMensagem = tipoMensagem;
this.message = message;
}
public TestChatMessage(){}
public String getNome() {
return nome;
}
public String getIp() {
return ip;
}
public String getTipoMensagem() {
return tipoMensagem;
}
public String getMessage() {
return message;
}
}
+104
View File
@@ -0,0 +1,104 @@
package plugin;
import GUI.ChatMenu;
import control.ChatController;
import net.TestChatMessage;
import pitiupi.GUI.MainWindow;
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 TestChat implements PrivateConnectionPlugin{
ChatController chatController;
MainWindow mainWindow;
@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(MainWindow mainWindow) {
this.mainWindow = mainWindow;
ChatMenu chatMenu = new ChatMenu(this);
chatController = new ChatController(mainWindow, chatMenu);
mainWindow.addMenu(chatMenu);
}
@Override
public Message getMessage() {
return new TestChatMessage();
}
@Override
public void receiveMessage(byte[] message) {
TestChatMessage testChatMessage = parseMessage(message);
chatController.receivePublic(testChatMessage);
}
@Override
public void receivePrivateConnection(TcpConnection tcpConnection) {
SwingUtilities.invokeLater(() -> {
chatController.addChatWindow(tcpConnection);
});
}
@Override
public void receiveBytes(TcpConnection connection, byte[] bytes) {
TestChatMessage testChatMessage = parseMessage(bytes);
chatController.receivePrivate(connection, testChatMessage);
}
@Override
public void onSocketClosed(TcpConnection tcpConnection) {
chatController.removeChatWindow(tcpConnection);
}
public boolean openConnection(String ip) {
try {
mainWindow.getSocketTCP().createPrivateConnection(InetAddress.getByName(ip), getIdentifier(), getIdentifier());
return true;
}
catch (Exception e) {
System.out.println("Error opening connection: " + e.getMessage());
}
return false;
}
private TestChatMessage parseMessage(byte[] bytes) {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois;
try {
ois = new ObjectInputStream(bis);
if (ois.readObject() instanceof TestChatMessage msg) {
return msg;
}
} catch (IOException | ClassNotFoundException ex) {
System.out.println(ex.toString());
}
return null;
}
}
+2
View File
@@ -8,3 +8,5 @@
# JARs gerados # JARs gerados
*.jar *.jar
.idea/
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>
+9
View File
@@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="pitiupi">
<CLASSES>
<root url="jar://$PROJECT_DIR$/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
+10
View File
@@ -0,0 +1,10 @@
<component name="libraryTable">
<library name="plugins">
<CLASSES>
<root url="jar://$PROJECT_DIR$/plugins/pitiupi.chat.jar!/" />
<root url="jar://$PROJECT_DIR$/plugins/pitiupi.screenCast.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
+5
View File
@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.iml" filepath="$PROJECT_DIR$/pitiupi.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+547
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
# Como criar plugins para o Share my Sheet
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
-1
View File
@@ -1 +0,0 @@
../../pitiupi.chat/dist/pitiupi.chat.jar
-1
View File
@@ -1 +0,0 @@
../../pitiupi.screenCast/dist/pitiupi.screenCast.jar
+72
View File
@@ -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("""
<html>
<body>
<h1>Share My Sheet</h1>
\s
<p>
<b>Versão:</b> 1.0
</p>
\s
<p>
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
</p>
\s
<p>
<b>Autores:</b><br>
Flávio<br>
Tony<br>
Gustavo Henrique
</p>
\s
<p>
Universidade Federal de São João del-Rei (UFSJ)
</p>
\s
</body>
</html>
\s""");
return new JScrollPane(texto);
}
}
+24 -7
View File
@@ -1,8 +1,3 @@
/*
* 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; package pitiupi.GUI;
import java.awt.BorderLayout; import java.awt.BorderLayout;
@@ -11,6 +6,7 @@ import java.awt.GridBagConstraints;
import java.awt.GridBagLayout; import java.awt.GridBagLayout;
import java.awt.Insets; import java.awt.Insets;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.util.prefs.Preferences;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JDialog; import javax.swing.JDialog;
@@ -20,11 +16,20 @@ import javax.swing.JSpinner;
import javax.swing.JTextField; import javax.swing.JTextField;
/** /**
* Janela de preferencias da aplicação.
*
* <p>Permite ao usuário informar o nome utilizado na rede e a porta
* multicast que será utilizada pela comunicação.</p>
* *
* @author flavio * @author flavio
* @author tony
*/ */
public final class Login extends JDialog { public final class Login extends JDialog {
private static final Preferences prefs = Preferences.userNodeForPackage(Login.class);
private static final String PREF_NOME = "lastUserName";
private static final String PREF_PORTA = "lastPort";
JLabel nameLabel = new JLabel("Name: "); JLabel nameLabel = new JLabel("Name: ");
JLabel portLabel = new JLabel("Port: "); JLabel portLabel = new JLabel("Port: ");
@@ -43,8 +48,11 @@ public final class Login extends JDialog {
public void initComponents() { public void initComponents() {
this.setTitle("Login"); this.setTitle("Login");
portField.setModel(new javax.swing.SpinnerNumberModel(8000, 8000, 9000, 1)); String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
nameField.setText(System.getProperty("user.name")); int portaSalva = prefs.getInt(PREF_PORTA, 8000);
portField.setModel(new javax.swing.SpinnerNumberModel(portaSalva, 8000, 9000, 1));
nameField.setText(nomeSalvo);
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
@@ -83,6 +91,15 @@ public final class Login extends JDialog {
this.add(buttonPanel, BorderLayout.SOUTH); this.add(buttonPanel, BorderLayout.SOUTH);
okButton.addActionListener((ActionEvent e) -> { okButton.addActionListener((ActionEvent e) -> {
try {
portField.commitEdit();
} catch (java.text.ParseException ex) {
return;
}
prefs.put(PREF_NOME, nameField.getText());
prefs.putInt(PREF_PORTA, (Integer) portField.getValue());
Login.this.setVisible(false); Login.this.setVisible(false);
}); });
+83 -11
View File
@@ -5,7 +5,8 @@
*/ */
package pitiupi.GUI; package pitiupi.GUI;
import pitiupi.net.Socket; import pitiupi.net.tcp.SocketTCP;
import pitiupi.net.udp.SocketUDP;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.swing.JMenu; import javax.swing.JMenu;
@@ -14,9 +15,18 @@ import pitiupi.plugin.Plugin;
import pitiupi.control.HeartbeatManager; import pitiupi.control.HeartbeatManager;
/** /**
* Janela principal da aplicação.
*
* <p>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.</p>
*
* <p>A instância desta classe é compartilhada com os plugins para permitir
* integração com a aplicação principal.</p>
* *
* @author flavio * @author flavio
* @author tony * @author tony
* @author Gustavo
*/ */
public class MainWindow extends javax.swing.JFrame { public class MainWindow extends javax.swing.JFrame {
@@ -30,24 +40,41 @@ public class MainWindow extends javax.swing.JFrame {
private OnlineUsersPanel onlineUsersPanel; private OnlineUsersPanel onlineUsersPanel;
private Socket socket; private SocketUDP socketUDP;
private SocketTCP socketTCP;
private final String userName; private String userName;
private final int port; private int port;
private String pitiupiIdentifier;
private PluginLoader pluginManager; private PluginLoader pluginManager;
private final List<Plugin> plugins; private final List<Plugin> 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) { public MainWindow(String userName, int port) {
this.userName = userName; this.userName = userName;
this.port = port; this.port = port;
this.pitiupiIdentifier = "Pitiupi.Alice.2026";
this.menuBar = new MenuBar(this); this.menuBar = new MenuBar(this);
this.plugins = new ArrayList<>(); this.plugins = new ArrayList<>();
this.initComponents(); this.initComponents();
} }
private void initComponents() { private void initComponents() {
initGUI();
initSocket();
initHeartbeat();
initPlugins();
this.setSize(new java.awt.Dimension(1024, 768));
}
private void initGUI() {
this.setJMenuBar(menuBar); this.setJMenuBar(menuBar);
this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
this.setTitle("Share My Sheet"); this.setTitle("Share My Sheet");
@@ -66,22 +93,29 @@ public class MainWindow extends javax.swing.JFrame {
this.getContentPane().add(statusPanel, java.awt.BorderLayout.PAGE_END); this.getContentPane().add(statusPanel, java.awt.BorderLayout.PAGE_END);
this.pack(); this.pack();
}
this.socket = new Socket(this); private void initSocket() {
this.socket.start(); this.socketUDP = new SocketUDP(this);
this.socketTCP = new SocketTCP(this);
this.socketUDP.start();
this.socketTCP.start();
}
private void initHeartbeat() {
this.heartbeatManager = new HeartbeatManager(this); this.heartbeatManager = new HeartbeatManager(this);
this.onlineUsersPanel = new OnlineUsersPanel(); this.onlineUsersPanel = new OnlineUsersPanel();
this.heartbeatManager.addPeerListener(this.onlineUsersPanel); this.heartbeatManager.addPeerListener(this.onlineUsersPanel);
this.getContentPane().add(this.onlineUsersPanel, java.awt.BorderLayout.LINE_END); this.getContentPane().add(this.onlineUsersPanel, java.awt.BorderLayout.LINE_END);
this.heartbeatManager.start(); this.heartbeatManager.start();
}
this.setSize(new java.awt.Dimension(1024, 768)); private void initPlugins() {
this.pluginManager = new PluginLoader(this); this.pluginManager = new PluginLoader(this);
this.pluginManager.loadPlugins(); this.pluginManager.loadPlugins();
} }
public void exit() { public void exit() {
this.setVisible(false); this.setVisible(false);
this.dispose(); this.dispose();
@@ -96,6 +130,10 @@ public class MainWindow extends javax.swing.JFrame {
return this.userName; return this.userName;
} }
public String getIdentifier(){
return this.pitiupiIdentifier;
}
public void addMenu(JMenu menu) { public void addMenu(JMenu menu) {
this.menuBar.addPlugin(menu); this.menuBar.addPlugin(menu);
} }
@@ -104,11 +142,45 @@ public class MainWindow extends javax.swing.JFrame {
return this.port; return this.port;
} }
public Socket getSocket() { public SocketUDP getSocketUDP() {
return this.socket; return this.socketUDP;
}
public SocketTCP getSocketTCP() {
return this.socketTCP;
} }
public List<Plugin> getPlugins() { public List<Plugin> getPlugins() {
return plugins; return plugins;
} }
public void setUserName(String userName) {
this.userName = userName;
updateStatus();
}
public void setPort(int port) {
this.port = port;
reconnect();
updateStatus();
}
private void updateStatus() {
this.status.setText("Connected on port " + this.port + " name " + this.userName);
}
private void reconnect() {
if (this.socketUDP != null) {
this.socketUDP.close();
}
if (this.socketTCP != null) {
this.socketTCP.close();
}
this.socketUDP = new SocketUDP(this);
this.socketTCP = new SocketTCP(this);
this.socketUDP.start();
this.socketTCP.start();
}
} }
+37 -3
View File
@@ -9,18 +9,31 @@ import javax.swing.JMenu;
import javax.swing.JMenuBar; import javax.swing.JMenuBar;
/** /**
* Barra de menus principal da aplicação.
*
* <p>Gerencia os menus padrões da interface e disponibiliza um espaço
* para que plugins adicionem seus próprios menus.</p>
* *
* @author flavio * @author flavio
* @author Gustavo
* @author tony
*
*/ */
public class MenuBar extends JMenuBar { public class MenuBar extends JMenuBar {
private javax.swing.JMenuItem setupMenu; private javax.swing.JMenuItem setupMenu;
private javax.swing.JMenuItem aboutMenu;
private javax.swing.JMenu fileMenu; private javax.swing.JMenu fileMenu;
private javax.swing.JMenu helpMenu; private javax.swing.JMenu helpMenu;
private javax.swing.JMenu pluginMenu; private javax.swing.JMenu pluginMenu;
private javax.swing.JMenuItem exitMenu; private javax.swing.JMenuItem exitMenu;
private final MainWindow mainWindow; 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) { public MenuBar(MainWindow mainWindow) {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.initComponents(); this.initComponents();
@@ -32,6 +45,7 @@ public class MenuBar extends JMenuBar {
helpMenu = new javax.swing.JMenu(); helpMenu = new javax.swing.JMenu();
pluginMenu = new javax.swing.JMenu(); pluginMenu = new javax.swing.JMenu();
setupMenu = new javax.swing.JMenuItem(); setupMenu = new javax.swing.JMenuItem();
aboutMenu = new javax.swing.JMenuItem();
fileMenu.setText("File"); fileMenu.setText("File");
exitMenu.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F4, java.awt.event.InputEvent.ALT_MASK)); exitMenu.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F4, java.awt.event.InputEvent.ALT_MASK));
@@ -44,24 +58,44 @@ public class MenuBar extends JMenuBar {
this.add(pluginMenu); this.add(pluginMenu);
helpMenu.setText("Help"); helpMenu.setText("Help");
setupMenu.setText("Preferences"); setupMenu.setText("Preferences");
setupMenu.addActionListener(this::setupMenuActionPerformed); setupMenu.addActionListener(this::setupMenuActionPerformed);
helpMenu.add(setupMenu); helpMenu.add(setupMenu);
aboutMenu.setText("About");
aboutMenu.addActionListener(this::aboutMenuActionPerformed);
helpMenu.add(aboutMenu);
this.add(helpMenu); this.add(helpMenu);
} }
private void exitMenuActionPerformed(java.awt.event.ActionEvent evt) { private void exitMenuActionPerformed(java.awt.event.ActionEvent evt) {
mainWindow.exit(); mainWindow.exit();
} }
private void setupMenuActionPerformed(java.awt.event.ActionEvent evt) {
private void setupMenuActionPerformed(java.awt.event.ActionEvent evt) {
Login login = new Login(); Login login = new Login();
login.setVisible(true); login.setVisible(true);
mainWindow.setUserName(login.getName());
mainWindow.setPort(login.getPort());
login.dispose(); 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){ public void addPlugin(JMenu menu){
this.pluginMenu.add(menu); this.pluginMenu.add(menu);
} }
@@ -11,7 +11,10 @@ import javax.swing.JScrollPane;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import pitiupi.control.PeerInfo; import pitiupi.control.PeerInfo;
import pitiupi.control.PeerListener; import pitiupi.control.PeerListener;
/** /**
* Painel da interface gráfica responsável por exibir os usuários ativos
* na rede.
* *
* @author tony * @author tony
*/ */
@@ -30,6 +33,11 @@ public class OnlineUsersPanel extends JPanel implements PeerListener {
add(new JScrollPane(userList), BorderLayout.CENTER); 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 @Override
public void onPeersChanged(List<PeerInfo> peers) { public void onPeersChanged(List<PeerInfo> peers) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
+5 -4
View File
@@ -12,9 +12,9 @@ import java.util.logging.Logger;
/** /**
* *
* @author flavio * @author flavio
* @author tony
*/ */
public class Main { public class Main {
/** /**
* @param args the command line arguments * @param args the command line arguments
*/ */
@@ -40,14 +40,15 @@ public class Main {
} catch (javax.swing.UnsupportedLookAndFeelException ex) { } catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} }
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() { java.awt.EventQueue.invokeLater(new Runnable() {
@Override @Override
public void run() { public void run() {
pitiupi.GUI.Login login = new pitiupi.GUI.Login();
login.setVisible(true);
pitiupi.GUI.MainWindow main; pitiupi.GUI.MainWindow main;
main = new pitiupi.GUI.MainWindow(System.getProperty("user.name"), 8000); main = new pitiupi.GUI.MainWindow(login.getName(), login.getPort());
main.setVisible(true); main.setVisible(true);
} }
}); });
@@ -14,7 +14,13 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import pitiupi.GUI.MainWindow; import pitiupi.GUI.MainWindow;
import pitiupi.net.HeartbeatMessage; import pitiupi.net.HeartbeatMessage;
import java.util.UUID;
/** /**
* Gerencia o mecanismo de heartbeat da aplicação.
*
* <p>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.</p>
* *
* @author tony * @author tony
*/ */
@@ -27,17 +33,30 @@ public class HeartbeatManager {
private final Map<String, PeerInfo> peers = new ConcurrentHashMap<>(); private final Map<String, PeerInfo> peers = new ConcurrentHashMap<>();
private final List<PeerListener> listeners = new CopyOnWriteArrayList<>(); private final List<PeerListener> listeners = new CopyOnWriteArrayList<>();
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
private final String instanceId = UUID.randomUUID().toString();
/**
* Cria um gerenciador de heartbeat associado à janela principal da aplicação.
*
* @param mainWindow janela principal utilizada para obter o nome do usuário
* e o socket de comunicação.
*/
public HeartbeatManager(MainWindow mainWindow) { public HeartbeatManager(MainWindow mainWindow) {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
} }
/**
* Inicia o mecanismo de heartbeat.
*/
public void start() { public void start() {
scheduler = Executors.newScheduledThreadPool(1); scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::announce, 0, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS); scheduler.scheduleAtFixedRate(this::announce, 0, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
scheduler.scheduleAtFixedRate(this::checkTimeouts, HEARTBEAT_INTERVAL_MS, 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() { public void stop() {
if (scheduler != null) { if (scheduler != null) {
scheduler.shutdownNow(); scheduler.shutdownNow();
@@ -46,21 +65,33 @@ public class HeartbeatManager {
private void announce() { private void announce() {
try { try {
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName()); HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName(), instanceId);
mainWindow.getSocket().send(msg.toByteArray()); mainWindow.getSocketUDP().sendMulticast(msg);
} catch (IOException ex) { } catch (IOException ex) {
Logger.getLogger(HeartbeatManager.class.getName()).log(Level.WARNING, "Falha ao enviar heartbeat", ex); Logger.getLogger(HeartbeatManager.class.getName()).log(Level.WARNING, "Falha ao enviar heartbeat", ex);
} }
} }
// Chamado pelo Socket quando um pacote é identificado como heartbeat // Chamado pelo Socket quando um pacote é identificado como heartbeat
/**
* Processa uma mensagem de heartbeat recebida de outro usuário.
*
* <p>Atualiza o momento da última atividade do peer correspondente e notifica os listeners
* caso outra instância da aplicação seja identificada.</p>
*
* <p>Mensagens de heartbeat enviadas pela própria aplicação são ignoradas.</p>
*
* @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) { public void receiveHeartbeat(HeartbeatMessage msg, InetAddress from) {
// ignora o próprio heartbeat // ignora o próprio heartbeat
if (msg.getUserName().equals(mainWindow.getUserName())) { if (msg.getInstanceId().equals(instanceId) ) {
return; return;
} }
String key = from.getHostAddress() + "#" + msg.getUserName(); String key = msg.getInstanceId();
boolean isNew = !peers.containsKey(key); boolean isNew = !peers.containsKey(key);
PeerInfo peer = peers.computeIfAbsent(key, k -> new PeerInfo(msg.getUserName(), from, msg.getTimestamp())); PeerInfo peer = peers.computeIfAbsent(key, k -> new PeerInfo(msg.getUserName(), from, msg.getTimestamp()));
@@ -86,15 +117,33 @@ public class HeartbeatManager {
} }
} }
/**
* Adiciona um listener para receber atualizações sobre os peers conhecidos.
*
* <p>O listener recebe imediatamente o estado atual dos peers ao ser
* registrado.</p>
*
* @param l listener que será notificado sobre alterações.
*/
public void addPeerListener(PeerListener l) { public void addPeerListener(PeerListener l) {
listeners.add(l); listeners.add(l);
l.onPeersChanged(new ArrayList<>(peers.values())); 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) { public void removePeerListener(PeerListener l) {
listeners.remove(l); listeners.remove(l);
} }
/**
* Retorna uma cópia da lista atual de peers conhecidos.
*
* @return lista contendo os peers atualmente conhecidos.
*/
public List<PeerInfo> getPeers() { public List<PeerInfo> getPeers() {
return new ArrayList<>(peers.values()); return new ArrayList<>(peers.values());
} }
+10
View File
@@ -2,6 +2,11 @@ package pitiupi.control;
import java.net.InetAddress; 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.
*
* <p>Armazena o nome do usuário, seu endereço de rede e o instante da
* última mensagem recebida.</p>
* *
* @author tony * @author tony
*/ */
@@ -29,6 +34,11 @@ public class PeerInfo {
return lastSeen; return lastSeen;
} }
/**
* Atualiza o instante da última atividade do usuário.
*
* @param timestamp novo instante de atividade.
*/
public void touch(long timestamp) { public void touch(long timestamp) {
this.lastSeen = timestamp; this.lastSeen = timestamp;
} }
@@ -1,10 +1,21 @@
package pitiupi.control; package pitiupi.control;
import java.util.List; import java.util.List;
/** /**
* Interface para receber notificações sobre alterações nos usuários
* conhecidos na rede.
*
* <p>Implementações desta interface são notificadas sempre que a lista
* de usuários ativos é modificada.</p>
* *
* @author tony * @author tony
*/ */
public interface PeerListener { 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<PeerInfo> peers); void onPeersChanged(List<PeerInfo> peers);
} }
@@ -17,7 +17,12 @@ import pitiupi.Main;
import pitiupi.plugin.Plugin; import pitiupi.plugin.Plugin;
/** /**
* Responsável por localizar, carregar e registrar os plugins
* presentes no diretório {@code plugins}.
* *
* <p>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.</p>
* @author flavio * @author flavio
*/ */
public class PluginLoader { public class PluginLoader {
+12 -1
View File
@@ -1,5 +1,10 @@
package pitiupi.net; package pitiupi.net;
/** /**
* Mensagem utilizada para sinalizar a presença de um usuário na rede.
*
* <p>É enviada periodicamente para permitir que a aplicação identifique
* usuários ativos e mantenha o controle dos participantes conectados.</p>
* *
* @author tony * @author tony
*/ */
@@ -7,9 +12,11 @@ public class HeartbeatMessage extends Message {
private final String userName; private final String userName;
private final long timestamp; private final long timestamp;
private final String instanceId;
public HeartbeatMessage(String userName) { public HeartbeatMessage(String userName, String instanceId) {
this.userName = userName; this.userName = userName;
this.instanceId = instanceId;
this.timestamp = System.currentTimeMillis(); this.timestamp = System.currentTimeMillis();
} }
@@ -20,4 +27,8 @@ public class HeartbeatMessage extends Message {
public long getTimestamp() { public long getTimestamp() {
return timestamp; return timestamp;
} }
public String getInstanceId() {
return instanceId;
}
} }
+14
View File
@@ -12,11 +12,25 @@ import java.io.ObjectOutputStream;
import java.io.Serializable; import java.io.Serializable;
/** /**
* Classe base para as mensagens trocadas entre a aplicação e os plugins.
*
* <p>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.</p>
*
* <p>Por implementar {@link Serializable}, objetos derivados desta classe
* podem ser convertidos em bytes para transmissão pela rede.</p>
* *
* @author flavio * @author flavio
*/ */
public abstract class Message implements Serializable{ 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 { public byte[] toByteArray() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutput out = new ObjectOutputStream(baos); ObjectOutput out = new ObjectOutputStream(baos);
-97
View File
@@ -1,97 +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;
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 receive() throws UnknownHostException, IOException, ClassNotFoundException {
byte[] buf = new byte[256000];
while (true) {
DatagramPacket msgPacket = new DatagramPacket(buf, buf.length);
multicastSocket.receive(msgPacket);
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);
}
}
}
@@ -0,0 +1,226 @@
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.
*
* <p>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.</p>
*
* <p>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}.</p>
*/
public class ConnectionFactory {
private static final int HANDSHAKE_TIMEOUT = 5000;
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.
*
* <p>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.</p>
*
* <p>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.</p>
*
* @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.
*
* <p>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}.</p>
*
* <p>O {@link Socket} recebido deve representar uma conexão TCP já aceita
* pelo servidor.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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;
}
}
@@ -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.
*
* <p>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.</p>
*
* <p>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.</p>
*/
class ConnectionManager implements PeerListener {
private final Logger logger;
private final Map<InetAddress, TcpConnection> publicConnections;
private final Map<InetAddress, List<TcpConnection>> privateConnections;
/**
* Cria um gerenciador de conexões TCP vazio.
*/
public ConnectionManager() {
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.
*
* <p>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.</p>
*
* @param connection conexão privada a ser registrada.
*/
public void addPrivateConnection(TcpConnection connection) {
List<TcpConnection> connections =
privateConnections.computeIfAbsent(connection.getAddress(), _ -> new ArrayList<>());
synchronized (connections) {
connections.add(connection);
}
}
/**
* Registra uma conexão pública associada a um peer.
*
* <p>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.</p>
*
* @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.
*
* <p>A mensagem enviada informa o motivo do encerramento antes que a
* conexão seja removida do gerenciamento e fechada.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>Para conexões públicas, a conexão é removida do mapa somente se
* ainda for a conexão registrada para seu endereço.</p>
*
* <p>Se a conexão já estiver fechada ou for {@code null}, nenhuma ação
* adicional de fechamento é realizada.</p>
*
* @param connection conexão TCP a ser removida e encerrada.
*/
public void closeConnection(TcpConnection connection) {
if (connection == null) return;
if (connection.isPrivate()) {
List<TcpConnection> 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<TcpConnection> 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.
*
* <p>Tanto a conexão pública quanto todas as conexões privadas associadas
* ao endereço são encerradas e removidas do gerenciamento.</p>
*
* @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<TcpConnection> 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<TcpConnection> 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.
*
* <p>Após a execução, não devem permanecer conexões registradas nos
* mapas de conexões deste gerenciador.</p>
*/
public void closeAllConnections() {
for (TcpConnection connection : publicConnections.values()) {
closeConnection(connection);
}
synchronized (privateConnections) {
for (List<TcpConnection> connectionsList : privateConnections.values()) {
for (TcpConnection connection : connectionsList) {
closeConnection(connection);
}
privateConnections.clear();
}
}
}
/**
* Atualiza as conexões de acordo com os peers atualmente ativos.
*
* <p>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.</p>
*
* @param peers lista atualizada de peers ativos.
*/
@Override
public void onPeersChanged(List<PeerInfo> peers) {
Set<InetAddress> 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);
}
}
}
}
@@ -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);
}
}
+476
View File
@@ -0,0 +1,476 @@
package pitiupi.net.tcp;
import pitiupi.GUI.MainWindow;
import pitiupi.net.Message;
import pitiupi.plugin.Plugin;
import pitiupi.plugin.PrivateConnectionPlugin;
import javax.swing.*;
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.
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>As conexões são associadas aos endereços dos peers e podem ser
* encerradas quando um peer deixa de estar ativo.</p>
*
* @author Gustavo
*/
public class SocketTCP extends Thread {
private final Logger logger;
private final ExecutorService connectionExecutor = Executors.newCachedThreadPool();
private final ConnectionManager connectionManager;
private ServerSocket serverSocket;
private InetAddress address;
private final MainWindow main;
private volatile boolean running = true;
/**
* Cria o servidor TCP da aplicação.
*
* <p>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.</p>
*
* @param main janela principal da aplicação, usada para obter a porta,
* o identificador local e os plugins disponíveis.
*/
public SocketTCP(MainWindow main) {
this.main = main;
logger = Logger.getLogger(getClass().getName());
connectionManager = new ConnectionManager();
try {
serverSocket = new ServerSocket(main.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.
*
* <p>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.</p>
*
* @param msg mensagem que será enviada.
* @param destinationAddress endereço IP do peer que receberá a mensagem.
*/
public void send(Message msg, InetAddress destinationAddress) {
try {
TcpConnection connection = getPublicConnection(destinationAddress, main.getIdentifier(), main.getIdentifier());
if (connection == null) {
System.out.println("Could not establish TCP connection.");
return;
}
connection.send(msg.toByteArray());
} catch (Exception ex) {
System.out.println("Could not send TCP message.");
System.out.println(ex.toString());
}
}
/**
* Estabelece uma conexão TCP privada com um plugin específico do peer de destino.
*
* <p>A conexão é criada como privada e passa a ser gerenciada pelo
* {@link ConnectionManager}. O método também inicia a tarefa responsável
* por receber os dados dessa conexão.</p>
*
* @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.
*
* <p>As conexões públicas são gerenciadas pelo {@link ConnectionManager}.
* 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 ConnectionManager} e disponibilizada para uso.</p>
*
* <p>O acesso ao método é sincronizado para evitar que múltiplas threads
* estabeleçam simultaneamente conexões públicas para o mesmo peer.</p>
*
* @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 = connectionManager.getPublicConnection(destinationAddress);
if (connection != null && !connection.isClosed()) return connection;
connection = establishOutgoingConnection(destinationAddress, senderId, receiverId, false);
return connection;
}
/**
* Aguarda e aceita novas conexões TCP.
*
* <p>Cada socket aceito é encaminhado para processamento assíncrono,
* interpretado como uma {@link TcpConnection}, registrado pelo
* {@link ConnectionManager} e associado ao receptor adequado.</p>
*/
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.
*
* <p>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.</p>
*
* @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, main.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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @param connection conexão que será registrada.
*/
private void saveConnection(TcpConnection connection) {
if (connection.isPrivate()) {
connectionManager.addPrivateConnection(connection);
}
else {
connectionManager.addPublicConnection(connection);
}
}
/**
* Seleciona e inicia o receptor adequado para uma conexão.
*
* <p>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.</p>
*
* @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(main.getIdentifier())) {
systemConnectionReceiver(connection);
return;
}
Plugin receiver = findConnectionReceiver(connection);
if (receiver instanceof PrivateConnectionPlugin plugin) {
plugin.receivePrivateConnection(connection);
privateConnectionReceiver(connection, plugin);
}
}
/**
* Localiza o plugin responsável por receber uma conexão privada.
*
* <p>A busca é feita pelo identificador do destinatário informado na
* própria conexão. Caso nenhum plugin correspondente seja encontrado,
* uma mensagem de erro é enviada ao peer remoto.</p>
*
* @param connection conexão privada cujo destinatário será localizado.
* @return o plugin com o identificador correspondente ao destinatário,
* ou {@code null} se nenhum plugin for encontrado.
*/
private Plugin findConnectionReceiver(TcpConnection connection) {
Plugin receiver = null;
for(Plugin plugin : main.getPlugins()) {
if (plugin.getIdentifier().equals(connection.getReceiver())) {
receiver = plugin;
break;
}
}
if (receiver == null) {
SysInfoMessage.sendSystemInfo(
connection,
SysInfoType.RECEIVER_NOT_FOUND,
"This peer does not have the plugin: " + connection.getReceiver()
);
}
return receiver;
}
/**
* Processa continuamente as mensagens recebidas em uma conexão pública.
*
* <p>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.</p>
*
* <p>O método também trata timeouts e erros de I/O, removendo a conexão
* do {@link ConnectionManager} quando ela deixa de ser utilizável.</p>
*
* @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) {
for (Plugin plugin : main.getPlugins()) {
plugin.receiveMessage(bytes);
}
continue;
}
switch (sysMessage.getType()) {
case CONNECTION_CLOSED, INTERNAL_ERROR -> {
connectionManager.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()) {
connectionManager.closeConnection(connection, "Connection timeout");
return;
}
}
catch (IOException ex) {
connectionManager.closeConnection(connection);
}
}
}
/**
* Processa os dados recebidos em uma conexão privada e os encaminha
* ao plugin destinatário.
*
* <p>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.</p>
*
* @param connection conexão privada da qual os dados serão recebidos.
* @param plugin plugin responsável pelo processamento dos dados recebidos.
*/
private void privateConnectionReceiver(TcpConnection connection, Plugin plugin) {
if (plugin instanceof PrivateConnectionPlugin privatePlugin) {
while (running) {
try {
byte[] message = connection.receive();
SysInfoMessage sysMessage = SysInfoMessage.parseSystemInfo(message);
if (sysMessage == null) {
privatePlugin.receiveBytes(connection, message);
continue;
}
switch (sysMessage.getType()) {
case CONNECTION_CLOSED, INTERNAL_ERROR -> {
privatePlugin.onSocketClosed(connection);
connectionManager.closeConnection(connection);
}
default -> {
continue;
}
}
}
catch (SocketTimeoutException ex) {
if (connection.hasTimedOut()) {
connectionManager.closeConnection(connection, "Connection timeout");
return;
}
}
catch (IOException ex) {
connectionManager.closeConnection(connection);
}
}
}
}
/**
* Processa mensagens recebidas em uma conexão destinada ao próprio sistema.
*
* <p>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.</p>
*
* @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) {
connectionManager.closeConnection(connection);
}
}
}
/**
* Encerra uma conexão TCP e registra o motivo informado.
*
* <p>Quando nenhuma descrição é fornecida, é utilizado um motivo padrão.
* O fechamento efetivo da conexão é delegado ao {@link ConnectionManager}.</p>
*
* @param connection conexão que será encerrada.
* @param description motivo do encerramento; pode ser {@code null}.
*/
public void disconnect(TcpConnection connection, String description) {
connectionManager.closeConnection(
connection,
Objects.requireNonNullElse(description, "No reason provided")
);
}
@Override
public void run() {
receive();
}
/**
* Encerra o servidor TCP e libera os recursos associados.
*
* <p>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.</p>
*
* <p>Após a chamada, esta instância não deve ser reutilizada para aceitar
* novas conexões.</p>
*/
public void close() {
running = false;
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
connectionManager.closeAllConnections("The peer must have closed the application or changed ports");
connectionExecutor.shutdownNow();
}
}
@@ -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;
}
}
@@ -0,0 +1,9 @@
package pitiupi.net.tcp;
public enum SysInfoType {
CONNECTION_REJECTED,
RECEIVER_NOT_FOUND,
MESSAGE_REJECTED,
INTERNAL_ERROR,
CONNECTION_CLOSED,
}
@@ -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.
*
* <p>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.</p>
*
* @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.
*
* <p>A mensagem é enviada precedida pelo seu tamanho, permitindo que o
* receptor determine quantos bytes devem ser lidos.</p>
*
* @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.
*
* <p>Primeiro lê o tamanho da mensagem e, em seguida, lê aquantidade
* correspondente de bytes.</p>
*
* @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.
*
* <p>A verificação é baseada no instante da última atividade registrada
* durante o envio ou recebimento de uma mensagem.</p>
*
* @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;
}
}
+190
View File
@@ -0,0 +1,190 @@
/*
* 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.net.HeartbeatMessage;
import pitiupi.net.Message;
import pitiupi.plugin.Plugin;
/**
* Gerencia a comunicação UDP multicast e unicast da aplicação.
*
* <p>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.</p>
*
* <p>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.</p>
*
* @author flavio
* @author tony
* @author Gustavo
*/
public class SocketUDP 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";
/**
* Cria e inicializa o socket UDP utilizado pela aplicação.
*
* <p>O socket é associado à porta da aplicação e ingressa no grupo
* multicast configurado.</p>
*
* @param main janela principal da aplicação.
*/
public SocketUDP(MainWindow main) {
this.main = main;
try {
this.address = InetAddress.getByName(SocketUDP.INET_ADDR);
} catch (UnknownHostException ex) {
System.getLogger(SocketUDP.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);
}
}
/**
* Envia uma mensagem para o grupo multicast da aplicação.
*
* @param msg mensagem serializada a ser enviada.
* @throws IOException caso ocorra um erro durante o envio.
*/
public void sendMulticast(Message msg) throws IOException {
send(msg, address);
}
/**
* Envia uma mensagem diretamente para um peer utilizando UDP.
*
* @param msg mensagem serializada 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, 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(Message msg, InetAddress destinationAddress) throws IOException {
DatagramPacket msgPacket;
byte[] buf = msg.toByteArray();
msgPacket = new DatagramPacket(buf, buf.length, destinationAddress, this.main.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.
*
* <p>Mensagens de heartbeat são encaminhadas ao gerenciador de heartbeat.
* As demais mensagens são encaminhadas aos plugins carregados pela
* aplicação.</p>
*
* @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) {
main.getHeartbeatManager().receiveHeartbeat(heartbeat, msgPacket.getAddress());
continue;
}
for (Plugin plugin : main.getPlugins()) {
System.out.println(msgPacket.getAddress().toString());
plugin.receiveMessage(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);
}
}
}
+30 -1
View File
@@ -4,11 +4,15 @@
*/ */
package pitiupi.plugin; package pitiupi.plugin;
import javax.swing.JMenu;
import pitiupi.GUI.MainWindow; import pitiupi.GUI.MainWindow;
import pitiupi.net.Message; import pitiupi.net.Message;
import pitiupi.net.tcp.TcpConnection;
/** /**
* Interface base para plugins da aplicação.
*
* <p>Cada plugin deve implementar esta interface para ser carregado
* pela aplicação e participar do sistema de comunicação por mensagens.</p>
* *
* @author flavio * @author flavio
*/ */
@@ -17,7 +21,32 @@ public interface Plugin {
public String getName(); public String getName();
public String getAuthor(); public String getAuthor();
public String getVersion(); public String getVersion();
public String getIdentifier();
/**
* Inicializa o plugin.
*
* <p>Este método é chamado pela aplicação após o carregamento do plugin.</p>
*
* @param window janela principal da aplicação.
*/
public void createPlugin(MainWindow window); public void createPlugin(MainWindow window);
/**
* Retorna uma mensagem utilizada pelo plugin.
*
* @return mensagem criada pelo plugin.
*/
public Message getMessage(); public Message getMessage();
/**
* Recebe uma mensagem enviada pela aplicação.
*
* <p>A mensagem é recebida em formato serializado. O plugin deve verificar
* se a mensagem pertence ao seu tipo e realizar o processamento necessário.</p>
*
* @param message mensagem serializada recebida.
*/
public void receiveMessage(byte[] message); public void receiveMessage(byte[] message);
} }
@@ -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);
}