Implementación simple de un servidor y un cliente de socket bajo linux

Tabla de contenido

1. Descriptor de archivo de Linux

2. Cree un socket en Linux

3. La función bind () y la función connect ()

3.1, función bind ()

3.2, función connect ()

4. La función listen () y accept ()

4.1, función listen ()

4.2, función accept ()

5 、 escribir () 和 leer ()

5.1, función write ()

5.2, función read ()

6, enviar () y recibir ()

7, una implementación simple de servicio y cliente


1. Descriptor de archivo de Linux

En Linux, todo es un archivo. Un dispositivo de hardware también se puede asignar a un archivo virtual, llamado archivo de dispositivo. Por ejemplo, stdin se denomina archivo de entrada estándar y su dispositivo de hardware correspondiente es generalmente un teclado, stdout se denomina archivo de salida estándar y su dispositivo de hardware correspondiente es generalmente una pantalla. Para todos los archivos, puede usar la función read () para leer datos y la función write () para escribir datos.

La idea de que "todo es un archivo" simplifica enormemente la comprensión y el funcionamiento del programador, haciendo que el procesamiento de dispositivos de hardware sea como archivos normales. Todos los archivos creados en Linux tienen un número de tipo int llamado File Descriptor. Al usar un archivo, solo necesita conocer el descriptor del archivo. Por ejemplo, el descriptor de stdin es 0 y el descriptor de stdout es 1.

En Linux, los sockets también se consideran un tipo de archivo, que no es diferente de las operaciones de archivo normales, por lo que las funciones relacionadas con la E / S de archivos se pueden utilizar naturalmente en el proceso de transmisión de datos de red. Se puede considerar que la comunicación entre dos computadoras es en realidad la lectura y escritura mutua de dos archivos de socket.

Los descriptores de archivo a veces se denominan Identificador de archivo, pero "identificador" es principalmente un término en Windows.

2. Cree un socket en Linux

En Linux, use la función socket () en el archivo de encabezado <sys / socket.h> para crear un socket. El prototipo es:

int socket(int af, int type, int protocol);
  • af: Familia de direcciones, es decir, tipo de dirección IP, de uso común AF_INET y AF_INET6. AF es la abreviatura de "Address Family" e INET es la abreviatura de "Internet". AF_INET representa una dirección IPv4. Por ejemplo, 127.0.0.1; AF_INET6 representa una dirección IPv6, como 1030 :: C9B4: FF12: 48AA: 1A2B. También puede utilizar el prefijo PF. PF es la abreviatura de "Protocol Family", que es lo mismo que AF. Por ejemplo, PF_INET es equivalente a AF_INET y PF_INET6 es equivalente a AF_INRT6.
  • tipo: método de transmisión de datos, comúnmente utilizados son SOCK_STREAM y SOCK_DGRAM .
  • Protocolo representa el protocolo de transmisión, los más utilizados son IPPROTO_TCP e IPPTOTO_UDP, que representan el protocolo de transmisión TCP y el protocolo UDP respectivamente.

Al ver esto, es posible que tenga una pregunta: con el tipo de dirección IP y el método de transmisión de datos, ¿no es suficiente decidir qué protocolo usar? ¿Por qué se necesita el tercer parámetro?

Así es, en circunstancias normales, puede crear un socket con dos parámetros af y type, y el sistema operativo deducirá automáticamente el tipo de protocolo, a menos que se encuentre en una situación de este tipo: hay dos protocolos diferentes que admiten el mismo tipo de dirección IP Y métodos de transmisión de datos. Si no sabemos qué protocolo usar, el sistema operativo no puede deducirlo automáticamente.

Si el valor de af se establece en PF_INET y se usa el método de transmisión SOCK_STREAM, entonces el único protocolo que cumple estas dos condiciones es TCP, por lo que la función SOCK () se puede llamar así:

int tcp_socket = socket(AD_INET,SOCK_STREAM,IPPROTO_TCP);  //TCP套接字

Si el valor de af se establece en PF_INET y se usa el método de transmisión SOCK_DGRAM, entonces el único protocolo que cumple estas dos condiciones es UDP, por lo que la función SOCKET () se puede llamar así:

int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); //UDP套接字

En los dos casos anteriores, solo un protocolo cumple las condiciones. Puede establecer el valor del protocolo en 0 y el sistema deducirá automáticamente qué protocolo debe utilizarse. El código es el siguiente

int tcp_socket = socket(AD_INET,SOCK_STREAM,0);  //TCP套接字

int udp_socket = socket(AF_INET,SOCK_DGRAM,0); //UDP套接字

3. La función bind () y la función connect ()

La función socket () se usa para crear un socket, determinar varios atributos del socket, y luego el lado del servidor necesita usar la función bind () para vincular el socket a una dirección IP y puerto específicos. Solo de esta manera, fluye a través del Los datos de la dirección IP y el puerto se pueden transferir al socket; el cliente necesita usar la función connect () para establecer una conexión.

3.1, función bind ()

El prototipo de la función bind () es:

int bind(int sock,struct sockaddr *addr,socklen_t addrlen);

sock es el descriptor del archivo de socket, addr es el puntero de la variable de estructura sockaddr y addrlen es el tamaño de la variable addr.

La definición de socklen_t es en  realidad un uint32.

Veamos un código que vincula el socket creado a la dirección IP 128.0.0.1 y al puerto 1123:

int serv_sock = sock(AF_INET,SOCK_STREAM,IPPROTO);  //创建一个TCP套接字

struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(servaddr));
serv_addr.sin_famil = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1123);  //端口

bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr));

Echemos un vistazo a la estructura sockaddr_in

struct sockaddr_in
{
    sa_family_t    sin_family;  //地址族(Address Family),也就是地址类型
    unit16_t       sin_port;    //16位的端口号
    struct in_addr sin_addr;   //32位IP地址
    char           sin_zero[8]; //不使用,一般用0填充
}
  1. El significado del primer parámetro de sin_family y socket () es el mismo, y el valor debería ser el mismo.
  2. sin_port es el número de puerto y la longitud de uint16_t es de dos bytes. Teóricamente, el rango de valores del número de puerto es 0 ~ 65536, pero el puerto 0 ~ 1023 generalmente se asigna a un programa de servicio específico por el sistema, por ejemplo, el puerto del servicio web es 70, el puerto del servicio FTP es 21, por lo que nuestro programa intenta asignar números de puerto entre 1024 ~ 65536.
  3. sin_addr es una variable del tipo de estructura struct in_addr.
  4. sin_zero tiene más de 8 bytes, lo cual es inútil y generalmente se llena con 0 usando la función memset (). En el código anterior, primero use memset () para llenar todos los bytes de la estructura con 0, y luego asigne valores a los primeros tres miembros. El sin_zero restante es naturalmente 0.

estructura in_addr

El tercer miembro de sockaddr_in es una estructura de tipo in_addr, que contiene solo un miembro, como se muestra a continuación:

struct in_addr
{
    in_addr_t s_addr;  //32位的IP地址
};

in_addr_t se define en el archivo de encabezado <netinet / in.h>, que es equivalente a unsigned long y tiene una longitud de cuatro bytes. En otras palabras, s_addr es un número entero y la dirección IP es una cadena, por lo que la función inet_addr () es necesaria para la conversión, por ejemplo:

unsigned long ip = inet_addr("127.0.0.1");

resultado de la operación:

¿Por qué usar sockaddr_in en lugar de sockaddr?

El tipo del segundo parámetro de bind () es sockaddr, pero en el código se usa sockaddr_in y luego se fuerza a sockaddr.

La definición de la estructura sockaddr es la siguiente:

struct sockaddr
{
    sa_family_t sin_family;   //地址族
    char        sa_data[14];  //IP地址和端口号
}

La siguiente figura es una comparación de sockaddr y sockaddr_in (el número entre paréntesis indica el número de bytes ocupados)

La longitud de sockaddr y sockaddr_in es la misma, ambos tienen 16 bytes, pero el área sa_data de sockaddr necesita especificar la dirección IP y el número de puerto al mismo tiempo, como "127.0.0.1:8080". Desafortunadamente, hay no hay una función relacionada para convertir esta cadena en la requerida. Es difícil asignar valores directamente a variables de tipo sockaddr, así que use sockaddr_in en su lugar. La longitud de estas dos estructuras es la misma y no se perderán bytes cuando se transmita el tipo y no habrá más bytes.

Se puede considerar que sockaddr es una estructura general que se puede usar para proteger múltiples tipos de direcciones IP y números de puerto, mientras que sockaddr_in es una estructura que se usa específicamente para almacenar IPv4. Además, está sockaddr_in6, que se utiliza para guardar la dirección IPv6. Su definición es la siguiente:

struct sockaddr_in6
{
    sa_family_t sin6_family;   //IP地址类型,取值为AF_INET6
    in_port_t   sin6_port;     //16位端口号
    uint32_t sin6_flowinfo;    //IPv6流信息
    struct in6_addr sin6_addr; //具体的IPv6地址
    unit32_t sin6_scpoe_id;    //接口范围ID
};

Los valores sockaddr_in y sockaddr_in6 declarados en in.h son los siguientes: 

3.2, función connect ()

La función connect () se utiliza para establecer una conexión y su prototipo es:

int connect(int sock,struct sockaddr *serv_addr,struct sockaddr*serv_addr,socklen_t addrlen);

La descripción de cada parámetro es la misma que la de la función bind ().

4. La función listen () y accept ()

Para el programa del lado del servidor, después de usar bind () para vincular el socket, también necesita usar la función listen () para hacer que el socket ingrese al estado de escucha pasiva, y luego llamar a la función accept () para responder a la función del cliente solicitarlo en cualquier momento.

4.1, función listen ()

La función listen () permite que el socket entre en el estado de escucha pasiva. Su prototipo es:

int listen(int sock,int backlog)

sock es el socket que necesita ingresar al estado de escucha, y backlog es la longitud máxima de la cola de solicitudes

