Socket 套接字编程-UDP
Socket 函数详解
Socket(套接字)是网络编程的核心接口,用于实现不同主机之间的进程通信(IPC,Inter-Process Communication)。它提供了一种标准的 API,使应用程序能够通过 TCP/IP、UDP 或其他协议进行网络数据传输。
Socket 的基本概念
Socket 可以看作是两个进程(客户端和服务器)之间的通信端点(Endpoint),它封装了 IP 地址和端口号,使得数据可以在网络上传输。
- IP 地址:标识网络上的主机(如
192.168.1.1或google.com)。 - 端口号(Port):标识主机上的具体服务(如 HTTP:80、SSH:22)。
Socket 通常用于:
- TCP(可靠传输):如 HTTP、FTP、SSH。
- UDP(无连接传输):如 DNS、视频流、在线游戏。
Socket 的核心函数
(1) socket() - 创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 功能:创建一个 Socket 文件描述符(fd)。
- 参数:
domain:协议族(AF_INET表示 IPv4,AF_INET6表示 IPv6)。type:通信类型:SOCK_STREAM(TCP,可靠连接)SOCK_DGRAM(UDP,无连接)
protocol:通常设为0(自动选择)。
- 返回值:
- 成功:返回 Socket 文件描述符(
int)。 - 失败:返回
-1,并设置errno。
- 成功:返回 Socket 文件描述符(
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
(2) bind() - 绑定 IP 和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:将 Socket 绑定到特定的 IP 地址和端口(服务器端使用)。
- 参数:
sockfd:Socket 文件描述符。addr:指向sockaddr结构的指针(存储 IP 和端口)。addrlen:sockaddr结构的大小。
- 返回值:
- 成功:
0。 - 失败:
-1,并设置errno。
- 成功:
示例:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡
server_addr.sin_port = htons(8080); // 绑定 8080 端口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
(3) listen() - 监听连接(TCP 服务器)
int listen(int sockfd, int backlog);
- 功能:使 Socket 进入监听状态,等待客户端连接(仅用于 TCP)。
- 参数:
sockfd:Socket 文件描述符。backlog:等待连接队列的最大长度。
- 返回值:
- 成功:
0。 - 失败:
-1,并设置errno。
- 成功:
示例:
if (listen(sockfd, 5) == -1) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
(4) accept() - 接受连接(TCP 服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接受客户端的连接请求,返回一个新的 Socket 用于通信。
- 参数:
sockfd:监听 Socket。addr:存储客户端地址信息(可设为NULL)。addrlen:客户端地址结构的大小(可设为NULL)。
- 返回值:
- 成功:返回一个新的 Socket 文件描述符(用于数据传输)。
- 失败:
-1,并设置errno。
示例:
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_sock == -1) {
perror("accept failed");
close(sockfd);
exit(EXIT_FAILURE);
}
(5) connect() - 连接服务器(TCP 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:客户端连接服务器(仅用于 TCP)。
- 参数:
sockfd:Socket 文件描述符。addr:服务器地址信息。addrlen:地址结构大小。
- 返回值:
- 成功:
0。 - 失败:
-1,并设置errno。
- 成功:
示例:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // 连接本地 8080 端口
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
(6) send() / recv() - TCP 数据收发
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 功能:TCP 方式发送/接收数据。
- 参数:
sockfd:Socket 文件描述符。buf:数据缓冲区。len:数据长度。flags:通常设为0(阻塞模式)。
- 返回值:
- 成功:返回实际发送/接收的字节数。
- 失败:
-1,并设置errno。
示例:
char buffer[1024];
ssize_t bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
perror("recv failed");
close(client_sock);
exit(EXIT_FAILURE);
}
buffer[bytes_received] = '\0'; // 确保字符串终止
printf("Received: %s\n", buffer);
(7) sendto() / recvfrom() - UDP 数据收发
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- 功能:UDP 方式发送/接收数据(不需要
connect)。 - 参数:
dest_addr/src_addr:目标/源地址。
- 示例:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
char message[] = "Hello UDP Server!";
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
Socket 工作流程
TCP 服务器
socket()→ 创建 Socketbind()→ 绑定 IP 和端口listen()→ 开始监听accept()→ 接受客户端连接recv()/send()→ 数据通信close()→ 关闭 Socket
TCP 客户端
socket()→ 创建 Socketconnect()→ 连接服务器send()/recv()→ 数据通信close()→ 关闭 Socket
UDP 服务器/客户端
socket()→ 创建 Socketbind()(客户端可选,但一般是不需要的)sendto()/recvfrom()→ 直接发送/接收数据close()→ 关闭 Socket
总结
| 函数 | 用途 | 适用协议 |
|---|---|---|
socket() |
创建 Socket | TCP/UDP |
bind() |
绑定 IP 和端口 | TCP/UDP(服务器) |
listen() |
监听连接 | TCP(服务器) |
accept() |
接受连接 | TCP(服务器) |
connect() |
连接服务器 | TCP(客户端) |
send() / recv() |
TCP 数据收发 | TCP |
sendto() / recvfrom() |
UDP 数据收发 | UDP |
close() |
关闭 Socket | TCP/UDP |
下面是对 UDP 收发数据接口的详细说明
UDP 收发数据接口详解
由于 UDP(User Datagram Protocol)是无连接的协议,每次发送和接收数据时都需要明确指定或获取对方的地址信息。因此,UDP 通信主要使用以下两个接口:
recvfrom—— 接收数据,并获取发送方的地址信息。sendto—— 发送数据,并指定目标地址信息。
1. recvfrom —— 接收 UDP 数据
函数原型
#include <sys/socket.h>
ssize_t recvfrom(
int sockfd, // UDP 套接字描述符
void *buf, // 接收数据的缓冲区
size_t len, // 缓冲区大小
int flags, // 控制选项(通常设为 0)
struct sockaddr *src_addr, // 保存发送方的地址信息
socklen_t *addrlen // 地址结构体的长度(输入输出参数)
);
参数说明
sockfd:UDP 套接字描述符(由socket(AF_INET, SOCK_DGRAM, 0)创建)。buf:存放接收数据的缓冲区。len:缓冲区的最大容量。flags:控制选项(如MSG_WAITALL、MSG_PEEK,通常设为0)。src_addr:用于保存发送方的地址(struct sockaddr_in或struct sockaddr)。addrlen:输入时为src_addr的大小,输出时为实际接收到的地址长度。
返回值
- 成功:返回接收到的字节数(
>0)。 - 失败:返回
-1,并设置errno(如EAGAIN、ECONNREFUSED)。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) {
perror("bind");
exit(1);
}
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
ssize_t recv_len = recvfrom(
sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&client_addr, &addr_len
);
if (recv_len < 0) {
perror("recvfrom");
exit(1);
}
printf("Received %zd bytes from %s:%d\n",
recv_len,
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
close(sockfd);
return 0;
}
2. sendto —— 发送 UDP 数据
函数原型
#include <sys/socket.h>
ssize_t sendto(
int sockfd, // UDP 套接字描述符
const void *buf, // 要发送的数据
size_t len, // 数据长度
int flags, // 控制选项(通常设为 0)
const struct sockaddr *dest_addr, // 目标地址
socklen_t addrlen // 目标地址长度
);
参数说明
sockfd:UDP 套接字描述符。buf:要发送的数据缓冲区。len:数据长度。flags:控制选项(通常设为0)。dest_addr:目标地址(struct sockaddr_in或struct sockaddr)。addrlen:目标地址的长度。
返回值
- 成功:返回发送的字节数(
>=0)。 - 失败:返回
-1,并设置errno(如EMSGSIZE、ENOBUFS)。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
const char *msg = "Hello, UDP Server!";
ssize_t sent_len = sendto(
sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr)
);
if (sent_len < 0) {
perror("sendto");
exit(1);
}
printf("Sent %zd bytes to server\n", sent_len);
close(sockfd);
return 0;
}
为什么 UDP 需要 recvfrom 和 sendto?
由于 UDP 是无连接的协议,每次通信时都需要明确:
- 接收数据时:要知道数据是谁发来的(
recvfrom返回src_addr)。 - 发送数据时:要指定数据发给谁(
sendto需要dest_addr)。
对比 TCP
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(connect/accept) |
无连接 |
| 收发接口 | send/recv |
sendto/recvfrom |
| 地址管理 | 连接建立后自动维护 | 每次通信需手动指定 |
recvfrom:用于接收 UDP 数据,并获取发送方的地址。sendto:用于发送 UDP 数据,并指定目标地址。- UDP 是无连接的,因此每次通信都需要显式处理地址信息,而 TCP 是面向连接的,地址信息在建立连接后自动维护。
UDP 通信服务实现
以下是对这段 UDP 封装代码的详细介绍和注释说明:
udp.hpp
这段代码实现了一个简单的 UDP 网络通信封装
#ifndef _UDP_HPP_
#define _UDP_HPP_ 1 // 头文件保护宏
#include <iostream>
#include <sys/socket.h> // socket相关函数
#include <netinet/in.h> // sockaddr_in结构体
#include <arpa/inet.h> // inet_ntop等转换函数
#include <cstring> // bzero函数
// 错误码枚举
enum {
socket_error = 1, // socket创建失败
bind_error, // 绑定端口失败
};
inline char addr_buffer[1024]; // 全局缓冲区(用于地址转换)
namespace udp {
/**
* 创建UDP Socket
* @return 成功返回socket文件描述符,失败退出程序
*/
int Socket() {
// 创建IPv4 UDP Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket"); // 打印错误信息
exit(socket_error); // 退出程序
}
return sockfd;
}
/**
* 绑定Socket到指定端口
* @param sockfd Socket文件描述符
* @param port 要绑定的端口号
*/
void Bind(int sockfd, int port) {
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
// 设置地址族和端口
local.sin_family = AF_INET; // IPv4
local.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
local.sin_port = htons(port); // 端口转为网络字节序
// 执行绑定
int n = bind(sockfd, (struct sockaddr*)(&local), sizeof(local));
if (n != 0) {
perror("bind");
exit(bind_error);
}
}
/**
* 从sockaddr_in结构体解析IP和端口
* @param addr_in 输入的网络地址结构体
* @param addr 输出的IP字符串
* @param port 输出的端口号(主机字节序)
*/
void GetAddrAndPort(struct sockaddr_in &addr_in, std::string &addr, uint16_t &port) {
port = ntohs(addr_in.sin_port); // 端口转为主机字节序
// 将二进制IP转为字符串
inet_ntop(AF_INET, &addr_in.sin_addr, addr_buffer, sizeof(addr_buffer) - 1);
addr = addr_buffer; // 存储结果
}
};
#endif
Socket()函数- 使用
socket(AF_INET, SOCK_DGRAM, 0)创建 UDP Socket - 失败时通过
perror输出错误信息并退出
- 使用
Bind()函数INADDR_ANY表示监听所有网卡(包括公网和本地)htons(port)将端口转为网络字节序(大端)bind()将Socket与地址绑定
GetAddrAndPort()函数ntohs()将网络字节序端口转为主机字节序inet_ntop()将二进制IP地址转为可读字符串
全局缓冲区
addr_buffer用于临时存储IP字符串- 注意:多线程环境下建议改用局部变量
以下是对这段 UDP 服务端代码的详细注释和解释:
udp_server.hpp
该代码实现了一个简单的 UDP 服务端
#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_ 1 // 头文件保护宏
#include <iostream>
#include <unistd.h> // close函数
#include "udp.hpp" // UDP基础封装
#include "log.hpp" // 日志模块
const int defaultport = 8080; // 默认监听端口
const int default_size = 1024; // 接收缓冲区大小
class UdpServer {
public:
/**
* 构造函数
* @param port 监听端口(默认8080)
*/
UdpServer(uint16_t port = defaultport)
: port_(port) {} // 初始化端口
/**
* 初始化Socket和绑定
*/
void Init() {
// 1. 创建UDP Socket
sockfd_ = udp::Socket();
lg(Info, "socket success, sockfd: %d", sockfd_);
// 2. 绑定端口
udp::Bind(sockfd_, port_);
lg(Info, "socket bind success");
}
/**
* 启动服务主循环
*/
void Start() {
char buffer[default_size]; // 接收缓冲区
while (true) { // 持续监听
struct sockaddr_in peer; // 客户端地址
socklen_t len = sizeof(peer);
// 3. 接收数据(阻塞式)
ssize_t n = recvfrom(sockfd_, buffer, default_size - 1, 0,
(struct sockaddr*)&peer, &len);
if (n > 0) { // 接收成功
buffer[n] = '\0'; // 确保字符串终止
// 4. 解析客户端地址
std::string addr;
uint16_t port;
udp::GetAddrAndPort(peer, addr, port);
// 5. 记录日志
lg(Info, "UDP Server Get a message from [%s: %d]: %s",
addr.c_str(), port, buffer);
// 6. 回显数据
sendto(sockfd_, buffer, strlen(buffer), 0,
(struct sockaddr*)&peer, len);
}
}
}
/**
* 析构函数(关闭Socket)
*/
~UdpServer() {
close(sockfd_);
}
private:
uint16_t port_; // 监听端口
int sockfd_; // Socket文件描述符
};
#endif
关键代码解析
| 代码段 | 功能说明 |
|---|---|
udp::Socket() |
创建UDP Socket(SOCK_DGRAM) |
udp::Bind() |
绑定到INADDR_ANY(所有网卡)和指定端口 |
recvfrom() |
接收客户端数据,同时获取客户端地址 |
udp::GetAddrAndPort() |
从sockaddr_in解析IP和端口 |
sendto() |
将数据回发给原客户端 |
lg(Info,...) |
记录通信日志(依赖外部日志模块) |
工作流程
初始化阶段
sequenceDiagram UdpServer->>+udp::Socket: 创建Socket UdpServer->>+udp::Bind: 绑定端口运行阶段
sequenceDiagram Client->>UdpServer: 发送UDP数据包 UdpServer->>UdpServer: 记录客户端地址和消息 UdpServer->>Client: 回显相同数据
udp_client.cc
以下是一个简单的UDP客户端示例代码,用于与服务器进行交互:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "udp.hpp" // 引入自定义UDP封装
using namespace std;
int main(int argc, char *argv[]) {
// 1. 参数检查
if (argc != 3) {
cerr << "Usage: " << argv[0] << " server_ip server_port" << endl;
return 1;
}
// 2. 解析命令行参数
string server_ip = argv[1]; // 服务器IP地址
uint16_t server_port = stoi(argv[2]);// 服务器端口号
// 3. 创建UDP Socket
int sockfd = udp::Socket(); // 调用封装函数创建Socket
// 4. 配置服务器地址结构
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 清空结构体
server.sin_port = htons(server_port); // 设置端口(转为网络字节序)
server.sin_family = AF_INET; // IPv4地址族
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 转换IP为二进制格式
// 5. 主交互循环
while (true) {
// 5.1 获取用户输入
string inbuffer;
cout << "Please Enter# ";
getline(cin, inbuffer);
// 5.2 发送数据到服务器
ssize_t n = sendto(sockfd, inbuffer.c_str(), inbuffer.size(), 0,
(struct sockaddr*)&server, sizeof(server));
if (n > 0) { // 发送成功
// 5.3 准备接收回显
char buffer[1024];
struct sockaddr_in temp; // 临时存储回复方地址(未使用)
socklen_t len = sizeof(temp);
// 5.4 接收服务器回复
ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&temp, &len);
if (m > 0) { // 接收成功
buffer[m] = '\0'; // 添加字符串终止符
cout << "server echo# " << buffer << endl;
}
} else { // 发送失败则退出
break;
}
}
// 6. 关闭Socket
close(sockfd);
return 0;
}
核心功能
- 实现一个 UDP 客户端,能够:
- 通过命令行参数指定服务器地址和端口
- 交互式发送用户输入的消息
- 接收并显示服务器的回显(Echo)
关键步骤
| 步骤 | 关键函数/操作 | 说明 |
|---|---|---|
| 参数检查 | argc != 3 |
确保输入格式正确 |
| 创建Socket | udp::Socket() |
创建UDP套接字 |
| 地址配置 | inet_addr() + htons() |
将字符串IP和端口转为网络格式 |
| 数据发送 | sendto() |
发送数据到指定服务器 |
| 数据接收 | recvfrom() |
接收服务器回复(不验证来源) |
| 资源清理 | close() |
关闭Socket |
工作流程图示
sequenceDiagram
participant User
participant Client
participant Server
User->>Client: 输入消息
Client->>Server: sendto(message)
Server->>Client: recvfrom(echo)
Client->>User: 打印回显
通过主函数挂起服务端:
#include <iostream>
#include "udp_server.hpp"
using namespace std;
int main(int argc, char *argv[])
{
int port;
if(argc == 1)
{
port = 8080;
}else if(argc == 2)
{
port = stoi(argv[1]);
}else{
cerr << "Usage: " << argv[0] << "(default port: 8080)" << endl <<
"OR" << endl << argv[0] << " port" << endl;
return 1;
}
UdpServer us(port);
us.Init();
us.Start();
return 0;
}
而后我们进行通信,可以看到通信正常:
# 客户端
╭─ljx@VM-16-15-debian ~/linux_review/udp
╰─➤ ./udp_client.o 82.156.255.140 8888
Please Enter# 1234
server echo# 1234
Please Enter# 12345
server echo# 12345
Please Enter# Liu Jiaxuan say: Hello!
server echo# Liu Jiaxuan say: Hello!
# 服务端
╭─ljx@VM-16-15-debian ~/linux_review/udp
╰─➤ ./main.o 8888
[Info][2025-7-28 23:43:22] socket success, sockfd: 3
[Info][2025-7-28 23:43:22] socket bind success
[Info][2025-7-28 23:43:30] UDP Server Get a message from [82.156.255.140: 55472]: 1234
[Info][2025-7-28 23:43:32] UDP Server Get a message from [82.156.255.140: 55472]: 12345
[Info][2025-7-28 23:43:56] UDP Server Get a message from [82.156.255.140: 55472]: Liu Jiaxuan say: Hello!