在服务器项目中最常使用tcp进行通信,但是在嵌入式开发中使用udp的场景变得多个起来。
我想一边回顾套接字通信,一边写一个udp版本
先做一下基础回忆
socket通信一般用三种:tcp、udp和uds
但是从实现的角度 这样分是不好的(至少个人这么认为),
可以从两个角度分
选网络还是本机(是创建套接字时候的参数)
是字节流(TCP)还是数据报(UDP)(uds可以使用字节流也可以使用数据报)
从函数入手
创建套接字
int socket(int domain,int type,int protrol)
| 参数名 | 作用 | 常见取值与说明 |
|---|---|---|
domain (协议族) |
指定通信所在的协议域,决定了可使用的地址类型。 |
AF_INET:IPv4 互联网协议族 AF_INET6:IPv6 互联网协议族 AF_UNIX (或 AF_LOCAL):用于同一台主机上的进程间通信 (IPC),基于文件路径 |
type (套接字类型) |
指定数据的传输语义,即通信风格。 |
SOCK_STREAM:面向连接的字节流,提供可靠、按序、双向的通信(如 TCP) SOCK_DGRAM:无连接的数据报,提供固定长度的报文传输,不保证可靠和按序(如 UDP) SOCK_RAW:原始套接字,允许程序直接访问底层网络协议 |
protocol (具体协议) |
指定套接字使用的具体协议。 | 通常设置为 0,表示由系统根据 domain和 type自动选择默认协议(如 AF_INET+ SOCK_STREAM默认选 TCP)。也可显式指定,如 IPPROTO_TCP、IPPROTO_UDP等 。 |
对于AF_UNIX来说,protocol也是设置为0就好,他会根据AF_UNIX是字节流还是数据报正确选择通信协议
绑定
int bind(int sockfd , const struct sockaddr* addr , socklen_t addren)
| 参数名 | 数据类型 | 作用与说明 |
|---|---|---|
sockfd |
int |
套接字描述符,是由 socket()函数成功创建后返回的值,用于唯一标识一个套接字 。 |
addr |
const struct sockaddr * |
指向通用地址结构体的指针,它包含了准备绑定到套接字的 IP 地址和端口号 信息。实际使用时,通常传入的是 struct sockaddr_in(IPv4)或 struct sockaddr_un(Unix Domain Socket)等具体结构的地址,但需要强制转换为这个通用类型 。 |
addrlen |
socklen_t |
指定了第二个参数 addr所指向的地址结构体的实际长度(以字节为单位)。通常直接使用 sizeof(struct sockaddr_in)或类似表达式来获取 。 |
这里要说一下这个第二个参数
对于网络通信要注意 网络字节序
Unix 域套接字 (struct sockaddr_un)
#include <sys/socket.h>
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; /* 地址族: 必须是 AF_UNIX 或 AF_LOCAL */
char sun_path[108]; /* 套接字在文件系统中的路径 */
};
网络套接字 (如 struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
绑定示例
#include <sys/un.h>
//unix套接字使用
/*
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
.sun_path = "/tmp/my_socket" // 这里是在初始化,是合法的
};
或
*/
struct sockaddr_un addr;
memset(&addr,0,sizeof(addr));
addr.sun_family = AF_UNIX; // 或 AF_LOCAL,两者等价
strncpy(addr.sun_path, "/tmp/my_socket", sizeof(addr.sun_path) - 1); // 绑定到文件路径
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
//如果是网络字节序 将socket()返回值和本地的ip端口绑定在一起
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(10000); //大端端口
// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
// 这个宏一般用于本地的绑定操作
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
//指定某一个特定的ip的写法
// inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
//最后传入的时候
(struct sockaddr*)&addr
udp socket通信
服务端流程:socket->bind->recvfrom->sendto->close
客户端流程:socket->sendto->recvfrom->close
对于udp来说,其是没有accept和listen的
(还想说一点,无论是tcp还是udp其本身都是支持双向通信的(全双工),在嵌入式、中间件或其他领域实际开发中常使用两个服务端、两个客户端那是另外一回事)
下面是服务端udp socket通信的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 通信的套接字和本地的IP与端口绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999); // 大端
addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
char buf[1024];
char ipbuf[64];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 3. 通信
while(1)
{
// 接收数据
memset(buf, 0, sizeof(buf));
int rlen = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&cliaddr, &len);
printf("客户端的IP地址: %s, 端口: %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("客户端say: %s\n", buf);
// 回复数据
// 数据回复给了发送数据的客户端
sendto(fd, buf, rlen, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
}
close(fd);
return 0;
}
epoll+udp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
// 设置文件描述符为非阻塞模式
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
// 1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(0);
}
// 2. 设置套接字为非阻塞模式
if (set_nonblocking(fd) == -1) {
perror("fcntl");
close(fd);
exit(0);
}
// 3. 通信的套接字和本地的IP与端口绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999); // 大端
addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(0);
}
// 4. 创建epoll实例
int epoll_fd = epoll_create1(0); //epoll_create(1) //参数大于0
if (epoll_fd == -1) {
perror("epoll_create1");
close(fd);
exit(0);
}
// 5. 注册UDP套接字到epoll,监听读事件
struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1) {
perror("epoll_ctl");
close(fd);
close(epoll_fd);
exit(0);
}
struct epoll_event events[MAX_EVENTS];
char buf[BUFFER_SIZE];
char ipbuf[64];
printf("UDP Server with epoll is running on port 9999...\n");
// 6. 事件循环
while(1) {
// 等待事件发生,超时时间设为-1(无限等待)
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
// 处理所有就绪的事件
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == fd && (events[i].events & EPOLLIN)) {
// UDP数据可读
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
// 接收数据
memset(buf, 0, sizeof(buf));
int rlen = recvfrom(fd, buf, sizeof(buf), 0,
(struct sockaddr*)&cliaddr, &len);
if (rlen > 0) {
printf("客户端的IP地址: %s, 端口: %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,
ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("客户端say: %s\n", buf);
// 回复相同的数据给客户端
sendto(fd, buf, rlen, 0,
(struct sockaddr*)&cliaddr, len);
} else if (rlen == -1) {
perror("recvfrom");
}
}
// 处理错误事件
if (events[i].events & (EPOLLERR | EPOLLHUP)) {
printf("Socket error or hang up occurred\n");
close(events[i].data.fd);
}
}
}
// 清理资源
close(fd);
close(epoll_fd);
return 0;
}