Socket编程入门:UDP服务器与客户端 (纯干货)
Socket编程入门:UDP服务器与客户端 (纯干货)
目录
Socket编程入门:UDP服务器与客户端 (纯干货)
一、UDP核心特点
二、Socket编程的UDP核心函数
三、UDP通信流程
客户端
服务器
四、UDP通信中值得注意的几个细节
五、简单的UDP服务器与客户端代码示例(详细注释)
客户端
服务器
关于Socket编程的TCP服务器和客户端可以查看Socket编程快速入门(全面干货)。
一、UDP核心特点
- 无连接:无需建立连接,直接发送数据包。
- 面向数据报:每次收发固定大小的数据块(报文)。
- 不可靠性:不保证顺序、不保证送达、不重传。
UDP方式相比于TCP方式,UDP方式不需要先建立连接,可以立即给对端发送数据,收发效率相比于TCP方式更高,但UDP方式不保证发送数据的可靠性,并以数据报文方式收发,适用于对数据的完整性要求不高,但对实时性要求高或数据量小的服务,例如实时音视频、DNS查询、广播/组播方式等。
二、Socket编程的UDP核心函数
1.socket()函数创建一个遵循某种传输协议的套接字
int socket(int domain, int type, int protocol);
SOCKET socket(int af,int type,int protocal);(Windows),SOCKET类型实质为int类型
socket函数返回得到的套接字socket本质上是一个内核内存的文件描述符fd,网络通信基于该文件描述符通信,下面是参数介绍:
domain: 使用的地址族协议枚举值:AF_INET/PF_INET: 使用IPv4格式的ip地址;AF_INET6/PF_INET6: 使用IPv6格式的ip地址
type:使用的传输方式枚举值:SOCK_STREAM: 使用流式的传输协议,对应于TCP方式;SOCK_DGRAM: 使用报式(报文)的传输协议
protocol: 一般写0使用默认的type类型传输协议:SOCK_STREAM: 流式传输默认使用的是tcp;SOCK_DGRAM: 报式传输默认使用的udp
返回值:成功返回大于0的套接字socket,失败返回-1
2.bind()函数绑定服务器进程和本地服务器的IP地址与固定端口号
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int bind(SOCKET sockfd,const struct sockaddr FAR* addr, int addrlen);(Windows)
该函数的作用是将socket()函数创建的socket与本地IP地址和端口号绑定,以便于本进程根据该IP地址和端口号对外访问和被访问,一般用于服务器进程,下面是参数介绍:
sockfd: 用于绑定本地服务的IP地址和端口号, 即通过socket()调用得到的返回值
addr: struct sockaddr类型的传入参数,存储要绑定本地的网络字节序的IP地址和固定端口号,一般先初始化struct sockaddr_in类型后,地址强制转化为struct sockaddr*类型,一般用于服务器进程和本地IP地址和端口号绑定,IP地址一般取INADDR_ANY(即"0.0.0.0"),表示服务器本地任一IP地址,端口号选择在1024-49151的任一不冲突的注册端口号。
addrlen: 参数addr指向的内存大小, 即sizeof(struct sockaddr)可以得到,socklen_t类型为有符号整数类似于int,专门用于套接字长度参数的数据类型。
返回值:成功返回0,失败返回-1
3.sendto()函数:UDP方式通过套接字socket文件描述符接收网络通信的数据
ssize_t sendto(int sockfd,void *buf,size_t len,int flags, struct sockaddr *to, socklen_t addrlen);
在UDP方式下,客户端和服务器通过该函数向对端发送数据,本质是先写入到sockfd的内核发送缓冲区,然后系统再从发送缓冲区将数据发出,下面是参数介绍。
sockfd:用于进行通信的套接字文件描述符,即socket()函数的返回值,客户端和服务器均通过其进行数据的发送和接收
buf:指向一块有效的内存地址,其存储要发送的数据。
len:要发送的实际buf缓冲区数据大小,以字节为单位
flags:特殊的属性, 一般不使用, 指定为 0
to:传入参数,UDP方式不需要先连接,因此每次发送数据前都要显示填写要发送对端的IP地址和端口号构成struct sockaddr类型
addrlen:传入参数,指定to结构体大小,使用sizeof(struct sockaddr)即可
返回值:成功时返回传输的字节数,失败时返回-1
4.recvfrom()函数:UDP方式通过套接字socket文件描述符接收网络通信的数据
ssize_t recvfrom(int sockfd, void *buf,size_t len, int flags, struct sockaddr* from, socklen_t* addrlen);
在UDP方式下对端通过sendto函数寻址将数据发送到本机后,客户端和服务器通过该函数从对端读取数据,本质是系统读取对端网络数据到sockfd的内核发送缓冲区,然后该函数将内核发送缓冲区数据读取到buf中
sockfd:用于进行通信的套接字文件描述符,即socket()函数的返回值
buf:指向一块有效的内存地址,其内部空闲用于储存读取的对端网络数据。
len:可接收的实际buf缓冲区数据大小,以字节为单位
flags:特殊的属性, 一般不使用, 指定为 0
from:传出参数,用于读取对端将数据发送到本机时保存对端的sock地址信息struct sockaddr,由函数填入的,用于验证接收的数据是否来自指定服务器。
addrlen:传入传出参数,需要初始化为from类型字节数大小,传出获取到的from大小
返回值:成功时返回实际接收到的数据字节数,失败时返回-1,并设置相应的errno。
5.close()函数:关闭套接字socket文件描述符的网络连接:
int close(int sockfd);
int closesocket (SOCKET sockfd);(Windows)
客户端和服务器都要分别使用close()函数关闭单向连接,从而关闭双向连接,由客户端先使用close()函数进行断开连接,服务器才会使用close()函数断开连接。
返回值:成功返回0,失败返回-1
sockfd:套接字文件描述符,即客户端和服务器通过socket()函数创建并进行通信的sockfd
关于TCP的Socket编程的核心函数和其他Socket使用到的相关函数和类型如:struct sockaddr,struct sockaddr_in等,可以查看另一篇文章:Socket编程快速入门(全面干货),后续关于C/C++技术栈的内容我会一直进行干货总结更新。
三、UDP通信流程
客户端
- 创建Socket文件描述符通信:sockfd =
socket(AF_INET, SOCK_DGRAM,0) - 给服务器发送数据:
sendto(sockfd ,data, len, 0, (struct sockaddr*)&server_sockaddrin, sizeof(struct sockaddr)),需要先创建并初始化的struct sockaddr_in变量。 - 接收服务器响应数据:
recvfrom(sockfd ,buf, len, 0, (struct sockaddr*)&server_sockaddrin, sizeof(struct sockaddr)),这里的server_sockaddrin不需要初始化是由函数填入的,用于验证接收的数据是否来自指定服务器。 - 通信结束后,关闭Socket:
close(sockfd)
服务器
- 创建Socket(同上)
- 绑定sockfd和服务器本地IP地址和端口号:
bind(sockfd, (struct sockaddr*)&sever_sockaddrin, sizeof(struct sockaddr)),sever_sockaddrin是sockaddr_in类型,需要初始化为本地服务器IP地址和端口号。 - 接收客户端发送的数据:
recvfrom(sockfd ,buf, len, 0, (struct sockaddr*)&client_sockaddrin, sizeof(struct sockaddr)),这里的client_sockaddrin不需要初始化是由函数填入的,用于保存客户端地址,用于服务器给该客户端发送数据。 - 给客户端发送数据回应:
sendto(sockfd ,data, len, 0, (struct sockaddr*)&client_sockaddrin, sizeof(struct sockaddr)),这里的client_sockaddrin时要发送给的客户端地址,即由3中recvfrom函数保存的客户端地址。 - 关闭Socket(同上)
四、UDP通信中值得注意的几个细节
1.UDP协议是面向数据报的,和TCP协议面向字节流不同,UDP的每次发送和接收都是一个完整的UDP数据包,不会出现类似于TCP协议的粘包问题(多个数据可能被一次读取)。UDP方式在接收数据报过大,大于用户接收缓冲区时会出现数据截断问题。因此要保证对端用户发送缓冲区比用户接收缓冲区小,以避免接收数据时出现截断问题。
2.recvfrom/sento函数在socket阻塞和非阻塞情况下的表现状态和返回值
1.sendto函数用于将用户待发送缓冲区数据buf传输到socket内核发送缓冲区中,然后由系统将内核发送缓冲区数据发送到网络。
①当内核发送缓冲区剩余空间小于用户待发送缓冲区buf时,默认阻塞socket会阻塞直到内核发送缓冲区可以容纳,并且不会部分写入。在非阻塞情况下,会直接返回-1。
②当内核发送缓冲区剩余空间大于用户待发送缓冲区buf时,无论socket阻塞不阻塞,都会将buf数据写入内核发送缓冲区,并返回实际写入数据长度。
2.recvfrom函数用于从socket内核接收缓冲区数据读取到用户接收缓冲区中
①当内核接收缓冲区数据为空时,默认阻塞情况下的recvfrom函数会直接阻塞,直到内核接收缓冲区接收到对端发送的数据。在非阻塞情况下,recvfrom函数会直接返回-1。
②在内核接收缓冲区数据不为空时,无论socket阻塞不阻塞情况,recvfrom函数会直接读取内核接收缓冲区数据到用户接收缓冲区buf中,如果buf过小时,则会出现截断问题,并返回实际读入的字节数。
3.UDP协议是不需要先连接的,每次客户端和服务器的发送和接收都需要显示保存对端服务器地址,因此客户端的UDP套接字socket允许同时向多个UDP服务器发送UDP数据包,并通过接收时保存的UDP服务器地址进行区分。服务器必须在接收客户端UDP数据包时显式保存客户端地址,以避免给客户端发送UDP数据时找不到客户端。
五、简单的UDP服务器与客户端代码示例(详细注释)
客户端
基本思路:按照UDP客户端流程搭建,并实验两次连接服务器时分别采用本机作为服务器的两个本机IP地址:192.168.137.3和127.0.0.1,由于服务器绑定0.0.0.0,因此可以同时监听本机所有ipv4地址,其中192,168.137.3要根据自己电脑实际ip地址查看,使用命令ifconfig,127.0.0.1是必有。
#include
#include
#include
#include
int main()
{
// 1.创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
printf("client:%d socket() error! exit.
", getpid());
return;
}
// 客户端与服务器循环通信
for (int i = 0; i < 2; i++) {
// 2.客户端向服务器发送UDP数据
// 服务器sock地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
// 模拟客户端UDP套接字无连接特性,访问多个服务器,其实还是同一个服务器的UDP服务
if (i % 2 == 0)
server_addr.sin_addr.s_addr = inet_addr("192.168.137.3");
else
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = 20086;
// 发送数据
char sdbuf[512];
sprintf(sdbuf, "Hello server:%s:%d form client:%d.", inet_ntoa(server_addr.sin_addr), server_addr.sin_port, getpid());
int sendbytes = sendto(sockfd, sdbuf, strlen(sdbuf), 0, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
// 根据返回值判断状态
if (sendbytes == -1) {
// 发送失败,出现未知错误,直接关闭套接字
printf("client:%d sendto error! exit.
", getpid());
close(sockfd);
return;
}
else
printf("client:%d sendto server:%s:%d number:%d data.
", getpid(), inet_ntoa(server_addr.sin_addr), server_addr.sin_port, sendbytes);
// 3.客户端等待服务器发送的UDP数据
char rvbuf[1024]; // 用户接收缓冲区
memset(rvbuf, 0, sizeof(rvbuf));
struct sockaddr_in rv_server_addr; // 使用前清空初始化
memset(&rv_server_addr, 0, sizeof(rv_server_addr)); // 使用前清空初始化
socklen_t rv_serveraddr_len = sizeof(rv_server_addr);
// 等待接收客户端数据
int recvbytes = recvfrom(sockfd, rvbuf, sizeof(rvbuf), 0, (struct sockaddr*)&rv_server_addr, &rv_serveraddr_len);
// 根据返回值和接收的服务器地址判断,接收数据是否正确
if (recvbytes == -1) {
// 接收数据错误,直接关闭
printf("client:%d recvfrom error! exit.
", getpid());
close(sockfd);
return;
}
else
printf("client:%d recvfrom server:%s:%d number:%d data:%s
", getpid(), inet_ntoa(rv_server_addr.sin_addr), rv_server_addr.sin_port, recvbytes, rvbuf);
}
// 4.正常结束,关闭客户端套接字
close(sockfd);
return 0;
}
服务器
基本思路:按照UDP服务器流程创建,并进行循环通信,ctrl+c终止
#include
#include
#include
#include
int main()
{
// 1.创建UDP方式的socket套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
printf("main process socket() error, exit!
");
// 回收子进程退出
int spid; // 保存回收子进程PID
int status; // 保存回收子进程的状态
while ((spid = waitpid(-1, &status, WNOHANG)) > 0)
printf("main process recycle son process:%d.
", spid);
return;
}
// 2.服务器绑定本地IP地址和端口号
// 创建本地IP地址和端口号的struct sockaddr_in变量
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 服务器绑定本地任意IP地址,均允许客户端访问
server_addr.sin_port = 20086; // 绑定任意一个未使用的注册端口号
int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
printf("main process bind() error, exit!
");
// 回收套接字
close(sockfd);
// 回收子进程退出
int spid; // 保存回收子进程PID
int status; // 保存回收子进程的状态
while ((spid = waitpid(-1, &status, WNOHANG)) > 0)
printf("main process recycle son process:%d.
", spid);
return;
}
// 服务器循环接收客户端发送的UDP数据并回复
while (1) {
// 3.recvfrom函数等待接收客户端的UDP数据
char rvbuf[1024]; // 用户读缓冲区
memset(rvbuf, 0, sizeof(rvbuf));
struct sockaddr_in client_addr; // 保存客户端sock地址
memset(&client_addr, 0, sizeof(client_addr));
socklen_t clientaddr_len = sizeof(struct sockaddr);
// 等待接收数据
int recvbytes = recvfrom(sockfd, rvbuf, sizeof(rvbuf), 0, (struct sockaddr*)&client_addr, &clientaddr_len);
// 根据recvfrom函数的返回值判断接收状态
if (recvbytes == -1) {
// 发生错误,直接进行下个客户端数据读取
printf("server recvfrom error!
");
memset(&client_addr, 0, sizeof(client_addr)); // 清空
memset(rvbuf, 0, sizeof(rvbuf));
clientaddr_len = 0;
continue; // 直接进行下个客户端数据的读取
}
else
printf("server recvfrom client:%s:%d numbers:%d data:%s
", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), recvbytes, rvbuf);
// 4.sendto函数服务器向客户端发送UDP数据
char sdbuf[] = "Hello client from server.";
// 发送数据
int sdbytes = sendto(sockfd, sdbuf, sizeof(sdbuf), 0, (struct sockaddr*)&client_addr, clientaddr_len);
// 根据sendto函数的返回值判断发送状态
if (sdbytes == -1) {
// 发生错误,直接进行下个客户端数据读取
printf("server sendto error!
");
memset(&client_addr, 0, sizeof(client_addr)); // 清空
memset(rvbuf, 0, recvbytes);
clientaddr_len = 0;
continue; // 直接进行下个客户端数据的读取
}
else
printf("server sendto client:%s:%d numbers:%d data.
", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), sdbytes);
}
// 5.关闭套接字,服务器结束工作
close(sockfd);
return 0;
}











