[Linux] Sockets de programación de red (C++)

Tabla de contenido

1. Conocimientos preliminares

【1.1】 Comprenda la dirección IP de origen y la dirección IP de destino

【1.2】Comprenda el número de puerto

【1.3】Comprenda el "número de puerto" y el "ID de proceso"

【1.4】 Comprenda el número de puerto de origen y el número de puerto de destino

【1.5】 Comprender el protocolo TCP

【1.6】 Comprender el protocolo UDP

2. Orden de bytes de la red

【2.1】Interfaz de programación de sockets

【2.1.1】API de socket

【2.1.2】vincular API

【2.1.3】escuchar API

【2.1.4】aceptar API

【2.1.5】conectar API

【2.1.6】recepción de API

【2.1.7】enviar a API

【2.2】Función de conversión de puerto

【2.3】 Función de conversión de direcciones

【2.3】estructura sockaddr

【2.3.1】 estructura sockaddr

【2.3.2】 estructura sockaddr_in

【2.3.3】 Función de inicialización especial para la estructura sockaddr

3. Verifique la red del sistema con netstat

【5】 UDP realiza la comunicación de red

【5.1】 Código de archivo Makefile

【5.2】Código de archivo UdpServer

【5.2.1】Código del archivo UdpServer.hpp

【5.2.2】Código del archivo UdpServer.cc

【5.3】Código de archivo UdpClient

【5.3.1】Código del archivo UdpClient.hpp

【5.3.2】Código del archivo UdpClient.cc

【7】TCP realiza comunicación de red

【7.1】Código de archivo Makefile

【7.2】Código del archivo log.hcc

【7.3】Código de archivo TcpServer

【7.3.1】Código del archivo TcpServer.hpp

【7.3.2】Código del archivo TcpServer.cc

【7.4】Código de archivo TcpClient

【7.4.1】Código del archivo TcpClient.hpp

【7.4.2】Código del archivo TcpClient.cc

【9】Proceso de comunicación del protocolo TCP

【10】 Comparación entre TCP y UDP


Zócalos de programación de red Linux (C++)

1. Conocimientos preliminares

【1.1】 Comprenda la dirección IP de origen y la dirección IP de destino

        En el encabezado del paquete IP, hay dos direcciones IP, llamadas dirección IP de origen y dirección IP de destino.

[Pensando] ¿Podemos completar la comunicación simplemente teniendo una dirección IP? Imagine el ejemplo de enviar mensajes QQ. Con la dirección IP, el mensaje se puede enviar a la máquina de la otra parte, pero es necesario que haya otro identificador para distinguirlo. Esto ¿Para qué programa se deben analizar los datos?

        Para expresar mejor la unicidad del proceso de servicio de host único, utilizamos el número de puerto para identificar el proceso del servidor y la unicidad del proceso del cliente.

【1.2】Comprenda el número de puerto

El número de puerto (puerto) es el contenido del protocolo de la capa de transporte:

  • El número de puerto es un entero de 2 bytes y 16 bits.

  • El número de puerto se utiliza para identificar un proceso e indicarle al sistema operativo a qué proceso se deben entregar los datos actuales para su procesamiento.

  • La dirección IP + el número de puerto pueden identificar un proceso en un determinado host de la red.

  • Un número de puerto sólo puede estar ocupado por un proceso.

La dirección IP (unicidad del host en toda la red) + número de puerto en el host identifica la unicidad del proceso en el servidor (ip + PortA, ip + PortB). La esencia de la comunicación de red: de hecho, es comunicación entre procesos Garantías IP (única en toda la red), puerto garantizado (único en el host).

【1.3】Comprenda el "número de puerto" y el "ID de proceso"

        Cuando estábamos aprendiendo programación de sistemas antes, aprendimos que pid representa el único proceso, aquí nuestro número de puerto también representa el único proceso, entonces, ¿cuál es la relación entre los dos?

Además, un proceso puede vincular varios números de puerto, pero un número de puerto no puede estar vinculado por múltiples procesos.

【1.4】 Comprenda el número de puerto de origen y el número de puerto de destino

Hay dos números de puerto en el segmento de datos del         protocolo de capa de transporte , que se denominan número de puerto de origen y número de puerto de destino y describen "quién envió los datos y a quién se envían";

【1.5】 Comprender el protocolo TCP

        Aquí primero tenemos una comprensión intuitiva de TCP (Protocolo de control de transmisión ); discutiremos algunos detalles de TCP en detalle más adelante.

  • protocolo de capa de transporte

  • Hay una conexión

  • Transmisión confiable

  • flujo de bytes orientado

【1.6】 Comprender el protocolo UDP

        Aquí también tenemos una comprensión intuitiva de UDP (Protocolo de datagramas de usuario ); lo discutiremos en detalle más adelante.

  • protocolo de capa de transporte

  • sin conexión

  • Transmisión poco confiable

  • orientado a datagramas

2. Orden de bytes de la red

【2.1】Interfaz de programación de sockets

#include <socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// domain : 域:本地通信、网络通信
// type   : 我们socket提供的能力类型
#include <socket.h>
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
 socklen_t address_len);
// socket : 绑定指定的文件描述符
// sockaddr : 参数结构
#include <sys/types.h> 
#include <sys/socket.h>
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
#include <sys/types.h> 
#include <sys/socket.h>
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
#include <sys/types.h> 
#include <sys/socket.h>
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
#include <sys/types.h>
#include <sys/socket.h>

// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
#include <sys/types.h>
#include <sys/socket.h>
// 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

【2.1.1】API de socket

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • socket() abre un puerto de comunicación de red y, si tiene éxito, devuelve un descriptor de archivo como open().

  • Las aplicaciones pueden usar lectura/escritura para enviar y recibir datos en la red al igual que leer y escribir archivos.

  • Si se produce un error en la llamada al socket(), se devuelve -1. Para IPv4, el parámetro de familia se especifica como AF_INET.

  • Para el protocolo TCP, el parámetro de tipo se especifica como SOCK_STREAM, que representa un protocolo de transmisión orientado a flujo.

  • Se omite la introducción del parámetro de protocolo, simplemente especifíquelo como 0.

【2.1.2】vincular API

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
  • La dirección de red y el número de puerto monitoreados por el programa del servidor generalmente son fijos. Una vez que el programa del cliente aprende la dirección y el número de puerto del programa del servidor, puede iniciar una conexión con el servidor; el servidor necesita llamar a bind para vincular una red fija dirección y puerto Número.

  • bind() devuelve 0 en caso de éxito y -1 en caso de error.

  • La función de bind() es vincular los parámetros sockfd y myaddr para que sockfd, un descriptor de archivo utilizado para la comunicación de red, escuche la dirección y el número de puerto descritos por myaddr.

  • Como se mencionó anteriormente, struct sockaddr * es un tipo de puntero general. El parámetro myaddr en realidad puede aceptar estructuras sockaddr de múltiples protocolos y sus longitudes son diferentes, por lo que se necesita el tercer parámetro addrlen para especificar la longitud de la estructura.

【2.1.3】escuchar API

#include <sys/socket.h>
int listen(int socket, int backlog);
  • listening() declara que sockfd está en estado de escucha y permite que, como máximo, los clientes pendientes estén en estado de espera de conexión. Si se reciben más solicitudes de conexión, se ignorarán. La configuración aquí no será demasiado grande (normalmente 5). Te enseñaré los detalles, estudia en profundidad más adelante.

  • listening() devuelve 0 en caso de éxito y -1 en caso de error.

【2.1.4】aceptar API

#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
  • Una vez completado el protocolo de enlace de tres vías, el servidor llama a aceptar () para aceptar la conexión.

  • Si no hay ninguna solicitud de conexión del cliente cuando el servidor llama a aceptar(), se bloquea y espera hasta que un cliente se conecte.

  • addr es un parámetro saliente, y la dirección del cliente y el número de puerto se pasan cuando acepta() regresa; si se pasa NULL al parámetro addr, significa que la dirección del cliente no está involucrada.

  • El parámetro addrlen es un parámetro entrante y saliente (argumento valor-resultado). Lo que se pasa es la longitud de la dirección del búfer proporcionada por la persona que llama para evitar problemas de desbordamiento del búfer. Lo que se pasa es la longitud real de la estructura de direcciones del cliente. (Es posible que el buffer proporcionado por la persona que llama no esté lleno).

【2.1.5】conectar API

#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
  • El cliente necesita llamar a connect() para conectarse al servidor.

  • Las formas de los parámetros de conexión y vinculación son las mismas, la diferencia es que el parámetro de vinculación es su propia dirección, mientras que el parámetro de conexión es la dirección de la otra parte.

  • connect() devuelve 0 en caso de éxito y -1 en caso de error.

【2.1.6】recepción de API

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd: el puerto de red del descriptor de archivo (socket)

  • buf: leer en el búfer

  • len: longitud del búfer

  • banderas: el modo de lectura predeterminado es 0 (bloqueo de lectura)

  • src_addr: leer información del socket

  • addrlen: longitud del zócalo

【2.1.7】enviar a API

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd: el puerto de red del descriptor de archivo (socket)

  • buf: buffer enviado

  • len: longitud del búfer

  • banderas: el método de envío predeterminado es 0 (bloqueo de envío)

  • dest_addr: envía tu propia información de socket

  • addrlen: longitud del zócalo

