socket 编程基础(网络编程)-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

socket 编程基础(网络编程)

在这里插入图片描述

socket 简介

定义与用途

  • 套接字(socket)是一种在Linux下的进程间通信机制(socket IPC)

  • 可以用于不同主机上的应用程序之间的通信(网络通信),也可以用于同一台主机上的不同应用程序之间的通信

通信模式

  • 通常采用客户端<—>服务器的模式进行通信

  • 多个客户端可以同时连接到服务器,与其进行数据交互

开发接口

  • 内核向应用层提供了socket接口,开发人员只需调用该接口来开发应用程序

抽象层描述

  • Socket是应用层与TCP/IP协议通信的中间软件抽象层,是一组接口

  • 在设计模式中,socket是一个门面模式(Facade Pattern),它将复杂的TCP/IP协议隐藏在socket接口后面,为用户提供简化的接口

简化开发

  • 用户无需深入理解TCP/UDP等复杂的TCP/IP协议,socket已经封装好了这些协议

  • 按照socket接口的规定编程即可,程序自然遵循TCP/UDP标准

广泛应用与移植性

  • 当前网络中的主流程序设计都使用socket进行编程,因为它简单易用且是一个标准(BSD socket)

  • 基于socket接口编写的应用程序可以方便地移植到任何实现BSD socket标准的平台上,如LwIP、Windows、RT-Thread等

socket 编程接口介绍

使用 socket 接口需要在我们的应用程序代码中包含两个头文件

  • #include <sys/types.h> /* See NOTES */

  • #include <sys/socket.h>

socket()函数

  • Socket()函数原型

    • #include <sys/types.h> /* See NOTES */
      #include <sys/socket.h>

int socket(int domain, int type, int protocol);

- domain

	- 指定通信域,选择用于通信的协议族

		-  

		- 协议族描述

	- 常见值

		- AF_INET:用于 TCP/IP 协议

		- AF_INET6:支持 IPv6 时使用

- type

	- 指定套接字的类型

		- 套接字类型描述

	- 常见值

		- SOCK_STREAM:流式套接字,默认协议为 TCP

		- SOCK_DGRAM:数据报套接字,默认协议为 UDP

- protocol

	- 通常设置为 0,表示选择默认协议

	- 特殊情况

		- 在同一域和套接字类型支持多个协议时,可使用此参数选择特定协议

- 返回值:成功时返回 socket 描述符,失败时返回 -1 并设置 errno 变量
  • 功能

    • 类似于 open() 函数,用于创建一个网络通信端点,成功则返回一个网络文件描述符,称为 socket 描述符
  • 资源释放

    • 不再需要时,调用 close() 函数关闭套接字,释放资源
  • 使用示例

    • int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字
      if (0 > socket_fd) {
      perror(“socket error”);
      exit(-1);
      }


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

bind()函数

  • 将一个 IP 地址或端口号与一个套接字进行绑定,即将套接字与地址关联

  • bind()函数原型

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

    • sockfd

      • 指定要绑定的套接字描述符
    • addr

      • 指向一个 struct sockaddr 类型变量的指针,包含要绑定的地址信息

        • struct sockaddr 结构体:
          struct sockaddr {
          sa_family_t sa_family;
          char sa_data[14];
          }

          • 通用 socket 地址结构体,不友好,用户无法直接赋值
        • sockaddr_in 和 sockaddr 是并列的结构(占
          用的空间是一样的)

        • struct sockaddr_in 结构体:
          struct sockaddr_in {
          sa_family_t sin_family; /* 协议族 /
          in_port_t sin_port; /
          端口号 /
          struct in_addr sin_addr; /
          IP 地址 */
          unsigned char sin_zero[8];
          };

          • 更友好的结构体,使用时需类型转换
    • addrlen

      • 指定 addr 所指向的结构体的字节长度
    • 成功返回 0,失败返回 -1 并设置 errno 以提示错误原因

  • 使用场景

    • 服务器端:通常将服务器的套接字绑定到一个众所周知的地址(IP 地址和端口号),以便客户端提前知道

    • 客户端:可以让系统选一个默认地址,不需要显式调用 bind()

  • 使用示例

    • struct sockaddr_in socket_addr;
      memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零

//填充变量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);

//将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));

  • 注意事项

    • 大小端问题:使用 htons 和 htonl 宏定义来避免大小端问题,需包含头文件 <netinet/in.h>

    • 可选调用:bind() 函数不是必须调用的,依赖内核的自动选址机制在客户端应用程序中常见

  • bind() 函数在网络编程中用于将套接字与特定地址绑定,确保服务器端有固定的通信地址,而客户端则可以依赖系统自动分配地址。

listen()函数

  • 让服务器进程进入监听状态,等待客户端的连接请求

    • 通过设置等待连接队列的大小,确保服务器能够有序处理客户端的连接请求,同时避免因请求过多导致系统资源耗尽
  • listen()函数原型

    • int listen(int sockfd, int backlog);

    • sockfd

      • 指定要进入监听状态的套接字描述符
    • backlog

      • 限制内核维护的等待连接队列的大小,防止队列无限增长

      • 描述 sockfd 的等待连接队列能够达到的最大值

      • 当队列满时,新的连接请求会被丢弃,客户端可能会收到连接失败的错误

    • 成功返回 0,失败返回 -1

  • 调用顺序

    • 一般在 bind() 函数之后,accept() 函数之前调用
  • 使用限制

    • 只能在服务器进程中使用:不能在已经连接的套接字(即已经成功执行 connect() 或由 accept() 返回的套接字)上执行 listen()
  • 处理连接请求

    • 队列机制:内核会在进程空间维护一个队列,按照先来后到的顺序处理连接请求

    • 队列上限:backlog 参数设置队列的最大长度,超过此值的请求会被丢弃

  • 错误处理

    • 队列满:当队列满时,客户端的连接请求会被丢弃,客户端可能会收到错误

accept()函数

  • 服务器进入监听状态

    • 服务器调用 listen() 函数后进入监听状态,等待客户端连接请求

    • 使用 accept() 函数获取客户端的连接请求并建立连接

  • accept() 函数原型

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

    • addr: 传出参数,用于返回已连接客户端的 IP 地址和端口号等信息

    • addrlen: 设置为 addr 所指向对象的字节长度

    • 如果对客户端的 IP 地址和端口号不感兴趣,可以将 addr 和 addrlen 置为空指针 NULL

  • 服务器处理流程

    • 步骤 1: 调用 socket() 函数打开套接字

    • 步骤 2: 调用 bind() 函数将套接字与一个端口号和 IP 地址绑定

    • 步骤 3: 调用 listen() 函数进入监听状态,等待客户端连接请求

    • 步骤 4: 调用 accept() 函数处理到来的连接请求

  • accept() 函数的特性与行为

    • accept() 通常只用于服务器应用程序

    • 如果调用 accept() 时没有客户端连接请求,函数会进入阻塞状态,直到有请求到达

    • 当客户端连接请求到达时,accept() 会建立连接并返回一个新的套接字

    • 这个新套接字与 socket() 函数返回的服务器套接字不同

    • 服务器通过新套接字与客户端进行数据交互(如发送或接收数据)

  • accept() 函数的关键点

    • accept() 会创建一个新套接字,与客户端建立连接

    • 这个新套接字代表服务器与客户端的一个连接

    • 如果 accept() 出错,会返回 -1,并设置 errno 指示错误原因

connect()函数

  • 用途

    • 该函数用于客户端应用程序中

    • 客户端调用 connect() 函数将套接字 sockfd 与远程服务器进行连接

  • connect() 函数原型

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

    • sockfd: 套接字描述符

    • addr: 指定待连接服务器的 IP 地址和端口号等信息

    • addrlen: 指定 addr 所指向的 struct sockaddr 对象的字节大小

    • 成功则返回 0,失败返回-1,并设置 errno 以指示错误原因

  • TCP 和 UDP 的不同处理

    • TCP连接

      • 调用 connect() 函数将触发 TCP 连接的握手过程,并最终建立一个 TCP 连接
    • UDP协议

      • 调用 connect() 函数只是在 sockfd 中记录服务器的 IP 地址与端口号,不发送任何数据

发送和接收函数

  • 通过套接字描述符收发数据

    • 客户端使用 socket() 返回的套接字描述符

    • 服务器使用 accept() 返回的套接字描述符

    • 可以调用 read() 或 recv() 函数读取网络数据,调用 write() 或 send() 函数发送数据

  • read() 函数

    • 从文件描述符读取指定字节大小的数据并放入缓冲区

    • 成功返回读取的字节数;返回值小于指定字节数并不意味着错误,可能是因为文件接近结尾或其他原因

    • 出错返回 -1 并设置 errno;如果到达文件末尾,返回 0

    • 套接字描述符也是文件描述符,读取网络数据时参数 fd 就是套接字描述符

  • recv() 函数

    • 客户端和服务器均可调用,用于读取网络数据

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

      • sockfd: 套接字描述符

      • buf: 数据接收缓冲区

      • len: 读取数据的字节大小

      • flags: 控制如何接收数据的标志

        • recv() 与 read() 类似,但可以通过 flags 控制接收行为

        • flags 通常设置为 0;可以设置 MSG_PEEK 查看但不取走数据,设置 MSG_WAITALL 等待全部数据返回

        • recv 函数标志描述

      • 成功返回实际读取的字节数;发送方已结束传输时,返回 0

    • 对于 SOCK_STREAM 类型,接收的数据可能少于指定字节大小;MSG_WAITALL 会阻止这种行为

  • write() 函数

    • 向套接字描述符写入数据

    • 成功返回写入的字节数,失败返回 -1 并设置 errno

  • send() 函数

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

    • 与 write 类似,但可以通过 flags 改变传输数据的处理方式

    • 成功返回并不表示对端进程一定接收到数据;表示数据已发送到网络驱动程序

