Try English version of Quizful



Раздаем бесплатные Q! подробности в группе Quizful.Alpha-test
Партнеры
Рекрутерам: Прескрининг кандидатов about
Топ контрибуторов
loading
loading
Знаете ли Вы, что

Если у вас есть уникальная статья и вы хотите, чтобы она стала достоянием общественности, вы можете разместить ее на Quizful.

Лента обновлений
ссылка 20:57:36
Комментарий от Recrut_rf:
Спасибо, сохранил функцию, может пригодится когда - ни...
ссылка 20:53:59
Добавлен вопрос в тест ASP.NET - Основы
ссылка 08:41:09
Комментарий от Krosster:
Гарний сайт для новачків. Правда деякі питання підступн...
ссылка Apr 22 22:06
Добавлен вопрос в тест SQL - Средний уровень
ссылка Apr 22 10:13
Комментарий от Entrery:
вроде выбрал ООП в сишарпе, а тут вопросы по джаве...
Статистика

Тестов: 153, вопросов: 8596. Пройдено: 402470 / 1961088.

Основы работы с сетью на примере консольного чата

head tail Статья
категория
Java
дата25.12.2013
авторVlad_Lastname
голосов27

Теория - IP, порт, сокет

Если вы начали читать эту статью, то, скорее всего, имеете какое-то отношение к IT-и понимаете что такое IP-адрес – уникальный адрес, который определяет компьютер в сети.

Но достаточно ли такой адресации для полноценной работы? Предположим, что на некотором компьютере запущены одновременно две программы, которые взаимодействуют с интернетом – получают и/или отсылают какие-то данные. Программы никак между собой не связаны и общаются с разными интернет-сервисами. Но они расположены на одном компьютере, следовательно, имеют один IP-адрес. Если они одновременно должны получить данные от двух разных серверов, как же они определят, кому какие данные предназначались?

Для того, чтобы решить эту проблему, нужно вместе с самими данными передавать информацию о том, какой программе они предназначаются. Эта информация и есть порт.

В нашем случае каждая из программ (точнее, программисты, написавшие их) должна определить, по какому порту она хочет взаимодействовать с сетью. Сервер, в свою очередь, должен тоже знать этот порт и посылать данные именно на него.

Что же собой представляет этот загадочный порт? Вы можете взять отвёртку и перебрать весь компьютер, но портов так и не найдёте. Это просто число, которое передаётся вместе с данными. Теоретически, оно может находиться в диапазоне от 1 до 65535, но порты 1..1024 используются системными программами и занимать их не стоит. Поэтому порт следует выбирать из диапазона 1025..65535.

Итак, IP-адрес это адрес компьютера в сети; порт – «адрес» программы на этом компьютере. А сокет – это их объединение, т.е. адрес программы в сети. Именно сокеты мы и будем использовать, чтобы указать программе, куда же стоит отправлять сообщения.

Планируем

Даже в таких простых программах, как чат, не нужно сразу лезть в IDEи писать что-то невнятное. Для начала стоит осмыслить теорию, с которой мы ознакомились (вы же её не пропустили, правда?) и понять, что она значит в контексте нашей программы.

Во-первых, в проектах, которые предназначены для работы с сетью, нужны две программы (не будет же она одна слать сама себе сообщения). Одна из них является сервером, а другая – клиентом. Отсюда первое следствие:

  • Необходимо два режима работы программы – серверный и клиентский

При этом можно создать либо две разные программы, либо одну и спрашивать пользователя, в каком режиме её запустить. Первый способ лучше тем, что серверу не придётся хранить файлы для клиентской части. С другой стороны, так чтобы проверить работу сервера и клиента придётся качать и запускать две разные программы. Остановимся на втором способе – в одной программе в зависимости от желания пользователя, будем запускать серверный или клиентский режим.

Дальше самое время подумать об архитектуре. С двумя пользователями всё легко – они просто пересылают друг другу свои сообщения. Но как мы помним, чат у нас планируется многопользовательский. Хранить у каждого клиента список всех остальных пользователей бессмысленно (следует понимать, что в данном случае клиент и пользователь – одно и то же). Поэтому сделаем так:

  • Сервер будет один, а пользователей – любое количество. Каждый пользователь подключается к этому серверу и посылает ему свои сообщения. Сервер хранит список подключенных пользователей и рассылает им все полученные сообщения.

