JavaRush /Blogue Java /Random-PT /As classes Socket e ServerSocket, ou “Olá, servidor? Você...
Sergey Simonov
Nível 36
Санкт-Петербург

As classes Socket e ServerSocket, ou “Olá, servidor? Você pode me ouvir?"

Publicado no grupo Random-PT
Introdução: “Havia um computador em cima da mesa, atrás dele havia um codificador...” Era As classes Socket e ServerSocket, ou “Olá, servidor?  Você pode me ouvir?"  - 1 uma vez, um dos meus colegas postou outro resultado de seu estudo de Java, na forma de uma captura de tela de um novo programa. Este programa era um chat multiusuário. Naquela época, eu estava apenas começando minha jornada para dominar a programação nesta linguagem, mas definitivamente percebi para mim mesmo que “Eu quero!” O tempo passou e, tendo terminado de trabalhar no próximo projeto como parte do aprofundamento de meus conhecimentos de programação, lembrei-me daquele incidente e decidi que era a hora. De alguma forma, já comecei a me aprofundar neste tópico por pura curiosidade, mas em meu livro principal de Java (era o manual completo de Schildt) apenas 20 páginas foram fornecidas para o pacote java.net. Isso é compreensível - o livro já é muito grande. Havia tabelas de métodos e construtores das classes principais, mas isso é tudo. O próximo passo é, claro, o todo-poderoso Google: uma miríade de vários artigos onde a mesma coisa é apresentada - duas ou três palavras sobre soquetes e um exemplo pronto. A abordagem clássica (pelo menos no meu estilo de estudo) é primeiro entender o que preciso das ferramentas para trabalhar, o que são, por que são necessárias, e só então, se a solução para o problema não for óbvia, escolher as listagens prontas, desaparafusando as porcas e os parafusos. Mas descobri o que era e acabei escrevendo um bate-papo multiusuário. Externamente, ficou mais ou menos assim: As classes Socket e ServerSocket, ou “Olá, servidor?  Você pode me ouvir?"  - 2Aqui tentarei fornecer a você uma compreensão dos fundamentos dos aplicativos cliente-servidor baseados em soquetes Java usando o exemplo do design de chat. No curso Javarash você fará chat. Será um nível completamente diferente, bonito, grande, multifuncional. Mas antes de tudo, você sempre precisa estabelecer as bases, então aqui precisamos descobrir o que está por trás de tal seção. (Se você encontrar alguma falha ou erro, escreva em uma PM ou em um comentário no artigo). Vamos começar. Cabeça Um: “A casa que...” Para explicar como ocorre uma conexão de rede entre um servidor e um cliente, tomemos o agora clássico exemplo de um prédio de apartamentos. Digamos que um cliente precise de alguma forma estabelecer uma conexão com um servidor específico. O que o pesquisador precisa saber sobre o objeto de pesquisa? Sim, o endereço. O servidor não é uma entidade mágica na nuvem e, portanto, deve estar localizado em uma máquina específica. Por analogia com uma casa, onde deveria ocorrer uma reunião de duas partes acordadas. E para se encontrarem num prédio de apartamentos não basta um endereço do prédio, é necessário indicar o número do apartamento onde será realizado o encontro. Da mesma forma, em um computador podem existir vários servidores ao mesmo tempo, e para que o cliente entre em contato com um específico, ele também precisa especificar o número da porta através da qual a conexão ocorrerá. Então, o endereço e o número da porta. Um endereço significa um identificador de uma máquina no espaço da Internet. Pode ser um nome de domínio, por exemplo, "javarush.ru" ou um IP normal. Porta- um número único ao qual está associado um determinado soquete (este termo será discutido mais adiante), ou seja, é ocupado por um determinado serviço para que possa ser utilizado para contatá-lo. Assim, para que pelo menos dois objetos se encontrem no território de um (servidor), o dono da área (servidor) deve ocupar um apartamento específico (porto) nela (carro), e o segundo deve encontrar o local de encontro sabendo o endereço da casa (domínio ou ip) e o número do apartamento (porta). Cabeça Dois: Conheça o Socket Dentre os conceitos e termos relacionados ao trabalho em rede, um muito importante é o Socket. Denota o ponto através do qual a conexão ocorre. Simplificando, um soquete conecta dois programas em uma rede. A classe Socketimplementa a ideia de um soquete. O cliente e o servidor se comunicarão através de seus canais de entrada/saída: As classes Socket e ServerSocket, ou “Olá, servidor?  Você pode me ouvir?"  - 3 Esta classe é declarada no lado do cliente, e o servidor a recria, recebendo um sinal de conexão. É assim que funciona a comunicação online. Para começar, aqui estão os possíveis construtores de classe Socket:
Socket(String Name_хоста, int порт) throws UnknownHostException, IOException
Socket(InetAddress IP-address, int порт) throws UnknownHostException
“host_name” - implica um nó de rede específico, endereço IP. Se a classe de soquete não puder convertê-lo em um endereço real existente, uma exceção será lançada UnknownHostException. Porto é um porto. Se 0 for especificado como número da porta, o próprio sistema alocará uma porta livre. Uma exceção também poderá ocorrer se a conexão for perdida IOException. Deve-se notar que o tipo de endereço no segundo construtor é InetAddress. Ele vem em socorro, por exemplo, quando você precisa especificar um nome de domínio como endereço. Além disso, quando um domínio significa vários endereços IP, InetAddressvocê pode usá-los para obter uma matriz deles. No entanto, também funciona com IP. Você também pode obter o nome do host, a matriz de bytes que compõe o endereço IP, etc. Iremos abordar isso um pouco mais adiante, mas você terá que consultar a documentação oficial para obter detalhes completos. Quando um objeto do tipo é inicializado Socket, o cliente ao qual ele pertence anuncia na rede que deseja se conectar ao servidor em um endereço e número de porta específicos. Abaixo estão os métodos da classe usados ​​com mais frequência Socket: InetAddress getInetAddress()– retorna um objeto contendo dados sobre o soquete. Se o soquete não estiver conectado - null int getPort()- retorna a porta na qual ocorre a conexão com o servidor int getLocalPort()- retorna a porta à qual o soquete está vinculado. O fato é que o cliente e o servidor podem “comunicar-se” na mesma porta, mas as portas às quais estão vinculados podem ser completamente diferentes boolean isConnected()- retorna verdadeiro se a conexão for estabelecida void connect(SocketAddress address)- indica uma nova conexão boolean isClosed()- retorna verdadeiro se o soquete estiver fechado boolean isBound()- retorna verdadeiro, se o soquete estiver realmente vinculado a um endereço, a classe Socketimplementa a interface AutoCloseable, para que possa ser usada no try-with-resources. No entanto, você também pode fechar um soquete da maneira clássica, usando close(). Cabeça três: e este é um ServerSocket Digamos que declaramos, na forma de uma classe Socket, uma solicitação de conexão no lado do cliente. Como o servidor adivinhará nosso desejo? Para isso, o servidor possui uma classe como ServerSockete o método accept() nela. Seus construtores são apresentados a seguir:
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_address) throws IOException
Ao declarar, ServerSocket não é necessário especificar o endereço de conexão, pois a comunicação ocorre na máquina servidora. Somente com um host multicanal você precisa especificar a qual IP o soquete do servidor está vinculado. Cabeça Três.Um: O Servidor Que Diz Não Como fornecer a um programa mais recursos do que ele precisa é caro e irracional, portanto, no construtor ServerSocketvocê será solicitado a declarar o máximo de conexões aceitas pelo servidor durante a operação. Se não for especificado, então por padrão este número será considerado igual a 50. Sim, em teoria podemos assumir que ServerSocketeste é o mesmo soquete, apenas para o servidor. Mas desempenha um papel completamente diferente do class Socket. Só é necessário na fase de criação da conexão. Depois de criar um objeto de tipo, ServerSocketvocê precisa descobrir se alguém deseja se conectar ao servidor. O método accept() está conectado aqui. O alvo espera até que alguém queira se conectar a ele, e quando isso acontece ele retorna um objeto do tipo Socket, ou seja, um soquete de cliente recriado. E agora que o soquete do cliente foi criado no lado do servidor, a comunicação bidirecional pode começar. Criar um objeto de tipo Socketno lado do cliente e recriá-lo usando ServerSocketo lado do servidor é o mínimo necessário para a conexão. Cabeça Quatro: Carta ao Papai Noel Вопрос: Como exatamente o cliente e o servidor se comunicam? Ответ:Através de fluxos de E/S. O que já temos? Um soquete com o endereço do servidor e o número da porta do cliente, e a mesma coisa, graças a accept(), no lado do servidor. Portanto, é razoável supor que eles se comunicarão através de um soquete. Para fazer isso, existem dois métodos que dão acesso a streams InputStreame OutputStreamobjetos do tipo Socket. Aqui estão eles:
InputStream getInputStream()
OutputStream getOutputStream()
Como a leitura e a gravação de bytes simples não são tão eficientes, os fluxos podem ser agrupados em classes de adaptadores, armazenados em buffer ou não. Por exemplo:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Para que a comunicação seja bidirecional, tais operações devem ser realizadas em ambos os lados. Agora você pode enviar algo usando entrada e receber algo usando saída e vice-versa. Na verdade, esta é praticamente a única função da classe Socket. E sim, não se esqueça do método flush() BufferedWriter- ele libera o conteúdo do buffer. Caso isso não seja feito, as informações não serão transmitidas e, portanto, não serão recebidas. A thread receptora também aguarda o indicador de fim de linha – “\n”, caso contrário a mensagem não será aceita, pois na verdade a mensagem não está completa e não está completa. Se isso parece inconveniente para você, não se preocupe, você sempre pode usar o class PrintWriter, que precisa ser encerrado, especificar true como o segundo argumento e, em seguida, o pop-up do buffer ocorrerá automaticamente:
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
Além disso, não há necessidade de indicar o fim da linha; esta classe faz isso para você. Mas a E/S de string é o limite do que um soquete pode fazer? Não, você deseja enviar objetos por meio de fluxos de soquete? Pelo amor de Deus. Serialize-os e pronto:
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Cabeça Cinco: Comunicação real via Internet Já que para se conectar através de uma rede real com um endereço IP real você precisa ter um servidor completo, e já que:
  1. Nosso futuro chat, como utilitário, não possui tais habilidades. Ele só pode estabelecer uma conexão e receber/enviar uma mensagem. Ou seja, não possui capacidades reais de servidor.
  2. Nosso servidor, contendo apenas dados de soquete e fluxos de E/S, não pode funcionar como um servidor WEB ou FTP real, então apenas com isso não conseguiremos nos conectar pela Internet.