【2.2】Función de conversión de puerto

        Ya sabemos que los datos de varios bytes en la memoria se dividen en big-endian y little-endian en relación con la dirección de memoria, y los datos de varios bytes en los archivos de disco también se dividen en big-endian y little-endian en relación con la dirección de desplazamiento en El archivo. Los flujos de datos de red también se dividen en big-endian y little-endian . Entonces, ¿cómo definir la dirección del flujo de datos de red?

  • El host de envío generalmente envía los datos en el búfer de envío en orden desde la dirección de memoria baja a la alta.

  • El host receptor almacena los bytes recibidos de la red en el búfer de recepción en orden, y también en el orden de las direcciones de memoria de menor a mayor.

  • Por lo tanto, la dirección del flujo de datos de la red debe especificarse de la siguiente manera: los datos enviados primero son la dirección baja y los datos enviados después son la dirección alta.

  • El protocolo TCP/IP estipula que el flujo de datos de la red debe estar en orden de bytes big-endian, es decir, dirección baja y byte alto.

  • Independientemente de si el host es una máquina grande o pequeña, los datos se enviarán/recibirán de acuerdo con el orden de bytes de la red especificado por TCP/IP.

  • Si el host de envío actual es little endian, los datos deben convertirse primero a big endian; de lo contrario, se ignoran y se envían directamente.

        Para hacer que los programas de red sean portátiles y que el mismo código C se ejecute normalmente después de la compilación en computadoras big-endian y little-endian, puede llamar a la siguiente función de biblioteca para convertir el orden de bytes de la red y el orden de bytes del host.

#include <arpa/inet.h>
// 函数将无符号整数hostlong从主机字节顺序转换为网络字节顺序.
uint32_t htonl(uint32_t, hostlong);
#include <arpa/inet.h>
// 函数将无符号短整数hostshort从主机字节顺序转换为网络字节顺序.
uint16_t htons(uint16_t, hostshort);
#include <arpa/inet.h>
// 函数将无符号整数netlong从网络字节顺序转换为主机字节顺序.
uint32_t ntohl(uint32_t netlong);
#include <arpa/inet.h>
// 函数将无符号短整数netshort从网络字节顺序转换为主机字节顺序.
uint16_t ntohs(uint16_t netshort);
  • Estos nombres de funciones son fáciles de recordar: h representa el host, n representa la red, l representa un entero largo de 32 bits y s representa un entero corto de 16 bits.

  • Por ejemplo, htonl significa convertir un entero de 32 bits de largo del orden de bytes del host al orden de bytes de la red, como convertir una dirección IP y prepararse para enviarla.

  • Si el host es little-endian, estas funciones convierten los parámetros en consecuencia y los devuelven.

  • Si el host es big-endian, estas funciones no realizan conversión y devuelven los parámetros sin cambios.

【2.3】 Función de conversión de direcciones

        Esta sección solo presenta la programación de redes de socket basada en IPv4. La estructura miembro in_addr sin_addr en sockaddr_in representa una dirección IP de 32 bits. Sin embargo, generalmente usamos cadenas decimales con puntos para representar direcciones IP. Las siguientes funciones se pueden usar entre la representación de cadenas e in_addr representación Convertir

        inet_aton, inet_addr, inet_network, inet_ntoa, inet_makeaddr, inet_lnaof, inet_netof - rutinas de operación de direcciones de Internet

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

int inet_aton(const char *cp, struct in_addr *inp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// inet_addr()函数的作用是:将Internet主机地址cp从IPv4的数字点法转换为网络字节顺序的二进制数据。如果输入无效,INADDR_NONE(通常是-1)返回。使用这个函数是有问题的,因为-1是一个有效的地址(255.255.255.255)。避免使用inet_aton()、inet_pton(3)或getad‐Drinfo(3)提供了一种更清晰的方式来指示错误返回。
in_addr_t inet_addr(const char *cp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_network(const char *cp);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// net_ntoa()函数的作用是:将Internet主机地址(以网络字节顺序给出)转换为IPv4点分十进制格式的字符串。字符串以静态方式返回已分配的缓冲区,后续调用将覆盖该缓冲区。
char *inet_ntoa(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

struct in_addr inet_makeaddr(int net, int host);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_lnaof(struct in_addr in);
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_netof(struct in_addr in);

        Entre ellos, inet_pton e inet_ntop no solo pueden convertir in_addr de IPv4, sino también in6_addr de IPv6, por lo que la interfaz de función es void*addrptr.

【2.3】estructura sockaddr

        La API de socket es una interfaz de programación de red abstracta que es adecuada para varios protocolos de red subyacentes, como IPv4, IPv6 y UNIX DomainSocket, que se analizarán más adelante, pero los formatos de dirección de varios protocolos de red no son los mismos.

        El formato de dirección de IPv4 e IPv6 se define en netinet/in.h. La dirección IPv4 está representada por la estructura sockaddr_in, que incluye el tipo de dirección de 16 bits, el número de puerto de 16 bits y la dirección IP de 32 bits.

        Los tipos de direcciones IPv4 e IPv6 se definen como constantes AF_INET y AF_INET6 respectivamente, de esta manera, siempre que se obtenga la primera dirección de una determinada estructura sockaddr, no es necesario saber qué tipo de estructura sockaddr es y la dirección en la estructura se puede determinar basándose en el contenido del campo de tipo de dirección.

        La API de socket se puede representar mediante el tipo struct sockaddr *, que debe forzarse a convertirse en sockaddr_in cuando se usa; la ventaja de esto es la versatilidad del programa, que puede recibir varios tipos de sockets de dominio IPv4, IPv6 y UNIX. de punteros de estructura sockaddr como parámetro.

【2.3.1】 estructura sockaddr

        sockaddr se define en el archivo de encabezado #include <sys/socket.h> . El defecto de sockaddr es que sa_data mezcla la dirección de destino y la información del puerto, de la siguiente manera:

struct sockaddr {  
     sa_family_t sin_family;	  //地址族
    char sa_data[14]; 			 //14字节,包含套接字中的目标地址和端口信息               
}; 

【2.3.2】 estructura sockaddr_in

        sockaddr_in se define en el archivo de encabezado #include<netinet/in.h> o #include <arpa/inet.h> `. Esta estructura resuelve las deficiencias de sockaddr y almacena el puerto y la dirección por separado en dos variables, de la siguiente manera:

struct sockaddr_in {
    sa_family_t			sin_family; 	// 地址族(Address Family)
    uint16_t			sin_port;		// 16位TCP\UDP端口号
    struct in_addr		sin_addr;		// 32位ip地址
    char				sin_zero[8]		// 不使用
}

// 该结构体中提到另一个结构体in_addr定义如下:它用来存放32位ip地址
struct in_addr {
    in_addr_t			s_addr;			// 32位IPv4地址
}

【2.3.3】 Función de inicialización especial para la estructura sockaddr

#include <strings.h>
// 对struct sockaddr 数据类型做初始化            
// bzero()函数将从s开始的区域的前n个字节设置为零(包含'\0'的字节)。
void bzero(void *s, size_t n);

3. Verifique la red del sistema con netstat

gramática

netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]

Función

        El comando netstat de Linux muestra el estado de la red.

        El uso del comando netstat le permite conocer el estado de la red de todo el sistema Linux.

Opciones

  • -a o --all muestra todos los sockets conectados.

  • -A<tipo de red> o --<tipo de red> enumera las direcciones relevantes en la conexión de este tipo de red.

  • -c o --continuous enumera el estado de la red continuamente.

  • -C o --cache muestra información de la caché de configuración del enrutador.

  • -e o --extend muestra otra información relacionada con la red.

  • -F o --fib muestra el caché de ruta.

  • -g o --groups Muestra la lista de miembros del grupo de funciones de multidifusión.

  • -h o --help ayuda en línea.

  • -i o --interfaces Muestra el formulario de información de la interfaz de red.

  • -l o --listening muestra el socket del servidor que se está monitoreando.

  • -M o --masquerade muestra conexiones de red enmascaradas.

  • -n o --numeric Utilice la dirección IP directamente sin pasar por el servidor de nombres de dominio.

  • -N o --netlink o --symbolic Muestra los nombres de enlaces simbólicos de los periféricos de hardware de red.

  • -o o --timers Muestra temporizadores.

  • -p o --programs muestra el código de identificación del programa y el nombre del programa que utiliza Socket.

  • -r o --route muestra la tabla de enrutamiento.

  • -s o --statistics muestra la tabla de estadísticas de información del trabajo de la red.

  • -t o --tcp muestra el estado de conexión del protocolo de transmisión TCP.

  • -u o --udp muestra el estado de la conexión del protocolo de transmisión UDP.

  • -v o --verbose muestra el proceso de ejecución de instrucciones.

  • -V o --version muestra información de la versión.

  • -w o --raw muestra el estado de conexión del protocolo de transferencia RAW.

  • -x o --unix Este parámetro tiene el mismo efecto que especificar el parámetro "-A unix".

  • --ip o --inet Este parámetro tiene el mismo efecto que especificar el parámetro "-A inet".

[Observaciones] Si desea ver más detalles, agregue sudo

[shaxiang@VM-8-14-centos 99_Lesson_20230707_CodeExecute_UdpNetWork_Linux]$ netstat -nuap // 监控端口号和IP
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
udp        0      0 0.0.0.0:68              0.0.0.0:*                           -                   
udp        0      0 10.0.8.14:123           0.0.0.0:*                           -                   
udp        0      0 127.0.0.1:123           0.0.0.0:*                           -                   
udp6       0      0 fe80::5054:ff:fec6::123 :::*                                -                   
udp6       0      0 ::1:123                 :::*                                -             
[shaxiang@VM-8-14-centos 20230811_TcpNewWork]$ netstat -nltp	// 监控TCP服务器
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:45638         0.0.0.0:*               LISTEN      20145/node          
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      4410/./TcpServer    
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::3306                 :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                       

【5】 UDP realiza la comunicación de red

【5.1】 Código de archivo Makefile

# 定义变量并且赋值相应的字符串信息
cc := g++
standard := -std=c++11

compile: udpServer udpClient
udpServer: UdpServer.cc 
	$(cc) -o $@ $^ $(standard)
udpClient: UdpClient.cc 
	$(cc) -o $@ $^ $(standard)

clean:
	rm -rf udpServer udpClient

# .PHONY: 可以避免与系统的命令冲突
.PHONY: compile clean 

【5.2】Código de archivo UdpServer

【5.2.1】Código del archivo UdpServer.hpp

#pragma once
/* C头文件包含 */
#include <cstdlib>
#include <cstring>
#include <cerrno>

/* C++头文件包含 */
#include <iostream>
#include <functional>
#include <string>

/* 系统头文件包含 */
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/* UDP服务器Demo封装命名空间 */
namespace Server
{
    enum { USAGE_ERR = 1, SOCKET_ERR = 2, BIND_ERR = 3 };
    const int g_num = 1024;


    /* UDP服务器命名空间 */
    class UdpServer
    {
    private:
        using func_t = std::function<void(const int&, const std::string&, const uint16_t&, const std::string&)>;

    public:
        /* 构造函数 */
        UdpServer(const func_t& func, const uint16_t& port)
            : _selfSockFd(-1)
            , _selfSockProt(port)
            , _callBack(func)
        {}

        /* 析构函数 */
        ~UdpServer() 
        {}

    public:
        /* 初始化 */
        void Init()
        {
            // 创建socket(打开网卡驱动文件)
            _selfSockFd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字    
            if(_selfSockFd < 0)
            {
                std::cerr << "socket error " << errno << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "socket success..." << std::endl;

            // 绑定自己的Ip和端口号
            struct sockaddr_in localAddr; // 套接字对象
            bzero(&localAddr, sizeof(localAddr)); // 初始化
            localAddr.sin_family = AF_INET; // 地址家族
            localAddr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
            localAddr.sin_port = htons(_selfSockProt); // 端口号
            
            int bindState = bind(_selfSockFd, (struct sockaddr*)& localAddr, sizeof(localAddr)); // 绑定套接字
            if(bindState < 0)
            {
                std::cerr << "bind error " << errno << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind success..." << std::endl;
        }

        /* 启动服务 */
        void Start()
        {
            // 启动
            char reBuffer[g_num] = "\0";
            while(true)
            {
                struct sockaddr_in clientAddr; // 套接字对象
                bzero(&clientAddr, sizeof(clientAddr)); // 初始化
                socklen_t addrLength = sizeof(clientAddr);
                // 读取
                int reCnt = recvfrom(_selfSockFd, reBuffer, sizeof(reBuffer) - 1, 0, (struct sockaddr*)& clientAddr, &addrLength); 
                if(reCnt > 0)
                {
                    reBuffer[reCnt] = '\0'; // 处理字符串
                    std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 获取客户端ip
                    uint16_t clientPort = ntohs(clientAddr.sin_port); // 获取客户端port
                    // 调用回调函数
                    _callBack(_selfSockFd, clientIp, clientPort, reBuffer);
                }
            }
        }

    private:
        uint16_t     _selfSockProt;     // 服务器(自己)UDP通讯端口号
        int          _selfSockFd;       // 服务器(自己)UDP通讯文件描述符   
        func_t       _callBack;         // 服务器(自己)UDP回调函数
    };
};

【5.2.2】Código del archivo UdpServer.cc

#include <memory>
#include "UdpServer.hpp"
using namespace Server;

/* 函数接口:用户启动提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}

/* 函数回调:通讯功能 */
void CallbackFunction(const int& selfSockFd, const std::string& clientIp, const uint16_t& clientPort, const std::string& message) 
{
    // 打印接收信息
    std::cout << "client ip[" << clientIp << "] " << "port[" << clientPort << "]: " << message << std::endl;
}

/* 函数接口:程序入口 */
int main(int argc, char** argv)
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 服务器对象交管给智能指针
    uint16_t userPort = atoi(argv[1]);
    std::unique_ptr<UdpServer> udpSevr(new UdpServer(CallbackFunction, userPort));
    udpSevr->Init();
    udpSevr->Start();

    return 0; 
}

【5.3】Código de archivo UdpClient

【5.3.1】Código del archivo UdpClient.hpp

#pragma once
/* C头文件包含 */
#include <string>
#include <cstring>

/* C++头文件包含 */
#include <iostream>

/* 系统头文件包含 */
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/* UDP客户端Demo封装命名空间 */
namespace Client
{
    enum { USAGE_ERR = 1, SOCKET_ERR = 2, BIND_ERR = 3 };
    const int g_num = 1024;

    /* UDP客户端命名空间 */
    class UdpClient
    {
    public:
        /* 构造函数 */
        UdpClient(const uint16_t& port, const std::string& ip)
            : _selfSockFd(-1)
            , _sevrSockIp(ip)
            , _sevrSockProt(port)
        {}

        /* 析构函数 */
        ~UdpClient() 
        {}

    public:
        /* 初始化 */
        void Init()
        {   
            // 创建socket(打开网卡驱动文件)
            _selfSockFd = socket(AF_INET, SOCK_DGRAM, 0);    
            if(_selfSockFd < 0)
            {
                std::cerr << "socket error " << errno << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "socket success..." << std::endl;
        }

        /* 启动服务 */
        void Start()
        {
            struct sockaddr_in serverAddr; // 套接字对象
            bzero(&serverAddr, sizeof(serverAddr)); // 初始化
            serverAddr.sin_family = AF_INET; // 地址家族
            serverAddr.sin_addr.s_addr = inet_addr(_sevrSockIp.c_str()); // 服务器ip
            serverAddr.sin_port = htons(_sevrSockProt); // 服务器port

            // 启动
            char stoBuffer[g_num];
            while(true)
            {   
                std::cout << "Please Say: ";
                std::cin.getline(stoBuffer, g_num);  
                // 发送信息
                sendto(_selfSockFd, stoBuffer, sizeof(stoBuffer), 0, (struct sockaddr*)& serverAddr, sizeof(serverAddr));

            }
        }

    private:
        std::string  _sevrSockIp;       // 服务器(对方)UDP通讯Ip地址
        uint16_t     _sevrSockProt;     // 服务器(对方)UDP通讯端口号
        int          _selfSockFd;       // 客户端(自己)UDP通讯文件描述符   
    };
};

【5.3.2】Código del archivo UdpClient.cc

#include <memory>
#include "UdpClient.hpp"
using namespace Client;

/* 函数接口:用户启动提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " ServerIp" << " ServerPort\n\n" << std::endl;
}

/* 函数接口:程序入口 */
int main(int argc, char** argv)
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 客户端对象交管给智能指针
    uint16_t userPort = atoi(argv[2]);
    std::string userIp = argv[1];
    std::unique_ptr<UdpClient> udpClit(new UdpClient(userPort, userIp));
    udpClit->Init();
    udpClit->Start();

    return 0; 
}

【7】TCP realiza comunicación de red

【7.1】Código de archivo Makefile

# 定义变量并且赋值相应的字符串信息
cc := g++
standard := -std=c++11

compile:tcpServer tcpClient
tcpServer:TcpServer.cc
	$(cc) -o $@ $^ $(standard)
tcpClient:TcpClient.cc
	$(cc) -o $@ $^ $(standard)

clean:
	rm -rf tcpServer tcpClient
	
# .PHONY: 可以避免与系统的命令冲突
.PHONY:clean compile

【7.2】Código del archivo log.hcc

#pragma once 
#include <iostream>

#define DEBUG    0 // 调试等级
#define NORMAL   1 // 正常等级
#define WARNING  2 // 警告等级
#define ERROR    3 // 错误等级
#define FATAL    4 // 致命等级

/* 函数:日志等级转为字符串 */
void LevelToString(const int& level)
{
    
}

/* 函数:日志打印 */
void LogMessage(const int& level, const std::string& message)
{
    // 格式:[日志等级] [时间戳/时间] [pid] [信息]
    // 比如:[WARNING] [2023-05-11 18:09:23] [12345] [创建socket文件描述符失败!]    
    std::cout << message << std::endl;
}

【7.3】Código de archivo TcpServer

【7.3.1】Código del archivo TcpServer.hpp

#pragma once
#include <iostream>

#include <unistd.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

#include "Log.hpp"

namespace Server
{
    /* 枚举常量 */
    enum{ 
        USAGE_ERR = 1, 
        SOCKET_ERR, 
        BIND_ERR,  
        LISTEN_ERR,
        ACCEPT_ERR
    };
    const int g_backLog = 10;
    const int g_num = 1024;

    class TcpServer
    {
    public:
        /* 构造函数 */
        TcpServer(const uint16_t& port)
            : _selfListenFd(-1)
            , _selfPort(port)
        {}

        /* 析构函数 */
        ~TcpServer() {}

    public:
        /* 初始化 */
        void Init()
        {
            // No.1 创建套socket文件套接字对象
            _selfListenFd = socket(AF_INET, SOCK_STREAM, 0);
            if(_selfListenFd < 0)
            {
                LogMessage(FATAL, "create socket fail!");
                exit(SOCKET_ERR);
            }
            LogMessage(NORMAL, "create socket success...");

            // No.2 绑定自己的网络信息
            struct sockaddr_in localAddr; // 创建sockAddr对象
            bzero(&localAddr, sizeof(localAddr)); // 初始化对象
            localAddr.sin_family = AF_INET; // 绑定协议家族
            localAddr.sin_addr.s_addr = INADDR_ANY; // 绑定回环地址[0.0.0.0]
            localAddr.sin_port = htons(_selfPort); // 绑定端口号
            int bindState = bind(_selfListenFd, (struct sockaddr*)&localAddr, sizeof(localAddr));
            if(bindState < 0)
            {
                LogMessage(FATAL, "bind socket fail!");
                exit(BIND_ERR); 
            }
            LogMessage(NORMAL, "bind socket success...");

            // No.3 开始监听网络
            int listenState = listen(_selfListenFd, g_backLog);
            if(listenState < 0)
            {
                LogMessage(FATAL, "listen socket fail!");
                exit(LISTEN_ERR); 
            }
            LogMessage(NORMAL, "listen socket success...");
        }

        /* 启动 */
        void Start()
        {
            // 运行服务器
            while(true)
            {
                // No.4 获取新连接
                struct sockaddr_in clientAddr; // sockAddr对象
                socklen_t addrLen = sizeof(clientAddr); // 求长度
                bzero(&clientAddr, sizeof(clientAddr)); // 初始化
                // 接收链接
                _selfSocketFd = accept(_selfListenFd, (struct sockaddr*)&clientAddr, &addrLen);
                if(_selfListenFd < 0)
                {
                    LogMessage(ERROR, "accept socket fail!");
                    exit(ACCEPT_ERR); 
                }
                LogMessage(NORMAL, "accept socket success...");
                std::cout << "listenFd: " << _selfListenFd << " " << "sockFd: " << _selfSocketFd << std::endl;

                // 面向字节流的读取(对文件进行读取)
                ServiceIO();
                close(_selfSocketFd); // 必须关闭,防止文件描述符泄露!
                break;
            }
        }

        /* 面向字节流读取消息 */
        void ServiceIO()
        {
            while(true)
            {
                // 接收
                char inBuffer[g_num] = { '\0' };
                ssize_t n = read(_selfSocketFd, inBuffer, sizeof(inBuffer) - 1);
                if(n > 0)
                {
                    // 处理读取到的内容
                    inBuffer[n] = '\n';
                    std::cout << "recv buffer: " << inBuffer << std::endl;

                    // 响应
                    std::string outBuffer;
                    outBuffer = "Server echo# ";
                    outBuffer += inBuffer;
                    write(_selfSocketFd, outBuffer.c_str(), outBuffer.size());
                }
                else if(n == 0) // 在读取的时候,如果读取到了0,说明客户端已经退出了,这时候服务器也可以退出了!
                {
                    LogMessage(NORMAL, "client quit me too...");
                    break;
                }
            }
        }

    private:
        int         _selfListenFd;  // TCP通讯(自己)网络监听文件描述符
        int         _selfSocketFd;  // TCP通讯(自己)网络服务文件描述符
        uint16_t    _selfPort;      // TCP通讯(自己)网络端口
    };
};

【7.3.2】Código del archivo TcpServer.cc

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

using namespace Server;

/* 函数:消息提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}

/* 函数:程序入口函数 */
int main(int argc, char** argv)
{
    // 检查用户启动命令
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 将服务器对象转交给智能指针进行管理
    // 获取用户指定的端口号
    uint16_t userPort = atoi(argv[1]);
    std::unique_ptr<TcpServer> pTcpSevr(new TcpServer(userPort));
    pTcpSevr->Init();
    pTcpSevr->Start();
    
    return 0;
}

【7.4】Código de archivo TcpClient

【7.4.1】Código del archivo TcpClient.hpp

#pragma once
#include <iostream>

#include <unistd.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

#include "Log.hpp"

namespace Client
{
    /* 枚举常量 */
    enum{ 
        USAGE_ERR = 1, 
        SOCKET_ERR, 
        BIND_ERR,  
        LISTEN_ERR,
        ACCEPT_ERR,
        CONNECT_ERR
    };
    const int g_num = 1024;


    class TcpClient
    {
    public:
        /* 构造函数 */
        TcpClient(const std::string& sevrIp, const uint16_t& sevrPort)
            : _selfSocketFd(-1)
            , _sevrSocketIp(sevrIp)
            , _sevrSocketPort(sevrPort)
        {}
        
        /* 析构函数 */
        ~TcpClient() {}

    public:
        /* 初始化 */
        void Init() 
        {
            // No.1 创建套socket文件套接字对象
            _selfSocketFd = socket(AF_INET, SOCK_STREAM, 0);
            if(_selfSocketFd < 0)
            {
                LogMessage(FATAL, "create socket fail!");
                exit(SOCKET_ERR);
            }
            LogMessage(NORMAL, "create socket success...");

            // No.2 创建链接
            struct sockaddr_in local; // 创建sockAddr对象
            bzero(&local, sizeof(local)); // 初始化对象
            local.sin_family = AF_INET; // 绑定协议家族
            local.sin_addr.s_addr = inet_addr(_sevrSocketIp.c_str()); // 绑定服务器Ip
            local.sin_port = htons(_sevrSocketPort); // 绑定服务器端口号
            int connectState = connect(_selfSocketFd, (struct sockaddr*)&local, sizeof(local));
            if(connectState < 0)
            {
                LogMessage(FATAL, "connect socket fail!");
                exit(CONNECT_ERR);   
            }
            LogMessage(NORMAL, "connect socket success...");
        }

        /* 启动 */
        void Start()
        {
            // 运行
            while(true)
            {
                // 响应
                std::cout << "Plase Say# ";
                std::string outBuffer;
                std::getline(std::cin, outBuffer);
                write(_selfSocketFd, outBuffer.c_str(), outBuffer.size());

                // 等待回复
                char inBuffer[g_num] = { '\0' };
                int n = read(_selfSocketFd, inBuffer, sizeof(inBuffer) - 1);
                if(n > 0)
                {   
                    // 处理读取到的内容
                    inBuffer[n] = '\0';
                    std::cout << inBuffer << std::endl;
                }
                else if(n == 0) // 在读取的时候,如果读取到了0,说明服务器已经退出了,这时候客户端也可以退出了!
                {
                    LogMessage(NORMAL, "server quit me too...");
                    break;
                }
            }

            close(_selfSocketFd);
        }

    private:
        int             _selfSocketFd;
        std::string     _sevrSocketIp;
        uint16_t        _sevrSocketPort;
    };
};

【7.4.2】Código del archivo TcpClient.cc

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

using namespace Client;

/* 函数:消息提示 */ 
void Usage(char* argv)
{
    std::cout << "Usage:\n\t" << argv << " local_port\n\n" << std::endl;
}

/* 函数:程序入口函数 */
int main(int argc, char** argv)
{
    // 检查用户启动命令
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 将客户端对象转交给智能指针进行管理
    // 获取用户指定的IP地址和端口号
    std::string userIp = argv[1];
    uint16_t userPort = atoi(argv[2]);
    std::unique_ptr<TcpClient> pTcpClit(new TcpClient(userIp, userPort));
    pTcpClit->Init();
    pTcpClit->Start();
    
    return 0;
}

【9】Proceso de comunicación del protocolo TCP

[Inicialización del servidor]

  • Llame al socket para crear un descriptor de archivo.

  • Llame a bind para vincular el descriptor de archivo actual y la ip/puerto juntos; si el puerto ya está ocupado por otros procesos, el enlace fallará.

  • Llame a escuchar para declarar el descriptor de archivo actual como un descriptor de archivo del servidor para prepararse para la aceptación posterior.

  • Llame a aceptar y bloquear, esperando que el cliente se conecte.

[Proceso de establecimiento de conexión]

  • Llame al socket para crear un descriptor de archivo.

  • Llame a connect para iniciar una solicitud de conexión al servidor.

  • connect enviará un segmento SYN y bloqueará esperando que el servidor responda (primera vez).

  • Cuando el servidor recibe el SYN del cliente, responderá con un segmento SYN-ACK que indica "acepto establecer una conexión"; (segunda vez).

  • Después de recibir SYN-ACK, el cliente regresará de connect() y responderá con un segmento ACK (la tercera vez).

Este proceso de establecer una conexión suele denominarse apretón de manos de tres vías .

[Proceso de transmisión de datos]

  • Una vez establecida la conexión, el protocolo TCP proporciona servicios de comunicación full-duplex; el llamado full-duplex significa que en la misma conexión, al mismo tiempo, ambas partes comunicantes pueden escribir datos al mismo tiempo; el concepto opuesto es Se llama half-duplex, en el que en la misma conexión solo una parte puede escribir datos al mismo tiempo.

  • El servidor llama a read() inmediatamente después de regresar de aceptar(). Leer el socket es como leer una tubería. Si no llegan datos, se bloquea y espera.

  • En este momento, el cliente llama a write () para enviar una solicitud al servidor. Después de recibirla, el servidor regresa de read () para procesar la solicitud del cliente. Durante este período, el cliente llama a read () para bloquear y esperar la respuesta del servidor.

  • El servidor llama a write() para enviar los resultados del procesamiento al cliente y llama a read() nuevamente para bloquear y esperar la siguiente solicitud.

  • Después de recibirlo, el cliente regresa de read() y envía la siguiente solicitud, y el ciclo continúa.

[Proceso de desconexión]

  • Si el cliente no tiene más solicitudes, se llama a close() para cerrar la conexión y el cliente enviará un segmento FIN al servidor (por primera vez).

  • En este momento, después de que el servidor reciba el FIN, responderá con un ACK y la lectura devolverá 0 (la segunda vez).

  • Después de que la lectura regresa, el servidor sabe que el cliente ha cerrado la conexión y llama a cerrar para cerrar la conexión. En este momento, el servidor enviará un FIN; (la tercera vez) al cliente.

  • El cliente recibe el FIN y devuelve un ACK al servidor (la cuarta vez).

Este proceso de desconexión suele denominarse cuatro ondas .

Al aprender la API de socket, preste atención a cómo interactúan la aplicación y la capa del protocolo TCP.

  • ¿Qué acciones completa la capa del protocolo TCP cuando una aplicación llama a una función de socket? Por ejemplo, llamar a connect() enviará un segmento SYN.

  • ¿Cómo sabe la aplicación los cambios de estado de la capa del protocolo TCP? Por ejemplo, regresar de una función de socket bloqueada indica que el protocolo TCP ha recibido ciertos segmentos. Por ejemplo, si read() devuelve 0, indica que el segmento FIN tiene sido recibido.

【10】 Comparación entre TCP y UDP

Transmisión confiable Transmisión poco confiable
comunicación TCP comunicación UDP
Hay un enlace sin conexión
flujo de bytes paquete de datos

Supongo que te gusta

Origin blog.csdn.net/lx473774000/article/details/132865452
Recomendado
Clasificación