[Linux] Programação de soquete de rede

prefácio

        Depois de dominar uma certa base de rede, podemos começar com o código, usar o protocolo UDP/TCP para escrever o programa de soquete e entender como o servidor e o cliente na rede estão conectados e se comunicam.

Índice

1. Entenda o IP de origem e destino, porta, ordem de byte de rede, soquete

O número da porta:

soquete:

Conheça o protocolo TCP/UDP na camada de transporte:

Ordem de bytes de rede:

Soquetes comuns:

Dois, programação de rede UDP

1.0 O cliente envia uma mensagem para o servidor e o servidor retorna a mensagem para o cliente correspondente.

soquete

vincular

A ordem de byte local e a ordem de byte de rede são convertidas entre si

recvfrom&recv&recvmsg

enviar&enviarpara&enviarmsg

cliente:

127.0.0.1 endereço de loopback local

 2.0 O cliente Windows envia uma mensagem para o servidor Linux e o servidor retorna a mensagem para o cliente correspondente.

3. Programação de rede TCP

1.0 O cliente envia uma mensagem para o servidor e o servidor retorna a mensagem para o cliente correspondente.

ouvir

aceitar

conectar


1. Entenda o IP de origem e destino, porta, ordem de byte de rede, soquete

        Em primeiro lugar, sabemos que dois hosts se comunicam na rede, portanto, um IP de origem e um IP de destino devem ser necessários . De acordo com esse IP (o IP aqui é considerado o IP da rede pública, e sua exclusividade é garantida em uma área específica), pode ser determinada uma exclusividade em toda a rede.

        Para se comunicar entre os seguintes hosts, vamos entender qual é o objetivo da comunicação:

         Normalmente, enviar dados para a máquina da outra parte é o objetivo? Pode-se descobrir que a máquina é usada apenas como uma ferramenta e é o software da máquina que se comunica. Portanto, a essência do processo de comunicação em rede real é a comunicação entre processos ! O encaminhamento de dados entre hosts é apenas um meio. Depois que a máquina é recebida, ela é entregue ao processo designado!

        Como o processo está envolvido, quando um dos hosts recebe as informações de rede, como ele pode executar o processo de rede correspondente armazenado na memória descompactando-o? Isso está relacionado ao número da porta.

O número da porta:

        O conteúdo do protocolo da camada de transporte. é a singularidade que identifica um processo de rede em um determinado host!

        Por que é a singularidade do processo de rede? A primeira é a exclusividade do IP do host em toda a rede + a exclusividade do processo neste host. Portanto, essa combinação é o único processo em toda a rede.

        E o número da porta e o id do processo são diferenciados, desacoplados - o id do processo é aquele que gerencia o sistema, e o número da porta é aquele que gerencia a rede. Para um processo, um processo pode vincular vários números de porta (ou seja, existem diferentes IPs para se comunicar com seu processo), mas o número da porta só pode ser usado para um processo. (Caso contrário, a exclusividade não será determinada)

        Obviamente, como o IP tem um IP de origem e um IP de destino, também existe um número de porta de origem e um número de porta de destino para o número da porta, indicando quem o enviou e quem o receberá.

soquete:

        Na verdade, combinamos o IP de origem e o número da porta de origem, e o IP de destino e o número da porta de destino, que é o socket .

        Soquete = {IP: número da porta};

        O número da porta é de 16 bits.

Conheça o protocolo TCP/UDP na camada de transporte:

UDP: Protocolo de datagrama do usuário
conexão: sem conexão
Confiável: transmissão não confiável (existem problemas como perda de pacotes)

Datagrama Orientado

TCP: Protocolo de Controle de Transmissão
Conexão: conectado
Confiável: transmissão confiável
orientado para o fluxo

        Confiável refere-se a uma descrição neutra. Há um problema de perda de pacotes e não é confiável tolerá-lo em alguns cenários. Confiabilidade é muita codificação e processamento. O UDP apenas envia os dados, o que é mais simples. ---Ao escolher um protocolo, de acordo com as necessidades da cena: por exemplo, transmissão ao vivo e sites de vídeo são adequados para usar o protocolo UDP.

Ordem de bytes de rede:

        Sabemos que, ao salvar dados na memória do computador, há uma diferença entre os bits altos e baixos e os bits altos e baixos do endereço - ou seja, a ordem de bytes big e small endian. Como no processo de comunicação de rede, não sabemos se os dois hosts em comunicação têm o mesmo endianness.Se o endianness do armazenamento for oposto, outro host cometerá um erro ao ler os dados.

        Portanto, a rede estipula que todas as transmissões do local para a rede devem estar em ordem big endian . Neste caso, seja uma máquina com armazenamento little-endian ou uma máquina de armazenamento big-endian, os dados obtidos da rede devem estar sempre na ordem de bytes big-endian, para que possam ser facilmente convertidos e lidos sem problemas .

Soquetes comuns:

1. Soquete entre domínios chamado pipe - semelhante a
2. Soquete original para escrever muitas ferramentas - poupe muitos protocolos da camada superior para usar a camada inferior.

