目录
一、了解UDP协议
二、了解端口和IP地址
三、套接字概述与Socket的概念
四、Socket的类型
五、 Socket的信息数据结构
六、网络字节序与主机字节序的互相转换
七、地址转换函数
八、UDP网络编程流程及相关函数
socket函数
bind函数
recvfrom函数
sendto函数
九、创建服务端与客户端之间的网络通信
注意:本文的重点为网络编程,并不会对TCP、UDP、IP等协议做详细介绍,仅做简要说明。
一、了解UDP协议
UDP(User Datagram Protocol,用户数据报协议)是一个简单的传输层协议,用于在网络中进行快速且无连接的数据传输,无需像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。
由于UDP不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多,因此我们先以UDP网络编程来导入对网络编程的学习。
下面是UDP协议的是主要特点:
主要特点
无连接:
- UDP 是无连接的协议,这意味着在数据传输之前,不需要建立和维护连接。每个数据包(称为数据报)是独立的,发送和接收之间不需要进行握手或维护连接状态。
不可靠传输:
- UDP 不提供可靠的数据传输保证。数据包可能丢失、重复或乱序到达。它不会对数据包的丢失进行重传,也不会进行数据的错误校验和修正。应用层需要自行处理这些问题(如果需要的话)。
面向数据报:
- UDP 以数据报的形式传输信息,每个数据报包含了目标地址和端口号。数据报大小受到限制,通常最大为 65,535 字节(包括头部和数据部分)。
低开销:
- UDP 的头部开销较小(仅 8 字节),因为它省略了许多 TCP 中的控制信息(如序列号、确认号、流量控制、重传等)。这使得 UDP 在网络中具有较低的延迟和开销,适用于需要高速传输的应用。
适用于实时应用:
- 由于其低延迟和简单的机制,UDP 常用于实时应用,如语音通话、视频会议、流媒体等。即使丢失了一些数据包,这些应用也可以继续运行,而不会对整体体验造成显著影响。
二、了解端口和IP地址
IP 地址和端口号在网络通信中扮演着重要角色,它们各自标识了网络中的不同层次。
-
IP 地址:这是一个唯一的标识符,用于确定网络中的主机或设备。每台联网的设备都有一个唯一的 IP 地址,这样它们才能在网络中相互识别和通信。IP 地址能够标识网络上的唯一设备,但并不能唯一标识设备上的具体应用或服务。
-
端口号:这是一个用于标识设备上特定应用或服务的数字。每个设备可以运行多个应用或服务,每个服务都通过不同的端口号来区分。端口号的范围为0~65 535,一类是由互联网指派名字和号码公司ICANN 负责分配给一些常用的应用程序固定使用的“周知的端口”,其值一般为0~1023,例如,http的端口号是80,fp为21,ssh为 22,telnet为23等;还有一类是用户自己定义的,通常是大于1024的整型值。端口号与 IP 地址一起,能够唯一标识网络上的具体服务或应用。
因此,IP 地址和端口号的组合可以唯一标识网络上的具体服务或应用(进程)。例如,一个 IP 地址为 192.168.1.1
的设备上的 HTTP 服务可以通过 192.168.1.1:80
来访问。
三、套接字概述与Socket的概念
套接字是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户,是网络进程的ID。网络通信归根到底还是进程间的通信(不同计算机上的进程间通信)。在网络中,每一个节点(计算机或路由)都有一个网络地址,也就是IP地址。在两个进程进行通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底要和网络中的哪一个进程进行通信,因此套接字中还需要包括其他的信息,也就是端口号(PORT)。在一台计算机中,一个端口号一次只能分配给一个进程。也就是说,在一台计算机中,端口号和进程之间是一一对应的关系,所以,使用端口号和网络地址的组合可以唯一地确定整个网络中的一个网络进程。
例如,假设网络中某一台计算机的IP地址为10.92.20.160,操作系统分配给计算机中某应用程序进程的端口号为1500,则此时10.92.20.160,1500就构成了一个套接字。
Linux中的网络编程是通过Socket来进行的。Socket 是一种特殊的IO接口,也是一种文件描述符。它是一种常用的进程之间的通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
每一个Socket都用一个半相关描述“{协议、本地地址、本地端口}”来表示:一个完整的套接字则用一个相关描述“(协议、本地地址、本地端口、远程地址、远程端口)”来表示。Socket也有一个类似于打开文件的函数调用,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过Socket来实现的。套接字(socket)是计算机网络中用于进行通信的一个端点。
四、Socket的类型
流式Socket(SOCK STREAM)用于TCP通信。流式套接字提供可靠的、面向连接的通信流;它使用传输控制协议TCP,从而保证数据传输的正确性和顺序性。
数据报Socket(SOCK DGRAM)用于UDP通信。数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的,它使用数据报协议UDP。
原始Socket(SOCK RAW)用于新的网络协议实现的测试等。原始套接字允许对底层协数据报协议 UDP。议如PP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
五、 Socket的信息数据结构
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,UNIX Domain Socket。 然而,,各种网络协议的地址格式并不相同,这些地址格式需要通过 Socket API 进行适配。
•IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型,16 位端口号和 32 位 IP 地址。
• IPv4、 IPv6 地址类型分别定义为常数 AF_INET、 AF_INET6。这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容。
• socket API 可以都用 struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性,可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数。
1. 地址类型
在
netinet/in.h
头文件中,IPv4 和 IPv6 的地址格式分别用struct sockaddr_in
和struct sockaddr_in6
结构体表示。sockaddr
结构体是一个通用的地址结构体,用于在编程中处理不同的地址类型:
IPv4 地址 (
struct sockaddr_in
):struct sockaddr_in {short int sin_family; // 地址族,通常为 AF_INETunsigned short int sin_port; // 端口号struct in_addr sin_addr; // IP 地址char sin_zero[8]; // 填充,以保证结构体大小为 16 字节 };struct in_addr {in_addr_t s_addr; // 32位 IPv4 地址,网络字节序,in_addr_t (unsigned long int ) };
IPv6 地址 (
struct sockaddr_in6
):struct sockaddr_in6 {u_int16_t sin6_family; // 地址族,通常为 AF_INET6u_int16_t sin6_port; // 端口号u_int32_t sin6_flowinfo; // 流量信息struct in6_addr sin6_addr; // IPv6 地址u_int32_t sin6_scope_id; // 作用域 ID };struct in6_addr {unsigned char s6_addr[16]; // IPv6 地址的 16 字节表示,网络字节序 };
2. 地址族常量
地址族常量定义了不同的协议族:
- AF_INET:表示 IPv4 地址族。
- AF_INET6:表示 IPv6 地址族。
- AF_UNIX:表示 UNIX Domain Socket 地址族。
这些常量用于指示
sockaddr
结构体中存储的地址类型,使得程序可以在运行时根据地址族来选择和处理合适的结构体。3. 使用
struct sockaddr
struct sockaddr
是一个通用的结构体,用于存储所有类型的地址。实际使用时,程序可以通过强制类型转换将struct sockaddr
转换为具体的地址结构体(如struct sockaddr_in
或struct sockaddr_in6
),以访问特定的字段:// 创建一个 IPv4 套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 定义 IPv4 地址 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(12345); // 设置端口号 inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr); // 设置 IP 地址// 使用 struct sockaddr * 作为参数 bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));// 定义 IPv6 地址 struct sockaddr_in6 addr6; addr6.sin6_family = AF_INET6; addr6.sin6_port = htons(12345); // 设置端口号 inet_pton(AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", &addr6.sin6_addr); // 设置 IPv6 地址// 使用 struct sockaddr * 作为参数 bind(sockfd, (struct sockaddr *)&addr6, sizeof(addr6));
六、网络字节序与主机字节序的互相转换
网络序列和主机序列的转化涉及到数据在网络上传输时的字节顺序问题。
字节序(Endianess):
- 大端序(Big-Endian):数据的高位字节存在低地址处,低位字节在高地址处。
- 小端序(Little-Endian):数据的低位字节存在低地址处,高位字节在高地址处。
不同的计算机系统可能使用不同的字节序来存储数据,这会导致同一数据在不同系统之间的表示方式不同。例如,一台使用大端序的机器和一台使用小端序的机器在数据传输时会有不同的解释。
网络序列和主机序列:
-
网络序列:网络协议通常采用大端序来表示数据,这种顺序被称为网络字节序。网络字节序是为了保证不同平台之间的数据能够一致地解释。
-
主机序列:是指计算机本身使用的字节序(可能是大端序也可能是小端序)。
转化的必要性:
在网络通信中,数据从一个主机发送到另一个主机时,数据的字节顺序必须统一,以确保接收方能够正确解析数据。由于不同主机可能使用不同的字节序,数据在网络上传输时需要进行转化:
-
从主机序列到网络序列:在发送数据之前,发送方需要将数据从主机字节序转化为网络字节序。这通常使用系统提供的函数来实现,如
htonl()
(将主机字节序的长整型转化为网络字节序)、htons()
(将主机字节序的短整型转化为网络字节序)。 -
从网络序列到主机序列:在接收数据时,接收方需要将数据从网络字节序转化为主机字节序。这通常使用函数如
ntohl()
(将网络字节序的长整型转化为主机字节序)、ntohs()
(将网络字节序的短整型转化为主机字节序)。
示例:
假设一个大端序主机发送一个整数 0x12345678
(十进制的305419896),在网络中,数据以大端序 0x12 0x34 0x56 0x78
传输。如果接收方是小端序主机,它接收到的字节顺序是 0x78 0x56 0x34 0x12
,需要进行转化以得到正确的整数值 0x12345678
。
字节序转换相关函数:
这些函数在
<arpa/inet.h>
头文件中声明:以下函数中,h代表host(主机),n代表network(网络),s代表short(16位字节序),l代表long(32位字节序)。
1.
htons
- 函数原型:
uint16_t htons(uint16_t hostshort);
- 功能:将主机字节序的
uint16_t
类型数值转换为网络字节序。2.
ntohs
- 函数原型:
uint16_t ntohs(uint16_t netshort);
- 功能:将网络字节序的
uint16_t
类型数值转换为主机字节序。3.
htonl
- 函数原型:
uint32_t htonl(uint32_t hostlong);
- 功能:将主机字节序的
uint32_t
类型数值转换为网络字节序。4.
ntohl
- 函数原型:
uint32_t ntohl(uint32_t netlong);
- 功能:将网络字节序的
uint32_t
类型数值转换为主机字节序。以下是一个示例程序,演示了如何使用以上函数进行字节序转换:
#include <stdio.h> #include <stdint.h> #include <arpa/inet.h>int main() {uint16_t port = 8080;uint32_t ip = 3232235776; // 192.168.1.0// 主机到网络字节序转换uint16_t net_port = htons(port);uint32_t net_ip = htonl(ip);// 网络字节序到主机字节序转换uint16_t host_port = ntohs(net_port);uint32_t host_ip = ntohl(net_ip);printf("Original port: %u\n", port);printf("Network port: %u\n", net_port);printf("Host port: %u\n", host_port);printf("Original IP: %u\n", ip);printf("Network IP: %u\n", net_ip);printf("Host IP: %u\n", host_ip);return 0; }
在上述示例中:
htons
将主机字节序的端口号port
转换为网络字节序。ntohs
将网络字节序的端口号net_port
转换回主机字节序。htonl
将主机字节序的 IP 地址ip
转换为网络字节序。ntohl
将网络字节序的 IP 地址net_ip
转换回主机字节序。
七、地址转换函数
地址转换函数用于在不同格式之间转换 IP 地址。它们在网络编程中非常重要,特别是在处理 IP 地址和域名时。以下是常见的地址转换函数的详细介绍:
1.
inet_addr
函数原型:
in_addr_t inet_addr(const char *cp);
功能:将点分十进制的 IP 地址(如 “192.168.1.1”)转换为网络字节序的
in_addr_t
类型(通常为uint32_t
)的二进制格式。返回值:成功时返回 IP 地址的网络字节序格式;失败时返回
INADDR_NONE
(通常为0xFFFFFFFF
)。示例:
#include <stdio.h> #include <arpa/inet.h>int main() {const char *ip_str = "192.168.1.1";in_addr_t ip = inet_addr(ip_str);if (ip == INADDR_NONE) {printf("Invalid IP address\n");} else {printf("IP address in network byte order: %u\n", ip);}return 0; }
2.
inet_ntoa
函数原型:
char *inet_ntoa(struct in_addr in);
功能:将网络字节序的
struct in_addr
结构体转换为点分十进制的 IP 地址字符串(如 “192.168.1.1”)。返回值:返回指向字符串的指针。如果失败,返回
NULL
。需要注意,返回的字符串是静态分配的,因此不适合多线程环境或并发使用。示例:
#include <stdio.h> #include <arpa/inet.h>int main() {struct in_addr addr;addr.s_addr = htonl(0xC0A80101); // 192.168.1.1char *ip_str = inet_ntoa(addr);printf("IP address: %s\n", ip_str);return 0; }
3.
inet_pton
函数原型:
int inet_pton(int af, const char *src, void *dst);
功能:将 IP 地址的文本表示形式(如 “192.168.1.1” 或 “2001:db8::1”)转换为网络字节序的二进制格式。
参数:
af
:地址族,通常为AF_INET
(IPv4)或AF_INET6
(IPv6)。src
:指向包含 IP 地址的字符串的指针。dst
:指向存储转换结果的内存地址。返回值:成功时返回
1
,无效地址时返回0
,出错时返回-1
。示例:
#include <stdio.h> #include <arpa/inet.h>int main() {struct in_addr addr;if (inet_pton(AF_INET, "192.168.1.1", &addr) == 1) {printf("Address converted successfully\n");} else {printf("Invalid address\n");}return 0; }
4.
inet_ntop
函数原型:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能:将网络字节序的二进制格式(如
struct in_addr
或struct in6_addr
)转换为 IP 地址的文本表示形式。参数:
af
:地址族,通常为AF_INET
(IPv4)或AF_INET6
(IPv6)。src
:指向包含网络字节序 IP 地址的内存地址。dst
:指向存储转换结果的缓冲区。size
:缓冲区大小。返回值:成功时返回指向结果字符串的指针;失败时返回
NULL
。示例:
#include <stdio.h> #include <arpa/inet.h>int main() {struct in_addr addr;addr.s_addr = htonl(0xC0A80101); // 192.168.1.1char ip_str[INET_ADDRSTRLEN];if (inet_ntop(AF_INET, &addr, ip_str, sizeof(ip_str))) {printf("IP address: %s\n", ip_str);} else {printf("Conversion failed\n");}return 0; }
总结
这些地址转换函数使得在处理 IP 地址时,可以在字符串表示和网络字节序之间进行转换。可以简化对 IP 地址的操作,并保证在网络通信中使用的地址格式正确。
八、UDP网络编程流程及相关函数
socket函数
函数功能:用于创建一个新的套接字,套接字(socket)是计算机网络中用于进行通信的一个端点。
1. 函数原型
在不同的编程语言和平台中,
socket
函数的原型可能有所不同,但在 C 语言和 POSIX 标准下,它的原型通常是:int socket(int domain, int type, int protocol);
2. 参数说明
domain:指定套接字的域或协议族,常见的有:
AF_INET
:IPv4 网络协议。AF_INET6
:IPv6 网络协议。AF_UNIX
:用于本地进程间通信的 Unix 域套接字。type:指定套接字的类型,常见的有:
SOCK_STREAM
:面向连接的流式套接字,通常用于 TCP 协议。SOCK_DGRAM
:数据报套接字,通常用于 UDP 协议。SOCK_RAW
:原始套接字,通常用于访问底层网络协议。protocol:指定协议类型。一般情况下,你可以设置为 0,这样系统会根据
domain
和type
自动选择合适的协议。例如,对于SOCK_STREAM
类型的套接字,系统会默认使用 TCP 协议;对于SOCK_DGRAM
类型的套接字,系统会默认使用 UDP 协议。3. 返回值
- 成功时,
socket
函数返回一个非负整数,这个整数是套接字的描述符。- 失败时,返回
-1
,并设置errno
以指示错误类型。4. 错误处理
当
socket
函数失败时,你可以通过errno
获取错误代码。常见的错误代码有:
EAFNOSUPPORT
:不支持指定的地址族。EINVAL
:提供了无效的参数。PROTONOSUPPORT
:不支持指定的协议。5. 示例代码
以下是一个简单的 C 语言示例,演示如何创建一个 IPv4 和 UDP 的套接字:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h>int main() {int sockfd;// 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}printf("Socket created successfully.\n");// 关闭套接字close(sockfd);return 0; }
bind函数
函数功能:
bind
函数在网络编程中用于将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。1. 函数原型
在 C 语言和 POSIX 标准下,
bind
函数的原型如下:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2. 参数说明
sockfd:套接字描述符,这是由
socket
函数创建并返回的套接字。addr:指向
sockaddr
结构体的指针,用于指定要绑定的本地地址。通常会使用具体的结构体如sockaddr_in
(用于 IPv4 地址)或sockaddr_in6
(用于 IPv6 地址)。addrlen:
addr
指向的地址结构体的长度。对于sockaddr_in
,通常是sizeof(struct sockaddr_in)
。3.
sockaddr
结构体
sockaddr_in
(用于 IPv4):struct sockaddr_in {sa_family_t sin_family; // 地址族,通常为 AF_INETuint16_t sin_port; // 端口号(网络字节顺序)struct in_addr sin_addr; // IP 地址 };struct in_addr {uint32_t s_addr; // IP 地址(网络字节顺序) };
sockaddr_in6
(用于 IPv6):struct sockaddr_in6 {sa_family_t sin6_family; // 地址族,通常为 AF_INET6uint16_t sin6_port; // 端口号(网络字节顺序)uint32_t sin6_flowinfo; // 流量信息struct in6_addr sin6_addr; // IPv6 地址uint32_t sin6_scope_id; // 范围 ID };struct in6_addr {unsigned char s6_addr[16]; // IPv6 地址 };
4. 返回值
- 成功时,返回
0
。- 失败时,返回
-1
,并设置errno
以指示错误类型。5. 错误处理
常见的错误代码包括:
EADDRINUSE
:地址已经在使用中,通常是端口被占用。EADDRNOTAVAIL
:提供的地址在本地不可用。EINVAL
:提供了无效的参数。ENOTSOCK
:描述符不是一个套接字。6. 示例代码
以下是一个简单的 C 语言示例,演示如何将一个套接字绑定到一个特定的本地地址和端口:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 创建 UDP 套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口server_addr.sin_port = htons(12345); // 设置端口号(转换为网络字节顺序)// 绑定套接字到本地地址if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind");close(sockfd);exit(EXIT_FAILURE);}printf("UDP socket successfully bound to port 12345.\n");// 关闭套接字close(sockfd);return 0; }
7. 使用场景
bind
函数通常在服务器端使用,绑定套接字到特定的 IP 地址和端口,以便监听来自客户端的连接请求。在客户端,通常不需要显式地调用bind
,除非你需要绑定到特定的本地地址和端口。
recvfrom函数
函数功能:
recvfrom
函数是用于接收数据的系统调用,适用于面向无连接的协议,如UDP(用户数据报协议)。1、函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
2、参数解释
- sockfd:已经创建并绑定的套接字的文件描述符。
- buf:指向缓冲区的指针,用于存储接收到的数据。
- len:缓冲区
buf
的大小,以字节为单位。- flags:标志位,用于修改
recvfrom
的行为,通常情况下设置为0,默认为阻塞读取。其他参数设置例如,MSG_DONTWAIT
表示非阻塞模式;MSG_WAITALL
表示等待直到接收到指定长度的数据。- src_addr:指向
sockaddr
结构体的指针,用于存储发送方的地址信息。如果不需要获取发送方的地址,可以设置为NULL
。- addrlen:指向整型的指针,用于指定
src_addr
结构体的大小。在调用recvfrom
之前,应将其设置为src_addr
结构体的大小;调用后,它将被设置为新接收到的地址的实际大小。3、返回值
- 成功时,
recvfrom
返回接收到的字节数。- 如果连接被对方优雅地关闭,则返回0。
- 如果发生错误,返回-1,并设置全局变量
errno
来指示错误的原因。4、注意事项
- 使用
recvfrom
时,需要处理可能出现的各种错误情况。例如,如果套接字处于非阻塞模式并且没有数据可读,recvfrom
可能会返回EAGAIN
或EWOULDBLOCK
错误。recvfrom
函数可以同时接收数据并获取数据发送方的地址信息,这是它与recv
函数的一个主要区别。- 对于UDP协议的套接字,由于UDP数据包可能会被分片,因此需要多次读取才能将一个完整的数据包接收完毕。
示例代码
以下是一个简单的 UDP 服务器示例,展示如何使用
recvfrom
函数接收数据并获取发送方的地址信息:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h>#define PORT 12345 #define BUF_SIZE 1024int main() {int sockfd;struct sockaddr_in server_addr, client_addr;socklen_t client_len;char buf[BUF_SIZE];ssize_t recv_len;// 创建 UDP 套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket");exit(EXIT_FAILURE);}// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口server_addr.sin_port = htons(PORT); // 设置端口号// 绑定套接字到本地地址if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind");close(sockfd);exit(EXIT_FAILURE);}printf("UDP server listening on port %d\n", PORT);// 接收数据client_len = sizeof(client_addr);recv_len = recvfrom(sockfd, buf, BUF_SIZE - 1, 0, (struct sockaddr *)&client_addr, &client_len);if (recv_len < 0) {perror("recvfrom");close(sockfd);exit(EXIT_FAILURE);}// Null-terminate and print received databuf[recv_len] = '\0';printf("Received message: %s\n", buf);printf("From address: %s\n", inet_ntoa(client_addr.sin_addr));printf("From port: %d\n", ntohs(client_addr.sin_port));// 关闭套接字close(sockfd);return 0; }
代码说明
- 创建套接字:使用
socket
函数创建一个 UDP 套接字。- 配置服务器地址:设置服务器地址信息,包括协议族(
AF_INET
)、地址(INADDR_ANY
)、端口号(使用htons
转换为网络字节顺序)。- 绑定套接字:将套接字绑定到指定的本地地址和端口。
- 接收数据:
- 使用
recvfrom
函数接收数据。recvfrom
填充buf
缓冲区并将客户端的地址信息填充到client_addr
结构体中。recv_len
保存接收到的字节数。- 处理数据:
- 将接收到的数据缓冲区
buf
以 null 字符终止,并打印出来。- 使用
inet_ntoa
函数将客户端的 IP 地址转换为字符串格式。- 使用
ntohs
函数将客户端的端口号转换为主机字节顺序。- 关闭套接字:结束时关闭套接字以释放资源。
sendto函数
函数功能:
sendto
函数是网络编程中用于发送数据的一个函数,通常用于 UDP 协议。它的主要功能是将数据包发送到指定的目标地址。sendto
函数常用于 Socket 编程,特别是在处理无连接的 UDP 套接字时。函数原型
在不同的编程语言和平台中,
sendto
的函数原型可能有所不同,但在 C 语言的 POSIX 标准中,sendto
的函数原型如下:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明
sockfd
: 套接字描述符,是通过socket
函数创建的套接字的文件描述符。该套接字应当是一个有效的、已创建的 UDP 套接字。
buf
: 指向要发送数据的缓冲区的指针。这个缓冲区中的数据会被发送到目标地址。
len
: 要发送的数据的字节数。它指定了buf
中的有效数据长度。
flags
: 发送数据的标志。通常情况下,这个参数设置为0
,但也可以使用不同的标志来控制发送行为(如MSG_CONFIRM
,MSG_DONTROUTE
等)。
dest_addr
: 指向sockaddr
结构体的指针,这个结构体包含了目标地址的信息。对于 UDP 套接字,这通常是一个sockaddr_in
结构体,用于指定目标主机的 IP 地址和端口号。
addrlen
:dest_addr
指向的地址结构体的长度。对于sockaddr_in
结构体,通常是sizeof(struct sockaddr_in)
。返回值
- 成功时,
sendto
返回发送的字节数(即len
),如果数据包部分发送成功,那么返回值可能小于len
。- 失败时,返回
-1
,并设置errno
以指示错误原因。示例代码
以下是一个使用
sendto
函数的简单示例,演示了如何通过 UDP 发送数据:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h>int main() {int sockfd;struct sockaddr_in servaddr;char *message = "Hello, UDP server!";// 创建 UDP 套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 配置目标地址memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(12345); // 目标端口servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 目标 IP 地址// 发送数据ssize_t n = sendto(sockfd, message, strlen(message), 0,(struct sockaddr *)&servaddr, sizeof(servaddr));if (n < 0) {perror("sendto failed");close(sockfd);exit(EXIT_FAILURE);}printf("Sent %zd bytes to server\n", n);// 关闭套接字close(sockfd);return 0; }
上述代码创建了一个 UDP 套接字,配置了目标地址,并通过
sendto
函数将一条消息发送到指定的 IP 和端口。
九、创建服务端与客户端之间的网络通信
为什么客户端不需要bind?
临时性端口:客户端通常由操作系统自动为其分配一个临时的本地端口。客户端只需要指定目标服务器的 IP 地址和端口号,操作系统会为客户端选择一个合适的本地IP地址及本地端口来完成通信。
简单的连接模型:客户端主要是发起连接请求并接收响应。由于客户端是发起方,网络栈会自动处理本地端口的分配和管理。
基于对上述知识的了解以及UDP编程相关函数的认识,接下来我们依照上述UDP编程流程图来创建一个简易的服务端与客户端之间的网络通信。
首先,我们需要封装一个服务端类,该类的功能包括:初始化服务端、启动服务端、关闭服务端;而服务端通常是不允许被拷贝的,所以我们需要禁止编译器默认生成拷贝构造和赋值运算符重载。
服务端方法设计接口如下:
#define BUFFER_SIZE 1024const int temp_socketfd = -1;
const std::string temp_IP = "127.0.0.1"; //回环地址,表示计算机自身的网络接口,作为测试使用
const uint16_t temp_port = 8888; //自行设置的端口号,大小必须在1024~65535之间enum {SOCKET_ERROR = 1,BIND_ERROR,RECVFROM_ERROR,SENDTO_ERROR
};class Socket_Server
{
private:int _socketfd; //套接字描述符bool _isrunning;//服务器运行标志uint16_t _port; //16位端口号std::string _ip;//服务器IP地址【注意:暂时设置,后续可以进行更改】public:Socket_Server(const Socket_Server&) = delete;Socket_Server& operator=(const Socket_Server&) = delete;//构造函数Socket_Server(const std::string& IP = temp_IP, const uint16_t port = temp_port):_socketfd(temp_socketfd), _isrunning(false), _port(port), _ip(IP){}//初始化服务器void Init(){//1、使用scoket函数建立套接字。AF_INET代表IPv4网络协议,SOCK_DGRAM指定为数据报套接字,通常用于 UDP 协议。//2、使用bind 函数将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。}//启动服务器void Start(){//1、保持服务器不退出,使用recvfrom接收客户端信息//2、信息接收成功后使用sendto向客户端回复信息}//析构函数~Socket_Server(){//1、关闭套接字描述符}
};
1、初始化服务器
//初始化服务器void Init(){//1、使用scoket函数建立套接字。AF_INET代表IPv4网络协议,SOCK_DGRAM指定为数据报套接字,通常用于 UDP 协议。_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);//第三个参数指定为0即可,标识根据前面两个参数选择指定协议if(_socketfd < 0){perror("Create Socket False!!!");exit(SOCKET_ERROR);}std::cout << "Socket Success!!!" <<std::endl;//2、使用bind 函数将一个套接字(socket)与一个本地地址(IP 地址和端口)绑定起来。struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;//地址族,设置为IPv4local.sin_port = htons(_port);//16位主机序端口号转换为网络序local.sin_addr.s_addr = inet_addr(_ip.c_str());//local.sin_addr.s_addr = INADDR_ANY; //也可以这样设置,后续单独解释int ret = ::bind(_socketfd, (struct sockaddr*)&local, sizeof(local));if(ret == -1){perror("Bind IP And Port False!!!");exit(BIND_ERROR);}std::cout << "Bind Success!!!" <<std::endl;}
INADDR_ANY宏:
INADDR_ANY
的值为0.0.0.0
,表示任意地址。它的具体定义通常是一个 32 位的整数,表示 IPv4 地址中的任意地址。在进行套接字绑定IP地址的操作时,我们可以不绑定具体的地址。
当绑定具体的地址时,服务器仅接受从该特定 IP 地址发来的连接请求。
当不绑定到具体的 IP 地址,而使用
INADDR_ANY
时,服务器会监听来自主机所有网络接口的连接请求。local.sin_addr.s_addr = INADDR_ANY;
对于一台主机来讲,它可能有多个网卡(网络接口),可能是物理网卡或虚拟网卡。每个网卡都有各自的独立的IP地址。当客户端想访问服务端时,它需要拿到服务端进程绑定的IP地址和端口号。
但是对于一台主机而言,服务端进程的端口号是唯一的,但是IP地址却不止一个。当需要接收来自多个接口的连接请求时,将绑定地址设置为
INADDR_ANY
,如此,客户端发送的信息只要发送的目的IP地址属于服务端主机,并且目的端口号也是服务端进程的端口号,那么服务端就可以接收到向这台主机发送的所有的数据和请求。
2、运行服务器
//启动服务器void Start(){//1、保持服务器不退出,使用recvfrom接收客户端信息,并使用sendto向客户端回复信息_isrunning = true;char buffer[BUFFER_SIZE];memset(buffer, 0, sizeof(buffer));while(_isrunning){struct sockaddr_in from_client;memset(&from_client, 0, sizeof(from_client));socklen_t client_len = sizeof(from_client);size_t ret = ::recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&from_client, &client_len);if(ret == 0){_isrunning = false;std::cout << "Server Exit!!!" << std::endl;}else if(ret < 0){perror("Recvfrom False!!!"); exit(RECVFROM_ERROR);}else{buffer[ret] = '\0';//将接收的信息打印出来:std::string client_ip_addr = inet_ntoa(from_client.sin_addr);std::string inf = "Receive Information From Client IP " + client_ip_addr + " : " + buffer;std::cout << inf << std::endl;//向客户端反馈已经收到信息std::string res = "Server Echo : ";res += buffer;size_t ret = ::sendto(_socketfd, res.c_str(), res.size(), 0, (struct sockaddr*)&from_client, client_len);if(ret == -1){perror("Sendto False!!!"); exit(SENDTO_ERROR);}}}}
3、关闭服务器
在main函数中,对服务器对象的管理我们可以采取两种方式:1、使用智能指针管控,当服务端接到信号进行退出时,智能指针会及时合理释放服务端对象占用的内存空间。2、在栈上创建对象,当程序退出后,函数的栈帧销毁,服务器对象资源也随之被清理。
我们要做的就是确保对象销毁时将套接字的描述符进行关闭,以确保文件描述符表中的无用资源被清理,这一步在析构函数中进行:
//析构函数~Socket_Server(){//关闭套接字描述符if(_socketfd >= 0)::close(_socketfd);}
需要注意的是,套接字描述符实际也属于文件描述符,同样遵守文件描述符的规则,即:从文件描述符表中最小的空闲位置开始为其分配文件描述符。由于文件描述符表是一个指针数组,而文件描述符实际是这个数组的下标,这也注定了套接字描述符不会小于0!
客户端代码:
#pragma once
#include <string>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>#define BUFFER_SIZE 1024const int temp_socketfd = -1;
const std::string temp_IP = "127.0.0.1"; //回环地址,表示计算机自身的网络接口,作为测试使用
const uint16_t temp_port = 8888; //自行设置的端口号,大小必须在1024~65535之间enum{SOCKET_ERROR = 1,BIND_ERROR,RECVFROM_ERROR,SENDTO_ERROR
};class Client_Server
{
private:int _socketfd; //套接字描述符bool _isrunning;//服务器运行标志uint16_t server_port; //服务器16位端口号std::string server_ip;//服务器IP地址【注意:暂时设置,后续有更改】public:Client_Server(const Client_Server&) = delete;Client_Server& operator=(const Client_Server&) = delete;//构造函数Client_Server(const std::string& IP = temp_IP, const uint16_t port = temp_port):_socketfd(temp_socketfd), _isrunning(false), server_port(port), server_ip(IP){}//初始化客户端void Init(){//1、使用scoket函数建立套接字。AF_INET代表IPv4网络协议,SOCK_DGRAM指定为数据报套接字,通常用于 UDP 协议。_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);//第三个参数指定为0即可,标识根据前面两个参数选择指定协议if(_socketfd < 0){perror("Client Create Socket False!!!");exit(SOCKET_ERROR);}std::cout << "Socket Success!!!" << std::endl;}//启动服务器void Start(){_isrunning = true;char buffer[BUFFER_SIZE];memset(buffer, 0, sizeof(buffer));while(_isrunning){//1、客户端输入信息std::cout << "Please Enter : ";std::string enter;getline(std::cin, enter);//2、设置服务端的套接字的信息结构,使用sendto向目的IP和端口号发送信息struct sockaddr_in server_sock;memset(&server_sock, 0, sizeof(server_sock));server_sock.sin_family = AF_INET;//地址族,设置为IPv4server_sock.sin_port = htons(server_port);//16位主机序端口号转换为网络序server_sock.sin_addr.s_addr = inet_addr(server_ip.c_str());size_t send_num = sendto(_socketfd, enter.c_str(), enter.size(), 0, (struct sockaddr*)&server_sock, sizeof(server_sock));if(send_num < 0){perror("Client Sendto False!!!");exit(SENDTO_ERROR);}//3、使用recvfrom获取服务端的反馈信息,并获取服务端的套接字的信息结构struct sockaddr_in from_server;memset(&from_server, 0, sizeof(from_server));socklen_t len = sizeof(from_server);char buffer[BUFFER_SIZE];size_t recv_num = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&from_server, &len);if(recv_num < 0){perror("Client Recvfrom False!!!");exit(RECVFROM_ERROR);}buffer[recv_num] = '\0';//4、将接收的信息打印出来:std::string client_ip_addr = inet_ntoa(from_server.sin_addr);std::string inf = "Receive Information From Server IP " + client_ip_addr + " : " + buffer;std::cout << inf << std::endl;}}//析构函数~Client_Server(){//关闭套接字描述符if(_socketfd >= 0)::close(_socketfd);}
};
因为客户端向服务端发送信息时需要指定服务端的IP地址和端口号,因此,在客户端的main函数中可以采取命令行参数的方式从命令行输入中获取用户主动输入的IP地址和端口号。
由于格式不同,使用相应的格式转换函数进行转换即可。
#include "UDP_Client.hpp"int main(int argc, char* argv[]) {if(argc < 3){std::cerr << "Usage: " << argv[0] << " server-ip and server-port not provided" << std::endl;exit(0);}Client_Server client(argv[1], std::stoi(argv[2]));client.Init();client.Start();return 0; }
Makefile:
.PHONY:all
all:server clientserver:Server_Main.ccg++ -o $@ $^ -std=c++14client:Client_Main.ccg++ -o $@ $^ -std=c++14.PHONY:clean
clean:rm -f server client
效果展示: