Aprenda programação de rede facilmente

Índice

1. Comparação das características de UDP e TCP

1. Com conexão e sem conexão

2. Transmissão confiável e transmissão não confiável

3. Orientado a fluxo de bytes e orientado a datagramas

4. Full duplex e half duplex

2. Soquete UDP.api

1. DatagramSocket

2. DatagramPacket

Implementação de servidor de eco

(1) Código do servidor

(2) Código do cliente

Implementação de servidor de tradução

3.TCP

1. ServerSocket   

Editar 2. Soquete

servidor de eco

(1) Código do servidor

(2) Código do cliente


A programação de rede é, na verdade, escrever um aplicativo para que o programa possa usar a comunicação de rede.

Aqui você precisa chamar a API fornecida pela camada de transporte

Existem dois protocolos fornecidos pela camada de transporte:

1.UDP 

2.TCP

Esses dois protocolos fornecem dois conjuntos diferentes de APIs


1. Comparação das características de UDP e TCP

UDP: transmissão sem conexão, não confiável, orientada a datagramas, full duplex

TCP: conexão, transmissão confiável, orientada a fluxo de bytes, full duplex


1. Com conexão e sem conexão

Quando estávamos aprendendo JDBC antes, tivemos esta etapa:

Primeiro crie um DtaSourse e depois crie uma conexão através do DataSourse

E a Conexão aqui é o que chamamos de conexão

Uma compreensão mais intuitiva:

Ao fazer uma chamada, disque um número e pressione a tecla de discagem. O estabelecimento da conexão é concluído somente quando a outra parte estiver conectada.

Ao programar o TCP, também existe um processo semelhante de estabelecimento de uma conexão.

Nenhuma conexão é semelhante ao envio de WeChat/SMS, e a comunicação pode ser realizada sem estabelecer uma conexão.

A "conexão" aqui é um conceito abstrato

Entre o cliente e o servidor, a memória é usada para salvar informações sobre a outra extremidade.

Ambas as partes salvam essas informações e “Conectar” aparece.

Um cliente pode se conectar a vários servidores e um servidor também pode corresponder às conexões de vários clientes.


2. Transmissão confiável e transmissão não confiável

Uma transmissão confiável não significa que a mensagem enviada de A para B possa ser transmitida 100% do tempo.

Em vez disso, A tenta ao máximo transmitir a mensagem para B e, quando a transmissão falha, A pode senti-la, ou quando a transmissão é bem-sucedida, ele também pode saber que sua transmissão foi bem-sucedida.

TCP é uma transmissão confiável, mas a eficiência de transmissão é baixa

UDP não é uma transmissão confiável e tem maior eficiência de transmissão

Embora o TCP seja uma transmissão confiável e o UDP não seja uma transmissão confiável, não se pode dizer que o TCP seja mais seguro que o UDP.

"Segurança de rede" refere-se a se os dados que você transmite são facilmente interceptados por hackers e se algumas informações importantes vazarão se forem interceptadas.


3. Orientado a fluxo de bytes e orientado a datagramas

TCP é semelhante às operações de arquivo, ambas são "stream" (já que as unidades de transmissão aqui são bytes, é chamado de fluxo de bytes)

UDP é orientado a datagramas, e a unidade básica de leitura e escrita é um datagrama UDP (contendo uma série de dados/atributos)


4. Full duplex e half duplex

Full duplex: Um canal que pode se comunicar em ambas as direções

Half-duplex: Um canal que só pode se comunicar em uma direção

O cabo de rede é full duplex.

Há um total de 8 cabos de cobre no cabo de rede, um grupo de 4 4, alguns são responsáveis ​​por uma direção e alguns são responsáveis ​​pela outra direção.


2. Soquete UDP.api

1. DatagramSocket

é um objeto Socket,

O sistema operacional usa o conceito de arquivos para gerenciar alguns recursos de software e hardware. O sistema operacional também usa arquivos para gerenciar placas de rede. Os arquivos que representam placas de rede são chamados de arquivos Socket.

O objeto Socket em Java corresponde ao arquivo Socket no sistema (eventualmente cairá na placa de rede)

Para comunicação em rede, você deve ter um objeto Socket

Método de construção:

O primeiro é frequentemente usado no cliente (a porta que o cliente usa é atribuída automaticamente pelo sistema)

O segundo é frequentemente usado no lado do servidor (a porta que o servidor usa é especificada manualmente)

O host de um cliente tem muitos resultados em execução. Deus sabe se a porta que você selecionou manualmente está ocupada por outros programas, então é uma escolha mais sábia deixar o sistema alocar uma porta automaticamente.

O servidor é totalmente controlado pelo programador, que pode organizar vários programas no servidor e deixá-los usar portas diferentes.

Outros métodos:


2. DatagramPacket

Representa um datagrama UDP, representando a estrutura binária do datagrama UDP definido no sistema.

Método de construção:

O primeiro construtor: usado para aceitar dados

DatagramPacket, como datagrama UDP, deve ser capaz de transportar alguns dados

Use byte[] especificado manualmente como espaço para armazenar dados

O segundo método de construção: usado para enviar dados

O endereço SocketAddress refere-se ao IP e ao número da porta

Outros métodos:

getData refere-se à obtenção da parte da carga útil do datagrama UDP (datagrama completo da camada de aplicativo)


Implementação de servidor de eco

Em seguida, começamos a escrever à mão o servidor cliente UDP

O servidor UDP mais simples: servidor de eco, o que o cliente envia, o que o servidor retorna

(1) Código do servidor

Ao escrever programas de rede, frequentemente vemos esse tipo de exceção, o que significa que o soquete pode falhar.

A situação mais típica é que o número da porta está ocupado

O número da porta é usado para distinguir aplicativos no host. Um aplicativo pode ocupar várias portas no host e uma porta só pode ser ocupada por um processo (há casos especiais, que não serão discutidos aqui) 

Ou seja, quando a porta já estiver ocupada por outros processos, se você tentar criar esse objeto socket e ocupar essa porta, será reportado um erro.

Um servidor tem que fornecer serviços a muitos clientes. O servidor não sabe quando os clientes chegarão. O servidor só pode estar "sempre pronto" e fornecer serviços a qualquer momento quando os clientes chegarem.

Um servidor tem três tarefas principais a serem executadas durante sua operação:

1. Leia a solicitação e analise-a
2. Calcule a resposta com base na solicitação
3. Escreva a resposta de volta ao cliente

Para o servidor echo, ele não se importa com o segundo processo, seja qual for a solicitação, ele retornará a resposta.

Mas para um servidor de nível comercial, o código principal completa a segunda etapa.

Neste método, o DatagramPacket do parâmetro é um “parâmetro de saída”

O que é passado para o recebimento é um objeto vazio, e o conteúdo do objeto vazio será preenchido dentro do recebimento.Quando a execução do recebimento terminar, um DatagramPacket preenchido com conteúdo será obtido.

O espaço de memória usado por este objeto para salvar dados precisa ser especificado manualmente. Ao contrário das classes de coleção que aprendemos, ele possui seus próprios recursos de gerenciamento de memória interna (pode solicitar memória, liberar memória, expandir memória, etc.)

O número 4096 é escrito casualmente, mas não pode ser escrito muito grande, não mais que 64kb.

Assim que o programa do servidor for iniciado, ele executará imediatamente o loop e receberá imediatamente.

Se a solicitação do cliente ainda não tiver chegado, o método de recebimento bloqueará e aguardará até que um cliente realmente inicie uma solicitação.

Neste código, um objeto DatagramPacket é construído e a resposta é enviada de volta ao cliente.

Nota: O segundo parâmetro não pode ser escrito como resposta, length()

Isso ocorre porque response,length() calcula o comprimento em caracteres, enquanto response.getBytes().length calcula o comprimento em bytes.

Se a string estiver em inglês, o número de bytes e o número de caracteres serão iguais, mas se a string contiver caracteres chineses, os resultados calculados dos dois serão diferentes.

A própria API do soquete é processada em bytes

requestPacket.getSocketAddress() Esta parte é para enviar o datagrama ao cliente. Você precisa saber o IP e a porta do cliente.

DatagramPacket Este objeto contém o IP e a porta das partes em comunicação. 

Código da parte do servidor:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

//UDP 的回显服务器
//客户端发的请求是啥,服务器的响应就是啥
public class UdpEchoServer {
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    //使用这个方法启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //反复的,长期的执行针对客户端请求处理的逻辑
            //一个服务器,运行过程中,要做的事情,主要是三个核心环节
            //1、读取请求,并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //这样的转字符串的前提是,后续客户端发的数据就是一个文本的字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2、根据请求,计算出响应
            String response = process(request);
            //3、把响应写回给客户端
            //此时需要告知网卡,要发的内容是啥,要发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //记录日志,方便观察程序执行效果
            System.out.printf("[%s:%d] req: %s, resp: %s\n",responsePacket.getAddress().toString(),responsePacket.getPort(),
                    request,response);
        }
    }
    //根据请求计算响应
    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

Isso está fechado?

Para o nosso programa servidor, não é um grande problema se o DatagramSocket não estiver fechado. Existe apenas um objeto de soquete em todo o programa e ele não é criado com frequência.

O ciclo de vida deste objeto é muito longo e acompanha todo o programa, neste momento o soquete precisa permanecer aberto.

O objeto soquete corresponde ao arquivo de soquete no sistema e ao descritor de arquivo ( o objetivo principal é fechar o objeto de soquete para determinar se o descritor de arquivo está disponível )

Quando o processo termina, o PCB é reciclado e a tabela de descritores de arquivos dentro dele é destruída.

Mas isso é limitado a: há apenas um objeto de soquete e o ciclo de vida segue o processo, portanto não há necessidade de liberá-lo neste momento.

Mas se houver vários objetos de soquete, o ciclo de vida do objeto de soquete será mais curto e precisará ser criado e lançado com frequência. Neste momento, você deve se lembrar de fechar


(2) Código do cliente

Escreva o cliente. Os parâmetros a seguir especificam para qual IP e porta enviar.

O que é necessário aqui é o objeto InetAdress, então use o método estático de InetAdress, getByName para construir (modo de fábrica/método de fábrica)

 

O servidor será automaticamente vinculado ao 9090 quando for iniciado.

O cliente acessará em seguida a janela 9090

 Código do cliente:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    //服务器的 ip 和 服务器窗口
    public UdpEchoClient(String ip ,int port) throws SocketException {
        serverIp = ip;
        serverPort = port;
        //这个 new 操作,就不再指定端口了,让系统自动分配一个空闲端口
        socket = new DatagramSocket();
    }

    //客户端启动,让这个客户端反复的从控制台读取用户输入的内容,把这个内容构造成 UPD 请求,发给服务器,再读取服务器返回的 UDP响应
    //最终再显示在客户端的屏幕上
    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while (true){
            //1、从控制台读取用户输入的内容
            System.out.println("-> ");//命令提示符,提示用户输入字符串
            String request = scanner.next();
            //2、构造请求对象,并发给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            //3、读取服务器的响应,并解析出响应内容
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //4、显示结果
            System.out.println(request);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.1",9090);
        client.start();
    }
}

Neste ponto, execute primeiro o servidor, depois o cliente e, em seguida, insira o conteúdo para ver os resultados da execução.

 Se vários clientes forem iniciados, vários clientes também poderão ser manipulados pelo servidor.


Implementação de servidor de tradução

O servidor de tradução solicita algumas palavras em inglês e a resposta é a tradução correspondente em chinês.

Este servidor é semelhante a parte do código do servidor de eco anterior, portanto, deixamos que ele herde diretamente o servidor anterior.

A herança em si serve para melhor "reutilização do código existente"

Se @Override não for adicionado aqui, se o nome do método/tipo de parâmetro/número de parâmetros/permissões de acesso estiverem errados, a reescrita não será possível neste momento e será difícil de encontrar.

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDicSever extends UdpEchoServer{
    private Map<String,String> dict = new HashMap<>();

    public UdpDicSever(int port) throws SocketException {
        super(port);

        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("duck","小鸭");
        //可以在这里继续添加千千万万个单词,每个单词都有一个对应的翻译
    }

    //是要复用之前的代码,但是又要做出调整
    @Override
    public String process(String request){
        //把请求对应单词的翻译给返回回去
        return dict.getOrDefault(request,"该词没有查询到");
    }

    public static void main(String[] args) throws IOException {
        UdpDicSever server = new UdpDicSever(9090);
        //start 就不需要在写一遍了,就直接复用了之前的 start
        server.start();
    }
}

3.TCP

TCP é um fluxo de bytes, transmitido byte por byte.

Em outras palavras, um datagrama TCP é uma matriz de bytes byte[] 

A API fornecida pelo TCP também possui duas classes

1. ServerSocket   

Soquete usado pelo servidor

Método de construção:

Outros métodos:

 2、Soquete

É usado tanto pelo servidor quanto pelo cliente.

Método de construção:

Outros métodos:


servidor de eco

(1) Código do servidor

Vamos agora tentar escrever uma versão TCP do servidor de eco

Haverá algumas diferenças em relação ao UDP:

Depois de entrar no loop, o que se deve fazer não é ler a solicitação do cliente, mas primeiro processar a "conexão" do cliente.

