嵌入式Linux C应用编程——socket网络编程(二、socket应用)

Socket 网络编程基础(二)------ Socket 应用

一、Socket 简介

Socket(套接字) 是 Linux 下的一种进程间通信机制(socket IPC),可实现不同主机或同一主机上不同应用程序之间的通信。

  • 采用 客户端-服务器(Client-Server) 模式,多个客户端可同时连接服务器进行数据交互。
  • Socket 的本质 :是应用层与 TCP/IP 协议通信的中间软件抽象层 ,属于门面模式------将复杂的 TCP/IP 协议隐藏在简单接口后面,开发者无需深入理解 TCP/UDP 细节即可编程。
  • BSD Socket 标准:跨平台性强,可在 Linux、LwIP、Windows、RT-Thread 等多平台移植。

二、核心 API 详解

1. 头文件

c 复制代码
#include <sys/types.h>
#include <sys/socket.h>

2. socket() ------ 创建套接字

c 复制代码
int socket(int domain, int type, int protocol);

功能 :类似于 open(),创建一个网络通信端点,返回 socket 描述符 (本质是文件描述符)。调用成功后通过该描述符进行后续读写操作;不再使用时调用 close() 释放。

参数

  • domain(协议族):指定通信域,选择协议族。

    • AF_INET --- IPv4 Internet 协议(最常用
    • AF_INET6 --- IPv6 Internet 协议
    • AF_UNIX / AF_LOCAL --- 本地通信
    • AF_PACKET --- 底层包接口
  • type(套接字类型):指定套接字的通信语义。

    • SOCK_STREAM --- TCP 协议:提供有序、可靠、双向、基于连接的字节流,能保证数据正确传送到对方,可支持带外数据传输
    • SOCK_DGRAM --- UDP 协议:固定长度的、无连接的、不可靠的报文传递
    • SOCK_RAW --- 原始套接字:允许应用程序访问网络层的原始数据包
    • SOCK_SEQPACKET --- 固定长度的、有序的、可靠的、面向连接的报文传递
  • protocol :通常设为 0,表示为给定的通信域和套接字类型选择默认协议。

    • AF_INET + SOCK_STREAM → 默认 TCP
    • AF_INET + SOCK_DGRAM → 默认 UDP

返回值 :成功返回 socket 描述符(非负整数);失败返回 -1 并设置 errno

示例

c 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
    perror("socket error");
    exit(-1);
}
close(sockfd);

3. bind() ------ 绑定地址

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能 :将一个 IP 地址和端口号与一个套接字进行绑定(关联)。服务器必须调用 ,以便客户端知道服务器的地址(固定的 IP + 端口);客户端通常不需要显式调用,由内核在 connect() 时自动分配一个临时端口。

参数

  • sockfdsocket() 返回的套接字描述符。

  • addr :指向 struct sockaddr 类型变量的指针,包含待绑定的 IP 地址和端口号。实际编程中通常使用 struct sockaddr_in (与 struct sockaddr 空间兼容,对用户更友好):

    c 复制代码
    struct sockaddr_in {
        sa_family_t sin_family;   // 协议族,如 AF_INET
        in_port_t sin_port;       // 端口号(网络字节序)
        struct in_addr sin_addr;  // IP 地址(网络字节序)
        unsigned char sin_zero[8]; // 保留,填充为 0
    };
    • sin_family --- 必须与 socket()domain 一致,通常为 AF_INET
    • sin_port --- 端口号,需用 htons() 转换为主机字节序到网络字节序
    • sin_addr.s_addr --- IP 地址,常用 htonl(INADDR_ANY)(绑定任意本地 IP)
  • addrlenaddr 所指向的结构体字节大小,通常为 sizeof(struct sockaddr_in)

返回值 :成功返回 0;失败返回 -1 并设置 errno

htons() / htonl() 是字节序转换宏,将主机字节序 转换为网络字节序 ,需包含 <netinet/in.h>INADDR_ANY 表示绑定所有本地网卡(值为 0.0.0.0)。
什么是主机字节序和网络字节序?

