目录
[1.1 UDP服务器](#1.1 UDP服务器)
[1.2 TPC和UDP的比较](#1.2 TPC和UDP的比较)
[1.3 C/S模型 -- UDP](#1.3 C/S模型 -- UDP)
[2.1 套接字比较](#2.1 套接字比较)
[2.2 函数参数选用](#2.2 函数参数选用)
[2.3 server](#2.3 server)
[2.4 client](#2.4 client)
[2.5 实现对比](#2.5 实现对比)
1.UDP
1.1 UDP服务器
UDP 是一种无连接的传输协议,类似于发送短信,不需要在通信前建立连接,也不需要维持连接状态。发送方只需将数据打包成独立的"数据报"(datagram)发给接收方。接收方收到这些数据报,但并不保证顺序、可靠性或数据的完整性。
由于UDP是"无连接的、不可靠的报文传递",通信速度会更快,但它不提供像TCP那样的流量控制和错误恢复机制。因此,UDP常被用于对实时性要求高,但对丢包、顺序错误容忍度较高的场景。
由于UDP不保证数据可靠性,因此在实际应用中需要采取一些措施来弥补其不足。以下是两种常见的UDP传输中可能遇到的问题以及对应的解决方法:
- 缓冲区溢出及丢包
当接收方的缓冲区被填满,无法继续接收数据时,接收方会丢弃后续到达的数据报。UDP 没有像 TCP 那样的滑动窗口机制来控制流量,这种丢包现象需要通过应用层的设计或系统参数调整来解决。
解决方法:
应用层流量控制:在应用层设计一个控制机制,比如让服务器根据接收方的处理能力来动态调整数据发送的速率。
调整缓冲区大小 :通过
setsockopt()
函数来调整接收方的缓冲区大小。例如,通过设置SO_RCVBUF
选项增加接收方的缓冲区,以便在高流量传输时能更好地处理数据。int sockfd;
int rcvbuf_size = 65535; // 增大接收缓冲区大小
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));
- 缺乏可靠性
UDP 的本质是不可靠的,因此在数据传输过程中可能会出现丢包、重复包或顺序错乱等问题。如果应用对数据可靠性有一定要求,需要通过应用层协议来弥补。
解决方法:
- 应用层协议:设计自己的应用层协议来提供确认机制、重传机制等。例如,每个数据报附带序号,接收方在收到数据后返回确认消息,如果发送方没有收到确认则重发。
- FEC(前向纠错)技术:在发送数据时加入冗余信息,这样即使接收方丢失部分数据,也能通过冗余信息还原完整数据。
1.2 TPC和UDP的比较
|---------------|-------------------------|---------------------|
| 特性 | TCP | UDP |
| 连接管理 | 面向连接(需要三次握手建立连接) | 无连接(不需要建立连接) |
| 可靠性 | 提供可靠传输,保证数据不丢失、顺序不乱 | 不保证可靠性,可能丢包或乱序 |
| 流量控制和拥塞控制 | 有流量控制和拥塞控制机制 | 没有流量控制和拥塞控制 |
| 传输方式 | 面向字节流,适合大量数据传输 | 面向数据报,适合发送小而独立的数据 |
| 实时性 | 较差(有重传和流量控制) | 强(无连接和握手,实时性好) |
| 应用场景 | 文件传输、电子邮件、Web 浏览等需要可靠传输 | 视频会议、实时游戏、广播等对速度要求高 |
UDP的优缺点
优点:
- 开销小,通信速度快:UDP 不需要像 TCP 一样在通信前通过三次握手来建立连接,也不需要在通信过程中维持状态。因此,传输数据时的开销要比 TCP 小,适合快速、频繁地传递数据。
- 实时性强:UDP 适用于需要低延迟的场景,因为它不会等待确认,数据报会直接发送出去。它经常用于需要即时传输的应用,比如视频会议、电话会议、网络直播等。
- 面向数据报:UDP 不像 TCP 那样面向流,数据是以独立的报文方式发送的。因此每个数据报是独立的单位,不依赖前后数据报的顺序,这使得它更适合广播和多播应用。
缺点:
- 不可靠传输:UDP 不保证数据的可靠性,也就是说,数据报可能丢失、重复或到达时顺序错乱。UDP 也不保证对丢失或损坏的数据进行重传。
- 无流量控制:UDP 缺少 TCP 的流量控制和拥塞控制机制,不能有效防止接收端处理不过来时发生丢包。如果发送方发送数据的速度过快,接收方可能会丢失一部分数据。
- 无连接状态:由于 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_DGRAM 或SOCK_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;
}