网络编程学习——4
一、UDP概述
无连接、不可靠、有失序、数据报格式
广播地址
主机号全为255的为广播地址
ip地址分类
A类:前1段位网络号,后3段位主机号
0.0.0.0——127.255.255.255
B类:前2段网络号,后2段主机号
128.0.0.0——191.255.255.255
C类:前3段网络号,后1段主机号
192.0.0.0——223.255.255.255
D类:组播地址
224.0.0.0——239.255.255.255
二、UDP通信
1.单播
(1)流程
服务器
1.创建套接字
2.绑定套接字
3.通信
while(1) { 4.接收数据 recvfrom }
客户端
1.创建套接字
2.绑定(可以省略)
3.通信
while(1) { 4.发送消息 sendto }
(2)recvfrom函数
函数功能
用于接收数据报(UDP 协议为主)的系统调用
函数原型
#include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
函数参数
参数 含义 sockfd
套接字描述符 buf
接收数据的缓冲区 len
接收字节数 flags
接收方式标志(通常为 0) src_addr
输出参数,用于存储发送方的地址信息( struct sockaddr
类型指针)addrlen
输入输出参数: - 输入: src_addr
缓冲区的大小(sizeof(*src_addr)
) - 输出:实际存储的地址长度
返回值
- 成功:返回接收到的字节数(≤
len
)。- 失败:返回
-1
,并设置errno
(如EAGAIN
表示非阻塞模式下无数据,EBADF
表示无效套接字)。- 连接关闭:TCP 中返回
0
(UDP 无连接,不会返回 0)。
(3)sendto函数
函数功能
发送数据报的系统调用,主要用于 无连接协议(如 UDP)
函数原型
#include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
函数参数
参数 含义 sockfd
套接字描述符(UDP 通常无需连接) buf
待发送数据的缓冲区 len
待发送数据的长度(字节),不能超过缓冲区大小 flags
发送方式标志(通常为 0) dest_addr
目标地址信息( struct sockaddr
类型指针,包含 IP 和端口)addrlen
dest_addr
结构体的大小(字节),通常为sizeof(*dest_addr)
返回值
- 成功:返回实际发送的字节数(≤
len
)。- 失败:返回
-1
,并设置errno
(如EINVAL
表示参数无效,ENETUNREACH
表示网络不可达)。
(4)UDP双向通信 示例
CA.c源码
/*===============================================
* 文件名称:CA.c
* 创 建 者:青木莲华
* 创建日期:2025年08月19日
* 描 述:UDP双向通信 A端
================================================*/
#include "socket_head.h"
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
char buf[1024] = {0};
if(sockfd == -1)
{
perror("socket");
return -1;
}
//2.初始化结构体 绑定套接字
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("192.168.6.174");
socklen_t len = sizeof(addr);
if(-1 == bind(sockfd,(struct sockaddr*)&addr,len))
{
perror("bind");
return -1;
}
//开子进程用来接收数据
pid_t pid = fork();
if(-1 == pid)
{
perror("fork");
return -1;
}
else if(0 == pid) //子进程
{
while(1)
{
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,&len);
printf("%s[%hd] : %s",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port),buf);
}
}
else //父进程
{
addr.sin_port = htons(7777);
while(1)
{
fgets(buf,sizeof(buf),stdin);
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,len);
}
}
return 0;
}
CB.c源码
/*===============================================
* 文件名称:CB.c
* 创 建 者:青木莲华
* 创建日期:2025年08月19日
* 描 述:UDP双向通信B端
================================================*/
#include "socket_head.h"
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
char buf[1024] = {0};
if(sockfd == -1)
{
perror("socket");
return -1;
}
//2.初始化结构体 绑定套接字
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(7777);
addr.sin_addr.s_addr = inet_addr("192.168.6.174");
socklen_t len = sizeof(addr);
if(-1 == bind(sockfd,(struct sockaddr*)&addr,len))
{
perror("bind");
return -1;
}
//开子进程用来接收数据
pid_t pid = fork();
if(-1 == pid)
{
perror("fork");
return -1;
}
else if(0 == pid) //子进程
{
while(1)
{
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,&len);
printf("%s[%d] : %s",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port),buf);
}
}
else //父进程
{
addr.sin_port = htons(6666);
while(1)
{
fgets(buf,sizeof(buf),stdin);
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,len);
}
}
return 0;
}
2.组播
组播可以跨网络
用D类地址 224.0.0.0---239.255.255.2555
发送方:
允许发送组播,向组播地址发送信息(需要路由器等设备支持组播)
接收方:
加入组播
setsockopt函数设置组播
IP_MULTICAST_IF --- 是否允许组播
IP_MULTICCAST_TTL --- Server加入组播
IP_ADD_MEMBERSHIP/IP_DROP_MEMBERSHIP --- 接收端 加入/退出组播
示例
发送端
/*===============================================
* 文件名称:mcs.c
* 创 建 者:青木莲华
* 创建日期:2025年08月19日
* 描 述:组播发送端
================================================*/
#include "socket_head.h"
int main(int argc, char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
char buf[1024] = {0};
if(sockfd == -1)
{
perror("socket");
return -1;
}
//2.初始化结构体
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("225.2.2.2");
//3.设置发送组播网络接口
struct in_addr in;
in.s_addr = inet_addr("0.0.0.0");
if(-1 == setsockopt(sockfd,IPPROTO_IP,IP_MULTICAST_IF,&in,sizeof(in)))
{
perror("sockopt");
return -1;
}
while(1)
{
fgets(buf,sizeof(buf),stdin);
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,sizeof(addr));
}
return 0;
}
接收端
/*===============================================
* 文件名称:mcc.c
* 创 建 者:青木莲华
* 创建日期:2025年08月19日
* 描 述:组播接收端
================================================*/
#include "socket_head.h"
int main(int argc, char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
char buf[1024] = {0};
if(sockfd == -1)
{
perror("socket");
return -1;
}
//2.初始化结构体 绑定套接字
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(addr);
if(-1 == bind(sockfd , (struct sockaddr*)&addr,len))
{
perror("bind");
return -1;
}
//3.设置 ip_mreq 并 添加进组播
struct ip_mreq req;
req.imr_multiaddr.s_addr = inet_addr("225.2.2.2"); //组播地址
req.imr_interface.s_addr = inet_addr("0.0.0.0"); //本地IP地址
if(-1 == setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&req,sizeof(req)))
{
perror("sockopt");
return -1;
}
while(1)
{
if(-1 == recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,&len))
{
perror("recvfrom");
return -1;
}
printf("%s[%d] : %s",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port),buf);
}
return 0;
}
3.广播
发送方:
发送地址为广播地址(主机地址全为255)
允许发送广播消息
接收方:
绑定广播地址 x.x.x.255 或 0.0.0.0
广播函数:setsockopt(设置套接字)
//设置允许广播,val为int类型 值为1 setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val))
示例
server
/*===============================================
* 文件名称:bcc.c
* 创 建 者:青木莲华
* 创建日期:2025年08月19日
* 描 述:广播通信 Client 接收端
================================================*/
#include "socket_head.h"
int main(int argc, char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
char buf[1024] = {0};
if(-1 == sockfd)
{
perror("socket");
return -1;
}
//2.初始化结构体 绑定套接字
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(addr);
if(-1 == bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)))
{
perror("bind");
return -1;
}
//3.通信
while(1)
{
if(-1 == recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,&len))
{
perror("recvfrom");
return -1;
}
printf("%s[%d] : %s",
inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port),buf);
}
return 0;
}
client
/*===============================================
* 文件名称:bcc.c
* 创 建 者:青木莲华
* 创建日期:2025年08月19日
* 描 述:广播通信 Client 接收端
================================================*/
#include "socket_head.h"
int main(int argc, char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
char buf[1024] = {0};
if(-1 == sockfd)
{
perror("socket");
return -1;
}
//2.初始化结构体 绑定套接字
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(addr);
if(-1 == bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)))
{
perror("bind");
return -1;
}
//3.通信
while(1)
{
if(-1 == recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&addr,&len))
{
perror("recvfrom");
return -1;
}
printf("%s[%d] : %s",
inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port),buf);
}
return 0;
}
4.setsockopt函数(设置套接字)
函数功能
用于设置套接字选项的系统调用(设置允许广播、组播、加入组播、退出组播等)
函数原型
#include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
函数参数
参数 含义 sockfd
套接字描述符 level
选项所属的协议层级(如通用套接字层、TCP 层、IP 层等) optname
层级下的具体的选项名称(与 level
对应,如SO_REUSEADDR
、TCP_NODELAY
等)optval
指向选项值的指针(根据选项类型传递不同数据,如 int
、struct timeval
等)optlen
optval
所指向数据的长度(字节数)
返回值
- 成功:返回
0
。- 失败:返回
-1
,并设置errno
(如EBADF
表示无效套接字,ENOPROTOOPT
表示不支持的选项)。
5.setsockopt 的常用level表
level 常量 | 含义说明 | 常用 option 选项 | option 含义说明 | 数据类型(optval) |
---|---|---|---|---|
SOL_SOCKET |
套接字级别(通用选项) | SO_BROADCAST |
允许发送广播消息 (仅 UDP 有效) |
int(0 禁用,1 启用) |
SO_REUSEADDR |
允许重用本地地址和端口 |
int(0 禁用,1 启用) | ||
SO_SNDTIMEO |
设置发送操作超时时间 |
struct timeval | ||
SO_RCVTIMEO |
设置接收操作超时时间 |
struct timeval | ||
SO_DEBUG | 启用调试信息(仅内核使用) | int(0 禁用,1 启用) | ||
SO_DONTROUTE | 禁用路由查找(仅发送到本地网络) | int(0 禁用,1 启用) | ||
IPPROTO_IP |
IPv4 协议级别 | IP_MULTICAST_IF |
指定发送IPV4组播的网络接口 (多网卡情况下) |
struct in_addr |
IP_ADD_MEMBERSHIP |
加入指定的多播组(组播) |
struct ip_mreq | ||
IP_DROP_MEMBERSHIP |
退出指定的多播组(组播) |
struct ip_mreq | ||
IP_MULTICAST_TTL | 设置多播数据包的 TTL 值(0 表示仅限本地,1 表示本地网络) | int(范围 0-255) | ||
IP_MULTICAST_LOOP | 控制多播数据包是否回环(本地发送者是否接收自己的多播数据) | int(0 禁用,1 启用) | ||
IP_TTL | 设置 IPv4 数据包的 TTL(生存时间)值 | int(默认 64,范围 1-255) | ||
IPPROTO_IPV6 | IPv6 协议级别 | IPV6_TTL | 设置 IPv6 数据包的 Hop Limit(类似 TTL) | int(默认 64,范围 1-255) |
IPV6_MULTICAST_HOPS | 设置 IPv6 多播数据包的 Hop Limit | int(范围 0-255) | ||
IPV6_MULTICAST_LOOP | 控制 IPv6 多播数据包是否回环 | int(0 禁用,1 启用) | ||
IPV6_JOIN_GROUP | 加入 IPv6 多播组 | struct ipv6_mreq | ||
IPV6_LEAVE_GROUP | 退出 IPv6 多播组 | struct ipv6_mreq | ||
IPV6_V6ONLY | 限制套接字仅使用 IPv6(不接受 IPv4 映射地址) | int(0 禁用,1 启用) | ||
IPPROTO_UDP | UDP 协议级别 | UDP_CSUM | 启用 UDP 校验和计算(部分系统默认启用) | int(0 禁用,1 启用) |
ip_mreq结构体
#include <netinet/in.h>
struct ip_mreq {
struct in_addr imr_multiaddr; // 多播组的 IP 地址(如 224.0.0.1)
struct in_addr imr_interface; // 本地网络接口的 IP 地址(或 INADDR_ANY 表示默认接口)
};
6.超时检测
1.发送/接收超时
SO_SNDTIMEO 发送超时
SO_RCVTIMEO 接收超时
#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd) { perror("socket"); return -1; } struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(6666); //s_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); s_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //允许重用本地地址和端口 int val = 1; if(-1 == setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR, &val, sizeof(int))) { perror("opt"); return -1; } struct timeval t_val; t_val.tv_sec = 5; //设置接收操作超时时间 if(-1 == setsockopt(sockfd, SOL_SOCKET,SO_RCVTIMEO, &t_val, sizeof(t_val))) { perror("opt"); return -1; } if(-1 == bind(sockfd, (struct sockaddr*)&s_addr, sizeof(s_addr))) { perror("bind"); return -1; } if(-1 == listen(sockfd, 5)) { perror("listen"); return -1; } socklen_t len = sizeof(s_addr); int connfd = accept(sockfd, (struct sockaddr*)&s_addr, &len); if(-1 == connfd) { perror("accept"); return -1; } printf("client ip=%s port=%d\n", inet_ntoa(s_addr.sin_addr), ntohs(s_addr.sin_port)); while(1) { char buf[100]; read(connfd, buf, sizeof(buf)); puts(buf); } }
2.IO多路复用
#include "head.h" int main() { //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd) { perror("socket"); return -1; } //2.绑定 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(6666); addr.sin_addr.s_addr = inet_addr("0.0.0.0"); if(-1 == bind(sockfd, (struct sockaddr*)&addr, sizeof(addr))) { perror("bind"); return -1; } //3.监听 if(-1 == listen(sockfd, 5)) { perror("listen"); return -1; } //4.创建位图 存储关心的 用来拷贝的 fd_set readfds, tempfds; //5.清空位图 FD_ZERO(&readfds); //6.把socket加入位图 关心的 FD_SET(sockfd, &readfds); int nfds = sockfd+1; struct timeval val = {10}; while(1) { //7.select 把拷贝的位图拷贝到内核, 内核轮询判断是否就绪, 有就绪 就拷贝回用户空间 tempfds = readfds; int ret; //设置超时 val. tv_sec = 10; if(-1 == (ret= select(nfds, &tempfds, NULL, NULL, &val))) { perror("select"); return -1; } if(ret == 0) { printf("timeout\n"); continue; } //8.遍历位图,处理就绪的文件描述符 int i; for(i = 0; i < nfds; i++) { if(FD_ISSET(i, &tempfds)) { if(i == sockfd) { //如果是sockfd就应该做连接 int connfd = accept(i, NULL, NULL); if(-1 == connfd) { perror("accept"); return -1; } //有新的连接,就需要把通信套节子加入位图 FD_SET(connfd, &readfds); //比较connfd+1和nfds大小,如果大于就需要更新nf connfd+1 > nfds ? (nfds = connfd+1) : (nfds = nfds); } else { //如果不是sockfd就应该做通信 char buf[100]; if(0 == recv(i, buf, sizeof(buf), 0)) { //更新位图,把对应文件描述符在位图中清空 FD_CLR(i, &readfds); //关闭对应文件描述符 close(i); } puts(buf); } } } } close(sockfd); }
3.alarm signal
#include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <signal.h> //超时函数 void sighanderl(int sig) { if(sig == SIGALRM) printf("timeout\n"); } int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd) { perror("socket"); return -1; } struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(6666); s_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); if(-1 == bind(sockfd, (struct sockaddr*)&s_addr, sizeof(s_addr))) { perror("bind"); return -1; } if(-1 == listen(sockfd, 5)) { perror("listen"); return -1; } socklen_t len = sizeof(s_addr); signal(SIGALRM, sighanderl); while(1) { //定时器 alarm(5); int connfd = accept(sockfd, (struct sockaddr*)&s_addr, &len); if(-1 == connfd) { perror("accept"); return -1; } printf("client ip=%s port=%d\n", inet_ntoa(s_addr.sin_addr), ntohs(s_addr.sin_port)); char buf[100]; puts(buf); } }
三、本地套接字/Unix域套接字
1.在地址族选择 AF_UNIX
2.结构体
struct sockaddr_un { sa_family_t sun_family; //AF_UNIX char sun_path[108]; //路径 }
3.不能存在多个(套接字文件)
4.sendmsg/recvmsg
5.传递文件描述符(例如 传递证书)