C/S模型的简单实现(UDP服务器)、本地套接字(sockaddr_un )的讲解

news/2024/10/7 16:00:12 标签: 嵌入式linux, socket, UDP, C/S模型, 网络

目录

1.UDP

1.1 UDP服务器

1.2 TPC和UDP的比较

1.3 C/S模型 -- UDP

recvfrom、sendto

server

client

2.本地套接字

2.1 套接字比较

2.2 函数参数选用

2.3 server

2.4 client

2.5 实现对比


1.UDP

1.1 UDP服务器

UDP 是一种无连接的传输协议,类似于发送短信,不需要在通信前建立连接,也不需要维持连接状态。发送方只需将数据打包成独立的“数据报”(datagram)发给接收方。接收方收到这些数据报,但并不保证顺序、可靠性或数据的完整性。

由于UDP是“无连接的、不可靠的报文传递”,通信速度会更快,但它不提供像TCP那样的流量控制和错误恢复机制。因此,UDP常被用于对实时性要求高,但对丢包、顺序错误容忍度较高的场景。

由于UDP不保证数据可靠性,因此在实际应用中需要采取一些措施来弥补其不足。以下是两种常见的UDP传输中可能遇到的问题以及对应的解决方法:

1. 缓冲区溢出及丢包

当接收方的缓冲区被填满,无法继续接收数据时,接收方会丢弃后续到达的数据报。UDP 没有像 TCP 那样的滑动窗口机制来控制流量,这种丢包现象需要通过应用层的设计或系统参数调整来解决。

解决方法:

  • 应用层流量控制:在应用层设计一个控制机制,比如让服务器根据接收方的处理能力来动态调整数据发送的速率。
  • 调整缓冲区大小:通过 setsockopt() 函数来调整接收方的缓冲区大小。例如,通过设置 SO_RCVBUF 选项增加接收方的缓冲区,以便在高流量传输时能更好地处理数据。
int sockfd;
int rcvbuf_size = 65535;  // 增大接收缓冲区大小
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));

2. 缺乏可靠性

UDP 的本质是不可靠的,因此在数据传输过程中可能会出现丢包、重复包或顺序错乱等问题。如果应用对数据可靠性有一定要求,需要通过应用层协议来弥补。

解决方法:

  • 应用层协议:设计自己的应用层协议来提供确认机制、重传机制等。例如,每个数据报附带序号,接收方在收到数据后返回确认消息,如果发送方没有收到确认则重发。
  • FEC(前向纠错)技术:在发送数据时加入冗余信息,这样即使接收方丢失部分数据,也能通过冗余信息还原完整数据。

1.2 TPC和UDP的比较

特性

TCP

UDP

连接管理

面向连接(需要三次握手建立连接)

无连接(不需要建立连接)

可靠性

提供可靠传输,保证数据不丢失、顺序不乱

不保证可靠性,可能丢包或乱序

流量控制和拥塞控制

有流量控制和拥塞控制机制

没有流量控制和拥塞控制

传输方式

面向字节流,适合大量数据传输

面向数据报,适合发送小而独立的数据

实时性

较差(有重传和流量控制)

强(无连接和握手,实时性好)

应用场景

文件传输、电子邮件、Web 浏览等需要可靠传输

视频会议、实时游戏、广播等对速度要求高

UDP的优缺点

优点:

  1. 开销小,通信速度快UDP 不需要像 TCP 一样在通信前通过三次握手来建立连接,也不需要在通信过程中维持状态。因此,传输数据时的开销要比 TCP 小,适合快速、频繁地传递数据。
  2. 实时性强UDP 适用于需要低延迟的场景,因为它不会等待确认,数据报会直接发送出去。它经常用于需要即时传输的应用,比如视频会议、电话会议、网络直播等。
  3. 面向数据报UDP 不像 TCP 那样面向流,数据是以独立的报文方式发送的。因此每个数据报是独立的单位,不依赖前后数据报的顺序,这使得它更适合广播和多播应用。

缺点:

  1. 不可靠传输UDP 不保证数据的可靠性,也就是说,数据报可能丢失、重复或到达时顺序错乱。UDP 也不保证对丢失或损坏的数据进行重传。
  2. 无流量控制UDP 缺少 TCP 的流量控制和拥塞控制机制,不能有效防止接收端处理不过来时发生丢包。如果发送方发送数据的速度过快,接收方可能会丢失一部分数据。
  3. 无连接状态:由于 UDP 是无连接的,所以无法维护通信双方之间的状态。这意味着每个数据报都是独立的,无法像 TCP 那样提供长时间的可靠连接。

1.3 C/S模型 -- UDP

由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现。

编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联系上。和前面TCP程序的运行结果相比较,体会无连接的含义。

recvfrom、sendto

recv() / send只能用于TCP通信,代替read和write,具体参数去按K查看man手册

而在UDP中能替换read和write则是:recvfrom()和sendto

ssize_t recvfrom(int sockfd,   //自己的套接字
                 void *buf,   //读取数据后存放的缓冲区
                 size_t len,   //缓冲区的大小
                 int flags,    //默认传0
                 struct sockaddr *src_addr,  //传出对端的地址结构,传出参数
                 socklen_t *addrlen)   //对端套接字的大小,传入传出
返回值:成功接收数据:字节数;失败:-1,errno;对端关闭:0

ssize_t sendto(int sockfd,       //自己的套接字
               const void *buf,  //存储数据的缓冲区
               size_t len,       //数据的长度
               int flags,        //默认0
               const struct sockaddr *dest_addr,  //发送数据给目标的地址结果,传入
               socklen_t addrlen);  //目标地址结构体的长度
返回值:成功:字节数;失败:-1,errno

server

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

#define SERVER_PORT 9876
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到本地所有IP
    server_addr.sin_port = htons(SERVER_PORT); // 指定端口

    if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("UDP 服务器启动,等待客户端发送数据...\n");

    // 接收客户端数据
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &client_len);
        if (n < 0) {
            perror("recvfrom error");
            continue;
        }

        buffer[n] = '\0';
        printf("客户端: %s\n", buffer);

        // 回复客户端
        char *message = "服务器已收到消息";
        sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&client_addr, client_len);
    }

    close(sockfd);
    return 0;
}

client

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

#define SERVER_PORT 9876
#define SERVER_IP "127.0.0.1"  // 服务器 IP 地址
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    socklen_t server_len = sizeof(server_addr);

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 服务器地址设置
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    while (1) {
        // 获取用户输入
        printf("请输入发送给服务器的消息: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = '\0';  // 移除换行符

        // 发送数据给服务器
        sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, server_len);

        // 接收服务器的回应
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
        if (n < 0) {
            perror("recvfrom error");
            continue;
        }

        buffer[n] = '\0';
        printf("服务器回复: %s\n", buffer);
    }

    close(sockfd);
    return 0;
}

2.本地套接字

IPC:pipe、fifo、mmap、信号、本地套接字(domain)

UNIX Domain Socket(也称为本地套接字)是一种用于同一台主机上不同进程间通信的机制,是在网络 Socket 的框架上发展出来的进程间通信 (IPC) 方式。与网络套接字不同的是,UNIX Domain Socket 不需要经过网络协议栈的处理,因此效率更高。下面是对 UNIX Domain Socket 的详细讲解。IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的

虽然可以通过网络套接字在同一台主机的进程间进行通信(使用 loopback 地址 127.0.0.1),但是网络套接字的设计初衷是用于跨网络通信。网络协议需要经过较为复杂的协议栈(TCP/IP),包括数据打包、拆包、校验和计算、维护序号和应答等步骤,这些操作对本地主机上的通信是多余的。

网络套接字通过 IP 地址和端口号标识通信双方不同,UNIX Domain Socket 通过文件系统中的路径来标识。地址结构为 struct sockaddr_un,其中包含一个文件路径,表示 socket 文件的位置。这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。

使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAMSOCK_STREAM,protocol参数仍然指定为0即可。socket函数介绍点击这里

  • SOCK_STREAM:面向流的通信,类似于 TCP,提供双向、可靠、无边界的字节流通信。
  • SOCK_DGRAM:面向数据报的通信,类似于 UDP,虽然消息是有边界的,但不同于 UDP,它是可靠的,数据包不会丢失或乱序。

2.1 套接字比较

对比网络套接字地址结构和本地套接字地址结构:

网络套接字的称地址结构 -- 封装了IP和端口号 
struct sockaddr_in {
    __kernel_sa_family_t sin_family; /* Address family */  //地址结构类型 -- AF_INET(IPv4)
    __be16 sin_port;/* Port number */端口号
        struct in_addr sin_addr;/* Internet address */ IP地址
    };

本地套接字的地址结构:
struct sockaddr_un {
    __kernel_sa_family_t sun_family;/* AF_UNIX */ //地址结构类型 -- AF_UNIX(本地协议)
    char sun_path[UNIX_PATH_MAX]; /* pathname */ socket文件名(含路径)
    };
以下程序将UNIX Domain socket绑定到一个地址。
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
#define offsetof(type, member) ((int)&((type *)0)->MEMBER)

2.2 函数参数选用

int socket(int domain, int type, int protocol);
domain:
AF_UNIX/AF_LOCAL 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:下面随便选一个
    SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
    SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
protocol:
    传0 表示使用默认协议
返回值:
    成功:返回用于引用socket的文件描述符,失败:返回-1,设置errno
 要注意的是返回的fd是个文件描述符,在UDP中需要绑定上地址结构后该套接字才真正被创建,服务器和客户端都需要整一个伪文件出来进行绑定创建伪文件(套接字)
    所以都需要给bind传入地址结构 -- 创建传入的名为(struct sockaddr_un).sun_path的伪文件 -- 绑定 --  socket真正形成
 因此调用bind前需要用unlink对名为srv.socket的文件进行删除 -- 减少其连接数为0,系统就会将其回收,防止文件名冲突而创建不了

 而在TCP网络通信中,即使不用bind绑定上IP和端口号,系统也会自动分配,一调用socket函数套接字便创建

 TCP中IP和port就对应一个socket,而UDP中是一个名为(struct sockaddr_un).sun_path的伪文件对应一个socket                          

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
    lfd = socket(AF_UNIX, SOCK_STREAM,0);  //空壳,是文件描述符 -- 套接字
addr:
    // struct sockaddr_un {
    // __kernel_sa_family_t sun_family;/* AF_UNIX */ //地址结构类型 -- AF_UNIX(本地协议)
    // char sun_path[UNIX_PATH_MAX]; /* pathname */ socket文件名(含路径)
    // };
    struct sockaddr_un srv_addr;
    srv_addr.faimly = AF_UNIX;  //根据socket函数参数domain指定一样的协议
    strcpy(srv_addr.sun_path,"srv.socket");   //给套接字起名字 -- 给伪文件命名
    addr = (struct sockaddr *)srv_addr;
addrlen:
    地址结构的长度
    addrlen = offsetof(struct sockaddr_un, sun_path) + strlen(srv_addr.sun_path);
    // #define offsetof(type, member) ((int)&((type *)0)->MEMBER)
    //offsetof求的参2到参1的首地址偏移大小为多少,其实就是两字节

    传出一个绑定伪文件的套接字文件描述符 参1 socket,指向名为“srv.socket”的伪文件
    因此调用bind前需要用unlink对名为srv.socket的文件进行删除 -- 减少其连接数为0,系统就会将其回收
    防止文件名冲突而创建不了
    所以UDP中套接字是调用bind创建的 -- (struct sockaddr_un).sun.path的伪文件

2.3 server

使用本地套接字(也叫 Unix 域套接字,AF_UNIX 地址族)实现一个简单的 C/S 模型。与 TCP 或 UDP 不同,Unix 域套接字在本地通信中使用文件路径作为地址标识,通常用于同一主机内的进程间通信。

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

#define SOCKET_PATH "/tmp/unix_socket"  // 本地套接字文件路径
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];

    // 创建本地套接字
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    unlink(SOCKET_PATH);  // 删除已经存在的文件,避免 bind 失败
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind error");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 5) < 0) {
        perror("listen error");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("服务器正在等待客户端连接...\n");

    client_len = sizeof(client_addr);
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
    if (client_fd < 0) {
        perror("accept error");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 接收数据
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = read(client_fd, buffer, BUFFER_SIZE);
        if (n <= 0) {
            perror("read error");
            break;
        }
        printf("客户端发送: %s\n", buffer);

        // 回复客户端
        char *message = "服务器收到消息!";
        write(client_fd, message, strlen(message) + 1);
    }

    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);  // 关闭服务器时删除套接字文件

    return 0;
}

2.4 client

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

#define SOCKET_PATH "/tmp/unix_socket"
#define BUFFER_SIZE 1024

int main() {
    int client_fd;
    struct sockaddr_un server_addr;
    char buffer[BUFFER_SIZE];

    // 创建本地套接字
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_fd < 0) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 连接服务器
    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect error");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 读取用户输入
        printf("请输入要发送给服务器的消息: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = '\0';  // 移除换行符

        // 发送数据给服务器
        write(client_fd, buffer, strlen(buffer) + 1);

        // 接收服务器的回复
        int n = read(client_fd, buffer, BUFFER_SIZE);
        if (n <= 0) {
            perror("read error");
            break;
        }
        printf("服务器回复: %s\n", buffer);
    }

    close(client_fd);
    return 0;
}

2.5 实现对比


http://www.niftyadmin.cn/n/5693027.html

相关文章

力扣110:判断二叉树是否为平衡二叉树

利用二叉树遍历的思想编写一个判断二叉树&#xff0c;是否为平衡二叉树 示例 &#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;true思想&#xff1a; 代码&#xff1a; int getDepth(struct TreeNode* node) {//如果结点不存在&#xff0c;返回…

【深度学习基础模型】液态状态机(Liquid State Machines, LSM)详细理解并附实现代码。

【深度学习基础模型】液态状态机&#xff08;Liquid State Machines, LSM&#xff09;详细理解并附实现代码。 【深度学习基础模型】液态状态机&#xff08;Liquid State Machines, LSM&#xff09;详细理解并附实现代码。 文章目录 【深度学习基础模型】液态状态机&#xff0…

初始项目托管到gitee教程,开箱即用

0.本地仓库与远程仓库关联&#xff08;需先在gitee创建仓库&#xff09; ①打开powershell生成ssh key ssh-keygen -t ed25519 -C "Gitee SSH Key"-t key 类型-C 注释 生成成功如下&#xff0c;并按下三次回车 ②查看公私钥文件 ls ~/.ssh/输出&#xff1a; id_…

解决npm install安装出现packages are looking for funding run `npm fund` for details问题

当我们运行npm install时&#xff0c;可能会收到类似以下的提示信息&#xff1a;"x packages are looking for funding." 这并不是错误提示&#xff0c;也不会影响项目的正常运行。相反&#xff0c;这是提醒您&#xff0c;有一些软件包正在寻求资金支持。 这个提示的…

Linux的发展历史与环境

目录&#xff1a; 引言Linux的起源早期发展企业级应用移动与嵌入式系统现代计算环境中的Linux结论 引言 Linux&#xff0c;作为开源操作系统的代表&#xff0c;已经深刻影响了全球的计算环境。从其诞生之初到如今成为服务器、嵌入式系统、移动设备等多个领域的核心&#xff0c…

C语言 | Leetcode C语言题解之第459题重复的子字符串

题目&#xff1a; 题解&#xff1a; bool kmp(char* query, char* pattern) {int n strlen(query);int m strlen(pattern);int fail[m];memset(fail, -1, sizeof(fail));for (int i 1; i < m; i) {int j fail[i - 1];while (j ! -1 && pattern[j 1] ! pattern…

Java之String类

目录 初识String 字符串比较相等 字符串常量池 理解字符串的不可变 字符与字符串 字符串常见操作 字符串比较 compareTo()函数的原码 字符串查找 字符串替换 字符串拆分 字符串截取 其它操作 StringBuffer和StringBuilder 面试题&#xff1a;请解释String、Strin…

C++面试速通宝典——14

220. static关键字的作用 ‌‌‌‌  static关键字在编程中有多种作用&#xff1a; 在类的成员变量前使用&#xff0c;表示该变量属于类本身&#xff0c;而不是任何类的实例。在类的成员函数前使用&#xff0c;表示该函数不需要对象实例即可调用&#xff0c;且只能访问类的静…