3. Tomada de rede  

        Teoricamente, existem três cenários de aplicação, correspondentes a três conjuntos de interfaces. Mas o Linux não quer projetar muitas interfaces, então todas as interfaces são unificadas.

        E para gerenciar o conteúdo no soquete, é definida a estrutura sockaaddr, uma geral (todas relacionadas ao tipo unificado de interface de soquete) e outra estrutura para diferentes soquetes, o que é conveniente para conversão mútua e uso de interface unificada.

 estrutura sockaddr:
    soquete de rede: tipo de sinalizador porta de 16 bits endereço IP 32 _in AF_INET // PF_INET
    soquete entre domínios: tipo de sinalizador nome do caminho de 108 bytes _un AF_UNIX
    comum: dois primeiros bytes: tipo de sinalizador sockaddr
    ....

Dois, programação de rede UDP

         Depois de me familiarizar com o conteúdo preparatório acima pela primeira vez, apresentarei a interface da programação de rede do protocolo UDP por meio do processo de escrita de código e pode realmente tornar o processo de comunicação de bate-papo simples entre o cliente e o servidor:

1.0 O cliente envia uma mensagem para o servidor e o servidor retorna a mensagem para o cliente correspondente .

lado do servidor :

        Encapsulamos o lado servidor do servidor como um arquivo .hpp. Imagino que o servidor possa ser inicializado e iniciado em duas etapas.

UDPServer.hpp

         Primeiro determine os atributos do membro. Para programação de socket, o primeiro atributo indispensável é socket, que é um socket, que na verdade é um descritor de arquivo (fd), e o tipo é int. O segundo é ip e porta. Observe que ip é um inteiro de 16 bits e ip é de 32 bits. No entanto, ip é normalmente expresso em ponto decimal , e cada parte do número separada por . é 1 byte. Se não for assinado, a representação é 0~255.

// 服务器封装
class UDPServer
{
    //......
private:
    // 源套接字 = 源ip + 源端口
    std::string _SRCip;
    uint16_t _SRCport;
    // UDP对应一套读写套接字对应文件描述符
    int _socket;
};

        Depois, há o construtor, que precisa inicializar essas propriedades. Por que o código a seguir define o parâmetro padrão de ip como vazio? Isso será explicado no código em execução abaixo.

class UDPServer
{
public:
    UDPServer(uint16_t port, std::string ip = "")
        : _SRCip(ip), _SRCport(port), _socket(-1)
    {
    }
//......
};

         Depois, há a inicialização. Para o servidor de protocolo UDP, primeiro precisamos vincular o soquete que criamos ao IP de entrada local e ao número da porta para inicialização. Neste momento, as chamadas de interface de rede estão envolvidas. Aqui as apresentamos uma a uma:

        Primeiro crie o soquete:

soquete

arquivo de cabeçalho :

        #include <sys/types.h> 

        #include <sys/socket.h>

Protótipo da função :

        int socket(int domínio, int tipo, int protocolo);

Introdução da função :

        socket() cria um endpoint para comunicação e retorna um descritor. (Socket() cria um endpoint para comunicação e retorna um descritor.)

        domain: Este parâmetro especifica o domínio de comunicação; isso seleciona a família de protocolos para comunicação. (IPV4 - AF_INET (IPV6 é seguido por um 6))

        tipo: O tipo especificado, que especifica a semântica de comunicação. UDP é SOCK_DGRAM - Datagrama TCP é fluxo SOCK_STREAM.

        protocol: O protocolo especifica o protocolo específico a ser usado pelo soquete. Na verdade, ele pode ser derivado automaticamente selecionando os dois primeiros parâmetros e pode ser definido como 0.

        Valor de retorno: em caso de sucesso, o descritor de arquivo para o novo soquete é retornado. Se ocorrer um erro, -1 será retornado e errno será definido apropriadamente.

        Depois de criar o soquete, precisamos vincular o IP local e a porta, usando a interface bind:

vincular

arquivo de cabeçalho :

        #include <sys/socket.h>

Protótipo da função :

        int bind(int socket, const struct sockaddr *address, socklen_t address_len);

Introdução da função :

        socket: descritor de arquivo socket.

        endereço: soquete estrutura:

               Para a rede, você precisa usar a estrutura struct sockaddr_in e, em seguida, forçar a struct sockaddr ao passar parâmetros.

                Os atributos de conteúdo contidos em struct sockaddr_in são os seguintes:

                sin_family - família de protocolo sin_port - porta sin_addr.s_addr - ip

                Para ip, o servidor geralmente é configurado para 0.0.0.0 ou 0, o que significa que este servidor pode receber qualquer ip (porque um servidor pode ter várias placas de rede, de modo que o servidor pode obter dados de qualquer IP durante o trabalho). também recomendado ligar assim OK, basta definir a macro INADDR_ANY.

                Deve-se observar que a família de protocolos é consistente com o domínio de comunicação especificado acima. Além disso, como port e s_addr devem ser enviados para a rede, é necessário converter a ordem de byte local para a ordem de byte de rede . Além disso , antes da inicialização, ele precisa ser limpo , você pode usar a função memset ou bzero para limpá-lo.

        address_len: O tamanho da estrutura do soquete.

        Valor de retorno: Após a conclusão bem-sucedida, bind() retornará 0; caso contrário, retornará -1 e definirá errno para indicar um erro.

        Pois ao inicializar a estrutura do soquete é necessário converter os atributos em ordem de byte da rede, e a seguinte interface disponibiliza as opções:

A ordem de byte local e a ordem de byte de rede são convertidas entre si

Conversão inteira :

        #include <arpa/inet.h>

       uint32_t htonl(uint32_t hostlong); // Converte o inteiro não assinado (32 bytes) hostlong da ordem de bytes do host para a ordem dos bytes da rede.

       uint16_t htons(uint16_t hostshort); //Converte hosthort inteiro curto não assinado (16 bytes) da ordem de bytes do host para a ordem dos bytes da rede.

       uint32_t ntohl(uint32_t netlong); // Converte um inteiro não assinado (32 bytes) netlong da ordem de byte da rede para a ordem de byte do host.

       uint16_t ntohs(uint16_t netshort); //Converte unsigned short (16 bytes) netshort da rede byte order para host byte order.

As interfaces acima são geralmente usadas na conversão de portas.Para ip, uma vez que geralmente é representado por uma string, também existem interfaces correspondentes para convertê-lo em números e ordem de byte de rede.

       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       int inet_aton(const char *cp, struct in_addr *inp) ;

// Converte o endereço do host da Internet cp da notação de ponto do IPv4 para a forma binária (na ordem de byte da rede) e armazena-o na estrutura apontada por inp. inet_aton() retorna diferente de zero se o endereço for válido, zero caso contrário.

       i n_addr_t inet_addr(const char *cp);

//Converte o endereço de host da Internet cp da notação de ponto IPv4 em dados binários na ordem de byte da rede. Se a entrada for inválida, INADDR_NONE (geralmente -1) será retornado. O uso dessa função é problemático porque -1 é um endereço válido (255.255.255.255). Deve ser evitado em favor de inet_aton(), inet_pton(3) ou getaddrinfo(3), que fornecem uma maneira mais concisa de expressar retornos de erro.

       in_addr_t inet_network(const char *cp);

// Converte cp (uma string em notação de ponto numérico IPv4) em um número na ordem de byte do host para uso como um endereço de rede para a Internet. Se for bem-sucedido, o endereço é retornado de acordo. Retorna -1 se a entrada for inválida.

       char *inet_ntoa(struct in_addr in);

// Converte o endereço do host da Internet fornecido na ordem de bytes da rede em uma string representada pela notação decimal com pontos IPv4. A string é retornada em um buffer alocado estaticamente, que será substituído por essas chamadas subseqüentes.

       struct in_addr inet_makeaddr(int net, int host);

//É a função inversa de inet_netof() e inet_lnaof(). Ele retorna endereços de host da Internet em ordem de byte de rede, criados por rede de número de rede e hosts de endereço local, ambos em ordem de byte de host.

       in_addr_t inet_lnaof(struct in_addr in);

//O retorno é o endereço da rede local no endereço da Internet. O valor de retorno está na ordem de byte do host.

       in_addr_t inet_netof(struct in_addr in);

//O papel da função é retornar a parte do número de rede do endereço da Internet. O valor de retorno está na ordem de byte do host.

        Dessa forma, usando a interface acima, podemos vincular o soquete ao ip e endereço do servidor.

class UDPServer
{
public:
//......
// UDP服务器初始化:创建套接字+绑定
    void initserver()
    {
        // 创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0); // 网络-IPV4  面向数据报 协议-填0即可,会根据前面两个选项进行判断
        if (_socket < 0)
        {
            // 返回-1表示创建套接字失败,致命错误
            logMessage(FATAL, "套接字创建失败-%d:%s", errno, strerror(errno));
            exit(1);
        }

        // 绑定本地进程
        struct sockaddr_in local; // 注意头文件必须包含完 man in_addr
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;                                                      // 协议家族
        local.sin_port = htons(_SRCport);                                                // 注意,此处是要发送到网上的,需要转化为网络字节序,使用接口 hton 本地转网络 s是2字节16位
        local.sin_addr.s_addr = _SRCip.empty() ? INADDR_ANY : inet_addr(_SRCip.c_str()); // 如果为空,设置默认ip,此时可以接收任意ip发送的消息,不局限于一个ip。
        // 上述套接字结构初始化完毕,现在进行绑定
        if (bind(_socket, (struct sockaddr *)&local, sizeof local) < 0)
        {
            // 小于零绑定失败!
            logMessage(FATAL, "绑定失败-%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "UDP服务器初始化成功..... %s", strerror(errno));
        // UDP无连接 -初始化完成-
    }
//......
};

        Observe que o log acima (logMessage) é escrito usando o arquivo log.h em meu blog anterior, que pode ser substituído por print ou cout.

        Após a conclusão da inicialização, precisamos escrever a função de inicialização do servidor. Como o protocolo UDP não requer conexão e usa transmissão de datagrama, a seguinte interface pode ser usada para enviar e receber informações do host de destino:

recvfrom&recv&recvmsg

arquivo de cabeçalho :

       #include <sys/types.h>
       #include <sys/socket.h>

Protótipo da função :

       ssize_t recv(int sockfd, void *buf, size_t len, int sinalizadores);

       ssize_t recvfrom(int sockfd, void *buf, size_t len, sinalizadores int,
                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

Introdução da função :

        recvfrom() e recvmsg() são usados ​​para receber mensagens de um soquete, independentemente de o soquete ser orientado a conexão, eles podem ser usados ​​para receber dados. Geralmente usado para programação de soquete de protocolo UDP.

        A chamada recv() normalmente é usada apenas em soquetes conectados (consulte connect(2)) e é a mesma que recvfrom() com um argumento NULL src_addr. Geralmente usado para programação de soquete de protocolo TCP.

        sockfd é o descritor de arquivo de soquete correspondente, buf é o buffer onde as informações recebidas são armazenadas, len é o tamanho do buffer, flags é o modo de leitura, geralmente definido como 0 para bloquear a leitura. src_addr é a estrutura de soquete do host que transmite informações, addrlen é um parâmetro de entrada e saída, a entrada precisa passar no tamanho da estrutura de soquete original e o retorno é o tamanho da estrutura de soquete retornada.

        Valor de retorno: Essas chamadas retornam o número de bytes recebidos ou -1 se ocorreu um erro. Se ocorrer um erro, errno é definido para indicar o erro. Quando a execução do ponto final é concluída, o valor de retorno é 0 e o desligamento ordenado é executado.

        Em particular, para o protocolo TCP (recv, read ), se o valor de retorno for > 0, significa leitura normal. Quando o valor de retorno for igual a 0, significa que o peer encerrou a conexão. Quando o valor de retorno for menor de 0, significa que a leitura falhou ao buscar, defina o código de erro.

enviar&enviarpara&enviarmsg

arquivo de cabeçalho :

       #include <sys/types.h>
       #include <sys/socket.h>

Protótipo da função :

       ssize_t send(int sockfd, const void *buf, size_t len, int sinalizadores);

       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

Introdução da função :

        A chamada send() só deve ser usada enquanto o soquete estiver conectado (para que o destinatário pretendido seja conhecido). Uso de programação de soquete de protocolo TCP.

        sendto é geralmente usado para programação de soquete de protocolo UDP.

        Entre eles, sockfd é o descritor do arquivo de soquete, buf é a área de armazenamento a ser enviada, len é o tamanho da área, flags geralmente é definido como 0, dest_addr é a estrutura do soquete a ser enviada ao host de destino, addrlen é este estrutura do tamanho de.

        Valor de retorno: Em caso de sucesso, essas chamadas retornam o número de caracteres enviados. Se ocorrer um erro, -1 será retornado e errno será definido apropriadamente.

        A função de inicialização do servidor, antes de tudo, fica claro que o servidor é um processo residente, portanto deve ser um loop infinito e continuará recebendo mensagens enviadas a ele por diferentes clientes. Digite de volta a mensagem enviada quando solicitado aqui. Você pode usar a porta recebida e o ip do host de destino para exibir seu conteúdo no lado do servidor e, em seguida, retornar os dados originais.

class UDPServer
{
public:
//......
    // UDP服务器通信开始!
    void start()
    {
        // 正式启动UDP服务器
        // 1.0版本 UDP接收客户端信息,返回给客户端本身消息
        char buffer[1024];
        while (true) // 常驻进程,永远不退出
        {
            // 创建客户端套接字结构,用来接收
            struct sockaddr_in client;
            socklen_t clientlen = sizeof(client);
            bzero(&client, sizeof client); // 清零空间
            // 面向数据报接收消息
            ssize_t n = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &clientlen); // 0阻塞读取
            if (n > 0)
            {
                buffer[n] = '\0';
                // 可以把对应主机的套接字信息提取出来
                std::string clientip = inet_ntoa(client.sin_addr); // 网络 转化ip
                uint16_t clientport = ntohs(client.sin_port);
                printf("[%s: %d]# %s\n", clientip.c_str(), clientport, buffer);
            }
            // 发送对应主机
            sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&client, sizeof(client));
        }
    }
//......
};

        O destruidor só precisa liberar a memória do soquete, você precisa prestar atenção para julgar se ele foi inicializado, você pode usar o valor padrão do soquete para julgar.

class UDPServer
{
public:
//.......
    ~UDPServer()
    {
        if (_socket != -1)
            close(_socket);
    }
//.......
};

UDPServer.cpp

        Em seguida, usamos os parâmetros de linha de comando no arquivo de origem para montar e iniciar o servidor.

#include "UDPServer.hpp"
#include <iostream>
#include <memory>

static void UserManual()
{
    std::cout << "please:./UDPServer port" << std::endl;
}

int main(int arc, char* argv[])
{
    if (arc != 2)
    {
        UserManual();
        exit(-1);
    }
    std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(atoi(argv[1])));
    UDPServer_ptr->initserver();
    UDPServer_ptr->start();
    return 0;
}

cliente:

        A interface do cliente foi apresentada acima e não será repetida abaixo.

        O cliente, antes de tudo, naturalmente precisa obter o ip do servidor e a porta correspondente, para depois criar uma estrutura de soquete para o servidor e usar a interface sento para enviá-lo.

        Portanto, precisamos pensar sobre a questão aqui: o soquete do cliente deve estar vinculado ao ip e à porta locais? Geralmente, para o cliente, é na verdade o aplicativo. Se um aplicativo vincular uma porta específica a cada vez, com base na base de rede que aprendemos antes, podemos saber que a porta é o único processo, portanto, é provável que diferentes aplicativos tenham a mesma porta vinculada ao escrever, então use-a neste tempo Haverá problemas, ou seja, uma porta corresponde a diferentes processos e os dados retornados pelo servidor não sabem para quem enviá-los.

        Portanto, o soquete do cliente não precisa vincular o ip e a porta sozinho. Quando for enviado ao servidor pela primeira vez, será organizado aleatoriamente pelo sistema operacional, para que o problema de conflito de número de porta possa ser evitado .

        O cliente não está mais encapsulado e o código simples é o seguinte:

#include "log.hpp"
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>

static void UserManual()
{
    std::cout << "please:./UDPClient ip port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        UserManual();
        exit(-1);
    }
    // 首先创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    // 注意,此套接字不可显示绑定 - 不能绑定确定的port

    // 将服务器端套接字结构初始化好
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);  // 首先点分式转化为数字然后转化为网络字节序保存起来

    char buffer[1024];
    while (true)
    {
        std::cout << "请输入# ";
        std::string message;
        std::getline(std::cin, message);
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);

        // 接收消息
        struct sockaddr_in _server;
        socklen_t server_len = sizeof(_server);
        bzero(&_server, server_len);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&_server, &server_len);
        if (s > 0)
        {
            buffer[s] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}

        Tentamos executar o código no Linux:

127.0.0.1 endereço de loopback local

        Primeiro, faça um teste local. O teste local significa vincular o servidor ao ip 127.0.0.1 , que é o endereço de loopback local (loopback address), ou seja, não será carregado na rede, mas testado na máquina local A mesma pilha de protocolo local ainda será executada novamente, mas não por meio da interface de rede.

        Dessa forma, o cliente e o servidor primeiro executam localmente para solucionar o problema e, em seguida, podem se conectar à rede para executar, mas preste atenção à necessidade de simplesmente modificar o processamento dos parâmetros da linha de comando gravados pelo servidor acima.

// 在文件UDPServer.cpp 服务器源文件内修改
int main(int arc, char* argv[])
{
    int port;
    std::string ip = "";
    if (arc == 2)
    {
        port = atoi(argv[1]);
    }
    else if (arc == 3)
    {
        port = atoi(argv[2]);
        ip = argv[1];
    }
    else
    {
        UserManual();
        exit(-1);
    }

    std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(port, ip));
    UDPServer_ptr->initserver();
    UDPServer_ptr->start();
    return 0;
}

        Os resultados do teste são os seguintes:

        O teste local foi bem-sucedido, então transfira para a rede e tente novamente:

 (O local onde o mosaico é reproduzido é o ip da rede pública do seu próprio servidor)

        Depois de testar no ambiente Linux, podemos nos comunicar com o sistema Windows?

 2.0 O cliente Windows envia uma mensagem para o servidor Linux e o servidor retorna a mensagem para o cliente correspondente .

        De fato, em diferentes ambientes de sistema operacional, embora existam algumas variáveis ​​de chamada do sistema, elas são as mesmas para a programação do soquete e para a rede. Para o protocolo UDP, o cliente Windows possui as seguintes inicializações em comparação com o cliente Linux (ou programação de rede no Windows):

#include <WinSock2.h>  // 引入套接字编程库 windows为此库 只需引入一个库就可以了

#pragma warning(disable:4996)  // 屏蔽错误  一般用一些不安全的函数,可以利用此进行屏蔽
#pragma comment(lib, "ws2_32.lib")  // 固定用法,加载入库

int main()
{
	WSADATA WSAData;  // win 初始化
	if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
	{
		//WSACleanup();
		std::cout << "初始化失败!" << std::endl;
		return -1;
	}

    // ......

	closesocket(sock);
	WSACleanup();  // 结束
    return 0;
}

        O resto está basicamente inalterado.

         Pode-se ver que o lado do Windows ainda pode enviar informações para o servidor no lado do Linux. No entanto, se o protocolo UDP for usado para comunicação de rede, haverá caracteres ilegíveis ao enviar caracteres chineses, o que precisa ser resolvido por transcodificação.

        De acordo com os dois exemplos acima de operação de código UDP, podemos encontrar o problema:

1. Primeiro, o descritor de arquivo de soquete do protocolo UDP pode ser escrito e lido, indicando que o descritor de arquivo de soquete do protocolo UDP é full-duplex.

2. O protocolo UDP não possui nenhuma conexão, desde que as informações relacionadas ao soquete sejam criadas e inicializadas, a comunicação é realizada diretamente.

        Vamos dar uma olhada na diferença entre a rede escrita pelo protocolo TCP e o protocolo UDP, e usar algumas funções do lado do servidor TCP para tornar nossos serviços de rede mais complexos e visualizados.

3. Programação de rede TCP

1.0 O cliente envia uma mensagem para o servidor e o servidor retorna a mensagem para o cliente correspondente .

        Primeiro, vamos começar com o básico.

TCPServer.hpp

        Em primeiro lugar, o servidor TCP ainda está encapsulado para completar as duas funções a seguir: 1. Inicialização 2. Inicialização.

        Para a codificação do servidor UDP, depois que o TCP cria o soquete do servidor e vincula o ip e a porta do servidor na implementação da codificação , o UDP é inicializado, mas observe que a diferença mais importante entre o protocolo TCP e o protocolo UDP é se existe um conexão. O TCP está conectado, portanto geralmente é necessário distinguir o tipo de soquete.

        É como um restaurante. O proprietário envia especialmente um membro para ligar para os clientes na beira da estrada. Quando esse membro (doravante referido como membro A) liga para os convidados, ele o entrega ao garçom da loja e, em seguida, o membro A continuará ligando para os clientes. A implementação do soquete do TCP é, na verdade, assim, dividida em dois soquetes, um soquete de escuta e um soquete de processamento ou serviço. O soquete de escuta usa a interface para retornar o descritor de arquivo do soquete de serviço, que já vinculou as informações relacionadas ao soquete do cliente e, como suporta fluxo de bytes, também pode usar funções de operação de arquivo, como leitura e gravação, para operar.

        Então, no momento da inicialização, você precisa definir o soquete de escuta para o modo de escuta no final e, em seguida, a inicialização é concluída.

ouvir

arquivo de cabeçalho :

       #include <sys/types.h>
       #include <sys/socket.h>

Protótipo da função :

        int listen(int sockfd, int backlog);

Introdução da função :

        listen() marca o soquete referido por sockfd como um soquete passivo, isto é, como um soquete que será usado para aceitar solicitações de conexão recebidas usando accept(2) (listen() marca o soquete referido por sockfd como um soquete passivo socket, isto é, como o socket que será usado para aceitar requisições de conexão de entrada usando accept(2))

        sockfd: descritor de arquivo de soquete.

        backlog: O parâmetro backlog define o tamanho máximo que a fila de conexões pendentes do sockfd pode crescer. Se uma solicitação de conexão chegar quando a fila estiver cheia, o cliente poderá receber um erro com uma indicação ECONNREFUSED ou, se o protocolo subjacente oferecer suporte a retransmissões, poderá ignorar a solicitação para que uma nova tentativa posterior de conexão seja bem-sucedida.

        Valor de retorno: Em caso de sucesso, zero é retornado. Retorna -1 em caso de erro e define errno apropriadamente.

        Se for iniciado, para o protocolo UDP, é uma comunicação direta, pois existem informações relevantes sobre a estrutura do soquete do cliente para recebimento, como recvform. Mas para TCP, neste momento, o soquete de escuta primeiro precisa se conectar ao cliente conectado ao servidor atual no modo de escuta e retornar um soquete de serviço. Neste momento, para este soquete de serviço, o cliente pode As informações do terminal são extraído ou enviado. Ou seja, um cliente está conectado neste momento e, após o retorno do soquete de escuta, ele continuará a escutar e continuará a retornar até que seja monitorado.

aceitar

arquivo de cabeçalho :

       #include <sys/types.h>
       #include <sys/socket.h>

Protótipo da função :

        int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Introdução da função :

        A chamada de sistema accept() é usada com tipos de soquete baseados em conexão (SOCK_STREAM, SOCK_SEQPACKET). Ele busca a primeira solicitação de conexão na fila de conexão pendente para ouvir, o soquete sockfd cria um novo soquete de conexão e retorna um novo descritor de arquivo referenciando esse soquete. O soquete recém-criado não está no estado de escuta. O soquete original não é afetado por esta chamada.

        sockfd: soquete de escuta

        addr: parâmetro de saída, recebendo a estrutura socket do cliente.

        addrlen: Parâmetros de entrada e saída, primeiro insira o tamanho de addr e retorne o tamanho da estrutura de soquete do cliente receptor.

        Valor de retorno: Retorna um inteiro não negativo em correto, -1 e define errno em caso de erro.

        Através da introdução da interface acima, podemos basicamente encapsulá-la, porque a versão 1.0 simplesmente retorna dados do servidor para o cliente, primeiro implementamos a seguinte versão de processo único ou thread:

#ifndef _TCPSERVER_
#define _TCPSERVER_

#include "log.hpp"
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

const static int gbacklog = 20; // 一般不能设置太大和太小 -后面再说

static void handleTheClient(int &serviceSock, const std::string &clientIp, uint16_t clientPort)
{
    char buffer[1024];
    while (true)
    {
        printf("[%s: %d]# ", clientIp.c_str(), clientPort);
        // 接收的话注意是字节流,所以以前的文件那一套可以使用的
        ssize_t n = recv(serviceSock, buffer, sizeof(buffer) - 1, 0); // 阻塞等待,没有from就是适合面向字节流的,即TCP的,但是UDP写可以用的
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << buffer << std::endl;

            // 然后发送
            // 可以使用以前文件操作那一套或者send
            // send(serviceSock, buffer, sizeof(buffer) - 1, 0);  // 阻塞发送,使用处理套接字
            ssize_t s = write(serviceSock, buffer, strlen(buffer));
            if (s < 0)
            {
                logMessage(ERROR, "发送信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                break;
            }
        }
        else if (n == 0)
        {
            // 对方关闭了连接,这里也进行关闭
            printf("[%s: %d]客户端关闭连接,我也关闭此连接\n");
            close(serviceSock);
            serviceSock = -1; // 防止后面析构再次释放
            break;
        }
        else
        {
            // 小于零说明接收失败
            logMessage(ERROR, "接收信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            close(serviceSock);
            break;
        }
    }
}

class TCPServer
{
public:
    TCPServer(uint16_t port, std::string ip = "") : _server_ip(ip), _server_port(port),
                                                    _listen_sock(-1), _service_sock(-1)
    {
    }

    void initTCPServer()
    {
        // 初始化TCP服务器
        _listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 流式套接 自动识别为TCP协议,面向字节流
        if (_listen_sock < 0)
        {
            logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(1);
        }

        // 服务器将自己的ip和端口绑定到对应的套接字上。
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof server_addr); // 初始化为0
        server_addr.sin_family = AF_INET;            // 家族还是网络IPV4
        server_addr.sin_port = htons(_server_port);  // 转化为网络字节序的端口号
        if (_server_ip.empty())
            server_addr.sin_addr.s_addr = INADDR_ANY;  // 如果是空字符串就如此处理
        else
        {
            int n = inet_aton(_server_ip.c_str(), &server_addr.sin_addr); // 直接写入结构中,将点分式的ip地址转化为数字然后转化为网络字节序
            if (n == 0)
            {
                logMessage(FATAL, "写入ip地址无效!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                exit(2);
            }
        }

        // bind
        if (bind(_listen_sock, (struct sockaddr *)&server_addr, sizeof server_addr) < 0)
        {
            logMessage(FATAL, "服务器端ip与port与套接字绑定失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(3);
        }
        logMessage(NORMAL, "绑定成功!......");
        // UDP到这一步初始化完毕,但是TCP还存在一步,需要进行连接
        // 因为TCP是面向连接的,我们正式通信前需要先建立连接
        // 此时就相当于设置_sock套接字为监听模式了
        if (listen(_listen_sock, gbacklog) < 0)
        {
            logMessage(FATAL, "连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(4);
        }
    }

    // 初始化完就是启动了
    void start()
    {
        // 服务器先接收消息,然后在发送消息
        // TCP协议能否简单的像UDP那样直接进行通信吗?显然不能,在连接阶段使用的套接字是监听套接字,对信息处理并且发送是处理套接字所要干的事情
        struct sockaddr_in clientAddr;
        socklen_t clientAddrLen = sizeof clientAddr; // 用来接收客户端信息的套接字结构体
        while (true)
        {
            // 首先确保常驻
            // 首先获取连接,连接我返回,不连接在这里进行阻塞
            _service_sock = accept(_listen_sock, (struct sockaddr *)&clientAddr, &clientAddrLen);
            if (_service_sock == -1)
            {
                logMessage(ERROR, "连接客户端失败,重新连接... %d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
                continue;
                // 连接失败不是致命的错误,重连一次即可
            }
            logMessage(NORMAL, "客户端连接成功.....");
            // 现在使用堆客户端信息处理的代码即可,这里我将他们封装为一个函数
            // 首先在外面先获取客户端的ip和端口
            std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 将网络字节序的网络地址ip转化为点分十进制的字符串
            uint16_t clientPort = ntohs(clientAddr.sin_port);      // 网络转为本地字节序,注意是16位整数

            // 处理信息
            handleTheClient(_service_sock, clientIp, clientPort);
        }
    }

    ~TCPServer()
    {
        if (_listen_sock != -1)
            close(_listen_sock);
        if (_service_sock != -1)
            close(_service_sock);
    }

private:
    std::string _server_ip;
    uint16_t _server_port;
    int _listen_sock;  // 监听套接字
    int _service_sock; // 处理套接字
};

#endif

TCPServer.cpp

        Em seguida, chame create neste arquivo.

#include "TCPServer.hpp"
#include <memory>

static void UserManual()
{
    std::cout << "please:./TCPServer ip port or /TCPServer port" << std::endl;
}

int main(int argc, char* argv[])
{
    std::string ip = "";
    uint16_t port;
    if (argc == 2)
    {
        port = atoi(argv[1]);
    }
    else if (argc == 3)
    {
        ip = argv[1];
        port = atoi(argv[2]);
    }
    else
    {
        UserManual();
        exit(-1);
    }

    std::unique_ptr<TCPServer> TCP_server(new TCPServer(port, ip));
    TCP_server->initTCPServer();
    TCP_server->start();
    return 0;
}

TCPClient.cpp 

        Para o cliente, em primeiro lugar, não o encapsulamos.

        Já introduzimos no UDP que devido à particularidade do cliente, deixamos que o sistema operativo nos ajude a vincular o ip e a porta por defeito, pelo que o cliente apenas necessita de criar um socket e inicializar a estrutura do socket no lado do servidor. As etapas acima ainda são as mesmas, mas a partir de agora o UDP pode ser recebido diretamente - datagrama, sem conexão.

        O TCP é baseado em conexões e fluxos de bytes, portanto, naturalmente, o cliente primeiro precisa estabelecer uma conexão com o servidor. (Handshake de três vias) Depois que a conexão é estabelecida, o soquete do cliente pode se comunicar com o servidor normalmente. Neste ponto, o soquete do cliente pode ser considerado como um descritor de arquivo comum e é legível e gravável, portanto, as operações de arquivo podem ser usadas juntas.

        Obviamente, o cliente precisa se desconectar do servidor no final. (acena quatro vezes)

        Vamos apresentar brevemente a função de interface para o cliente se conectar ao servidor: conectar

conectar

arquivo de cabeçalho :

       #include <sys/types.h>
       #include <sys/socket.h>

Protótipo da função :

       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

Introdução da função :

        A chamada de sistema connect() conecta o soquete referenciado pelo descritor de arquivo sockfd ao endereço especificado por addr.

        sockfd: soquete do cliente

        addr: estrutura de soquete do lado do servidor

        addrlen: tamanho da estrutura do soquete do lado do servidor

        Valor de retorno: 0 é retornado se a conexão for bem-sucedida, caso contrário, -1 é retornado.

#include "log.hpp"
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
#include <errno.h>

static void UserManual()
{
    std::cout << "please:./TCPclient server_ip server_port" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        UserManual();
        exit(-1);
    }

    // 创建客户端的套接字
    int clientSock = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSock < 0)
    {
        logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(1);
    }

    // TCP客户端同样的不需要主动绑定自己的ip和port
    // 对于TCP客户端来说,需要的是连接能力,那么一个套接字足以
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof server_addr);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));  // 是要发送到网络上的,所以千万别忘了转为网络字节序
    if (0 > inet_aton(argv[1], &server_addr.sin_addr))  // 主机转网络 ip 点分十进制
    {
        logMessage(FATAL, "ip从本地转化网络字节序失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(2);
    }
    socklen_t sever_len = sizeof server_addr;
    if ( 0 > connect(clientSock, (struct sockaddr*)&server_addr, sever_len))
    {
        logMessage(FATAL, "服务器连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
        exit(3);
    }
    logMessage(NORMAL, "服务器连接成功~");

    // 客户端不断向服务器端发送信息接收信息即可
    std::string message;
    char buffer[1024];
    while (true)
    {
        printf("请输入# ");
        std::getline(std::cin, message);
        if (message == "quit") break;

        // 使用send可以发送
        if ( 0 > send(clientSock, message.c_str(), message.size(), 0))  // 阻塞发送
        {
            logMessage(ERROR, "客户端发送失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            continue;
        }

        // 使用read接收
        ssize_t n = read(clientSock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << "server# " << buffer << std::endl;
        }
        else if (n == 0)
        {
            // 此时对端关闭,我也关闭即可
            break;
        }
        else
        {
            logMessage(FATAL, "接收服务器数据失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);
            exit(4);
        }
    }
    close(clientSock);
    return 0;
}

        O exposto acima é o processo geral e as ideias do TCP para realizar a programação do soquete. A interface básica foi introduzida e o efeito não será demonstrado aqui. Se houver suplementos de acompanhamento, continuarei a escrever neste momento. Se houver algum erro, por favor, aponte-o!

        Além disso, para TCP, podemos usar netstat -antp para visualizar o serviço do protocolo tcp no ambiente Linux, l é apenas para visualizar o status de monitoramento, t é tcp e u é udp.

        Existe uma ferramenta que pode simplesmente substituir o cliente TCP: telnet Se não, use sydo yum install telnet para instalá-lo. Enter para executar, ctrl+] + quit para sair.

Acho que você gosta

Origin blog.csdn.net/weixin_61508423/article/details/129086875
Recomendado
Clasificación