主机字节序取决于 CPU 架构:

  • 小端(Little-Endian) :低地址存低位字节。x86 架构(如 PC)使用小端。例如 0x1234 在内存中为 [0x34, 0x12]
  • 大端(Big-Endian):低地址存高位字节。ARM 架构可配置,但 Linux 下通常也设为小端

网络字节序 :TCP/IP 协议规定,所有多字节整数在网络上传输时必须使用大端,以确保不同架构的机器能正确解析对方的数据。

端口号(16 位)和 IP 地址(32 位)都是多字节整数,所以在填充 sin_portsin_addr.s_addr 时必须用 htons() / htonl() 将主机字节序转为网络字节序。对应的反向转换是 ntohs() / ntohl()(网络序 → 主机序),用于解析收到的地址数据。
为什么用 INADDR_ANY(0.0.0.0)而不是服务器真实 IP?

INADDR_ANY 表示绑定所有本地网络接口 。服务器可能有多个网卡(如 eth0: 192.168.1.100、wlan0: 10.0.0.1):

  • htonl(INADDR_ANY) : 监听所有网卡上的连接请求,无论客户端连哪个 IP 都能收到
  • inet_addr("192.168.1.100") : 只监听该特定网卡,其他网卡上的连接会被忽略

在嵌入式开发中,开发板的 IP 常由 DHCP 动态分配,用 INADDR_ANY 更灵活,无论分配到什么 IP 都不会影响服务器接收连接。
为什么客户端不需要 bind?

客户端其实也需要地址,否则服务器收到请求后不知道该把响应数据发往哪里。区别在于:

  • 服务器 的地址(IP + 端口)必须是固定、众所周知的 ,这样客户端才能找到它,所以需要显式 bind()
  • 客户端 的地址无需提前固定,内核会在调用 connect() 时自动从本地选择一个空闲的临时端口 (通常 > 32768)分配给客户端套接字,同时客户端的 IP 地址由网络接口配置决定,所以不需要显式 bind()

服务器在 accept() 返回时,可以通过传出参数 addr 获取到客户端的自动分配地址,从而知道数据该发回给谁。

示例

c 复制代码
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(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));

4. listen() ------ 监听连接

c 复制代码
int listen(int sockfd, int backlog);

功能仅服务器使用 ,使服务器进程进入监听状态,等待客户端的连接请求。通常在 bind() 之后、accept() 之前调用。无法在一个已连接的套接字上执行 listen()

参数

  • sockfdsocket() 返回的套接字描述符。
  • backlog :等待连接队列能够达到的最大值。当服务器进程正处理客户端的连接请求时,若有其他客户端同时请求连接,内核将这些请求放入队列中,按先来后到顺序处理。backlog 告诉内核使用该值作为队列上限。队列满时,新客户端会收到连接失败错误。

返回值 :成功返回 0;失败返回 -1 并设置 errno


5. accept() ------ 接受连接

c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能仅服务器使用 ,获取客户端的连接请求并建立连接。accept() 返回一个新的套接字 ,该套接字与调用 connect() 的客户端建立了连接,服务器后续通过这个新套接字与该客户端进行数据交互。

socket() 返回的是服务器监听套接字(一直监听),而 accept() 返回的是代表与客户端之间连接的新套接字。

参数

  • sockfdsocket() 返回的服务器套接字描述符。
  • addr传出参数 ,指向 struct sockaddr 的指针,用于返回已连接客户端的 IP 地址和端口号等信息。若不关心可设为 NULL
  • addrlen :传入/传出参数,应初始化为 addr 所指向对象的字节长度,函数返回时会更新为实际长度。若 addrNULL 则此参数也应置 NULL

返回值 :成功返回新的套接字描述符(非负整数);失败返回 -1 并设置 errno

服务器标准流程

复制代码
socket() → bind() → listen() → accept() → read/recv、write/send → close()

6. connect() ------ 发起连接

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能客户端使用 ,将套接字 sockfd 与远程服务器进行连接。

  • 对于 TCP 连接 :调用该函数将触发三次握手过程,最终建立一个 TCP 连接。
  • 对于 UDP 协议 :调用该函数只是在 sockfd 中记录服务器的 IP 地址与端口号,不发送任何数据。

参数

  • sockfdsocket() 返回的客户端套接字描述符。
  • addr :指向 struct sockaddr 的指针,指定待连接的服务器的 IP 地址和端口号。
  • addrlenaddr 所指向的结构体字节大小。

返回值 :成功返回 0;失败返回 -1 并设置 errno


7. 数据收发函数

read() ------ 从套接字读取数据

c 复制代码
ssize_t read(int fd, void *buf, size_t count);

功能 :从一个文件描述符中读取指定字节大小的数据并放入到指定缓冲区。套接字描述符也是文件描述符,所以可直接使用 read() 读取网络数据。

参数

  • fd:套接字描述符。
  • buf:指向数据接收缓冲区的指针。
  • count:期望读取的字节数。

返回值 :成功返回实际读取到的字节数(可能小于 count);已到达末尾返回 0;出错返回 -1 并设置 errno


recv() ------ 从套接字接收数据(带标志)

c 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

功能 :与 read() 类似,从套接字描述符中读取网络数据,但可通过 flags 参数指定标志来控制接收行为。

参数

  • sockfd:套接字描述符。
  • buf:指向数据接收缓冲区的指针。
  • len:读取数据的字节大小。
  • flags :控制接收方式的标志,通常设为 0。可选标志:
    • MSG_DONTWAIT --- 启动非阻塞操作
    • MSG_OOB --- 如果协议支持,获取带外数据
    • MSG_PEEK --- 窥探数据但不取走(再次调用仍可读到相同数据)
    • MSG_WAITALL --- 等待所有数据就绪才返回(仅 SOCK_STREAM
    • MSG_TRUNC --- 即使数据包被截断,也返回数据包的长度
    • MSG_ERRQUEUE --- 接收错误信息作为辅助数据

返回值 :成功返回实际读取到的字节数;对方已关闭返回 0;出错返回 -1 并设置 errno


write() ------ 向套接字发送数据

c 复制代码
ssize_t write(int fd, const void *buf, size_t count);

功能:向套接字描述符中写入数据(发送网络数据)。

参数

  • fd:套接字描述符。
  • buf:指向待发送数据的缓冲区。
  • count:写入的字节数。

返回值 :成功返回写入的字节数;失败返回 -1 并设置 errno


send() ------ 向套接字发送数据(带标志)

c 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能 :与 write() 类似,向套接字描述符发送数据,但可通过 flags 参数指定标志来控制发送行为。

send() 成功返回仅表示数据已无错误地发送到网络驱动程序上,并不代表连接的另一端已接收数据。

参数

  • sockfd:套接字描述符。
  • buf:指向待发送数据的缓冲区。
  • len:发送数据的字节大小。
  • flags :控制发送方式的标志,通常设为 0。可选标志:
    • MSG_DONTWAIT --- 允许非阻塞操作
    • MSG_NOSIGNAL --- 在写无连接的套接字时不产生 SIGPIPE 信号
    • MSG_OOB --- 如果协议支持,发送带外数据
    • MSG_CONFIRM --- 提供链路层反馈以保持地址映射有效
    • MSG_DONTROUTE --- 勿将数据包路由出本地网络
    • MSG_MORE --- 延迟发送数据包,允许写更多数据

返回值 :成功返回实际发送的字节数;失败返回 -1 并设置 errno


8. close() ------ 关闭套接字

c 复制代码
int close(int fd);

功能 :当不再需要套接字描述符时,调用 close() 关闭套接字,释放相应资源。

参数

  • fd :要关闭的套接字描述符(可以是 socket() 返回的监听套接字,也可以是 accept() 返回的连接套接字)。

返回值 :成功返回 0;失败返回 -1 并设置 errno


三、IP 地址格式转换

传统函数(已废弃,仅旧代码中可见)

函数 功能
inet_aton() 点分十进制字符串 → 二进制
inet_addr() 点分十进制字符串 → 二进制
inet_ntoa() 二进制 → 点分十进制字符串

推荐函数(支持 IPv4 和 IPv6)

inet_pton() ------ 点分十进制字符串 → 二进制地址

c 复制代码
int inet_pton(int af, const char *src, void *dst);

功能:将点分十进制表示的字符串形式的 IP 地址转换为二进制的 IPv4 或 IPv6 地址。

参数

  • af :指定地址族。
    • AF_INET --- 转换 IPv4 地址,dst 应为 struct in_addr *
    • AF_INET6 --- 转换 IPv6 地址,dst 应为 struct in6_addr *
  • src :指向点分十进制字符串(如 "192.168.1.222")。
  • dst:指向存放转换后二进制地址的结构体对象。

返回值 :成功返回 1src 是无效地址返回 0af 无效返回 -1 并设置 errnoEAFNOSUPPORT

示例

c 复制代码
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.222", &addr);
printf("ip addr: 0x%x\n", addr.s_addr);
// 输出: 0xde01a8c0

inet_ntop() ------ 二进制地址 → 点分十进制字符串

c 复制代码
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

功能 :执行与 inet_pton() 相反的操作,将二进制 IP 地址转换为点分十进制形式的字符串。

参数

  • af :指定地址族(AF_INETAF_INET6)。
  • src :指向二进制地址结构体(struct in_addr *struct in6_addr *)。
  • dst:指向存放转换后点分十进制字符串的缓冲区。
  • size :指定 dst 缓冲区的大小。

返回值 :成功返回 dst 指针;size 太小返回 NULL 并设置 errnoENOSPC

示例

c 复制代码
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);
// 输出: 192.168.1.222

in_addr这个结构体实际上只有一个参数,所以 &addr 实际上就是 addr.s_addr


四、编程实战 ------ TCP 服务器与客户端

服务器程序流程

复制代码
① socket()  --- 创建 TCP 套接字
② bind()    --- 绑定端口和 IP 地址
③ listen()  --- 进入监听状态
④ accept()  --- 阻塞等待并接受客户端连接
⑤ 通信循环  --- recv() 接收数据 / send() 发送数据
⑥ close()   --- 关闭套接字

服务器示例(端口 8888):

c 复制代码
#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

int main(void)
{
    struct sockaddr_in server_addr = {0};
    struct sockaddr_in client_addr = {0};
    char ip_str[20] = {0};
    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;
    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);
}

端口选择:端口不能与其它服务冲突,不常用的端口号通常大于 5000。


客户端程序流程

复制代码
① socket()   --- 创建 TCP 套接字
② connect()  --- 连接远端服务器
③ 交互循环   --- 用户输入 → send() 发送数据
④ close()    --- 关闭套接字

客户端示例

c 复制代码
#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.1.150"  // 根据实际服务器 IP 修改

int main(void)
{
    struct sockaddr_in server_addr = {0};
    char buf[512];
    int sockfd;
    int ret;

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

    /* ② 连接服务器 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
    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);
}

编译与测试

bash 复制代码
# 编译服务器(ARM 交叉编译,用于开发板)
arm-linux-gnueabihf-gcc -o server socket_server.c

# 编译客户端(PC 端 Ubuntu)
gcc -o client socket_client.c

测试步骤

  1. server 拷贝到开发板 /home/root/ 目录
  2. 开发板运行 ./server(进入监听状态)
  3. Ubuntu 运行 ./client(连接服务器)
  4. 客户端输入字符串,服务器端打印接收到的内容
  5. 客户端输入 exit,双方断开连接

值得注意的是,开发板的IP,也就是服务器的地址是动态分配的,有可能会发生改变,此时客户端的代码中的IP就需要修改后重新编译运行才能连接上。

否则可能出现问题:


相关推荐
Javenwww3 天前
嵌入式Linux C应用编程——Framebuffer应用编程
嵌入式linux c应用编程
Javenwww4 天前
嵌入式Linux C应用编程——tslib库的使用
嵌入式linux c应用编程
Javenwww5 天前
嵌入式Linux C应用编程——GPIO
嵌入式linux c应用编程