[Linux] Programación de sockets de red

prefacio

        Después de dominar cierta base de red, podemos comenzar con el código, usar el protocolo UDP/TCP para escribir el programa de socket y comprender cómo se conectan y comunican el servidor y el cliente en la red.

Tabla de contenido

1. Comprenda la IP de origen y destino, el puerto, el orden de bytes de la red, el zócalo

El número de puerto:

enchufe:

Conoce el protocolo TCP/UDP en la capa de transporte:

Orden de bytes de la red:

Enchufes comunes:

Dos, programación de red UDP

1.0 El cliente envía un mensaje al servidor y el servidor devuelve el mensaje al cliente correspondiente.

enchufe

unir

El orden de bytes local y el orden de bytes de la red se convierten entre sí

recvfrom&recv&recvmsg

enviar&enviara&enviarmsg

cliente:

127.0.0.1 dirección de bucle invertido local

 2.0 El cliente de Windows envía un mensaje al servidor Linux y el servidor devuelve el mensaje al cliente correspondiente.

3. Programación de red TCP

1.0 El cliente envía un mensaje al servidor y el servidor devuelve el mensaje al cliente correspondiente.

escuchar

aceptar

conectar


1. Comprenda la IP de origen y destino, el puerto, el orden de bytes de la red, el zócalo

        En primer lugar, sabemos que dos hosts se comunican en la red, por lo que se debe necesitar una IP de origen y una IP de destino . De acuerdo con esta IP (la IP aquí se considera como la IP de la red pública, y su unicidad está garantizada en un área específica), se puede determinar una unicidad en toda la red.

        Para comunicarse entre los siguientes hosts, comprendamos cuál es el propósito de la comunicación:

         Normalmente, ¿el propósito es enviar datos a la máquina de la otra parte? Se puede encontrar que la máquina solo se usa como una herramienta, y es el software en la máquina el que se comunica. ¡Así que la esencia del proceso de comunicación de red real es la comunicación entre procesos ! El reenvío de datos entre hosts es solo un medio. ¡Después de que se recibe la máquina, se entrega al proceso designado!

        Debido a que el proceso está involucrado, cuando uno de los hosts recibe la información de la red, ¿cómo puede ejecutar el proceso de red correspondiente almacenado en la memoria al descomprimirlo? Esto está relacionado con el número de puerto.

El número de puerto:

        El contenido del protocolo de la capa de transporte. es la unicidad que identifica un proceso de red en un host particular.

        ¿Por qué es la singularidad del proceso de red? El primero es la unicidad de la IP del host en toda la red + la unicidad del proceso en este host. Entonces esta combinación es el único proceso en toda la red.

        Y el número de puerto y la identificación del proceso se distinguen, desacoplando: la identificación del proceso es la que administra el sistema y el número de puerto es el que administra la red. Para un proceso, un proceso puede vincular múltiples números de puerto (es decir, hay diferentes direcciones IP para comunicarse con su proceso), pero el número de puerto solo se puede usar para un proceso. (De lo contrario, no se determinará la unicidad)

        Por supuesto, dado que IP tiene una IP de origen y una IP de destino, también hay un número de puerto de origen y un número de puerto de destino para el número de puerto, que indica quién lo envió y quién lo recibirá.

enchufe:

        De hecho, combinamos la IP de origen y el número de puerto de origen, y la IP de destino y el número de puerto de destino, que es el socket .

        Socket = {IP: número de puerto};

        El número de puerto es de 16 bits.

Conoce el protocolo TCP/UDP en la capa de transporte:

UDP: protocolo de datagramas de usuario
conexión: sin conexión
Confiable: transmisión no confiable (hay problemas como la pérdida de paquetes)

Orientado a datagramas

TCP: Protocolo de control de transmisión
Conexión: conectado
Confiable: transmisión confiable
orientado a la corriente

        Confiable se refiere a una descripción neutral. Existe un problema de pérdida de paquetes y no es confiable tolerarlo en algunos escenarios. La confiabilidad es mucha codificación y procesamiento. UDP solo envía los datos, que es más simple. --- Al elegir un protocolo, de acuerdo con las necesidades de la escena: por ejemplo, los sitios web de transmisión en vivo y video son adecuados para usar el protocolo UDP.

Orden de bytes de la red:

        Sabemos que al guardar datos en la memoria de la computadora, hay una diferencia entre los bits altos y bajos y los bits altos y bajos de la dirección, es decir, el orden de bytes endian grande y pequeño. Porque en el proceso de comunicación de la red, no sabemos si los dos hosts en comunicación tienen el mismo endianness.Si el endianness del almacenamiento es opuesto, entonces otro host cometerá un error al leer los datos.

        Por lo tanto, la red estipula que todas las transmisiones del local a la red deben estar en orden big endian . En este caso, ya sea una máquina con almacenamiento little-endian o una máquina con almacenamiento big-endian, los datos obtenidos de la red deben estar en orden de bytes big-endian cada vez, para que se puedan convertir fácilmente y leer sin problemas. .

Enchufes comunes:

1. Conector de interdominio llamado canalización: similar a
2. Conector original para escribir muchas herramientas: ahorre muchos protocolos de capa superior para usar la capa inferior.

3. Toma de red  

        Teóricamente, existen tres escenarios de aplicación, correspondientes a tres conjuntos de interfaces. Pero Linux no quiere diseñar demasiadas interfaces, por lo que todas las interfaces están unificadas.

        Y para gestionar el contenido en el socket, se define la estructura sockaaddr, una general (todas relacionadas con el tipo unificado de interfaz de socket), y otra estructura para diferentes sockets, que es conveniente para la conversión mutua y el uso de interfaz unificada.

 estructura sockaddr:
    socket de red: tipo de indicador puerto de 16 bits 32 dirección IP _in AF_INET // PF_INET
    conector entre dominios: tipo de indicador 108 byte nombre de ruta _un AF_UNIX
    común: primeros dos bytes: tipo de indicador sockaddr
    ....

Dos, programación de red UDP

         Después de familiarizarme con el contenido preparatorio anterior por primera vez, presentaré la interfaz de programación de red de protocolo UDP a través del proceso de escritura de código, y realmente puedo hacer el proceso de comunicación de chat simple entre el cliente y el servidor:

1.0 El cliente envía un mensaje al servidor y el servidor devuelve el mensaje al cliente correspondiente .

lado del servidor :

        Encapsulamos el lado del servidor del servidor como un archivo .hpp Imagino que el servidor se puede inicializar e iniciar en dos pasos.

UDPServer.hpp

         Primero determine los atributos de los miembros. Para la programación de sockets, el primer atributo indispensable es socket, que es un socket, que en realidad es un descriptor de archivo (fd), y el tipo es int. El segundo es ip y puerto. Tenga en cuenta que ip es un número entero de 16 bits e ip es de 32 bits. Sin embargo, ip generalmente se expresa en decimales con puntos , y cada parte del número separada por . es 1 byte.Si no tiene signo, la representación es 0~255.

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

        Luego está el constructor, que necesita inicializar estas propiedades. ¿Por qué el siguiente código establece que el parámetro predeterminado de ip esté vacío?Se explicará en el código de ejecución a continuación.

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

         Luego está la inicialización. Para el servidor de protocolo UDP, primero debemos vincular el socket que creamos a la IP entrante local y al número de puerto para la inicialización. En este momento están involucradas las llamadas de interfaz de red, aquí las presentamos una por una:

        Primero crea el socket:

enchufe

archivo de encabezado :

        #include <sys/tipos.h> 

        #incluye <sys/socket.h>

Prototipo de función :

        socket int (dominio int, tipo int, protocolo int);

Introducción a la función :

        socket() crea un punto final para la comunicación y devuelve un descriptor (Socket() crea un punto final para la comunicación y devuelve un descriptor).

        dominio: Este parámetro especifica el dominio de comunicación, esto selecciona la familia de protocolos para la comunicación. (IPV4 - AF_INET (IPV6 va seguido de un 6))

        tipo: El tipo especificado, que especifica la semántica de comunicación. UDP es SOCK_DGRAM - Datagram TCP es flujo SOCK_STREAM.

        protocolo: El protocolo especifica el protocolo específico que utilizará el socket. De hecho, se puede derivar automáticamente seleccionando los dos primeros parámetros y se puede establecer en 0.

        Valor devuelto: en caso de éxito, se devuelve el descriptor de archivo para el nuevo socket. Si se produce un error, se devuelve -1 y errno se establece de forma adecuada.

        Después de crear el socket, debemos vincular la IP local y el puerto mediante el enlace de interfaz:

unir

archivo de encabezado :

        #incluye <sys/socket.h>

Prototipo de función :

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

Introducción a la función :

        socket: descriptor de archivo de socket.

        dirección: estructura del zócalo:

               Para la red, debe usar la estructura struct sockaddr_in y luego forzar la estructura sockaddr al pasar los parámetros.

                Los atributos de contenido incluidos en struct sockaddr_in son los siguientes:

                sin_family - familia de protocolos sin_port - puerto sin_addr.s_addr - ip

                Para ip, el servidor generalmente se establece en 0.0.0.0 o 0, lo que significa que este servidor puede recibir cualquier ip (porque un servidor puede tener varias tarjetas de red, de modo que el servidor puede obtener datos de cualquier IP durante el trabajo). también se recomienda enlazar así OK, solo configure la macro INADDR_ANY.

                Debe tenerse en cuenta que la familia de protocolos es coherente con el dominio de comunicación especificado anteriormente. Además, debido a que el puerto y s_addr deben enviarse a la red, es necesario convertir el orden de bytes local al orden de bytes de red . Además , antes de la inicialización, debe borrarse . Puede usar la función memset o bzero para borrarlo.

        address_len: El tamaño de la estructura del socket.

        Valor devuelto: al completarse con éxito, bind() devolverá 0; de lo contrario, devolverá -1 y establecerá errno para indicar un error.

        Porque al inicializar la estructura del socket, es necesario convertir los atributos en orden de bytes de red, y la siguiente interfaz proporciona opciones:

El orden de bytes local y el orden de bytes de la red se convierten entre sí

Conversión de enteros :

        #incluye <arpa/inet.h>

       uint32_t htonl(uint32_t hostlong); // Convierte el entero sin signo (32 bytes) hostlong del orden de bytes del host al orden de bytes de la red.

       uint16_t htons(uint16_t hostshort); //Convertir entero corto sin signo (16 bytes) hostshort del orden de bytes del host al orden de bytes de la red.

       uint32_t ntohl(uint32_t netlong); // Convierte un entero sin signo (32 bytes) netlong del orden de bytes de la red al orden de bytes del host.

       uint16_t ntohs(uint16_t netshort); //Convertir netshort corto sin firmar (16 bytes) del orden de bytes de la red al orden de bytes del host.

Las interfaces anteriores se utilizan generalmente en la conversión de puertos.Para ip, dado que generalmente se representa mediante una cadena, también existen interfaces correspondientes para convertirlo en números y orden de bytes de red.

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

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

// Convierta la dirección de host de Internet cp de la notación de puntos de IPv4 a forma binaria (en orden de bytes de red) y guárdela en la estructura señalada por inp. inet_aton() devuelve un valor distinto de cero si la dirección es válida, cero en caso contrario.

       i n_addr_t inet_addr(const char *cp);

//Convierta la dirección de host de Internet cp de la notación de puntos IPv4 a datos binarios en orden de bytes de red. Si la entrada no es válida, se devuelve INADDR_NONE (generalmente -1). Usar esta función es problemático porque -1 es una dirección válida (255.255.255.255). Debe evitarse en favor de inet_aton(), inet_pton(3) o getaddrinfo(3), que proporcionan una forma más concisa de expresar los retornos de error.

       in_addr_t inet_network(const char *cp);

// Convertir cp (una cadena en notación de punto numérico IPv4) a un número en orden de bytes de host para usar como dirección de red para Internet. Si tiene éxito, la dirección se devuelve en consecuencia. Devuelve -1 si la entrada no es válida.

       char *inet_ntoa(struct in_addr in);

// Convierta la dirección de host de Internet proporcionada en orden de bytes de red en una cadena representada por la notación decimal con puntos de IPv4. La cadena se devuelve en un búfer asignado estáticamente, que será sobrescrito por esas llamadas posteriores.

       estructura in_addr inet_makeaddr(int net, int host);

//Es la función inversa de inet_netof() e inet_lnaof(). Devuelve direcciones de host de Internet en orden de bytes de red, creadas por hosts de dirección local y de red de números de red, ambos en orden de bytes de host.

       in_addr_t inet_lnaof(struct in_addr in);

//El retorno es la dirección de la red local en la dirección de Internet. El valor devuelto está en el orden de bytes del host.

       in_addr_t inet_netof(struct in_addr in);

//El papel de la función es devolver la parte del número de red de la dirección de Internet. El valor devuelto está en el orden de bytes del host.

        De esta manera, utilizando la interfaz anterior, podemos vincular el socket con la dirección IP y la dirección del 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无连接 -初始化完成-
    }
//......
};

        Tenga en cuenta que el registro anterior (logMessage) se escribe usando el archivo log.h en mi blog anterior, que se puede reemplazar por print o cout.

        Una vez completada la inicialización, necesitamos escribir la función de inicio del servidor. Dado que el protocolo UDP no requiere conexión y utiliza la transmisión de datagramas, la siguiente interfaz se puede utilizar para enviar y recibir información del host de destino:

recvfrom&recv&recvmsg

archivo de encabezado :

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

Prototipo de función :

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

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

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

Introducción a la función :

        recvfrom() y recvmsg() se usan para recibir mensajes de un socket, independientemente de si el socket está orientado a la conexión, se pueden usar para recibir datos. Por lo general, se utiliza para la programación de sockets de protocolo UDP.

        La llamada recv() normalmente solo se usa en sockets conectados (ver connect(2)), y es lo mismo que recvfrom() con un argumento NULL src_addr. Por lo general, se utiliza para la programación de sockets de protocolo TCP.

        sockfd es el descriptor de archivo de socket correspondiente, buf es el búfer donde se almacena la información recibida, len es el tamaño del búfer, flags es la forma de lectura, generalmente se establece en 0 para bloquear la lectura. src_addr es la estructura de socket del host que transmite información, addrlen es un parámetro de entrada y salida, la entrada debe pasar el tamaño de la estructura de socket original y el retorno es el tamaño de la estructura de socket devuelta.

        Valor devuelto: estas llamadas devuelven el número de bytes recibidos o -1 si se produjo un error. Si ocurre un error, se establece errno para indicar el error. Cuando se completa la ejecución del fin del par, el valor de retorno es 0 y se realiza el apagado ordenado.

        En particular, para el protocolo TCP (recv, read ), si el valor de retorno > 0, significa lectura normal. Cuando el valor de retorno es igual a 0, significa que el par ha cerrado la conexión. Cuando el valor de retorno es menor que 0, significa que la lectura no se pudo obtener, establezca el código de error.

enviar&enviara&enviarmsg

archivo de encabezado :

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

Prototipo de función :

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

       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);

Introducción a la función :

        La llamada send() solo debe usarse mientras el socket está conectado (para que se conozca el destinatario previsto). Uso de programación de socket de protocolo TCP.

        sendto se usa generalmente para la programación de sockets de protocolo UDP.

        Entre ellos, sockfd es el descriptor del archivo de socket, buf es el área de almacenamiento que se enviará, len es el tamaño del área, flags generalmente se establece en 0, dest_addr es la estructura de socket que se enviará al host de destino, addrlen es this estructura del tamaño de.

        Valor devuelto: en caso de éxito, estas llamadas devuelven la cantidad de caracteres enviados. Si se produce un error, se devuelve -1 y errno se establece de forma adecuada.

        La función de inicio del servidor, en primer lugar, está claro que el servidor es un proceso residente, por lo que debe ser un bucle infinito y continuará recibiendo mensajes que le envíen diferentes clientes. Escriba el mensaje enviado cuando se le solicite aquí. Puede usar el puerto recibido y la ip del host de destino para mostrar su contenido en el lado del servidor y luego devolver los datos originales.

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));
        }
    }
//......
};

        El destructor solo necesita liberar la memoria del zócalo, debe prestar atención para juzgar si está inicializado, puede usar el valor predeterminado del zócalo para juzgar.

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

UDPServer.cpp

        Luego, usamos los parámetros de la línea de comando en el archivo fuente para montar e iniciar el 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:

        La interfaz del cliente se ha presentado anteriormente y no se repetirá a continuación.

        El cliente, en primer lugar, naturalmente necesita obtener la ip del servidor y el puerto correspondiente, y luego crear una estructura de socket para el servidor y usar la interfaz sento para enviarlo.

        Entonces, debemos pensar en la pregunta aquí: ¿Debería el socket del cliente estar vinculado a la IP y el puerto locales? Generalmente, para el cliente, en realidad es la aplicación. Si una aplicación vincula un puerto específico cada vez, según la base de red que hemos aprendido antes, podemos saber que el puerto es el único proceso, por lo que es probable que diferentes aplicaciones tengan el mismo puerto vinculado al escribir, luego utilícelo en este tiempo Habrá problemas, es decir, un puerto corresponde a diferentes procesos, y los datos devueltos por el servidor no saben a quién enviarlos.

        Por lo tanto, el socket del cliente no necesita vincular la ip y el puerto por sí mismo. Cuando se envía al servidor por primera vez, el sistema operativo lo organizará aleatoriamente, de modo que se pueda evitar el problema del conflicto del número de puerto. .

        El cliente ya no está encapsulado y el código simple es el siguiente:

#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;
}

        Intentamos ejecutar el código bajo Linux:

127.0.0.1 dirección de bucle invertido local

        Primero, haga una prueba local.La prueba local significa vincular el servidor a ip 127.0.0.1 , que es la dirección de bucle invertido local (loopback address), es decir, no se cargará en la red, sino que se probará en la máquina local. La misma pila de protocolos locales seguirá ejecutándose de nuevo, pero no a través de la interfaz de red.

        De esta manera, el cliente y el servidor primero se ejecutan localmente para solucionar el problema y luego pueden conectarse a la red para ejecutarse, pero preste atención a la necesidad de modificar simplemente el procesamiento de los parámetros de la línea de comandos escritos por el servidor anterior.

// 在文件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;
}

        Los resultados de la prueba son los siguientes:

        La prueba local es exitosa, luego transfiera a la red e intente nuevamente:

 (El lugar donde se reproduce el mosaico es la ip de la red pública de su propio servidor)

        Después de probar en el entorno Linux, ¿podemos comunicarnos con el sistema Windows?

 2.0 El cliente de Windows envía un mensaje al servidor Linux y el servidor devuelve el mensaje al cliente correspondiente .

        De hecho, en diferentes entornos de sistemas operativos, aunque existen algunas variables de llamada al sistema, son las mismas para la programación de sockets y para la red. Para el protocolo UDP, el cliente de Windows tiene las siguientes inicializaciones en comparación con el cliente de Linux (o la programación de red en 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;
}

        El resto son básicamente sin cambios.

         Se puede ver que el lado de Windows aún puede enviar información al servidor del lado de Linux. Sin embargo, si se usa el protocolo UDP para la comunicación de red, habrá caracteres ilegibles al enviar caracteres chinos, lo que debe resolverse mediante la transcodificación.

        De acuerdo con los dos ejemplos anteriores de operación de código UDP, podemos encontrar el problema:

1. Primero, el descriptor de archivo de socket del protocolo UDP se puede escribir y leer, lo que indica que el descriptor de archivo de socket del protocolo UDP es dúplex completo.

2. El protocolo UDP no tiene ninguna conexión, siempre que se cree e inicialice la información relacionada con el socket, la comunicación se realiza directamente.

        Echemos un vistazo a la diferencia entre la red escrita por el protocolo TCP y el protocolo UDP, y usemos algunas funciones en el lado del servidor TCP para hacer que nuestros servicios de red sean más complejos y visualizados.

3. Programación de red TCP

1.0 El cliente envía un mensaje al servidor y el servidor devuelve el mensaje al cliente correspondiente .

        Primero, comencemos con lo básico.

TCPServer.hpp

        En primer lugar, el servidor TCP todavía está encapsulado para completar las dos funciones siguientes: 1. Inicialización 2. Inicio.

        Para la codificación del servidor UDP, después de que TCP crea el socket del servidor y vincula la IP y el puerto del servidor en la implementación de la codificación , se inicializa UDP, pero tenga en cuenta que la diferencia más importante entre el protocolo TCP y el protocolo UDP es si hay un conexión. TCP está conectado, por lo que generalmente es necesario distinguir el tipo de socket.

        Es como un restaurante. El propietario envía especialmente a un miembro para llamar a los clientes al costado de la carretera. Cuando este miembro (en lo sucesivo, miembro A) llame a los invitados, se lo entregará al mesero en la tienda y luego el miembro A seguirá llamando a los clientes. La implementación del socket de TCP es en realidad así, dividida en dos sockets, un socket de escucha y un socket de procesamiento o servicio. El socket de escucha usa la interfaz para devolver el descriptor de archivo del socket de servicio, que ya ha vinculado la información relacionada con el socket del cliente, y debido a que admite el flujo de bytes, también puede usar funciones de operación de archivos como lectura y escritura para operar.

        Luego, en el momento de la inicialización, debe configurar el enchufe de escucha en el modo de escucha al final , y luego se completa la inicialización.

escuchar

archivo de encabezado :

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

Prototipo de función :

        int listen(int sockfd, int backlog);

Introducción a la función :

        listen() marca el socket al que se refiere sockfd como un socket pasivo, es decir, como un socket que se usará para aceptar solicitudes de conexión entrantes usando accept(2) (listen() marca el socket al que se refiere sockfd como un socket pasivo socket, es decir, como el socket que se usará para aceptar solicitudes de conexión entrantes usando accept(2))

        sockfd: descriptor de archivo de socket.

        backlog: el parámetro backlog define la longitud máxima a la que puede crecer la cola de conexiones pendientes de sockfd. Si llega una solicitud de conexión cuando la cola está llena, el cliente puede recibir un error con una indicación ECONNREFUSED o, si el protocolo subyacente admite retransmisiones, puede ignorar la solicitud para que un reintento posterior de conexión tenga éxito.

        Valor de retorno: en caso de éxito, se devuelve cero. Devuelve -1 en caso de error y establece errno de forma adecuada.

        Si se inicia, para el protocolo UDP, es una comunicación directa, porque hay información relevante sobre la estructura del socket del cliente para recibir, como recvform. Pero para TCP, en este momento, el socket de escucha primero necesita conectarse al cliente conectado al servidor actual en el modo de escucha y devolver un socket de servicio.En este momento, para este socket de servicio, el cliente puede La información del terminal es extraído o enviado. Es decir, un cliente está conectado en este momento, y después de que el socket de escucha regrese, continuará escuchando y continuará regresando hasta que sea monitoreado.

aceptar

archivo de encabezado :

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

Prototipo de función :

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

Introducción a la función :

        La llamada al sistema accept() se utiliza con tipos de socket basados ​​en conexión (SOCK_STREAM, SOCK_SEQPACKET). Obtiene la primera solicitud de conexión en la cola de conexión pendiente para escuchar, socket sockfd crea un nuevo socket de conexión y devuelve un nuevo descriptor de archivo que hace referencia a ese socket. El socket recién creado no está en estado de escucha. El socket original no se ve afectado por esta llamada.

        sockfd: toma de escucha

        addr: parámetro de salida, recibiendo la estructura de socket del cliente.

        addrlen: parámetros de entrada y salida, primero ingrese el tamaño de addr y devuelva el tamaño de la estructura de socket del cliente receptor.

        Valor devuelto: Devuelve un entero no negativo en caso de error, -1 y establece errno en caso de error.

        A través de la introducción de la interfaz anterior, básicamente podemos encapsularla, porque la versión 1.0 simplemente devuelve datos del servidor al cliente, primero implementamos la siguiente versión de proceso único o subproceso:

#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

ServidorTCP.cpp

        Luego llame a create en este archivo.

#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 el cliente, en primer lugar, no lo encapsulamos.

        Ya hemos introducido en UDP que, debido a la particularidad del cliente, permita que el sistema operativo nos ayude a vincular la ip y el puerto de forma predeterminada, por lo que el cliente solo necesita crear un socket e inicializar la estructura del socket en el lado del servidor. Los pasos anteriores siguen siendo los mismos, pero a partir de ahora UDP se puede recibir directamente: datagrama, sin conexión.

        TCP se basa en conexiones y flujos de bytes, por lo que, naturalmente, el cliente primero necesita establecer una conexión con el servidor. (Apretón de manos de tres vías) Una vez que se establece la conexión, el socket del cliente puede comunicarse con el servidor normalmente. En este punto, el socket del cliente se puede considerar como un descriptor de archivo común y se puede leer y escribir, por lo que las operaciones de archivo se pueden usar juntas.

        Por supuesto, el cliente necesita desconectarse del servidor al final. (saluda cuatro veces)

        Presentemos brevemente la función de interfaz para que el cliente se conecte al servidor: conectar

conectar

archivo de encabezado :

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

Prototipo de función :

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

Introducción a la función :

        La llamada al sistema connect() conecta el socket al que hace referencia el descriptor de archivo sockfd a la dirección especificada por addr.

        sockfd: enchufe del cliente

        addr: estructura de socket del lado del servidor

        addrlen: tamaño de la estructura del socket del lado del servidor

        Valor de retorno: se devuelve 0 si la conexión es exitosa, de lo contrario se devuelve -1.

#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;
}

        Lo anterior es el proceso general y las ideas de TCP para realizar la programación de sockets. Se ha introducido la interfaz básica y el efecto no se demostrará aquí. Si hay suplementos de seguimiento, continuaré escribiendo en este momento. Si hay algún error, ¡por favor indíquelo!

        Además, para TCP, podemos usar netstat -antp para ver el servicio del protocolo tcp en el entorno Linux, l es solo para ver el estado de monitoreo, t es tcp y u es udp.

        Hay una herramienta que simplemente puede reemplazar el cliente TCP: telnet Si no, use sydo yum install telnet para instalarlo. Enter para ejecutar, ctrl+] + quit para salir.

Supongo que te gusta

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