Além disso, estamos apenas começando a desenvolver o programa, o que significa que ele não é estável o suficiente para funcionar imediatamente com uma rede real, então usaremos o host local como endereço de conexão. Ou seja, em tese, cliente e servidor ainda não estarão conectados de forma alguma a não ser por meio de um soquete, mas para depurar o programa estarão na mesma máquina, sem contato real pela rede. Para indicar no construtor Socketque o endereço é local, existem 2 formas:
  1. Escreva “localhost” como argumento de endereço, significando um stub local. “127.0.0.1” também é adequado para isso - é apenas um formato digital de esboço.
  2. Usando InetAddress:
    1. InetAddress.getByName(null)- null aponta para localhost
    2. InetAddress.getByName("localhost")
    3. InetAddress.getByName("127.0.0.1")
Para simplificar, usaremos “localhost” do tipo String. Mas todas as outras opções também são viáveis. Head Six: É hora de conversar Então, já temos tudo que precisamos para implementar uma sessão de conversação com o servidor. Só falta juntar tudo: A listagem a seguir mostra como o cliente se conecta ao servidor, envia uma mensagem para ele, e o servidor, por sua vez, confirma que recebeu a mensagem usando-a como argumento em seu: "Servidor. Java"
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    private static Socket clientSocket; // socket for communication
    private static ServerSocket server; // server socket
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try  {
                server = new ServerSocket(4004); // server socket listening on port 4004
                System.out.println("Server is running!"); // server would be nice
                // announce your launch
                clientSocket = server.accept(); // accept() will wait until
                //someone won't want to connect
                try { // after establishing a connection and recreating the socket for communication with the client, you can go
                    // to create I/O streams.
                    // now we can receive messages
                    in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                    // and send
                    out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                    String word = in.readLine(); // wait for the client to write something to us
                    System.out.println(word);
                    // without hesitation responds to the client
                    out.write("Hi, this is the Server! I confirm you wrote: " + word + "\n");
                    out.flush(); // push everything out of the buffer

                } finally { // in any case, the socket will be closed
                    clientSocket.close();
                    // streams would also be nice to close
                    in.close();
                    out.close();
                }
            } finally {
                System.out.println("Server closed!");
                    server.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
"Cliente.java"
import java.io.*;
import java.net.Socket;

public class Client {

    private static Socket clientSocket; // socket for communication
    private static BufferedReader reader; // we need a reader that reads from the console, otherwise
    // do we know what the client wants to say?
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try {
                // address - local host, port - 4004, same as the server
                clientSocket = new Socket("localhost", 4004); // with this line we request
                // the server has access to the connection
                reader = new BufferedReader(new InputStreamReader(System.in));
                // read messages from the server
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // write there
                out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                System.out.println("Did you have something to say? Enter it here:");
                // if the connection happened and the threads were successfully created - we can
                // work further and offer the client something to enter
                // if not, an exception will be thrown
                String word = reader.readLine(); // wait for the client to do something
                // will not write to the console
                out.write(word + "\n"); // send a message to the server
                out.flush();
                String serverWord = in.readLine(); // wait for the server to say
                System.out.println(serverWord); // received - display
            } finally { // in any case, you need to close the socket and streams
                System.out.println("The client has been closed...");
                clientSocket.close();
                in.close();
                out.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }

    }
}
Claro, você deve iniciar o servidor primeiro, porque a que o cliente se conectará na inicialização se não houver algo que o conecte? :) A saída será assim: /* Você quer dizer alguma coisa? Digite aqui: Olá, servidor? Você pode me ouvir? Olá, este é o Servidor! Confirmo, você escreveu: Olá, servidor? Você pode me ouvir? O cliente foi fechado... */ Viva! Ensinamos o servidor a se comunicar com o cliente! Para que a comunicação ocorra não em duas réplicas, mas em quantas você quiser, basta envolver a leitura e escrita das threads em um loop while (true) e indicar para a saída que, de acordo com uma determinada mensagem, por exemplo, “exit” , o ciclo era interrompido e o programa terminava. Cabeça Sete: Multiusuário é melhor. O fato do servidor poder nos ouvir é bom, mas seria muito melhor se pudéssemos nos comunicar com alguém da nossa espécie. Anexarei todas as fontes no final do artigo, então aqui mostrarei trechos de código nem sempre grandes, mas importantes que tornarão possível, se usados ​​corretamente, criar um chat multiusuário. Então, queremos poder nos comunicar com algum outro cliente através do servidor. Como fazer isso? Obviamente, como o programa cliente possui seu próprio método main, significa que ele pode ser iniciado separadamente do servidor e em paralelo com outros clientes. O que isso nos dá? De alguma forma, é necessário que a cada nova conexão o servidor não entre imediatamente em comunicação, mas grave essa conexão em algum tipo de lista e passe a aguardar uma nova conexão, e algum tipo de serviço auxiliar esteja engajado na comunicação com um determinado cliente. E os clientes devem escrever no servidor e aguardar uma resposta independentemente uns dos outros. Os tópicos vêm em socorro. Digamos que temos uma classe responsável por lembrar novas conexões: Ela deve ter o seguinte especificado:
  1. Número da porta.
  2. A lista na qual escreve a nova conexão.
  3. E ServerSocket, em uma única (!) cópia.
public class Server {

    public static final int PORT = 8080;
    public static LinkedList<ServerSomthing> serverList = new LinkedList<>(); // list of all threads

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(PORT);
            try {
            while (true) {
                // Blocks until a new connection is made:
                Socket socket = server.accept();
                try {
                    serverList.add(new ServerSomthing(socket)); // add a new connection to the list
                } catch (IOException e) {
                    // If it fails, the socket is closed,
                    // otherwise, the thread will close it when it exits:
                    socket.close();
                }
            }
        } finally {
            server.close();
        }
    }
}
Pronto, agora cada soquete recriado não será perdido, mas será armazenado no servidor. Avançar. Todo cliente precisa de alguém para ouvi-lo. Vamos criar um thread com as funções de servidor do último capítulo.
class ServerSomthing extends Thread {

    private Socket socket; // socket through which the server communicates with the client,
    // except for it - the client and server are not connected in any way
    private BufferedReader in; // socket read stream
    private BufferedWriter out; // socket write stream

    public ServerSomthing(Socket socket) throws IOException {
        this.socket = socket;
        // если потоку ввода/вывода приведут к генерированию исключения, оно пробросится дальше
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        start(); // call run()
    }
    @Override
    public void run() {
        String word;
        try {

            while (true) {
                word = in.readLine();
                if(word.equals("stop")) {
                    break;                }
                for (ServerSomthing vr : Server.serverList) {
                    vr.send(word); // send the received message with
                   // bound client to everyone else including him
                }
            }

        } catch (IOException e) {
        }
    }

    private void send(String msg) {
        try {
            out.write(msg + "\n");
            out.flush();
        } catch (IOException ignored) {}
    }
}
Portanto, no construtor da thread do servidor, deve ser inicializado um soquete através do qual a thread se comunicará com um cliente específico. Também threads de E/S e tudo o mais que você precisa para iniciar um thread diretamente do construtor. Ok, mas o que acontece quando um thread de servidor lê uma mensagem de um cliente? Enviar de volta apenas para o seu cliente? Não é muito eficaz. Estamos fazendo um chat multiusuário, então precisamos que cada cliente conectado receba o que uma pessoa escreveu. Você precisa usar a lista de todos os threads do servidor associados aos seus clientes e enviar cada mensagem enviada para um thread específico para que ele a envie ao seu cliente:
for (ServerSomthing vr : Server.serverList) {
    vr.send(word); // send the received message
    // from the linked client to everyone else, including him
}
private void send(String msg) {
    try {
        out.write(msg + "\n");
        out.flush();
    } catch (IOException ignored) {}
}
Agora todos os clientes saberão o que um deles disse! Se você não deseja que a mensagem seja enviada para quem a enviou (ele já sabe o que escreveu!), simplesmente ao iterar pelas threads, especifique que ao processar um objeto, o thisloop passará para o próximo elemento sem executar quaisquer ações sobre ele. Ou, se preferir, envie uma mensagem ao cliente informando que a mensagem foi recebida e enviada com sucesso. Tudo está claro com o servidor agora. Passemos ao cliente, ou melhor, aos clientes! Tudo é igual aí, por analogia com o cliente do capítulo anterior, só que na hora de criar uma instância você precisa, como foi mostrado neste capítulo com o servidor, criar tudo o que for necessário no construtor. Mas e se, ao criar um cliente, ele ainda não teve tempo de inserir nada, mas algo já foi enviado para ele? (Por exemplo, o histórico de correspondência de quem já se conectou ao chat antes dele). Portanto, os ciclos em que as mensagens enviadas serão processadas devem ser separados daqueles em que as mensagens são lidas do console e enviadas ao servidor para encaminhamento a outros. Os tópicos vêm em socorro novamente. Não faz sentido criar um cliente como um thread. É mais conveniente fazer um thread com um loop no método run que lê mensagens e também, por analogia, escreve:
// thread reading messages from the server
private class ReadMsg extends Thread {
    @Override
    public void run() {

        String str;
        try {
            while (true) {
                str = in.readLine(); // waiting for a message from the server
                if (str.equals("stop")) {

                    break; // exit the loop if it's "stop"
                }
                            }
        } catch (IOException e) {

        }
    }
}
// thread sending messages coming from the console to the server
public class WriteMsg extends Thread {

    @Override
    public void run() {
        while (true) {
            String userWord;
            try {
               userWord = inputUser.readLine(); // messages from the console
                if (userWord.equals("stop")) {
                    out.write("stop" + "\n");
                    break; // exit the loop if it's "stop"
                } else {
                    out.write(userWord + "\n"); // send to the server
                }
                out.flush(); // clean up
            } catch (IOException e) {

            }

        }
    }
}
No construtor do cliente você só precisa iniciar esses threads. Como fechar corretamente os recursos de um cliente se ele quiser sair? Preciso fechar os recursos de thread do servidor? Para fazer isso, você provavelmente precisará criar um método separado que será chamado ao sair do loop de mensagens. Lá você precisará fechar o soquete e os fluxos de E/S. O mesmo sinal de encerramento de sessão para um determinado cliente deve ser enviado para seu thread de servidor, que deve fazer o mesmo com seu soquete e remover-se da lista de threads na classe de servidor principal. Cabeça Oito: Não há limite para a perfeição Você pode inventar infinitamente novos recursos para melhorar seu projeto. Mas o que exatamente deve ser transferido para um cliente recém-conectado? Acho que os últimos dez eventos aconteceram antes de sua chegada. Para fazer isso, você precisa criar uma classe na qual a última ação com qualquer thread do servidor será inserida na lista declarada, e se a lista já estiver cheia (ou seja, já são 10), exclua a primeira e adicione o último que veio. Para que o conteúdo desta lista seja recebido por uma nova conexão, ao criar um thread do servidor, no fluxo de saída, é necessário enviá-lo ao cliente. Como fazer isso? Por exemplo, assim:
public void printStory(BufferedWriter writer) {
// ...
}
O thread do servidor já criou fluxos e pode passar o fluxo de saída como argumento. Em seguida, basta passar tudo o que precisa ser transferido para o novo cliente em um ciclo de busca. Conclusão: Isto é apenas o básico e muito provavelmente esta arquitetura de chat não funcionará ao criar um aplicativo real. Este programa foi criado para fins educacionais e com base nele mostrei como você pode fazer o cliente se comunicar com o servidor (e vice-versa), como fazer isso para diversas conexões e, claro, como isso é organizado em soquetes. As fontes estão reorganizadas a seguir, e o código-fonte do programa em análise também está anexado. Esta é minha primeira experiência escrevendo um artigo) Obrigado pela atenção :)
  1. Pensando em Java Enterprise, por Bruce Eckel et. Al. 2003
  2. Java 8, The Complete Guide, Herbert Schildt, 9ª edição, 2017 (Capítulo 22)
  3. Programação de soquete em Java artigo sobre soquetes
  4. Soquete na documentação oficial
  5. ServerSocket na documentação oficial
  6. fontes no GitHub
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION