网络编程UDP—socket实现
- 前言
- UDP客户端和服务端
- UDP使用场景
- UDP socket C++代码示例
- 客户端发送数据示例:
- 服务端接收数据示例:
- 关键函数的介绍
- bind 绑定-监听 函数
- 为什么一般都是监听所有网络接口呢?
- 为什么需要用inet_addr进行转换?
- sendto 发送 函数
- socket函数
- sockaddr_in结构体
前言
-
UDP通信需要哪些必要信息
- IP地址
- 用于定位通讯双方
- 端口号
- 用于标识通信的具体应用或服务。
- 传输层通信都需要端口号的。
- IP地址
-
网络要求
- 双方必须是可以进行ip通信的
- UDP依赖IP协议栈(IPv4或IPv6)完成路由、传输
- 双方需要用同一协议
- 双方必须是可以进行ip通信的
UDP客户端和服务端
-
客户端
-
构造数据报:包含目标IP、目标端口、数据内容。
-
发送数据报:使用套接字 sendto() 函数将数据发送到目标地址。
-
等待响应(如果有):接收服务端返回的数据。
-
服务端
-
创建监听套接字:绑定到指定IP和端口。
-
等待数据:通过 recvfrom() 函数接收数据。
-
处理请求:解析数据内容并执行相应操作。
-
返回响应:将结果数据发送回客户端。
UDP使用场景
UDP适用于以下需要高效传输但容忍数据丢失的场景:
- 实时通信:
- 视频通话、语音通话(如VoIP)。
- 在线游戏:
- 游戏中快速同步状态。
- 流媒体传输:
- 实时视频、音频传输。
- 广播/组播:
- 数据包同时发送给多个主机(如局域网中发现服务)。
- 轻量级请求/响应:
- DNS查询、简单的远程控制。
UDP socket C++代码示例
客户端发送数据示例:
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <cstring>
int main() {
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字
if (sock_fd < 0) {
perror("Socket creation failed");
return -1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 目标端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // 目标IP地址
const char* message = "Hello, UDP!";
sendto(sock_fd, message, strlen(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 发送数据
close(sock_fd);
return 0;
}
服务端接收数据示例:
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <cstring>
int main() {
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字
if (sock_fd < 0) {
perror("Socket creation failed");
return -1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 监听端口
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
close(sock_fd);
return -1;
}
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int bytes_received = recvfrom(sock_fd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &addr_len); // 接收数据
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
std::cout << "Received message: " << buffer << std::endl;
}
close(sock_fd);
return 0;
}
关键函数的介绍
bind 绑定-监听 函数
-
功能:
- bind 函数用于将套接字绑定到特定的IP地址和端口号,通常用于服务端监听套接字。
- 服务器先运行监听特定的IP地址和端口号;然后客户端再
- bind 函数用于将套接字绑定到特定的IP地址和端口号,通常用于服务端监听套接字。
-
函数声明:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
- sockfd:
- 套接字描述符,由 socket 函数返回。
- addr:
- 指向 sockaddr 结构体,表示要绑定的地址和端口。
- 通常使用 sockaddr_in,需强制转换为 sockaddr。
- 指向 sockaddr 结构体,表示要绑定的地址和端口。
- addrlen:
- addr 的长度(使用 sizeof(sockaddr_in))。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno。
示例:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 绑定端口号
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用IP地址
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
close(sock_fd);
}
为什么一般都是监听所有网络接口呢?
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用IP地址
这行代码中的 INADDR_ANY 是一个常用的常量,它代表了一个特殊的 IP 地址,即 0.0.0.0。当你将它设置为服务器套接字的地址时,表示该服务器将 监听所有网络接口。
-
理解什么是网络接口?
- 网络接口指计算机或设备上 每一个可以用于发送或接收数据的网络连接通道。
- 可以先理解为网卡,但是网络接口还包括一些虚拟网卡、本地回环接口(127.0.0.1)、甚至 VPN接口等,总的就是软硬 网络通道。
- 网络接口指计算机或设备上 每一个可以用于发送或接收数据的网络连接通道。
-
服务器为什么一般监听所有网络接口?
- 因为理论上我们希望只要是服务器这个端口号接收的,不管是哪一个网络接口,都交给服务器应用程序处理;
- 除非我们就只想让服务器处理从某个网络接口 接收的数据,才设置某一个网络接口的ip地址。例如:
- 只想让服务器接受本地(同一台机器上的应用程序)发出的数据
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
- 只想让服务器接受本地(同一台机器上的应用程序)发出的数据
为什么需要用inet_addr进行转换?
因为实际是采用网络字节序进行传输的,而用字符串形式十进制格式(如 “192.168.1.1”)只是为了人类方便阅读,所以需要转为网络字节序。
-
inet_addr作用
- 用于将一个点分十进制表示的 IPv4 地址(例如 “192.168.1.1”)转换为网络字节序的二进制格式。
-
inet_addr 将一个 IPv4 地址的点分十进制字符串(如 “192.168.1.1”)转换为网络字节序的 32 位整数。例如:
- “192.168.1.1” 在十进制中是:192 * 256^3 + 168 * 256^2 + 1 * 256^1 + 1 * 256^0
- 对应的二进制表示是:11000000 10101000 00000001 00000001
-
值得注意的是,inet_addr 已经不推荐使用,特别是在现代网络编程中,因为它对无效地址的处理可能不够清晰(比如返回 -1 会被误认为是有效的地址)。推荐使用 inet_pton 函数来替代,它更加健壮和安全。inet_pton 允许支持不同的地址族(IPv4 和 IPv6),并且不会出现类似 inet_addr 那样的错误返回值。
sendto 发送 函数
-
功能:
- 用于通过UDP套接字发送数据报到指定地址和端口
-
函数声明
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
- sockfd:
- 套接字描述符,由 socket 函数返回。
- buf:
- 指向要发送的数据的缓冲区。
- len:
- 要发送的数据长度(字节数)。
- flags:
- 传输标志,通常设置为 0。
- dest_addr:
- 指向一个 sockaddr 结构体,表示目标地址。
- 通常传入 sockaddr_in,需要通过强制类型转换为 sockaddr。
- 指向一个 sockaddr 结构体,表示目标地址。
- addrlen:
- dest_addr 的长度(使用 sizeof(sockaddr_in))。
返回值:
成功:返回实际发送的字节数。
失败:返回 -1,并设置 errno。
- dest_addr 的长度(使用 sizeof(sockaddr_in))。
示例:
const char *message = "Hello, UDP!";
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.1");
sendto(sock_fd, message, strlen(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
socket函数
-
作用
- 用于创建套接字socket,套接字是网络通信的基础,用于在客户端和服务端之间建立通信。
- 确定协议族(IPv4还是)、TCP还是UDP
-
函数声明
int socket(int domain, int type, int protocol);
参数说明:
- domain:指定通信的协议族(地址类型)。
- AF_INET:IPv4。
- AF_INET6:IPv6。
- AF_UNIX:本地通信(不使用网络)。
- type:指定套接字的类型。
- SOCK_STREAM:TCP(面向连接,保证数据可靠性)。
- SOCK_DGRAM:UDP(无连接,适合快速传输)。
- protocol:通常指定为 0,表示使用默认协议。
- 如果 type 是 SOCK_DGRAM,默认使用 UDP 协议。
- 如果 type 是 SOCK_STREAM,默认使用 TCP 协议。
返回值:
成功:返回套接字描述符(非负整数)。
失败:返回 -1,并设置 errno。
示例:
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
perror("Socket creation failed");
}
sockaddr_in结构体
-
功能
- 用于表示 IPv4 地址和端口信息,通常在网络通信中用于绑定或指定目标地址。
-
定义:
struct sockaddr_in {
short sin_family; // 地址族(必须为 AF_INET)
unsigned short sin_port; // 端口号(网络字节序,需要使用 htons() 转换)
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 填充字节,保持与 struct sockaddr 的大小一致(不使用,置 0)
};
字段说明:
- sin_family:
- 必须设置为 AF_INET(IPv4协议)。
- sin_port:
- 16位端口号,必须用 htons() 将主机字节序转换为网络字节序。
- sin_addr:
- 一个 struct in_addr 结构体,表示IPv4地址。
- 可以用 inet_addr() 或 inet_aton() 转换字符串形式的IP地址。
- 也可以设置为 INADDR_ANY,表示绑定到本地所有可用IP。
- 一个 struct in_addr 结构体,表示IPv4地址。
- sin_zero:
- 填充字段,不使用,应设置为 0。
示例:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(8080); // 端口号(转换为网络字节序)
server_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // 目标IP地址
memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero)); // 填充为0