El llamado monitoreo pasivo significa que cuando no hay una solicitud del cliente, el socket está en un estado de "suspensión". Solo cuando se recibe una solicitud del cliente, el socket se "despierta" para responder a la solicitud.

Cola de solicitudes

Cuando el socket está procesando una solicitud de cliente, si entra una nueva solicitud, el socket no se puede procesar, solo se puede colocar en el búfer primero y luego desde el búfer después de que se procese la solicitud actual. Léalo para procesarlo. Si entran nuevas solicitudes, se pondrán en cola en el búfer en orden hasta que se llene. Este búfer se denomina cola de solicitudes.

La longitud del búfer (cuántas solicitudes de cliente se pueden almacenar) se puede especificar mediante el parámetro de backlog de la función listen (), pero no hay un estándar para cuánto es y depende de sus necesidades.

Si el valor de la acumulación se establece en SOMAXCONN, el sistema determina la longitud de la cola de solicitudes. Este valor es generalmente relativamente grande y puede ser de varios cientos o más.

Cuando la cola de solicitudes está llena, no se reciben nuevas solicitudes. Para Linux, el cliente recibirá un error ECONNREFUSED.

Nota: La función listen () solo mantiene el socket en estado de escucha y no recibe la solicitud. Para recibir una solicitud, debe utilizar la función accept () para bloquear la ejecución del proceso hasta que llegue una nueva solicitud.

4.2, función accept ()

Cuando el socket está en estado de escucha, la solicitud del cliente se puede recibir a través de la función acceot (). Su prototipo es:

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

Sus parámetros son los mismos que listen () y connect (); sock es el socket del lado del servidor, addr es la variable de estructura sockaddr_in y addrlen es la longitud del parámetro addr, que se puede obtener mediante sizeof ().

accept () devuelve un nuevo socket para comunicarse con el cliente, addr guarda la dirección IP y el número de puerto del cliente, y sock es el socket del lado del servidor. Cuando se comunique con el cliente más tarde, use este socket recién generado en lugar del socket original del lado del servidor.

5 、 escribir () 和 leer ()

Linux no distingue entre archivos de socket y archivos ordinarios. Use write () para escribir datos en el socket y use read () para leer datos desde el socket.

La comunicación entre dos computadoras es equivalente a la comunicación entre dos sockets. Use write () en el servidor para escribir datos en el socket, y el cliente puede recibirlos, y luego use read () para conectarse desde el socket. Después de leer la palabra , se completa una comunicación.

5.1, función write ()

El prototipo de write () es:

/*
fd:待写入的文件的描述符
buf:待写入的数据的缓冲区地址
nbytes:写入的数据的字节数
ssize_t:signed int
*/
ssize_t write(int  fd,const void *buf,size_t nbytes);

La función write () escribirá los nbytes bytes en el búfer buf en el archivo fd. Si tiene éxito, devuelve el número de bytes escritos, y si falla, devuelve -1.

5.2, función read ()

/*
fd:待读取的文件的描述符
buf:待读取的数据的缓冲区地址
nbytes:读取的数据的字节数
ssize_t:signed int
*/
ssize_t read(int  fd,void *buf,size_t nbytes);

La función read () lee nbytes bytes del archivo fd y los guarda en el búfer buf. Devuelve el número de bytes leídos si tiene éxito (devuelve 0 cuando encuentra el final del archivo) y devuelve -1 si falla .

6, enviar () y recibir ()

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
                       int __flags, __CONST_SOCKADDR_ARG __addr,
                       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
                         int __flags, __SOCKADDR_ARG __addr,
                         socklen_t *__restrict __addr_len);


/* Send a message described MESSAGE on socket FD.
   Returns the number of bytes sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendmsg (int __fd, const struct msghdr *__message,
                        int __flags);

#ifdef __USE_GNU
/* Send a VLEN messages as described by VMESSAGES to socket FD.
   Returns the number of datagrams successfully written or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int sendmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags);
#endif

/* Receive a message as described by MESSAGE from socket FD.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvmsg (int __fd, struct msghdr *__message, int __flags);

#ifdef __USE_GNU
/* Receive up to VLEN messages as described by VMESSAGES from socket FD.
   Returns the number of messages received or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int recvmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags,
                     struct timespec *__tmo);
#endif

7, una implementación simple de servicio y cliente

/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:service.c
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(){
    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);

    //接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    while(1)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

        //向客户端发送数据
        char str[] = "不要艾特我";
        write(clnt_sock, str, sizeof(str));

        //关闭套接字
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:client.cpp
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(){
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //读取服务器传回的数据
    char buffer[40];
    read(sock, buffer, sizeof(buffer)-1);

    printf("Message form server: %s\n", buffer);

    //关闭套接字
    close(sock);

    return 0;
}

resultado de la operación:

Iniciar servidor

el servidor está en estado de supervisión

Inicie el cliente:

En este punto, se completa un código de comunicación de socket simple

 

 

 

 

 

 

 

Supongo que te gusta

Origin blog.csdn.net/weixin_40179091/article/details/113024907
Recomendado
Clasificación