И последнее, с чем нужно разобраться это протокол – алгоритм взаимодействия программ в сети. Очевидно, что и сервер и клиент должны слать друг другу сообщения по одному алгоритму. Ведь если клиент отправит строку, в то время как сервер будет ждать число, они друг друга не поймут, и будут работать неправильно.

Раз пользователей будет много, неплохо бы их как-то идентифицировать. Для этого сразу после подключения клиент должен отправить серверу свой ник. Дальше клиент посылает сообщения в чат до тех пор, пока очередное сообщение не будет равно «exit». Это означает, что пользователь хочет покинуть чат и закрыть программу.

Написание программы

Теперь, когда все подготовительные момент ясны, можно преступить к самому интересному – написанию программы.

Файлы и структура пакетов

Вы, конечно, знаете, что любая программа на Javaначинается с метода main(String[] args). Для большей наглядности не будем добавлять его к другим классам, а создадим отдельный класс Mainи пакет для него – main. В любой программе наверняка будут какие-то константы. Я предпочитаю выносить их в отдельный файл в виде publicstaticполей, поэтому создам класс Constи также добавлю его в пакет main.

Как мы помним, программа должна работать в режиме клиента или сервера. Создадим два соответствующих класса Clientи Server.

В итоге дерево пакетов выглядит так:

Структура каталогов

Выбор режима работы

Для начала нужно выбрать, в каком режиме запускать программу – сервер или клиент. Это нам и нужно первым делом узнать у пользователя, поэтому в метод main(…) пишем следующее:


public static void main(String[] args) {
    Scanner in = new Scanner(System.in);

    System.out.println("Запустить программу в режиме сервера или клиента? (S(erver) / C(lient))");
    while (true) {
        char answer = Character.toLowerCase(in.nextLine().charAt(0));
        if (answer == 's') {
            new Server();
            break;
        } else if (answer == 'c') {
            new Client();
            break;
        } else {
            System.out.println("Некорректный ввод. Повторите.");
        }
    }
}

Здесь всё достаточно просто – спрашиваем, как запускать программу, считываем букву ответа и запускаем соответствующий класс. Стоит пояснить только по поводу класса Scanner – это класс стандартной библиотеки, который облегчает работу с вводом данных из консоли. Он инициализируется стандартным потоком ввода.

Режим клиента

Пойдём от простого к сложному и сначала реализует клиентский режим работы.

Если сервер просто запускается и ждём пользователей, то клиентам приходится проявлять некоторую активность, а именно – подключиться к серверу. Для этого нужно знать его IPи порт подключения. Порт является константой, поэтому зададим его в Const.java:


public abstract class Const {
    public static final int Port = 8283;
}

Constобъявлен как abstract, т.к. содержит только статические данные и создавать его экземпляр ни к чему.

IPдолжен ввести пользователь, поэтому в конструкторе Client пишем:


Scanner scan = new Scanner(System.in);

System.out.println("Введите IP для подключения к серверу.");
System.out.println("Формат: xxx.xxx.xxx.xxx");

String ip = scan.nextLine();

Теперь у нас есть все необходимые данные – ip, порт, режим работы программы. Можно подключаться к серверу. Сначала создадим сокет:


Socket socket = newSocket(ip, Const.Port);

При этом сразу же производится подключение и можно передавать и считывать данные. Но как это сделать, если данные передаются только через потоки? Каждый Socketсодержит входной и выходной потоки класса InputStreamи OutputStream. Можно работать прямо с ними, но лучше для удобства «завернуть» их во что-то более функциональное:


try{
    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    out = new PrintWriter(socket.getOutputStream(), true);
} catch (Exception e) {
    e.printStackTrace();
}

Любые операции с потоками и сокетами должны выполняться внутри блока try..catch, для того, чтобы обрабатывать ошибки.

Вот теперь можно начать обмен сообщениями с сервером. Как мы помним, по протоколу, нужно сначала передать имя пользователя а затем каждое введённое (в консоль) сообщение отправлять серверу. Так и сделаем:


System.out.println("Введите свой ник:");
out.println(scan.nextLine());
String str = "";
while (!str.equals("exit")) {
    str = scan.nextLine();
    out.println(str);
}

