Diskuze: Sítové programování Socket - blokování
V předchozím kvízu, Online test znalostí Java, jsme si ověřili nabyté zkušenosti z kurzu.


Petr Štechmüller:15.11.2018 17:30
Ahoj, třída Socket obsahuje dva data streamy:
- inputStream - slouží pro čtení dat z "internetu"
- outputStream - slouží k zápisu dat ven
Čtení je obecně blokující záležitost. Když se podíváš na jakoukoliv implementaci třídy InputStream, tak všechny metody typu read* budou blokující. To znamená, že budou číst až do "konce". Jak je konec definovaný, záleží na konkrétní implementaci.
V síťovém programování je tedy doporučený, aby jsi četl data v samostatném vlákně (tzv. čtecí vlákno) a když přijdou data, tak "nějakým" způsobem informuješ "někoho", aby se o ta data postaral.
To samé platí pro zápis dat do streamu. To je taky blokující záležitost, která může trvat velmi dlouho. Proto se pro zápis vytváří také samostatné vlákno (tzv. zapisovací vlákno), které se pouze stará o odesílání zpráv.
Ahoj, děkuji za odpověd. Takže ideální stav je, že budu mít dvě vlákna v klientovi (jedno, co se bude starat o outputstream a jedno o inputStream)? Jakým způsobem zařídit to vlákno, které se bude starat o output? Jak mu dát vědět, že má poslat zprávu? Tak mě napadá nějaká fronta zpráv nebo něco takového?
A je tedy ideální mít: 2 vlákna v klientovi a i 2 vlákna na straně druhé? Třeba budu mít třídu Server, dal bych tam vnitřní třídu privátní, která bude vlákno a bude udržovat informaci o spojení, takže bude mít přes Konstruktor Socket. A tohle vlákno se bude pouštět vždy, když se připojí klient na server. A do této třídy vložím ještě další dvě vlákna - pro čtení a druhé pro zápis? Chápu to správně?
Petr Štechmüller:16.11.2018 9:10
- Používej tlačítko odpovědět.
- Na klientovi opravdu budeš mít dvě vlákna (čtení, zápis)
- Pro odesílání zpráv je fronta zpráv ideální
- Co se serveru týče, tam je to na zvážení.
Existuje několik variant, jak řešit vlákna na serveru, vše asi nejvíce záleží na předpokládané zátěži.
- každému klientovi vytvoř dvě vlákna (čtení, zápis)
- trochu lepší řešení je, mít jenom jedno odesílací vlákno na serveru a veškeré zprávy odesílat v tomto vlákně, tím se dost ušetří zdroje
Nedávno jsem tu publikoval články o tvorbě serveru v Javě, tak se na to můžeš podívat, všechno je tam řešený.
Erik Šťastný:16.11.2018 9:36
V případě aplikace pro větší množství klientů bych rozhodně možnost "a." vynechal. V případě, že by server měl stovky, ne-li tisíce klientů, tak mít pro každého vytvořené dva thready je dost špatné.
Petr Štechmüller:16.11.2018 9:52
Jasně, v takovém případě by to ale už asi nebyl jeden server, ale rovnou nějaký distribuovaný systém s mnohem složitější logikou. Pro začátek bude úspěch, když se mu podaří komunikovat s alespoň dvěma klienty. Zbytek je otázka škálování...
Jiří Hrdina:16.11.2018 10:32
Děkuji za odpověd. Mám v tom docela zmatek. Za jaké situace dojde k bloknutí? Když bude se v jeden čas číst a zároven zapisovat?
Petr Štechmüller:16.11.2018 10:42
Ne, jak jsem psal výše, I/O operace jsou "extrémně" časově náročné operace, prostě trvají (o proti řekněme přiřazení proměnné) velmi dlouho. Dále jsou tyto operace synchronní, to znamená, že vlakno musí počkat, dokud se všechna data nezapisou/neprectou. Proto je to blokující - musí clse čekat na dokončení tě operace.
Jiří Hrdina:16.11.2018 12:10
A za předpokladu, že mi to zamrzne, tak to znamená, že nějaké čtení/zápis zablokovalo nějaké vlákno?
Petr Štechmüller:16.11.2018 12:11
Jestli Ti zamrzlo GUI, tak to znamená, že tu I/O operaci neprovádíš v samostatném vlákně.
Ve tvem pripade bych to resil tak, ze
- klient1 - zpravy send
- klient2 - zpravy send (pro klient1 je to receive zpravy vlakno)
- klient1 - stream send
- klient2 - stream send (pro klient1 je to receive stream vlakno)
Pokud nebude posilat ten druhy stream, tak mu to vlakno neotvirat, takze ti
staci 3, mozna.
Mozna by sis vystacil se dvema, kdyby sis domluvil synchronizacni interval pro
vlakno pro zpravy.
Jiří Hrdina:18.11.2018 10:35
Tak se mi to podařilo zprovoznit v konzoli, prozatím bez GUI. Nicméně měl bych ještě jeden dotaz. V těch vláknech mám cykly - v read vláknu, ve write vláknu a i v ClientHandler vláknu, což je privátní vnořená třída a toto vlákno se vytváří vždy, když se připojí nový klient. Úplně mi to nepřijde nejlepší, tam mít všude nekonečné cykly - nebo je to v pohodě?
Run metoda ve vnitřní třídě Server - v ClientHandler
@Override
public void run() {
while(true){
try {
Message msg = (Message) in.readObject();
System.out.println("Přečtená zpráva " + msg);
msg.setMessage(msg.getMessage() + " - SENDIMG TO ALL");
sendAll(msg);
} catch (Exception e){
System.out.println("Exception in CLIENTHANDLER THREAD");
}
}
}
}
VLÁKNO PRO POSÍLÁNÍ v Clientovi
Thread sendMessages = new Thread(() -> {
String fromUser = "";
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
while(true){
System.out.println("IN SENDMESSAGES");
try {
fromUser = stdIn.readLine();
Message message = new Message("Peter", null);
message.setMessage(fromUser);
System.out.println(message);
if(fromUser != null){
System.out.println("POSÍLÁM NA SERVER" + fromUser);
out.writeObject(message);
}
} catch (IOException e){
System.out.println("Exception in SENDMESSAGES thread");
}
}
});
Vlákno pro přijímání zpráv
Thread readMessages = new Thread(() -> {
Message ms;
try {
while (true) {
ms = (Message) in.readObject();
System.out.println("Received message " + ms);
}
} catch (Exception e){
System.out.println("EXCEPTION IN READ THREAD");
}
});
Děkuji
Petr Štechmüller:18.11.2018 10:37
Nekonečná smyčka je v pořádku, ale musíš vytvořit nějaký mechanismus, jak z té smyčky "bezpečně" vylezeš ven. To znamená, že když klient ukončí spojení, nebo to spojení nějakým způsobem "umře", tak musíš ta vlákna zastavit.
Jiří Hrdina:22.11.2018 21:38
Díky za všechny rady. Dostal jsem se do stádia, kdy pracuji již i s Javou FX.
Mám ze strany treeView, kde se mi načtou všechny online kontakty, když
načtu, tak s nimi chci psát, ale pro každého chci mít vytvořený vlastní
ListView, který mám vedle těch online kontaktů. Jakým způsobem toho
docílit?
Běží mi tam service, který hlídá nové zprávy, které mám uložené v
Queue v daném klientovi, ale bohužel se mi to všechno vypisuje do jednoho
listView...
Díky
Petr Štechmüller:22.11.2018 21:40
Bez kódu asi těžko poradím, ale opět tě musím odkázat na místní seriál o tvorbě chatu, který jsem nedavno publikoval. Mělo by tam být vše, co potřebuješ...
Jiří Hrdina:22.11.2018 21:53
O co mi jde... Jakmile kliknu na někoho, kdo je online, tak mu chci poslat zprávu... což se mi daří, aspon do konzole... jenže se mi nedaří to vypsat do toho listView - aby měla každá konverzace svuj vlastní listView na jedné Stage a aktualizovala zpráva do toho daného listView...
public class Controller extends AbstractController implements Initializable {
private Client client;
private Client selectedClient;
private ObservableList<String> observableList = FXCollections.emptyObservableList();
@FXML
private TreeView<Client> treeView;
private TreeItem<Client> root = new TreeItem<Client>();
@FXML
private ListView<String> mainChat;
@FXML
private TextField sendField;
@FXML
void sendMessage(ActionEvent event) {
String msg = sendField.getText();
System.out.println("MESSAGE " + msg);
if(msg != null){
System.out.println("POSÍLÁM ZPRÁVU!!!");
client.sendMessage(new Message(ServerProtocols.PRIVATE, selectedClient.getId(), selectedClient.getName(), msg));
}
}
public Controller(AccessData accessData) {
super(accessData);
}
@Override
public void initialize(URL location, ResourceBundle resources) {
client = getAccessData().getActiveClient();
treeView.setRoot(root);
root.setExpanded(true);
OnlineUsersService onlineUsersService = new OnlineUsersService(client, root);
onlineUsersService.start(); // PŘIDÁVÁ ONLINE PŘÁTELÉ
treeView.setOnMouseClicked(e -> {
selectedClient = treeView.getSelectionModel().getSelectedItem().getValue();
if(selectedClient != null){
System.out.println(observableList.size() + "OBS LIST SIZE");
}
});
MessageCheckingService messageCheckingService = new MessageCheckingService(client, observableList, mainChat);
messageCheckingService.start();
}
}
public class MessageCheckingService extends Service<Void> {
private ListView<String> listView;
private Client client;
private ObservableList<String> messages;
public MessageCheckingService(Client client,ObservableList<String> observableList, ListView<String> listView){
this.client = client;
this.messages = observableList;
this.listView = listView;
listView.setItems(messages);
}
@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() throws Exception {
while(true){
Message m = client.getLastMessage();
System.out.println("MESSAGE " + m.getMessage());
messages.add(m.getMessage());
}
}
};
}
}
Petr Štechmüller:22.11.2018 22:09
aby měla každá konverzace svuj vlastní listView na jedné Stage
Aby jsi toho docílil, tak máš podle mě 2 možnosti:
- při výběru uživatele změníš obsah toho listView na konverzaci s vybraným uživatelem
- uděláš si pro každého uživatele listView a podle výběru budeš zobrazovat vybrané listView
Momentálně nemáš implementováno ani jedno, ale 1. možnost bude podle
mě snažší.
Ve listeneru na kliknutí do listView treeView.setOnMouseClicked
jenom přidáš refresh konverzace.
Pokud se Ti ty přijaté zprávy vypisují do konzole, ale ne do listView, tak to bude nejspíš tím, že upravuješ grafický prvek (ListView) v jiném, než JavaFX vlákně. Zkus ještě upravit přidání zprávy v tasku:
Platform.runLater(() -> {
messages.add(m.getMessage());
});
Jiří Hrdina:22.11.2018 22:26
"Ve listeneru na kliknutí do listView treeView.setOnMouseClicked jenom přidáš refresh konverzace"
Jak myslíš ten "refresh konverzace" ? Mohl by jsi mi, prosím, trošku uvéct příklad, jak toho docílit, že refreshnu konverzaci?
Petr Štechmüller:23.11.2018 13:22
No musíš někde uchovávat celou konverzaci s daným uživatelem. Pak, při změně uživatele jenom na teď danou historii a prepises všechny polozky v tom listView...
Jiří Hrdina:23.11.2018 14:28
ale tímto způsobem nedocílím toho, že se mi budou zprávy přidávat
dynamicky, ne? Jenom pouze, když kliknu na toho klienta, chápu to správně?
Petr Štechmüller:23.11.2018 14:35
Dynamický by se měly přidávat, protože tu logiku již máš naimplentovanou.
@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() throws Exception {
while(true){
Message m = client.getLastMessage();
System.out.println("MESSAGE " + m.getMessage());
messages.add(m.getMessage());
}
}
};
}
}/code]
Jiří Hrdina:23.11.2018 14:58
A jakým způsobem by bylo nejlepší ukládat ty zprávy a do jaké kolekce? Na straně kontroleru a v jaké kolekci? Musím nějak rozlišit, jak načíst správnou kolekci, když někdo klikne na nějakého klienta... ty klienti mají svá ID, takže s tím by se dalo pracovat, ale přemýšlím, jak, kam ty kolekce ukládat...
Petr Štechmüller:23.11.2018 15:00
To už je otázka tvé vlastní implementace.
Když koukneš na místní tutorial, tak se můžeš inspirovat tam...
Jiří Hrdina:23.11.2018 15:20
Dobře, díky, mrknu na to zítra. Každopádně bych měl ještě jeden dotaz... Jak moc takováto
aplikace by měla žrát CPU? Pokud pustím 3 klienty naráz na jednom ntb a
server, tak to teda docela žere opravdu hodně...
Petr Štechmüller:23.11.2018 16:11
Jestli to hodně žere, tak je něco hodně špatně. Nemáš tam nějaký poololing?
Jiří Hrdina:23.11.2018 17:43
Co myslíš pod pooling? Nejsem si toho vědom... Když to pustím, tak jedna žere cca 25% CPU
Pooling - neustálé dotazování klienta serveru, zda-li pro něj nemá nějakou zprávu, ale to snad nebude Tvůj případ.
Jiří Hrdina:24.11.2018 10:22
Tak jsem na to přišel zřejmě.
Thread sendMessages = new Thread(() -> {
while(true){
try {
while(messagesToSend.size() > 0){
Message message = messagesToSend.take();
System.out.println("BUDU POSÍLAT ZPRÁVZ");
out.writeObject(message);
}
} catch (Exception e){
System.out.println("Exception in SENDMESSAGES thread");
}
}
});
Po tom, co jsem přidal do tohodle vlákna Thread.sleep(3000) za catch, tak
mi to žere 0,2-0,8% CPU (Což je z 25% docela slušný skok
Šlo by to asi vyřešit lépe. než ten sleep, že? Ideálně nějakou blokovací metodu, že když nebudou k dispozici zprávy k poslání tak spi...
Petr Štechmüller:24.11.2018 10:49
Samozřejmě, že to jde udělat lépe pomocí fronty zpráv.. Prosím, přečti si ten seriál, co tu vyšel, najdeš tam odpovědi na veškeré dosud položené otázky. Tady konkrétně na ten zápis zpráv: https://www.itnetwork.cz/…verem-1-cast
Martin Dráb:24.11.2018 10:58
Šlo by to asi vyřešit lépe. než ten sleep, že? Ideálně nějakou blokovací metodu, že když nebudou k dispozici zprávy k poslání tak spi...
Na toto se obvykle používá semafor, což je synchronizační primitivum obsahující jeden interní čítač a dovoluje jeho inkrementaci a dekrementaci. V případě, že se vlákno pokusí onen čítač snížit pod nulu, je uspáno, dokud toto "nebepečí" není zažehnáno (dokud někdo jiný nezvýší čítač tak, že již pod nulu neklesne).
Semafor s čítačem reflektujícím počet zpráv ve front bude dělat přesně to, co potřebuješ.
Jiří Hrdina:25.11.2018 16:50
Ahoj, narazil jsem na komplikaci. Podařilo se mi nějak vyřešit ty chaty,
uložit do kolekce a načítat i ty správné, podle toho, kdo na co klikne.
Nicméně se mi nedaří správně dynamicky updatovat zprávy.
Při prvním chatu, co se vytvoří vše funguje, zprávy se odesílají a
vkládají, jakmile přepnu na jiný chat, nějaké zprávy se ztratí a
zapíšou se do odlišného listu i přesto, že se vybírá správný list.
Předem děkuji
public class Controller extends AbstractController implements Initializable {
private Client client;
private Client selectedClient;
private ObservableList<String> observableList = FXCollections.emptyObservableList();
@FXML
private TreeView<Client> treeView;
private TreeItem<Client> root = new TreeItem<Client>();
@FXML
private ListView<String> mainChat;
List<ListView<String>> listViews = new ArrayList<>();
@FXML
private TextField sendField;
@FXML
void sendMessage(ActionEvent event) {
String msg = sendField.getText();
if(msg != null){
client.sendMessage(new Message(ServerProtocols.PRIVATE, selectedClient.getId(), selectedClient.getName(), msg));
}
}
public Controller(AccessData accessData) {
super(accessData);
}
@Override
public void initialize(URL location, ResourceBundle resources) {
client = getAccessData().getActiveClient();
treeView.setRoot(root);
root.setExpanded(true);
OnlineUsersService onlineUsersService = new OnlineUsersService(client, root, getAccessData(), selectedClient);
onlineUsersService.start();
treeView.setOnMouseClicked(e -> {
selectedClient = treeView.getSelectionModel().getSelectedItem().getValue();
if(selectedClient != null){
getAccessData().addChat(client.getId(), selectedClient.getId());
ObservableList<String> chat = getAccessData().getActiveChat();
System.out.println("Active chat is..." + chat);
MessageCheckingService messageCheckingService = new MessageCheckingService(client, getAccessData().getActiveChat(), mainChat);
messageCheckingService.restart();
}
});
}
}
public class AccessData {
private Client activeClient;
private ObservableList<String> activeChat;
private Map<String, ObservableList<String>> chats = new HashMap<>();
public Client getActiveClient() {
return activeClient;
}
public void setActiveClient(Client activeClient) {
this.activeClient = activeClient;
}
public ObservableList<String> getChat(int id1, int id2) {
String value = convertChatID(id1, id2);
System.out.println("CHCI CHAT S ID " + value);
if (chats.containsKey(value)) {
return chats.get(value);
}
return null;
}
public void addChat(int id1, int id2) {
String value = convertChatID(id1, id2);
System.out.println("Nastavuji view---" + value);
if (!chats.containsKey(value)) {
ObservableList<String> chat = FXCollections.observableArrayList();
System.out.println("VKLÁDÁM " + value + " - " + chat);
chats.put(value, chat);
setActiveChat(chat);
} else {
System.out.println("Chat alredy IN");
setActiveChat(chats.get(value));
}
}
public ObservableList<String> getActiveChat(){
return activeChat;
}
public void setActiveChat(ObservableList<String> chat) {
this.activeChat = chat;
}
private String convertChatID(int id1, int id2) {
String value = "";
// 1; 2
// 2; 1
if (id1 < id2) {
value = id1 + "" + id2;
} else if (id1 > id2) {
value = id2 + "" + id1;
}
return value;
}
}
public class MessageCheckingService extends Service<Void> {
private ListView<String> listView;
private Client client;
private ObservableList<String> messages;
public MessageCheckingService(Client client,ObservableList<String> observableList, ListView<String> listView){
this.client = client;
this.messages = observableList;
this.listView = listView;
listView.setItems(messages);
}
@Override
protected Task<Void> createTask() {
System.out.println("STARTING NEW MESSAGECHECKING SERVICE!");
return new Task<Void>() {
@Override
protected Void call() throws Exception {
while(true){
Message m = client.getLastMessage();
System.out.println("MESSAGE " + m.getMessage());
Platform.runLater(() -> {
messages.add(m.getMessage());
});
System.out.println("OBSERVABLE LIST IN MESSAGESERVICE" + messages);
}
}
};
}
}
public class MessageCheckingService extends Service<Void> {
private ListView<String> listView;
private Client client;
private ObservableList<String> messages;
private String id;
public MessageCheckingService(Client client,ObservableList<String> observableList, ListView<String> listView, String id){
this.client = client;
this.messages = observableList;
this.listView = listView;
this. id = id;
listView.setItems(messages);
}
@Override
protected Task<Void> createTask() {
System.out.println("STARTING NEW MESSAGECHECKING SERVICE!");
return new Task<Void>() {
@Override
protected Void call() throws Exception {
while(true){
Message m = client.getLastMessage();
System.out.println("MESSAGE " + m.getMessage());
Platform.runLater(() -> {
messages.add(m.getMessage());
});
System.out.println("OBSERVABLE LIST IN MESSAGESERVICE" + messages);
}
}
};
}
}
Zobrazeno 32 zpráv z 32.