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

ops/2024/10/10 16:46:17/

目录

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,    //默认传0struct sockaddr *src_addr,  //传出对端的地址结构,传出参数socklen_t *addrlen)   //对端套接字的大小,传入传出
返回值:成功接收数据:字节数;失败:-1,errno;对端关闭:0ssize_t sendto(int sockfd,       //自己的套接字const void *buf,  //存储数据的缓冲区size_t len,       //数据的长度int flags,        //默认0const 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 1024int 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;  // 绑定到本地所有IPserver_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 1024int 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 1024int 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 1024int 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.ppmy.cn/ops/123574.html

相关文章

[C#]使用纯opencvsharp部署yolov11-onnx图像分类模型

【官方框架地址】 https://github.com/ultralytics/ultralytics.git 【算法介绍】 使用纯OpenCvSharp部署YOLOv11-ONNX图像分类模型是一项复杂的任务&#xff0c;但可以通过以下步骤实现&#xff1a; 准备环境&#xff1a;首先&#xff0c;确保开发环境已安装OpenCvSharp和必…

如何处理各行业的DDOS问题

分布式拒绝服务&#xff08;Distributed Denial of Service, DDoS&#xff09;攻击是网络安全领域最为常见且危害性极大的攻击方式之一。通过控制大量计算机、物联网终端或网络僵尸&#xff0c;攻击者能够向目标网站或服务器发送大量请求&#xff0c;从而耗尽其资源&#xff0c…

k8s杂记

在node节点使用kubectl&#xff1a; rootmultinode-demo-m02:/# ps aux | grep kubelet root 218 3.1 1.6 2066316 62516 ? Ssl 07:35 0:29 /var/lib/minikube/binaries/v1.30.0/kubelet --bootstrap-kubeconfig/etc/kubernetes/bootstrap-kubelet.conf --…

rabbitMq------信道管理模块

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言信道管理的字段申明/删除交换机申明/删除队列绑定/解绑消息的发布消息确认订阅队列取消订阅信道内存管理类打开信道关闭信道/获取指定信道 总结 前言 信道是在…

Vue3常用API总结

因为这个月的月初给自己定了个小目标&#xff0c;学完Vue3的基本使用&#xff0c;并使用Vue3亲手做一个小项目&#xff08;稍微透露一下&#xff0c;我制作的是一个小工具&#xff0c;现在已经完成了90&#xff05;了&#xff0c;这个月月底之前会通过博客的形式向大家展示&…

JMeter中线程组、HTTP请求的常见参数解释

在JMeter中&#xff0c;线程组和HTTP请求是进行性能测试的两个核心组件。以下是它们的一些常见相关参数的解释&#xff1a; 线程组参数 线程数 指定模拟的用户数&#xff0c;即并发执行的线程数。 Ramp-Up时间&#xff08;秒&#xff09; 指定所有线程启动的时间间隔。在这…

RISC-V笔记——基础

1. 前言 RISC-V旨在支持广泛的定制和专业化。RISC-V的ISA是由一个基本整型ISA和其它对基本ISA的可选扩展组成。每个整型ISA可以使用一个或多个可选的ISA扩展进行扩展。 基本整型ISA精选了最小的一组指令&#xff0c;这些指令足以为编译器、汇编器、链接器和操作系统提供足够的…

叉车毫米波雷达防撞技术,保护叉车作业安全

在叉车作业频繁的仓库与物流中心&#xff0c;安全隐患往往隐藏于细微之处&#xff0c;稍有不便可能引发重大事故。我们的叉车毫米波防撞系统方案&#xff0c;正是针对这一痛点而精心设计的创新之作。该系统通过集成的毫米波雷达技术&#xff0c;实现了对叉车周边环境的实时、精…