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
57 changed files with 2397 additions and 479 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
@@ -1,5 +0,0 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-83
View File
@@ -1,83 +0,0 @@
<?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="62118a1b-608f-4144-b29c-cf46419ca676" name="Changes" comment="" />
<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;: 0
}</component>
<component name="ProjectId" id="3GbOdQKFlKqBjAjqjAv5TRPxr6Z" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Application.Main.executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;feature/help-and-documentation&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.keymap&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\extre\OneDrive\Documentos\Faculdade\2026_02\Monitoria\Pitiupi\pitiupi\plugins" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\extre\OneDrive\Documentos\Faculdade\2026_02\Monitoria\Pitiupi\pitiupi\plugins" />
</key>
</component>
<component name="RunManager">
<configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="pitiupi.Main" />
<module name="pitiupi" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pitiupi.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<recent_temporary>
<list>
<item itemvalue="Application.Main" />
</list>
</recent_temporary>
</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="62118a1b-608f-4144-b29c-cf46419ca676" name="Changes" comment="" />
<created>1784236888334</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1784236888334</updated>
</task>
<servers />
</component>
</project>
-258
View File
@@ -1,258 +0,0 @@
# Como criar um plugin para o pitiupi ?
O objetivo aqui, além de criar plugins para esse sistema, é entender **por que cada parte existe**. Isso é importante porque a ideia de **carregar código externo em tempo de execução** aparece em várias ferramentas nas áreas da computação.
---
## 1. Visão rápida
O repositório contém projetos Java **independentes**:
| Projeto | O que é |
|---|---|
| `pitiupi` | A ferramenta principal : P2P, janela, rede, e o "motor" que carrega plugins |
| `pitiupi.chat` | Um plugin que adiciona um chat |
| `pitiupi.screenCast` | Outro plugin que adiciona compartilhamento de tela |
Cada plugin é compilado separadamente com `ant` e o `.jar` gerado é colocado (ou linkado) dentro da pasta `pitiupi/plugins`.
Quando o pitiupi principal inicia, ele varre essa pasta e **carrega dinamicamente** cada plugin encontrado, sem que o código principal precise conhecer o plugin em tempo de compilação.
Isso é possível graças a três partes que trabalham juntas:
1. A **interface `Plugin`** : o contrato que todo plugin deve cumprir.
2. O **`PluginLoader`** : a classe do pitiupi que varre a pasta `plugins/` e carrega os `.jar`.
3. O **arquivo de registro do `ServiceLoader`** : um arquivo de texto dentro do `.jar` que diz "esta classe aqui implementa `Plugin`".
---
## 2. O contrato: a interface `Plugin`
Todo plugin **precisa** implementar esta interface (`pitiupi/src/pitiupi/plugin/Plugin.java`):
```java
public interface Plugin {
public String getName();
public String getAuthor();
public String getVersion();
public void createPlugin(MainWindow window);
public Message getMessage();
public void receiveMessage(byte[] message);
}
```
O que cada método faz:
- **`getName()` / `getAuthor()` / `getVersion()`** : indentificação de dados básicos.
- **`createPlugin(MainWindow window)`** : É aqui que o plugin cria sua interface gráfica, adiciona itens de menu na janela principal, etc.
- **`getMessage()`** : Serve como modelo para a camada de rede saber que tipo de mensagem esse plugin envia.
- **`receiveMessage(byte[] message)`** : recebe as mensagens que chegam pela rede e permite que o plugin interprete esses dados.
---
## 3. Como o pitiupi encontra e carrega os plugins
A classe `PluginLoader` (`pitiupi/src/pitiupi/control/PluginLoader.java`) faz o seguinte:
1. Olha dentro da pasta `plugins/` e lista todos os arquivos `.jar`.
2. Cria um `URLClassLoader` : um "carregador de classes" isolado, que sabe ler código de dentro desses `.jar`.
3. Usa `ServiceLoader.load(Plugin.class, pluginClassLoader)` para perguntar: *"dentre essas classes, quais implementam `Plugin`?"*
4. Para cada plugin encontrado, chama `createPlugin(mainWindow)` e guarda o plugin numa lista.
```java
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, pluginClassLoader);
for (Plugin plugin : serviceLoader) {
plugin.createPlugin(this.mainWindow);
this.mainWindow.getPlugins().add(plugin);
}
```
**Obs:** o `ServiceLoader` do Java **não escaneia o bytecode procurando quem implementa a interface**. Ele lê um arquivo de texto simples dentro do `.jar`. Se esse arquivo não existir ou estiver errado, o plugin é ignorado : mesmo que o código esteja perfeito.
---
## 4.`META-INF/services`
Dentro do projeto `pitiupi.chat` por exemplo, existe este arquivo:
```
pitiupi.chat/src/META-INF/services/pitiupi.plugin.Plugin
```
Repare em duas coisas:
- A **pasta** se chama `META-INF/services`.
- O **nome do arquivo** é o nome completo da interface: `pitiupi.plugin.Plugin`.
- O **conteúdo** do arquivo é uma linha com o nome completo da classe que implementa essa interface:
```
pitiupi.chat.plugin.ChatPlugin
```
- O`Plugin` diz **"eu sei fazer o trabalho"**.
- O arquivo em `META-INF/services` diz **"aqui está quem sabe fazer o trabalho"**.
- Sem o segundo, o pitiupi nunca fica sabendo que a classe existe.
---
## 5. Exemplo real: o `ChatPlugin`
Veja como o plugin de chat implementa o contrato : use isso como referência ao escrever o seu:
```java
public class ChatPlugin implements Plugin {
private ChatWindow chatWindow;
private MainWindow mainWindow;
@Override
public void createPlugin(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.chatWindow = new ChatWindow(mainWindow);
this.mainWindow.addMenu(new ChatMenu(chatWindow));
}
@Override
public void receiveMessage(byte[] message) {
// desserializa os bytes recebidos de volta em um objeto ChatMessage
ByteArrayInputStream bis = new ByteArrayInputStream(message);
try {
ObjectInputStream ois = new ObjectInputStream(bis);
if (ois.readObject() instanceof ChatMessage msg) {
this.chatWindow.receiveChatMessage(msg);
}
} catch (IOException | ClassNotFoundException ex) {
System.out.println(ex.toString());
}
}
@Override
public String getName() { return "Chat"; }
@Override
public String getAuthor() { return "Flávio, Tony e Gustavo "; }
@Override
public String getVersion() { return "0"; }
@Override
public Message getMessage() { return new ChatMessage(); }
}
```
Em `createPlugin` é onde o bum de "aparecer na tela" acontece: ele cria a janela do plugin e adiciona um menu na janela principal do pitiupi.
---
## 6. Passo a passo: criando um plugin novo do zero
Vamos chamar o plugin de exemplo de `SeuPlugin`.
### Passo 1 : Criar a estrutura do projeto
Se você quiser um jeito mais facil sem se aprofundar, você pode criar o projeto de duas formas:
- **Mais simples:** copie um projeto Ant existente, como `pitiupi.chat` por exemplo, e remova o código específico do Chat.
- **Outra opção:** crie um novo projeto no NetBeans usando **"Java Class Library"**, mantendo a mesma estrutura de pastas.
```
pitiupi.seuplugin/
├── build.xml
├── nbproject/
└── src/
├── META-INF/services/
└── pitiupi/seuplugin/plugin/
```
### Passo 2 : Adicionar o `pitiupi.jar` como dependência
O plugin precisa do arquivo **`pitiupi/dist/pitiupi.jar`** para acessar `Plugin` e `MainWindow`.
- **Se criou do zero:** configure o `pitiupi.jar` como dependência.
- **Se copiou um projeto existente:** essa configuração já está pronta.
- **Se criou pelo NetBeans:** **Properties → Libraries → Add JAR/Folder** → selecione `pitiupi/dist/pitiupi.jar`.
### Passo 3 : Implementar a interface `Plugin`
Crie `src/pitiupi/seuplugin/plugin/SeuPlugin.java`:
```java
package pitiupi.seuplugin.plugin;
import pitiupi.GUI.MainWindow;
import pitiupi.net.Message;
import pitiupi.plugin.Plugin;
public class SeuPlugin implements Plugin {
@Override
public void createPlugin(MainWindow window) {
System.out.println("SeuPlugin foi carregado!");
// aqui você criaria sua janela/menu, como o ChatPlugin faz
}
@Override
public void receiveMessage(byte[] message) {
// trate as mensagens recebidas da rede aqui
}
@Override
public String getName() { return "MeuPlugin"; }
@Override
public String getAuthor() { return "Seu Nome"; }
@Override
public String getVersion() { return "0.1"; }
@Override
public Message getMessage() { return null; } // troque por uma Message real, se tiver
}
```
### Passo 4 : Registrar o plugin no `ServiceLoader`
Crie o arquivo (sem extensão):
```
src/META-INF/services/pitiupi.plugin.Plugin
```
Com **uma única linha** dentro, o nome completo da sua classe:
```
pitiupi.seuplugin.plugin.MeuPlugin
```
### Passo 5 : Compilar com Ant
```bash
cd pitiupi.seuplugin
ant jar
```
Isso gera o `.jar` em `pitiupi.seuplugin/dist/`.
### Passo 6 : Linkar o `.jar` na pasta de plugins
```bash
cd pitiupi/plugins
ln -s ../../pitiupi.meuplugin/dist/*.jar .
```
Usar link simbólico (`ln -s`) é melhor durante o desenvolvimento: toda vez que você recompilar o plugin, não precisa copiar de novo.
## 7. Geral
```
Plugin.java → "o que todo plugin precisa saber fazer" (o contrato)
MeuPlugin.java → "como EU faço esse trabalho" (a implementação)
META-INF/services/pitiupi.plugin.Plugin → "aqui está quem faz o trabalho" (o registro)
PluginLoader.java → "eu procuro registros e chamo quem se registrou" (o carregador, já pronto no pitiupi)
```
Entendendo essas quatro peças, criar qualquer plugin novo : chat, screencast, ou algo totalmente diferente (arquivos, jogos, sensores) : é sempre o mesmo padrão: implementar a interface, registrar no `META-INF/services`, compilar e linkar.
+1 -1
View File
@@ -1,4 +1,4 @@
#Sat, 18 Jul 2026 10:32:46 -0300 #Mon, 17 Aug 2026 14:26:26 -0300
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.chat= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.chat=
Binary file not shown.
+9
View File
@@ -7,5 +7,14 @@
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <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> </component>
</module> </module>
@@ -91,7 +91,7 @@ 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.getUserName(), jTextArea1.getText()); ChatMessage message = new ChatMessage(mainWindow.getUserName(), jTextArea1.getText());
@@ -101,7 +101,7 @@ public class ChatWindow extends javax.swing.JDialog {
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>
+73 -11
View File
@@ -5,14 +5,24 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment=""> <list default="true" id="083e72ce-fa9b-4ddf-a0ef-24105aa3eabb" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/META-INF/services/pitiupi.plugin.Plugin" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../.idea/misc.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/GUI/QuizWindow.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../.idea/vcs.xml" beforeDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/control/Pergunta.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/control/Quiz.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/net/QuizMessage.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/../pitiupi.quiz/src/pitiupi/plugin/Quiz.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pitiupi/screenCast/net/ScreenCastMessage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/pitiupi/screenCast/net/ScreenCastMessage.java" 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> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -22,9 +32,9 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component> </component>
<component name="ProjectColorInfo"><![CDATA[{ <component name="ProjectColorInfo">{
"associatedIndex": 7 &quot;associatedIndex&quot;: 7
}]]></component> }</component>
<component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" /> <component name="ProjectId" id="3Gs0wFaRD4HyhP9VXLUWBeq0qTL" />
<component name="ProjectViewState"> <component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
@@ -34,13 +44,65 @@
"keyToString": { "keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true", "ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "QuizPlugin", "git-widget-placeholder": "feature/unicast",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true", "kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/extre/OneDrive/Documentos/Faculdade/2026_02/Monitoria/Pitiupi/pitiupi.screenCast", "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" "settings.editor.selected.configurable": "preferences.keymap"
} }
}]]></component> }]]></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"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
@@ -1,4 +1,4 @@
#Sat, 18 Jul 2026 10:33:07 -0300 #Mon, 17 Aug 2026 14:35:40 -0300
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.screenCast= C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi.screenCast=
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/
+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>
+406 -73
View File
@@ -1,21 +1,23 @@
# Arquitetura da aplicação # Visão geral
## Visão geral O `Pitiupi` é uma plataforma P2P baseada em uma arquitetura de plugins,
O Share My Sheet é uma plataforma P2P baseada em uma arquitetura de plugins,
desenvolvida para permitir a criação de aplicações distribuídas sobre uma desenvolvida para permitir a criação de aplicações distribuídas sobre uma
infraestrutura comum de comunicação em rede. infraestrutura comum de comunicação em rede.
A aplicação fornece mecanismos de descoberta de usuários, comunicação A aplicação fornece mecanismos de descoberta de usuários, comunicação entre
multicast, troca de mensagens serializadas e integração de extensões por meio peers, troca de mensagens serializadas e integração de extensões por meio de
de plugins. Dessa forma, novos comportamentos podem ser adicionados sem plugins. A comunicação de rede utiliza dois protocolos de transporte: UDP,
modificar o núcleo da aplicação. 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 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 Computadores, servindo também como base para experimentação de aplicações
distribuídas e desenvolvimento de projetos acadêmicos. distribuídas e desenvolvimento de projetos acadêmicos.
## Arquitetura geral # Arquitetura geral
A aplicação é dividida em três grupos principais: A aplicação é dividida em três grupos principais:
@@ -23,103 +25,396 @@ A aplicação é dividida em três grupos principais:
sendo responsável por inicializar e integrar esses componentes durante a sendo responsável por inicializar e integrar esses componentes durante a
execução. execução.
- **Sistema de plugins**: responsável por adicionar funcionalidades sem - **Sistema de plugins**: responsável por adicionar funcionalidades sem
alterar o núcleo. alterar o núcleo da aplicação.
- **Comunicação de rede**: responsável pela troca de mensagens entre - **Comunicação de rede**: responsável pela troca de mensagens entre
instâncias da aplicação. instâncias da aplicação.
## Inicialização da aplicação ## Inicialização da aplicação
1. A MainWindow é criada. 1. A MainWindow é criada.
2. O Socket é inicializado e inicia a comunicação multicast. 2. O SocketUDP é inicializado, associa-se à porta configurada e ingressa no
3. O HeartbeatManager é criado e inicia o envio periódico de heartbeats. grupo multicast.
4. O painel de usuários online é registrado como listener do HeartbeatManager. 3. O SocketTCP é inicializado e cria um ServerSocket na porta da aplicação
5. O PluginLoader procura e carrega os plugins disponíveis. 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 # Sistema de comunicação
### Socket A comunicação entre as instâncias da aplicação é realizada por meio de dois
mecanismos complementares:
A classe `Socket` é responsável pela comunicação de rede da aplicação. - **UDP multicast/unicast**, implementado por `SocketUDP`
Ela utiliza comunicação multicast UDP para permitir que diferentes - **TCP**, implementado por `SocketTCP` e `TcpConnection`
instâncias da aplicação troquem mensagens sem a necessidade de um servidor
central.
A classe estende `Thread`, pois mantém um processo contínuo de recepção de Ambos os mecanismos utilizam mensagens serializadas em `byte[]`, permitindo que
mensagens enquanto a aplicação continua executando suas demais atividades. os plugins permaneçam independentes dos detalhes do protocolo de transporte.
Durante sua inicialização, o socket: ## Comunicação UDP
- obtém o endereço do grupo multicast;
- cria um `MulticastSocket` utilizando a porta configurada;
- configura os buffers de comunicação;
- entra no grupo multicast.
#### Envio de mensagens A classe `SocketUDP` é responsável pela comunicação UDP da aplicação.
O método `send()` recebe uma mensagem já serializada em bytes e cria um Ela utiliza um `MulticastSocket` associado à porta configurada pela aplicação e
`DatagramPacket`, enviando-o para o grupo multicast. 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.
De forma geral, o socket atua apenas como mecanismo de transporte das mensagens. A classe estende `Thread`, mantendo um processo contínuo de recepção de datagramas
A única exceção é o tratamento de HeartbeatMessage, utilizado pela infraestrutura enquanto a aplicação está em execução.
da aplicação para manter a lista de peers ativos.
#### Recepção de mensagens O SocketUDP suporta dois modos de envio:
O método `receive()` mantém um loop aguardando novas mensagens multicast. - **multicast**, destinado ao grupo de peers
- **unicast**, destinado diretamente ao endereço IP de um peer específico
Ao receber uma mensagem: ### Inicialização
1. Os dados são analisados para verificar se correspondem a um
`HeartbeatMessage`.
2. Caso seja um heartbeat, a mensagem é encaminhada ao `HeartbeatManager`
para atualização dos usuários ativos.
3. Caso contrário, a mensagem é enviada para todos os plugins carregados
através do método `receiveMessage()`.
Esse comportamento permite que a aplicação principal trate mensagens de Durante sua inicialização, o `SocketUDP`:
infraestrutura (como heartbeat), enquanto mensagens específicas de plugins
são processadas pelas extensões correspondentes. 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 ### Message
A classe `Message` representa a estrutura base das mensagens trocadas pela A classe `Message` representa a estrutura base das mensagens trocadas pela aplicação.
aplicação.
Todas as mensagens utilizadas pelo sistema devem herdar dessa classe. Ela Todas as mensagens utilizadas pelo sistema devem herdar dessa classe. Ela implementa
implementa `Serializable`, permitindo que objetos de mensagem sejam `Serializable`, permitindo que objetos de mensagem sejam convertidos em vetores de
convertidos em vetores de bytes para transmissão pela rede. bytes para transmissão pela rede.
A serialização é realizada pelo método `toByteArray()`, que transforma uma A serialização é realizada pelo método `toByteArray()`, que transforma uma instância
instância da mensagem em uma representação binária enviada pelo `Socket`. da mensagem em uma representação binária enviada pelos mecanismos de comunicação.
Mensagens específicas devem estender essa classe adicionando os atributos e As mensagens específicas devem estender essa classe, adicionando os atributos e
comportamentos necessários para cada funcionalidade. 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 ### Heartbeat
O mecanismo de heartbeat é utilizado para identificar quais usuários estão O mecanismo de heartbeat é utilizado para identificar quais usuários estão ativos
ativos na rede. na rede.
A aplicação envia periodicamente uma `HeartbeatMessage` contendo A aplicação envia periodicamente uma `HeartbeatMessage` contendo informações do usuário,
informações do usuário, um identificador único da instância da aplicação um identificador único da instância da aplicação e um timestamp.
e um timestamp. Quando outra instância recebe essa mensagem, 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 Essas mensagens são transmitidas utilizando a comunicação UDP multicast.
sem receber um novo heartbeat. O `HeartbeatManager` então remove o peer
da lista e notifica os listeners sobre a alteração.
O fluxo de heartbeat é independente das mensagens dos plugins: 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.
1. `HeartbeatManager` cria uma `HeartbeatMessage`. O `HeartbeatManager` registra ou atualiza o peer correspondente, utilizando o
2. A mensagem é serializada utilizando `Message.toByteArray()`. identificador da instância para diferenciá-lo das demais.
3. O `Socket` transmite a mensagem pela rede multicast.
4. O `Socket` identifica mensagens de heartbeat recebidas e encaminha ao Um peer é considerado inativo quando permanece mais de 15 segundos sem receber um novo
`HeartbeatManager`. heartbeat. O `HeartbeatManager` então remove o peer da lista e notifica todos os
5. O `HeartbeatManager` atualiza a lista de peers ativos e notifica os componentes registrados como listeners de mudanças de peers.
listeners.
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 ## 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 ### Interface Plugin
A interface `Plugin` define o contrato que deve ser implementado por qualquer A interface `Plugin` define o contrato que deve ser implementado por qualquer
@@ -186,7 +481,7 @@ MeusPlugins.plugins.NomeDaClasse
Para serem carregados, os plugins devem estar empacotados como arquivos JAR Para serem carregados, os plugins devem estar empacotados como arquivos JAR
e colocados no diretório plugins da aplicação. e colocados no diretório plugins da aplicação.
### Carregamento ### Carregamento de plugins
Ao iniciar a aplicação, o `PluginLoader` executa os seguintes passos: Ao iniciar a aplicação, o `PluginLoader` executa os seguintes passos:
@@ -209,6 +504,44 @@ Durante a inicialização, cada plugin recebe uma referência para a
Essa referência permite integrar componentes gráficos à aplicação, sendo a 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: principal forma de extensão a adição de menus à barra de menus:
```java ```
JMenu menu = new JMenu("Meu Plugin"); JMenu menu = new JMenu("Meu Plugin");
window.addMenu(menu); 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.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -48,7 +48,6 @@ public final class Login extends JDialog {
public void initComponents() { public void initComponents() {
this.setTitle("Login"); this.setTitle("Login");
String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name")); String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
int portaSalva = prefs.getInt(PREF_PORTA, 8000); int portaSalva = prefs.getInt(PREF_PORTA, 8000);
+32 -10
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;
@@ -39,11 +40,14 @@ public class MainWindow extends javax.swing.JFrame {
private OnlineUsersPanel onlineUsersPanel; private OnlineUsersPanel onlineUsersPanel;
private Socket socket; private SocketUDP socketUDP;
private SocketTCP socketTCP;
private String userName; private String userName;
private int port; private int port;
private String pitiupiIdentifier;
private PluginLoader pluginManager; private PluginLoader pluginManager;
private final List<Plugin> plugins; private final List<Plugin> plugins;
@@ -56,6 +60,7 @@ public class MainWindow extends javax.swing.JFrame {
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();
@@ -91,8 +96,11 @@ public class MainWindow extends javax.swing.JFrame {
} }
private void initSocket() { private void initSocket() {
this.socket = new Socket(this); this.socketUDP = new SocketUDP(this);
this.socket.start(); this.socketTCP = new SocketTCP(this);
this.socketUDP.start();
this.socketTCP.start();
} }
private void initHeartbeat() { private void initHeartbeat() {
@@ -122,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);
} }
@@ -130,8 +142,12 @@ 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() {
@@ -154,11 +170,17 @@ public class MainWindow extends javax.swing.JFrame {
} }
private void reconnect() { private void reconnect() {
if (this.socket != null) { if (this.socketUDP != null) {
this.socket.close(); this.socketUDP.close();
} }
this.socket = new Socket(this); if (this.socketTCP != null) {
this.socket.start(); this.socketTCP.close();
} }
this.socketUDP = new SocketUDP(this);
this.socketTCP = new SocketTCP(this);
this.socketUDP.start();
this.socketTCP.start();
}
} }
@@ -66,7 +66,7 @@ public class HeartbeatManager {
private void announce() { private void announce() {
try { try {
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName(), instanceId); 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);
} }
@@ -1,4 +1,5 @@
package pitiupi.net; package pitiupi.net;
/** /**
* Mensagem utilizada para sinalizar a presença de um usuário na rede. * Mensagem utilizada para sinalizar a presença de um usuário na rede.
* *
@@ -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;
}
}
@@ -3,50 +3,58 @@
* To change this template file, choose Tools | Templates * To change this template file, choose Tools | Templates
* and open the template in the editor. * and open the template in the editor.
*/ */
package pitiupi.net; package pitiupi.net.udp;
import pitiupi.GUI.MainWindow; import pitiupi.GUI.MainWindow;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInput; import java.io.ObjectInput;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.net.DatagramPacket; import java.net.*;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import pitiupi.plugin.Plugin;
import pitiupi.net.HeartbeatMessage; import pitiupi.net.HeartbeatMessage;
import pitiupi.net.Message;
import pitiupi.plugin.Plugin;
/** /**
* "Responsável pela comunicação multicast da aplicação e pelo encaminhamento de mensagens aos plugins." * Gerencia a comunicação UDP multicast e unicast da aplicação.
* *
* <p>Esta classe gerencia o envio e o recebimento de mensagens, além de * <p>Mantém a conexão com o grupo multicast da aplicação e permite o envio
* manter a conexão com o grupo multicast utilizado pela aplicação.</p> * 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 flavio
* @author tony * @author tony
* @author Gustavo
*/ */
public class Socket extends Thread { public class SocketUDP extends Thread {
private MulticastSocket multicastSocket; private MulticastSocket multicastSocket;
private InetAddress address; private InetAddress address;
private MainWindow main; private MainWindow main;
private volatile boolean running = true; private volatile boolean running = true;
public final static String INET_ADDR = "224.0.0.3"; public final static String INET_ADDR = "224.0.0.3";
/** /**
* Cria e inicializa a conexão multicast utilizada pela aplicação. * 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. * @param main janela principal da aplicação.
*/ */
public Socket(MainWindow main) { public SocketUDP(MainWindow main) {
this.main = main; this.main = main;
try { try {
this.address = InetAddress.getByName(Socket.INET_ADDR); this.address = InetAddress.getByName(SocketUDP.INET_ADDR);
} catch (UnknownHostException ex) { } catch (UnknownHostException ex) {
System.getLogger(Socket.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex); System.getLogger(SocketUDP.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
} }
try { try {
multicastSocket = new MulticastSocket(this.main.getPort()); multicastSocket = new MulticastSocket(this.main.getPort());
@@ -56,7 +64,7 @@ public class Socket extends Thread {
multicastSocket.joinGroup(address); multicastSocket.joinGroup(address);
} catch (IOException ex) { } catch (IOException ex) {
System.out.println("There is no socket connection. Sorry."); System.out.println("There is no socket connection. Sorry.");
System.out.println(ex.toString()); System.out.println(ex);
} }
} }
@@ -64,11 +72,34 @@ public class Socket extends Thread {
* Envia uma mensagem para o grupo multicast da aplicação. * Envia uma mensagem para o grupo multicast da aplicação.
* *
* @param msg mensagem serializada a ser enviada. * @param msg mensagem serializada a ser enviada.
* @throws IOException caso ocorra um erro durante o envio da mensagem. * @throws IOException caso ocorra um erro durante o envio.
*/ */
public void send(byte[] msg) throws IOException { 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; DatagramPacket msgPacket;
msgPacket = new DatagramPacket(msg, msg.length, this.address, this.main.getPort()); byte[] buf = msg.toByteArray();
msgPacket = new DatagramPacket(buf, buf.length, destinationAddress, this.main.getPort());
multicastSocket.send(msgPacket); multicastSocket.send(msgPacket);
} }
@@ -81,7 +112,7 @@ public class Socket extends Thread {
try { try {
multicastSocket.leaveGroup(address); multicastSocket.leaveGroup(address);
} catch (IOException ex) { } catch (IOException ex) {
System.out.println(ex);
} }
multicastSocket.close(); multicastSocket.close();
} }
@@ -89,15 +120,17 @@ public class Socket extends Thread {
/** /**
* Inicia o loop de recepção de mensagens da aplicação. * Recebe e processa mensagens UDP.
* *
* <p>As mensagens recebidas são verificadas inicialmente para identificar * <p>Mensagens de heartbeat são encaminhadas ao gerenciador de heartbeat.
* mensagens de heartbeat. Caso não sejam heartbeats, seu conteúdo é * As demais mensagens são encaminhadas aos plugins carregados pela
* encaminhado para todos os plugins carregados, que decidem se devem ou * aplicação.</p>
* não processá-lo.</p>
* *
* @throws IOException caso ocorra um erro durante a recepção da mensagem. * @throws UnknownHostException caso não seja possível resolver o endereço
* @throws ClassNotFoundException caso a desserialização de uma mensagem falhe. * 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 { public void receive() throws UnknownHostException, IOException, ClassNotFoundException {
byte[] buf = new byte[256000]; byte[] buf = new byte[256000];
@@ -111,11 +144,13 @@ public class Socket extends Thread {
} }
throw ex; throw ex;
} }
HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData()); HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData());
if (heartbeat != null) { if (heartbeat != null) {
main.getHeartbeatManager().receiveHeartbeat(heartbeat, msgPacket.getAddress()); main.getHeartbeatManager().receiveHeartbeat(heartbeat, msgPacket.getAddress());
continue; continue;
} }
for (Plugin plugin : main.getPlugins()) { for (Plugin plugin : main.getPlugins()) {
System.out.println(msgPacket.getAddress().toString()); System.out.println(msgPacket.getAddress().toString());
plugin.receiveMessage(msgPacket.getData()); plugin.receiveMessage(msgPacket.getData());
@@ -124,11 +159,11 @@ public class Socket extends Thread {
} }
/** /**
* Tenta desserializar os dados recebidos como uma {@code HeartbeatMessage}. * Tenta identificar os dados recebidos como uma mensagem de heartbeat.
* *
* @param data dados serializados da mensagem. * @param data dados serializados recebidos.
* @return a mensagem desserializada caso os dados representem uma * @return a mensagem de heartbeat caso os dados correspondam a uma;
* {@code HeartbeatMessage}; caso contrário, {@code null}. * {@code null} caso contrário.
*/ */
private HeartbeatMessage tryParseHeartbeat(byte[] data) { private HeartbeatMessage tryParseHeartbeat(byte[] data) {
try { try {
@@ -141,12 +176,15 @@ public class Socket extends Thread {
} }
} }
/**
* Inicia a recepção de mensagens UDP.
*/
@Override @Override
public void run() { public void run() {
try { try {
this.receive(); this.receive();
} catch (IOException | ClassNotFoundException ex) { } catch (IOException | ClassNotFoundException ex) {
Logger.getLogger(Socket.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(SocketUDP.class.getName()).log(Level.SEVERE, null, ex);
} }
} }
} }
+3 -1
View File
@@ -4,9 +4,9 @@
*/ */
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. * Interface base para plugins da aplicação.
@@ -21,6 +21,7 @@ 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. * Inicializa o plugin.
@@ -47,4 +48,5 @@ public interface Plugin {
* @param message mensagem serializada recebida. * @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);
}