close()关闭套接字

  • 当不再需要套接字描述符时,可调用 close()函数来关闭套接字,释放相应的资源

IP 地址格式转换函数

转换需求

  • 易于阅读的是点分十进制的 IP 地址形式,如 192.168.1.110、192.168.1.50

  • 这些点分十进制形式实际上是字符串

  • 计算机理解的是二进制形式的 IP 地址

inet_aton、inet_addr、inet_ntoa 函数

  • 这些函数可将一个 IP 地址在点分十进制表示形式和二进制表示形式之间进行转换,这些函数已经废弃了,基本不用这些函数了,但是在一些旧的代码中可能还会看到这些函数

inet_ntop、inet_pton 函数

  • 支持 IPv6 地址

    • 这些函数不仅支持 IPv4 地址,还支持 IPv6 地址

    • 它们可以将二进制 IPv4 或 IPv6 地址转换成点分十进制表示的字符串形式,或者将点分十进制表示的字符串形式转换成二进制 IPv4 或 IPv6 地址

  • 包含头文件

    • 使用 inet_ntop() 和 inet_pton() 函数只需包含 <arpa/inet.h> 头文件
  • inet_pton() 函数

    • 将点分十进制表示的字符串形式转换成二进制 IPv4 或 IPv6 地址

    • int inet_pton(int af, const char *src, void *dst);

      • af: 地址族

        • AF_INET 表示待转换的是 IPv4 地址

        • AF_INET6 表示待转换的是 IPv6 地址

      • src: 指向待转换的字符串

      • dst: 指向存储转换后地址的结构体对象

        • 如果 af 为 AF_INET,则 dst 应指向 struct in_addr 结构体对象

        • 如果 af 为 AF_INET6,则 dst 应指向 struct in6_addr 结构体对象

      • 返回值

        • 成功返回 1(表示已成功转换)

        • 如果 src 不包含有效的地址字符串,返回 0

        • 如果 af 不包含有效的地址族,返回 -1 并设置 errno 为 EAFNOSUPPORT

    • 使用示例

      • #include <stdio.h>
        #include <stdlib.h>
        #include <arpa/inet.h>

#define IPV4_ADDR “192.168.1.222”

int main(void)
{
struct in_addr addr;

inet_pton(AF_INET, IPV4_ADDR, &addr); 
printf("ip addr: 0x%x\n", addr.s_addr); 
exit(0);

}

	- 测试结果

		-  
  • inet_ntop()函数

    • 将二进制 IP 地址转换为点分十进制形式的字符串

    • const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

      • af: 地址族

        • 与 inet_pton() 函数的 af 参数意义相同
      • src: 指向二进制 IP 地址的结构体对象

        • 应依据 af 参数指向 struct in_addr 或 struct in6_addr 结构体对象
      • dst: 指向存储转换后字符串的缓冲区

      • size: 指定缓冲区的大小

      • 返回值

        • 成功时返回 dst 指针

        • 如果 size 太小,返回 NULL 并设置 errno 为 ENOSPC

    • 使用示例

      • #include <stdio.h>
        #include <stdlib.h>
        #include <arpa/inet.h>

int main(void)
{
struct in_addr addr;
char buf[20] = {0};

addr.s_addr = 0xde01a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf)); 
printf("ip addr: %s\n", buf);
exit(0);

}

	- 测试结果

		-  

socket 编程实战

编写服务器程序

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

#define SERVER_PORT     8888    //端口号不能发生冲突,不常用的端口号通常大于5000

