Socket编程UDP
UDP是一种无连接、面向数据报、不可靠的传输层协议。具有不建立连接,不保证到达,不保证顺序,不重传,不拥塞控制,速度快,开销小 的特点。
我们想要网络通信,想要UDP的编写,我们想要以网络收发的话首先得把网络文件打开.
创建一个套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:第一个参数是一个域的概念,表明所创建出来的套接字是本地通信还是网络通信,AF_UNIX表示本地通信,AF_INET表示网络通信。
type:表示所要创建的套接字的类型,最常见的有两种UDP和TCP
protocol:表示我们要设定的协议类型。
返回值:创建成功返回一个文件描述符,创建失败-1被返回
为套接字绑定一个端口号
创建套接字相当于打开了文件,将来别人给我们发消息时,我们的ip是多少,端口号是多少别人并不清楚,而且这个网络文件并没有套接字相关的信息,所以我们需要把我们刚刚打开的网络文件和套接字信息进行绑定。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:就是我们刚才创建socket时对应的返回值。
sockaddr *addr:sockaddr结构
addrlen:第二个结构体参数的大小
返回值:绑定成功0被返回,否则-1被返回
清零一个缓冲区
我是一个服务器,将来客户端要给我发消息,我也要给客户端发消息,我会不会把我的端口号和ip地址也发给对方?
IP地址的信息和端口号信息一定要发送给网络!
而当前我们定义的IP地址和端口号在我们本地存储,属于本地存储格式。
所以我们就要把我们的数据从本地格式转化成网络序列
IP地址属于4字节风格的IP地址。而我们服务器里面保存的ip地址用的是字符串风格的ip地址,点分十进制,所以在IP地址这里我们不仅仅要做本地风格转网络序列,我们还要将IP地址转成4字节。
所以:a.将IP地址转成4字节。b.将4字节转成网络序列。
inet_addr
这一个函数就将上面的两步工作全做了.
UDP套接字的初始化部分
void Init()
{
//1. 创建套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0); //创建套接字
if(_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket error,sockfd:" << _sockfd;
//2.绑定套接字信息 IP+port
//2.1 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;//协议家族,表示网络通信
local.sin_port = htons(_port);
//ip地址也是如此
local.sin_addr.s_addr = inet_addr(_ip.c_str()); //结构体不能直接赋值
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local)); //绑定
if(n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success,sockfd:" << _sockfd;
}
为什么要用htons?
因为网络字节序是大端
启动(Start)
我们使用的大部分软件,一旦打开都是不会自动退出的。死循环
我们对应的服务端一要收消息,二要发消息
收消息
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
//成功返回实际收到的字节数。-1表示出错
sockfd:我们刚刚创建的套接字
*buf,len:用户自定义的缓冲区和该缓冲区的长度。把收来的数据放入缓冲区中
flags:阻塞式的读或者非阻塞式的读。默认设置为0表示阻塞式的IO
*src_addr:输出型参数,要求我们用户传递一个sockaddr_in结构
*addrlen:既是输入又是输出,作为输入时表明用户传进来的结构体的大小。返回时,即输出时,是实际读到的信息的大小
返回值;成功返回实际收到的数据的字节数,失败返回-1
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//1. 收消息
size_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s > 0)
{
buffer[s] = 0; //最后一位置0
LOG(LogLevel::DEBUG) << "buffer:" << buffer;
//2. 发消息
}
发消息
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
UDP的sockfd既可以读又可以写。UDP通信其实是全双工的。
sockfd:
*buf,len:待发送数据的起始地址和长度。
flags:为0表示阻塞式发送
*dest_addr,addrlen:表示消息要发送给谁
发送成功返回实际发送的字节数,失败返回-1
//2. 发消息
std::string echo_string = "server echo@: ";
echo_string += buffer;
sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);
所以未来客户端给服务器发消息,服务器就能直接收到,所以UDP不面向连接。
Client访问目标服务器要知道服务器的什么??
必须知道服务器的IP地址和端口号
但是作为服务端,我怎么指代服务器端的ip地址和端口号呢?
客户端和服务器是一家公司写的,所以客户端就内置了服务端的ip和端口号
// 问题:client要不要绑定,client要不要显示的绑定?
//答案是client是需要绑定的,但是client端不需要我们显式的调用bind 函数
在我们首次使用UDP发送消息的时候,调用sendto这样的接口的时候,操作系统会自动给我们的客户端进行绑定。
1.客户端的ip地址操作系统是知道的
2.客户端的端口号采用随机端口号的方式
为什么?
一个端口号只能被一个进程绑定,
为什么要让客户端进行随机的绑定端口号呢?
答案是如果让我们今天显式的去bind,其实显式的去bind也是可以的,但是我们不推荐这样干,我们客户端采用随机端口的方式是为了保证客户端的端口号不发生冲突。
结论:客户端采用随机端口号的方式,解决端口号冲突的问题
客户端的端口号是几不重要,知道保证是唯一的就行。
为什么服务器端需要显式的绑定呢?
服务器端将来会有很多的客户端来访问服务端,所以服务端的端口号和ip地址必须是众所周知的并且不能随意改变的。
所以客户端我们就只需创建套接字就可以了
小知识:
AF_INET表示的是协议家族
AF_INET也叫做PF_INET
绑定本地环回
可以通信
绑定内网ip
也可以通信
现象1:绑定公网ip无法绑定
现象2:绑定127.0.0.1 或者内网 ip可以绑定
现象3:服务端绑定内网ip,客户端用127.0.0.1访问不了。反过来也是不行。
为什么用公网ip不行?
主要原因是公网ip并没有配置到我们的机器上,公网ip是和我们的内网ip是做了映射的。所以公网ip无法被直接Bind
为什么服务端绑定内网ip,客户端用127.0.0.1访问不了?
我们可以用netstat -anup
其中的a表示的是所有的意思,u表示的是udp,p表示显示进程相关的信息,n表示把能显示成数字的数字化。
我i们的服务器绑定的ip地址是172的,如果我们拿客户端127去访问,但是服务器中并没有127的ip地址,所以我们访问不到。
结论: 如果我们显示的进行地址bind,client未来访问的时候,就必须使用server端绑定的地址信息。
我们一般在做服务器开发的时候,我们所对应的服务器端不建议手动绑定特定的ip,不仅仅是因为公网ip不让我们绑定,并且客户端只能用绑定的ip。
我们的服务端在绑定的时候必须绑成INADDR_ANY,直接填成这宏即可,而这个宏对应的值其实是0.
为什么呢?
如果我们的服务端绑定固定的ip,那么服务端就只能和固定ip的客户端进行通信,只能收到特定ip的报文,但是如果我们今天有多个ip呢?
服务端的ip设置为0表示的是来自任何ip的特定端口号的报文我们都能进行接收。
那么就意味着我们创建服务端的时候不用把ip传进来,在构造的时候,只需要一个端口号。
服务端起来了
我们可以看到服务端的地址是0.0.0.0,端口号是8080
服务端的ip地址为0的话就意味着拿这台机器上的任何一个ip地址就都能访问服务端了。
客户端绑定本地环回
客户端绑定内网IP
客户端绑定公网IP
我们所对应的文件描述符在线程之间是共享的吗?
答案是是的
我们的udp是支持全双工的,有没有一种可能一个线程正在读,另一个线程正在写呢?
答案是可以的
我们的线程对应的任务是什么?
消息转发,即消息路由
补充内容:
地址转化函数
sockaddr_in 中的成员 struct in_addr
sin_addr 表示 32 位 的 IP 地址
但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和in_addr 表示之间转换;
字符串转 in_addr 的函数:
in_addr 转字符串的函数:
带const的参数只能是输入。
UDP通信流程拆分
服务端(Server):
创建一个Socket.
绑定(Bind)一个固定的地址和端口(不然别人不知道往哪里寄信)。
等着收信(recvfrom),收到后可以回信(sendto)。
客户端(Client)
创建一个Socket.
直接写好地址发信(sendto)。
等着收回信(recvfrom)。
简单演示:
第一步:准备“信封”结构体
我们用struct sockaddr_in来表示地址。
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 使用 IPv4
addr.sin_port = htons(8888); // 端口号,htons 是为了处理字节序
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡地址
第二步:服务端代码示例(server.c)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main() {
// 1. 创建 UDP Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 准备地址并绑定
struct sockaddr_in servaddr, cliaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY; // 监听任何 IP
servaddr.sin_port = htons(8888); // 监听 8888 端口
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("服务器已启动,等待数据...\n");
// 3. 接收数据
char buffer[1024];
socklen_t len = sizeof(cliaddr);
int n = recvfrom(sockfd, buffer, 1024, 0, (struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("收到来自客户端的消息: %s\n", buffer);
// 4. 原样发回去
sendto(sockfd, buffer, n, 0, (struct sockaddr *)&cliaddr, len);
close(sockfd);
return 0;
}
第三步:客户端代码示例(client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
// 1. 创建 UDP Socket
// AF_INET: 使用 IPv4 地址
// SOCK_DGRAM: 使用数据报协议(即 UDP)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// 2. 准备服务端的地址信息
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888); // 必须和服务器监听的端口一致
// 将字符串形式的 IP(如 127.0.0.1)转换为网络二进制格式
// 127.0.0.1 是本地回环地址,代表“这台电脑自己”
if (inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0) {
printf("Invalid address\n");
return -1;
}
// 3. 发送数据给服务端
char *msg = "你好,服务器!我是小白。";
sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("消息已发送...\n");
// 4. 接收服务端的“回声”
char buffer[1024];
socklen_t len = sizeof(servaddr); // 此时用 servaddr 接收返回信息即可
int n = recvfrom(sockfd, buffer, 1024, 0, (struct sockaddr *)&servaddr, &len);
buffer[n] = '\0'; // 添加字符串结束符
printf("收到服务器的回应: %s\n", buffer);
// 5. 关门大吉
close(sockfd);
return 0;
}
扩展:
虽然TCP更稳定,但是UDP在以下场景是王者:
视频通话/直播:卡一下没关系,只要实时就行。
在线游戏:你不能接受点一下技能要等1秒的握手延迟。
简单查询:比如DNS查询。
————————————————
版权声明:本文为CSDN博主「孙同学_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2301_81290732/article/details/158701030