liunx嵌入式基础:socket通信

如果多台通过网线连接的电脑,它们之间需要进行通信,那有什么方式呢?那就是Socket通信了

什么是Socket通信?

Socket(套接字)是网络通信的编程接口,用于不同设备之间的数据传输。它基于IP地址 + 端口号的组合,允许应用程序通过网络发送和接收数据。

Socket通信的核心是客户端-服务器模型:

  1. 服务器:监听某个端口,等待客户端连接。

  2. 客户端:主动向服务器的IP和端口发起连接请求。

  3. 建立连接后,双方可以互相发送数据。

生活中的例子:电话通话

  1. 服务器(10086):

    1. 10086服务器会一直监听(等待来电)。

    2. 当有人拨打10086(IP+端口)。

    3. 客服接听(Accept连接),建立通话通道。

  2. 客户端(拨号方):

    1. 你拿起手机,输入10086(目标IP+端口)。

    2. 按下拨号键(发起连接请求)。

    3. 对方接听后,双方可以说话(数据交换)。

  3. 通信结束:

    1. 一方挂断电话(关闭Socket连接)

项目中什么情况下会用到Socket通信?

Socket一般用于网络通信,可以用来创建TCP UDP的客户端或服务器,在我们项目中需要用HTTP请求服务器天气信息,而HTTP是基于TCP的,也就是我们可以通过Socket去实现HTTP请求,虽然现在项目一般不需要我们去手写HTTP请求,但是我们了解这块的原理,以后遇到HTTP、MQTT、Websocket这些协议时,都会更加清晰。

那Socket和IP、TCP、UDP、HTTP等协议,他们之间的关系是怎么样的呢?

大家可以看下下方的TCP/IP 5层网络模型,我们可以从下往上看,了解每一层的作用是什么。

复制代码
+-----------------------+
| HTTP FTP MQTT(应用层)   |  ← 基于文本/二进制的应用协议(如GET /index.html)
+-----------------------+
|    TCP UDP(传输层)      |  ← 可靠连接、流量控制、数据分段(端口号:80/443)
+-----------------------+
|      IP (网络层)        |  ← 寻址和路由(如192.168.1.1 → 公网IP)
+-----------------------+
|    Socket (编程接口)    |  ← 操作系统提供的API(如socket()、bind()、send())
+-----------------------+
|物理层(有线网络/WiFi模块) |  ← 实际数据传输(如WiFi模组)
+-----------------------+

TCP和UDP有什么区别?

TCP和UDP是两种计算机网络传输协议,用于在网络中传输数据。

TCP(传输控制协议)是一种需要连接的,可靠的协议,它确保数据在发送和接收之间的完整性和顺序。它通过使用序列号、确认和重传机制来实现这一点。TCP在需要可靠传输的应用程序中使用,例如网页浏览器、电子邮件和文件传输。

UDP(用户数据报协议)是一种无连接的,不可靠的协议,它不保证数据的完整性或顺序。UDP更适合需要快速传输数据的应用程序,例如视频和音频流媒体、在线游戏和DNS查询。UDP在传输数据时速度更快,因为它不需要进行确认或重传机制,但是在网络不稳定或拥塞时,会导致数据包丢失或错误。

创建套接字

cs 复制代码
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
socket() 为通讯创建一个端点,为套接字返回一个文件描述符。 

参数:
domain 为创建的套接字指定协议集。 例如:
AF_INET 表示IPv4网络协议
AF_INET6 表示IPv6
type 参数指定套接字的类型
常见的类型有 SOCK_STREAM(流套接字/TCP 套接字)和 SOCK_DGRAM(数据报套接字/UDP套接字)等。
protocol 指定实际使用的传输协议。 
最常见的就是IPPROTO_TCP、IPPROTO_SCTP、IPPROTO_UDP、IPPROTO_DCCP。
如果该项为0的话,即根据选定的domain和type选择使用缺省协议。
返回值:
socket 函数的返回值为新创建的套接字的文件描述符,如果创建失败则返回 -1。

示例:
创建TCP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

设置服务器监听/客户端连接的IP和端口信息

cs 复制代码
#include<netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family; /* 套接字协议族 IPV4  AF_INET */
    in_port_t      sin_port;   /* 端口号 例如 8888*/
    struct in_addr sin_addr;   /* IP 地址 */
};
struct in_addr {
    uint32_t       s_addr;     /* IP地址 */
};

示例:
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET; //IPV4
serv_addr.sin_port = htons(8080); //端口设置
// 将 IP 地址从字符串转换为二进制形式并存入serv_addr.sin_addr
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)

htons说明:
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
作用:将 16位无符号整数 从 主机字节序 转换为 网络字节序
"htons" 是 "host to network short" 的缩写
字节序背景:
主机字节序:可能是大端序(Big-Endian)或小端序(Little-Endian),取决于CPU架构
网络字节序:TCP/IP协议规定使用大端序(Big-Endian)

inet_pton说明:
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
作用:将 IP 地址从字符串转换为二进制形式
参数类型说明
afint地址族 (Address Family):
• AF_INET - IPv4地址
• AF_INET6 - IPv6地址
srcconst char*源字符串(IP字符串:"127.0.0.1")
dstvoid*目标缓冲区:
• IPv4: 指向struct in_addr
• IPv6: 指向struct in6_addr
返回值:
返回1转换成功,其它值转换失败

connect,用于客户端,连接服务器

cs 复制代码
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数参数说明:
sockfd:通过socket()创建的有效套接字描述符
addr:参数 addr 是指向目标服务器地址的指针,通常是一个 sockaddr_in(IPV4 结构体) 或 sockaddr_in6(IPV6 结构体) 结构体类型的指针。
addrlen:目标服务器地址的长度。
通常使用sizeof(struct sockaddr_in)或sizeof(struct sockaddr_in6)
返回值:
0连接成功建立
-1连接失败,错误码存储在errno中

示例:
connect(sock, (struct sockaddr *)&serv_addr, sizeof(sockaddr_in));

bind,用于服务器端,绑定监听地址和端口

cpp 复制代码
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind 函数的作用是将一个 socket 绑定到一个具体的地址和端口上。这个地址和端口可以是任意的本地或远程地址和端口,但必须是未被其他 socket 使用的地址和端口。

参数:
sockfd:表示要绑定的 socket 描述符。
addr:表示要绑定的地址信息,是一个指向 struct sockaddr 类型的指针。
addrlen:表示地址信息的长度,通常使用 sizeof 运算符获取。

返回值
成功时返回 0
失败时返回 -1

示例
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));

listen,用于服务器端,将套接字设置为监听模式,使其能够接收来自客户端的连接请求。

cs 复制代码
#include <sys/socket.h>  
int listen(int sockfd, int backlog);
将套接字设置为监听模式,使其能够接收来自客户端的连接请求。
参数:
sockfd 套接字文件描述符
backlog:决定监听队列大小

函数成功执行时返回 0,失败返回 -1

accept,用于服务器端,接受客户端的连接请求并创建一个新的套接字来处理与该客户端的通信。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
用于接受客户端的连接请求并创建一个新的套接字来处理与该客户端的通信。

sockfd:表示监听套接字的文件描述符。
addr:表示传出参数,指向客户端地址的结构体指针。
addrlen:表示传入传出参数,传入的是指向客户端地址结构体的长度,传出的是客户端地址结构体的实际长度。

如果 accept() 函数调用成功,则返回一个新的套接字描述符,这个套接字描述符用于和客户端进行通信。
如果调用失败,则返回 -1。

当一个客户端发起连接请求时,服务器端的 accept() 函数会从连接请求队列中取出一个连接请求,然后创建一个新的套接字用于和该客户端进行通信

连接成功后,通过write或read函数进行读写,使用完成使用close函数关闭。

TCP服务器端

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    char buffer[BUFFER_SIZE] = {0};
    const char *message = "Hello from server";

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        printf("socket failed");
        return 1;
    }

    // 绑定监听地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        printf("bind failed");
        close(server_fd);
        return 1;
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        printf("listen failed");
        close(server_fd);
        return 1;
    }

    printf("Server listening on port %d\n", PORT);

    // 接受连接
    if ((new_socket = accept(server_fd, NULL, NULL)) < 0) {
        printf("accept failed");
        close(server_fd);
        return 1;
    }

    printf("Connection accepted\n");

    // 读取数据
    read(new_socket, buffer, BUFFER_SIZE);
    printf("Received from client: %s\n", buffer);

    // 发送数据
    write(new_socket, message, strlen(message));

    // 关闭套接字
    close(new_socket);
    close(server_fd);

    return 0;
}

TCP客户端

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    const char *message = "Hello from client";

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("socket failed");
        return 1;;
    }
    // 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IP 地址从字符串转换为二进制形式并存入serv_addr.sin_addr
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("inet_pton failed");
        close(sock);
        return 1;;
    }

    // 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("connect failed");
        close(sock);
        return 1;;
    }

    printf("Connected to server\n");

    // 发送数据
    write(sock, message, strlen(message));

    // 读取数据
    read(sock, buffer, BUFFER_SIZE);
    printf("Received from server: %s\n", buffer);

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

    return 0;
}

如果我们实现一个HTTP请求,已知网站IP是23.54.155.77

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 80     // HTTP默认端口
#define BUFFER_SIZE 4096

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("socket failed");
        return 1;
    }
    // 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IP 地址从字符串转换为二进制形式
    if (inet_pton(AF_INET, "23.54.155.77", &serv_addr.sin_addr) <= 0) {
        printf("inet_pton failed");
        close(sock);
        return 1;
    }
    // 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("connect failed");
        close(sock);
        return 1;
    }

    printf("Connected to www.example.com\n");
    // 构造HTTP请求(新增)
    const char *http_request = 
        "GET / HTTP/1.1\r\n"
        "Host: www.example.com\r\n"
        "Connection: close\r\n"
        "\r\n";
    // 发送HTTP请求
    if (write(sock, http_request, strlen(http_request)) < 0) {
        printf("write failed");
        close(sock);
        return 1;
    }

    // 读取服务器响应
    ssize_t bytes_received;
    char buffer[BUFFER_SIZE] = {0};
    printf("Response from server:\n");
    while ((bytes_received = read(sock, buffer, BUFFER_SIZE - 1)) > 0) {
        buffer[bytes_received] = '\0';  // 确保字符串终止
        printf("%s", buffer);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    // 关闭套接字
    close(sock);

    return 0;
}

又或者启动一个Web服务器,让其它客户端可以通过Web服务与设备进行通信

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>  

#define PORT 8080
#define BACKLOG 5
#define BUFFER_SIZE 1024

const char *html_response = 
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"    <title>Xiaozhi Server</title>\n"
"</head>\n"
"<body>\n"
"    <h1>Welcome to Xiaozhi Web Server!</h1>\n"
"    <p>This page is served by a Linux program.</p>\n"
"    <p>Current time: %s</p>\n"
"</body>\n"
"</html>\n";

void handle_client(int client_fd) {
    char buffer[BUFFER_SIZE] = {0};
    char response[BUFFER_SIZE * 2] = {0};
    time_t now;
    char time_str[50];
    // 读取客户端请求(简单起见,我们不处理请求内容)
    read(client_fd, buffer, BUFFER_SIZE);
    printf("Received request:\n%s\n", buffer);
    // 获取当前时间
    time(&now);
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&now));
    // 构造HTTP响应
    snprintf(response, sizeof(response), html_response, time_str);
    // 发送HTTP响应
    write(client_fd, response, strlen(response));
    // 关闭连接
    close(client_fd);
    printf("Response sent, connection closed.\n");
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr;
    socklen_t client_len;
    
    // 创建socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("socket fail\n");
        exit(EXIT_FAILURE);
    }
    // 设置socket选项(避免地址占用错误)
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
        printf("setsockopt fail\n");
        exit(EXIT_FAILURE);
    }
    // 绑定地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        printf("bind fail\n");
        exit(EXIT_FAILURE);
    }
    
    // 开始监听
    if (listen(server_fd, BACKLOG) == -1) {
        printf("listen fail\n");
        exit(EXIT_FAILURE);
    }
    
    printf("Server started on port %d\n", PORT);
    printf("Waiting for connections...\n");
    
    // 主循环,接受客户端连接
    while (1) {
        if ((client_fd = accept(server_fd, NULL, NULL)) == -1) {
            printf("accept fail\n");
            continue;
        }
        // 处理客户端请求
        handle_client(client_fd);
    }
    // 关闭服务器socket(实际上不会执行到这里)
    close(server_fd);
    return 0;
}

UDP 通信流程

recvfrom指定地址接收函数

cs 复制代码
#include <sys/socket.h>
ssize_t recvfrom(
    int sockfd,                // Socket 文件描述符
    void *buf,                 // 接收数据的缓冲区
    size_t len,                // 缓冲区大小
    int flags,                 // 控制标志(通常填 0-默认阻塞)
    struct sockaddr *src_addr, // 发送方的地址信息(输出参数)
    socklen_t *addrlen         // 地址结构体长度(输入输出参数)
);
参数:
sockfd:UDP Socket文件描述符
buf:存放接收数据的缓冲区
len:缓冲区大小
flags:控制选项-0(默认阻塞)-MSG_DONTWAIT(非阻塞)
src_addr:存放发送方的 IP + 端口,如果不需要发送方信息,可以传 NULL
addrlen:src_addr结构体的大小

返回值
成功:返回接收到的字节数(> 0)
失败:返回 -1

使用示例:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in sender_addr;
socklen_t sender_len = sizeof(sender_addr);
// 阻塞接收数据
char buffer[1024];
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,  (struct sockaddr *)&sender_addr, &sender_len);
if (n == -1) {
    printf("recvfrom failed");
} else {
    buffer[n] = '\0';  // 确保字符串终止
    printf("Received %d bytes from %s:%d\n", n, 
           inet_ntoa(sender_addr.sin_addr), ntohs(sender_addr.sin_port));
}

sendto指定地址发送函数

cpp 复制代码
#include <sys/socket.h>
ssize_t sendto(
    int sockfd,                      // Socket 文件描述符
    const void *buf,                 // 要发送的数据
    size_t len,                      // 数据长度
    int flags,                       // 控制标志(通常填 0)
    const struct sockaddr *dest_addr, // 目标地址(IP + 端口)
    socklen_t addrlen                // 目标地址结构体长度
);
参数:
sockfd:UDP Socket文件描述符
buf:要发送的数据
len:数据长度
flags:控制选项,-0(默认阻塞)-MSG_DONTWAIT(非阻塞)
dest_addr:目标地址
addrlen:目标地址结构体长度
返回值
成功:返回实际发送的字节数(通常等于 len)
失败:返回 -1

示例:
初始化服务器信息:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr = {
    .sin_family = AF_INET,
    .sin_port = htons(8080),
    .sin_addr.s_addr = inet_addr("127.0.0.1")
};
// 发送数据到UDP服务器
const char *message = "Hello, UDP Server!";
sendto(sockfd, message, strlen(message), 0,(struct sockaddr *)&server_addr, sizeof(server_addr));

UDP服务端

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    
    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        printf("socket failed");
        return 1;
    }
    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        printf("bind failed");
        close(server_fd);
        return 1;
    }
    printf("UDP Server listening on port %d\n", PORT);

    // 读取数据
    char buffer[BUFFER_SIZE] = {0};
    int n = recvfrom(server_fd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    printf("Received from client: %s\n", buffer);
    // 发送数据
    const char *message = "Hello from server";
    sendto(server_fd, message, strlen(message), 0, (struct sockaddr *)&address, addrlen);

    // 关闭套接字
    close(server_fd);
    return 0;
}

UDP客户端

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    
    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        printf("socket failed");
        return 1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IP 地址从字符串转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("inet_pton failed");
        close(sock);
        return 1;
    }

    // 发送数据
    const char *message = "Hello from client";
    sendto(sock, message, strlen(message), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    // 读取数据
    char buffer[BUFFER_SIZE] = {0};
    int n = recvfrom(sock, buffer, BUFFER_SIZE, 0, NULL, NULL);
    printf("Received from server: %s\n", buffer);

    // 关闭套接字
    close(sock);
    return 0;
}

总结:

其实现在开发基于socket api进行应用开发已经比较少了,网络上已经有非常多比较完整的库,例如你需要实现http,你可以使用curl库,需要实现MQTT,你可以使用Paho库,这些库的存在,可以让你写非常上的代码,就可以完成功能的开发,现在企业中开发产品时基本上是基于可靠的库进行开发,避免重复开发轮子。

但这些网络库的底层都是基于socket进行封装的,我们通过学习底层通信知识,后续如果遇到一些定制的要求或者定制协议,也可以快速上手开发。

相关推荐
在荒野的梦想2 小时前
LangChain4j 集成若依单体应用 | 5 大 AI 功能实战:多轮对话、流式输出、RAG 知识库
java·人工智能
我能坚持多久2 小时前
C++入门基础知识
开发语言·c++·学习
吴佳浩 Alben2 小时前
Claude Code 源码泄露事件深度剖析
人工智能·arcgis·语言模型·自然语言处理·npm·node.js
禁默2 小时前
自动化智能体生成+外接MCP,我用 ModelEngine Nexent 5分钟手搓了一个小红书爆款收割机
运维·人工智能·自动化
风曦Kisaki2 小时前
# Linux进阶Day06:scp远程拷贝、源码编译安装、rsync同步、inotify+rsync实时同步
linux·运维·服务器
AII_IIA2 小时前
Ubuntu 20.04 升级到 24.04 实战详细教程/记录
linux·ubuntu·ubuntu升级·ubunt配置
数智顾问2 小时前
(100页PPT)数字化转型德勤集团信息化顶层规划方案(附下载方式)
大数据·人工智能
Johnstons2 小时前
11款网络流量监控分析软件深度对比
运维·网络·网络故障排除·网络流量分析·网络性能监控
汽车仪器仪表相关领域2 小时前
动态间隙精准诊断:NHJX-13 型底盘间隙仪机动车底盘安全检测全方案
大数据·人工智能·机器学习·单元测试·压力测试·可用性测试