int main(void)
{
    struct sockaddr_in server_addr = {0};   // 服务器地址结构体,初始化为0
    struct sockaddr_in client_addr = {0};    // 客户端地址结构体,初始化为0
    char ip_str[20] = {0};  // 用于存储客户端IP地址的字符串
    int sockfd, connfd; // 套接字描述符
    int addrlen = sizeof(client_addr);  // 客户端地址结构体的大小
    char recvbuf[512];  // 接收缓冲区
    int ret;    // 用于存储系统调用返回值

    /* 打开套接字,得到套接字描述符 */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (0 > sockfd) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    /* 将套接字与指定端口号进行绑定 */
    server_addr.sin_family = AF_INET;   // 设置地址族为IPv4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 绑定所有网络接口
    server_addr.sin_port = htons(SERVER_PORT);  // 设置服务器端口号

    ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));    // 绑定套接字
    if (0 > ret) {
        perror("bind error");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    /* 使服务器进入监听状态 */
    ret = listen(sockfd, 50);
    if (0 > ret) {
        perror("listen error");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    /* 阻塞等待客户端连接 */
    connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
    if (0 > connfd) {
        perror("accept error");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("有客户端接入...\n");
    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
    printf("客户端主机的IP地址: %s\n", ip_str);
    printf("客户端进程的端口号: %d\n", client_addr.sin_port);

    /* 接收客户端发送过来的数据 */
    for ( ; ; ) {

        // 接收缓冲区清零
        memset(recvbuf, 0x0, sizeof(recvbuf));

        // 读数据
        ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
        if(0 >= ret) {
            perror("recv error");
            close(connfd);
            break;
        }

        // 将读取到的数据以字符串形式打印出来
        printf("from client: %s\n", recvbuf);

        // 如果读取到"exit"则关闭套接字退出程序
        if (0 == strncmp("exit", recvbuf, 4)) {
            printf("server exit...\n");
            close(connfd);
            break;
        }
    }

    /* 关闭套接字 */
    close(sockfd);
    exit(EXIT_SUCCESS);
}

  • 程序的流程

    • 调用 socket()函数打开套接字,得到套接字描述符

    • 调用 bind()函数将套接字与 IP 地址、端口号进行绑定

    • 调用 listen()函数让服务器进程进入监听状态

    • 调用 accept()函数获取客户端的连接请求并建立连接

    • 调用 read/recv、write/send 与客户端进行通信

    • 调用 close()关闭套接字

  • SERVER_PORT 宏指定了本服务器绑定的端口号,这里我们将端口号设置为 8888,端口不能与其它服务器的端口号发生冲突,不常用的端口号通常大于 5000

编写客户端程序

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

#define SERVER_PORT		8888          	//服务器的端口号
#define SERVER_IP   	"192.168.2.192"	//服务器的IP地址

int main(void)
{
    struct sockaddr_in server_addr = {0};   // 服务器地址结构体,初始化为0
    char buf[512];  // 数据缓冲区
    int sockfd; // 套接字描述符
    int ret;    // 用于存储系统调用返回值

    /* 打开套接字,得到套接字描述符 */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (0 > sockfd) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    /* 调用connect连接远端服务器 */
    server_addr.sin_family = AF_INET;   // 设置地址族为IPv4
    server_addr.sin_port = htons(SERVER_PORT);  //端口号
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP地址

    ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (0 > ret) {
        perror("connect error");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("服务器连接成功...\n\n");

    /* 向服务器发送数据 */
    for ( ; ; ) {
        // 清理缓冲区
        memset(buf, 0x0, sizeof(buf));

        // 接收用户输入的字符串数据
        printf("Please enter a string: ");
        fgets(buf, sizeof(buf), stdin);

        // 将用户输入的数据发送给服务器
        ret = send(sockfd, buf, strlen(buf), 0);
        if(0 > ret){
            perror("send error");
            break;
        }

        //输入了"exit",退出循环
        if(0 == strncmp(buf, "exit", 4))
            break;
    }

    close(sockfd);
    exit(EXIT_SUCCESS);
}

  • 连接上面所实现的服务器,连接成功之后向服务器发送数据,发送的数据由用户输入

  • 程序的流程

    • 调用 socket()函数打开套接字,得到套接字描述符

    • 调用connect连接远端服务器

      • 设置地址族为IPv4

      • 设置服务器的端口号

      • 将服务器的IP地址转换为二进制形式

    • 向服务器发送数据

      • 清理缓冲区

      • 接收用户输入的字符串数据

      • 将用户输入的数据发送给服务器

      • 输入了"exit",退出循环

    • 调用 close()关闭套接字

  • SERVER_IP 和 SERVER_PORT 指的是服务器的 IP 地址和端口号,服务
    器的 IP 地址根据实际情况进行设置,服务器应用程序示例代码中我们绑定的端口号为 8888,所以在客户端应用程序中我们也需要指定 SERVER_PORT 为 8888

编译测试

  • 以将服务器程序运行在开发板上,而将客户端应用程序运行在 Ubuntu 系统为例

  • 流程

    • 编译服务器应用程序和客户端应用程序

      • 用gcc编译client

        • gcc -o client socket_client.c
      • 用poky编译server

        • ${CC} -o server socket_server.c
    • 将服务器执行文件拷贝到开发板

    • 先执行服务器应用程序

    • 执行客户端应用程序

    • 服务器监测到客户端连接

    • 输入字符串信息

    • 服务器接收到客户端发送的信息

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木木不迷茫(˵¯͒¯͒˵)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值