Embora existam muitas conexões no kernel, elas ainda precisam ser processadas uma por uma no aplicativo.

As conexões no kernel são como tarefas pendentes: essas tarefas estão em uma estrutura de dados de fila e o aplicativo precisa concluir essas tarefas uma por uma.

Para concluir a tarefa, você precisa primeiro obtê-la e usar serverSocker.accept()

A conexão no kernel é obtida na aplicação. Este processo é semelhante ao "modelo produtor-consumidor"

Quando o programa for iniciado, ele será executado imediatamente para aceitar

Quando o servidor executa aceitar, o cliente pode ainda não ter chegado e o accpet será bloqueado.

Bloquear até que um cliente se conecte com sucesso

Aceitar é pegar a conexão que foi estabelecida no kernel e levá-la para a aplicação.

Mas o valor de retorno aqui não é um objeto como "Conexão", mas apenas um objeto Socket.

E esse objeto Socket é como um fone de ouvido, você pode falar e ouvir a voz da outra parte.

Comunique-se com a outra parte pela rede através do objeto Socket

Existem dois tipos de soquetes envolvidos no servidor TCP, serverSocket e clientSocket.

serverSocket pode ser entendido como a pessoa do departamento de vendas responsável por solicitar clientes, e clientSocket pode ser entendido como a pessoa do departamento de vendas responsável por apresentar detalhes. 

As operações IO são relativamente caras

Comparado ao acesso à memória, quanto mais vezes a E/S for executada, mais lento será o programa.

Use um pedaço de memória como buffer. Ao gravar dados, grave-os primeiro no buffer, salve uma onda de dados e execute IO uniformemente.  

PrintWriter possui um buffer integrado, que pode ser atualizado manualmente para garantir que os dados aqui sejam realmente enviados através da placa de rede e não deixados no buffer.

É mais seguro adicionar flash aqui, mas não necessariamente dará errado se você não adicioná-lo.

O buffer possui uma certa estratégia de atualização integrada, por isso é recomendado adicionar flash

Neste programa, dois tipos de soquetes estão envolvidos 

1. ServerSocket (só existe um, o ciclo de vida segue o programa, tudo bem se não for fechado)

2、Soquete

O soquete aqui é criado repetidamente

Precisamos garantir que o soquete possa ser fechado após a conexão ser desconectada, então adicione o código para finalmente fechar o soquete.

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    //这个操作就会绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        while (true){
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    //通过这个方法处理一个连接的逻辑
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s,%d] 客户端上线!\n,", clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来就可以读取请求,根据请求计算响应,返回响应
        //Socket 对象内部,包含两个字节流对象,可以把这两个字节流对象获取到,完成后续的读写工作
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //一次连接中,可能会涉及到多次请求 / 响应
            while (true){
              //1、读取请求,并解析,为了读取方便,直接使用Scanner
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()){
                    //读取完毕,客户端下线
                    System.out.printf("[%s:%d] 客户端下线! \n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //这个代码暗含一个约定:客户端发过来的请求,得是文本数据,同时,还得带有空白符作为分割
                String request = scanner.next();
                //2、根据请求,计算响应
                String response = process(request);
                //3、把响应写回给客户端,把 OutputStream 使用 PrinterWriter 包裹一下,方便进行发数据
                PrintWriter writer = new PrintWriter(outputStream);
                //使用 PrintWriter 的 println 方法,把响应返回给客户端
                //此处使用 println 而不是 print 就是为了在结尾加上换行符,方便客户端读取响应,使用 Scanner.next 读取
                writer.println(response);
                //这里还需要加一个 “刷新缓冲区” 的操作
                writer.flush();

                //日志,打印一下当前的请求详情
                System.out.printf("[%s:%d] req: %s,resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request,response);

            }
        }finally {
            //在 finally 中,加上 close 操作,确保当前 socket 能够被及时关闭
            clientSocket.close();
        }
    }

    public String process(String request){
        return  request;
    }


    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

(2) Código do cliente

O que o cliente deve fazer:

1. Leia a entrada do usuário no console

2. Construa o conteúdo de entrada em uma solicitação e envie-o ao servidor

3. Leia a resposta do servidor

4. Exiba a resposta no console

No código atual, Scanner e PrintWriter não estão fechados. Ocorrerá vazamento de arquivo?

Não vai! ! !

Os recursos mantidos no objeto stream têm duas partes:

1. Memória (quando o objeto é destruído, a memória é reciclada), o loop while completa um ciclo e a memória é naturalmente destruída.

2. Descritor de arquivo

Scanner e printWriter não contêm descritores de arquivo, mas contêm referências a inputstream e outputstream, e esses dois objetos foram fechados.

Para ser mais preciso, ele é mantido pelo objeto soquete, então basta fechar o objeto soquete.

Nem todo objeto de fluxo contém um descritor de arquivo . Para manter um descritor de arquivo, você precisa chamar o método open fornecido pelo sistema operacional.

hasNext também bloqueará quando o cliente não enviar uma solicitação. Ele bloqueará até que o cliente realmente envie dados ou o cliente saia, e hasNext retornará.

Ainda há um grande problema no código atual: o que acontece quando iniciamos dois clientes?

Fenômeno:

Depois que o primeiro cliente estiver conectado, o segundo cliente não poderá ser processado corretamente.

O servidor não consegue ver que o cliente está online e a solicitação do cliente não pode ser processada.

Após a saída do primeiro cliente, todas as solicitações anteriores enviadas pelo segundo cliente serão respondidas.

Quando um cliente chegar, accept retornará e entrará em processConnection

O loop processará a solicitação do cliente até que ele termine, e o método não terminará até retornar à primeira camada.

A chave do problema é que durante o processo de processamento da solicitação de um cliente, o aceite não pode ser chamado uma segunda vez, ou seja, mesmo que chegue o segundo cliente, ele não poderá ser processado.

Aqui, o processamento do próprio processConnection pelo cliente leva muito tempo para ser executado, porque não sabemos quando o cliente terminará, nem sabemos quantas solicitações o cliente enviará.

Então esperamos que durante a execução deste método, aceite também possa ser chamado.Neste momento, multi-threading pode ser usado.

Podemos ser responsáveis ​​​​pela solicitação de clientes no thread principal, após atrair clientes, podemos criar um novo thread e deixar que o novo thread seja responsável pelo processamento de diversas solicitações do cliente.

Após as melhorias acima, desde que os recursos do servidor sejam suficientes, é possível ter vários clientes.

Na verdade, se o código que acabamos de escrever não for escrito assim, por exemplo, cada cliente é obrigado a enviar apenas uma solicitação e desconectar após enviá-la, a situação acima será amenizada até certo ponto, mas ainda haverá semelhante problemas.

O processamento de múltiplas mensagens estenderá naturalmente o tempo de execução do proessConnection, tornando esse problema mais sério.

Ao escrever programas TCP, dois métodos de escrita estão envolvidos:

1. Apenas uma solicitação e resposta são transmitidas em uma conexão (conexão curta)

2. Uma solicitação pode transmitir várias solicitações e respostas (conexão longa) 

Agora estamos, há uma conexão, há um novo tópico

Se houver muitos clientes que se conectam/desconectam com frequência, o servidor envolverá a criação/liberação frequente de threads.

Usar um pool de threads é uma solução melhor

É errado escrever isso! ! !

processConnection e o thread principal são threads diferentes. Durante a execução de processConnecton, a tentativa do thread principal é concluída. Isso fará com que o clientSocket seja fechado antes de ser usado. 

Portanto, o clientSocket ainda precisa ser entregue ao processConnection para fechar, então deve ser escrito assim:

Embora o uso de um pool de threads evite a criação e destruição frequente de threads, afinal cada cliente corresponde a um thread.

Se houver muitos clientes correspondentes ao servidor, o servidor precisará criar um grande número de threads, o que causará muita sobrecarga para o servidor.

Quando o número de clientes e threads aumentar ainda mais, a carga do sistema ficará cada vez mais pesada e a velocidade de resposta será bastante reduzida.

Existe uma maneira de usar um thread (ou no máximo três ou quatro threads) para lidar com eficiência com solicitações simultâneas de muitos clientes?

Isso também é chamado de problema C10M : ao mesmo tempo, há 1kw de solicitações de clientes simultâneas.

Muitos meios técnicos foram introduzidos, um dos quais é muito eficaz/necessário: multiplexação IO/multiplexação IO

Esse é um dos meios importantes para resolver alta simultaneidade (C10M)

Para resolver o problema da alta simultaneidade, para ser franco, existem apenas quatro palavras:

1. Código aberto: introduza mais recursos de hardware

2. Limitação: Aumentar o número de solicitações que os recursos de hardware de uma unidade podem atender

A multiplexação IO é um método de limitação. A mesma solicitação consome menos recursos de hardware.

Acho que você gosta

Origin blog.csdn.net/weixin_73616913/article/details/132279919
Recomendado
Clasificación