Метод println() объекта outотправляет данные на сервер, а метод readLine() объекта in– считывает полученные данные. Но как нам печатать в консоль полученные от сервера сообщения? Ведь нам нужно одновременно ожидать сообщений из консоли (от пользователя) и сообщений из потока (от сервера). Придётся для этого создать дополнительную нить.

Thread– класс Java, который реализует такую незаменимую вещь, как многопоточность. Это возможность программы одновременно выполнять разные наборы действий. Как раз то, что нам сейчас нужно.

Создадим внутренний класс, который будет получать сообщения от сервера и выводить их в консоль.


private class Resender extends Thread {
    private boolean stoped;
                    
    public void setStop() {
        stoped = true;
    }
     @Override
    public void run() {
        try {
            while (!stoped) {
                String str = in.readLine();
                System.out.println(str);
            }
        } catch (IOException e) {
            System.err.println("Ошибка при получении сообщения.");
            e.printStackTrace();
        }
    }
}

До тех пор, пока поток не будет остановлен, он просто считывает все сообщения сервера и выводит их в консоль.

В конструкторе создадим объект этого класса и запустим поток:


Resender resend = new Resender();
resend.start();

Итоговый файл Client.java вместе с остальными приведён в конце статьи.

Режим сервера

Сервер, в отличие от клиента, работает не с классом Socket, а с ServerSocket. При создании его объекта программа никуда не подключается, а просто создаётся сервер на порту, переданном в конструктор.

Вся логика работы с конкретным пользователем будем находиться во внутреннем класса Connection, а Serverбудет только принимать новые подключения и оперировать существующими. Начнём «снизу» и создадим класс Connection, который должен в отдельной нити принимать от пользователя сообщения и рассылать их остальным клиентам:


private class Connection extends Thread {
    private BufferedReader in;
    private PrintWriter out;
    private Socket socket;

    private String name = "";

    public Connection(Socket socket) {
        this.socket = socket;

        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);

        } catch (IOException e) {
            e.printStackTrace();
            close();
        }
    }

    public void run() {
        try {
            name = in.readLine();
                                         
            for(Connection c : connections) {
                c.out.println(name + " cames now");
            }
                                             
            String str = "";
            while (true) {
                str = in.readLine();
                if(str.equals("exit")) break;
                                                              
                for(Connection c : connections) {
                    c.out.println(name + ": " + str);
                }
            }
                                             
            for(Connection c : connections) {
                c.out.println(name + " has left.");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close();
        }
    }
}

connection это массив со всеми соединениями пользователей. Когда необходимо отправить какое-то сообщение всем, мы перебираем этот массив и обращаемся к каждому клиенту.

Конструктор, как и в классе Client, преобразовывает потоки, связанные с сокетом. Метод run() запускается в отдельной нити и выполняется параллельно с остальной частью программы. В нём, согласно протоколу, сначала считывается имя пользователя, а потом все остальные его сообщения рассылаются всем клиентам чата. Когда приходит сообщение “exit”, пользователь отключается от чата, а все связанные с ним потоки закрываются методом close().

Теперь осталось только создать сервер, который будет принимать подключения, создавать объекты Connectionи добавлять их в массив. В конструкторе класса Server пишем:


try {
    server = new ServerSocket(Const.Port);

    while (true) {
        Socket socket = server.accept();
 
        Connection con = new Connection(socket);
        connections.add(con);
        con.start();

    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    closeAll();
}

Метод server.accept() указывает серверу ожидать подключения. Как только какой-то клиент подключится к серверу, метод вернёт объект Socket, связанный с этим подключением. Дальше создаётся объект Connection, инициализированный этим сокетом и добавляется в массив. Не забываем про try..catchи в конце закрываем все сокеты вместе с потоками методом closeAll();

Исходники

Где-то на середине статьи я вдруг осознал, что худшего учителя найти сложно, поэтому вот вам хотя бы исходники с комментариями. Может там что-то будет понятно. Спойлеров что-то нету, так что лучше выложу на bitbucket.

Если Вам понравилась статья, проголосуйте за нее

Голосов: 27  loading...
Vlad_Lastname   katch   Slewcel   Letos   Darwind_   skilgal   Andrey_G   c0nst   andrfas   Ambal   memor1s   Litman   Amboss   un1acker   Baev88   dill   dozor720   Bunny911   pristroistvo_ek   SkunS   Fesya   alexace013   gio   prying   valeska114   hardxx   alekseyter