UDP概述
UDP协议
面向无连接的用户数据报协议,在传输数据前不需要先建立连接;目地主机的运输层收到UDP报文后,不需要给出任何确认
UDP特点
- 相比TCP速度稍快些
- 简单的请求/应答应用程序可以使用UDP
- 对于海量数据传输不应该使用UDP
- 广播和多播应用必须使用UDP
UDP应用
DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
网络编程接口socket
网络通信要解决的是不同主机进程间的通信
- 不同主机识别,哪一个主机发,哪一个主机收
- 以及多重协议的识别问题,是UDP还是TCP
- 首要问题是网络间进程标识问题,因为一个主机存在很多进程,你需要准确的把数据给目的进程(使用端口表示一个进程),哪一个端口发,哪一个端口收
总结就是:IP、端口、协议
20世纪80年代初,加州大学Berkeley分校在BSD(一个UNIX OS版本)系统内实现了TCP/IP协议;其网络程序编程开发接口为socket
随着UNIX以及类UNIX操作系统的广泛应用, socket成为最流行的网络程序开发接口
socket作用
提供不同主机上的进程之间的通信
socket特点
- socket也称“套接字”
- 是一种文件描述符(特殊的文件描述符),代表了一个通信管道的一个端点(因此socket其实创建的就是一个虚拟的管道-全双工,可以这么简单的理解)
- 类似对文件的操作一样,可以使用read、write、close等函数对socket套接字进行网络数据的收取和发送等操作
- 得到socket套接字(描述符)的方法调用socket()
UDP编程C/S架构
这个架构决定我们写代码的流程
UDP编程-创建套接字
创建socket套接字
int socket(int family,int type,int protocol);
功能:
创建一个用于网络通信的socket套接字(描述符)
参数:
family:协议族(AF_INET、AF_INET6、PF_PACKET等)
type:套接字类(SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等)
protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP等
protocol写0(自动寻找),让它以type为主,确定协议SOCK_STREAM--TCP
SOCK_DGRAM--UDP
返回值:
套接字
特点:
创建套接字时,系统不会分配端口
创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器时,往往需要修改为被动的
头文件:
#include <sys/socket.h>
创建UDP套接字demo
int sockfd = 0;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
close(sockfd);
exit(-1);
}
注意:
AF_INET:IPv4协议
SOCK_DGRAM:数据报套接字
0:选择所给定的family和type组合的系统默认值
IPv4套接字地址结构
struct sockaddr_in { sa_family_t sin_family;//2字节 -- 协议族 in_port_t sin_port;//2字节 --端口 struct in_addr sin_addr;//4字节 --发送方地址或接收方地址 char sin_zero[8]//8字节 ---为0,起补位作用,因为要求结构体16个字节,因此定义了8字节的补位 }; struct in_addr //该结构体只有一个成员,属于历史遗留问题,之前是有其他成员的 { in_addr_t s_addr;//4字节 }
在赋值结构体的时候,由于 sin_zero的值需要赋值0,那么我们可以一开始就把结构体清零,然后只赋值协议、端口、IP地址即可
当主机是IPV4或者IPV6发送数据的时候,由于IP地址不同,导致函数使用的不同,因此为了让函数的实用性更强,那么我就把IPV4和IPV6当成整体,因此就把结构体转换为相同的类型即可
为了使不同格式地址能被传入套接字函数,地址须要强制转换成通用套接字地址结构
头文件:#include <netinet/in.h>
struct sockaddr
{
sa_family_t sa_family; // 2字节
char sa_data[14] //14字节
};
为了类型转换,就像一个函数能处理char*、int*,那么这个函数就需要成为void*,数据转换,其实我们自己设计一个通用函数的时候也需要注意,如果类型不同,那么我们需要存在一个通用的类型,像void*,能被任何一个数据类型赋值,因此同理,这里也产生了一个类似于void*的类型(为了数据转换,以便处理更多的数据类型)
两种地址结构使用场合
在定义源地址和目的地址结构的时候,选用struct sockaddr_in;
例:
struct sockaddr_in my_addr;
这个特点记住就好,等写代码的时候注意一下
当调用编程接口函数,且该函数需要传入地址结构时需要用struct sockaddr进行强制转换
例:
bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
发送数据—sendto函数
ssize_t sendto(int sockfd,const void *buf,
size_t nbytes,int flags,
const struct sockaddr *to,
socklen_t addrlen);
功能:
向to结构体指针中指定的ip,发送UDP数据
参数:
sockfd:套接字
buf: 发送数据缓冲区
nbytes: 发送数据缓冲区的大小
flags:一般为0
to: 指向目的主机地址结构体的指针
addrlen:to所指向内容的长度
注意:
通过to和addrlen确定目的地址
可以发送0长度的UDP数据包
返回值:
成功:实际发送数据的字符数
失败: -1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
//定义好端口和IP地址,并且IP地址是字符串形式,其实这里可以不用初始化
unsigned short server_port= 808; //可以赋值0
char *server_ip = "192.168.52.1"; //可以赋值NULL
//传递参数需要符合规定,否则错误,并且退出
if(argc < 3 )
{
printf("error\n");
exit(1);
}
//把从终端传进去的IP地址和端口号进行赋值
server_ip = argv[1];
//通过main函数传参,传入端口号
server_port= atoi(argv[2]); //由于终端输入的是字符串,而端口号是整型,因此使用atoi函数把数字字符串转换为数字
/*创建UDP套接字*/
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0); //IPV4和UDP
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
/*填充目的server socket地址*/
struct sockaddr_in dest_addr;
bzero(&dest_addr,sizeof(dest_addr)); //先清零
dest_addr.sin_family = AF_INET;//目的套接字地址的协议家族赋值
dest_addr.sin_port = htons(server_port);//目的套接字地址的端口号赋值,并且转成网络字节序
inet_pton(AF_INET,server_ip,&dest_addr.sin_addr);//目的套接字地址的ip地址赋值
printf("send data to UDP server %s:%d!\n",server_ip,port);
/*发送数据到目的server*/
while(1)
{
char send_buf[512];
fgets(send_buf,sizeof(send_buf),stdin); //使用fgets从终端中输入字符串
send_buf[strlen(send_buf)-1] = '\0';//字符串最后一个'\n'变成'\0' //这个很容易忘记,在末尾添加结束字符
sendto(sockfd,send_buf,strlen(send_buf),0,(struct sockaddr *)&dest_addr,sizeof(dest_addr));
}
close(sockfd); //记得使用完成后,关闭套接字
return 0;
}
绑定 bind函数
UDP网络程序想要收取数据需什么条件?
确定的ip地址
确定的port
怎样完成上面的条件呢?
接收端 使用bind函数,来完成地址结构与socket套接字的绑定,这样ip、port就固定了
发送端 在sendto函数中指定接收端的ip、port,就可以发送数据了
因此bind既可以使用在发送端,也可以使用在接收端(客户端和服务端都可以使用bind,都是一般服务端使用bind)
int bind(int sockfd,
const struct sockaddr *myaddr,socklen_t addrlen);
功能:
将本地协议地址与sockfd绑定
参数:
sockfd: socket套接字
myaddr: 指向特定协议的地址结构指针
addrlen:该地址结构的长度
返回值:
成功:返回0
失败:其他
struct sockaddr_in Mysource_addr;
bzero(&Mysource_addr,sizeof(Mysource_addr));
Mysource_addr.sin_family = AF_INET;//目的套接字地址的协议家族赋值
Mysource_addr.sin_port = htons(8000);//目的套接字地址的端口号赋值
Mysource_addr.sin_addr.s_addr = htonl(INADDR_ANY);
INADDR_ANY是通配地址,值为0
接收数据—recvfrom 函数
ssize_t recvfrom(int sockfd, void *buf,
size_t nbytes,int flags,
struct sockaddr *from,
socklen_t *addrlen);
功能:
接收UDP数据,并将源地址信息保存在from指向的结构中
并且recvfrom默认是带阻塞的,他需要等信息
参数:
sockfd: 套接字
buf:接收数据缓冲区
nbytes:接收数据缓冲区的大小
flags: 套接字标志(常为0)
from: 源地址结构体指针,用来保存数据的来源
addrlen: from所指内容的长度
注意:
通过from和addrlen参数存放数据来源信息
from和addrlen可以为NULL, 表示不保存数据来源
返回值:
成功:接收到的字符数
失败: -1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
struct sockaddr_in Mysource_addr;
bzero(&Mysource_addr,sizeof(Mysource_addr));
Mysource_addr.sin_family = AF_INET;//目的套接字地址的协议家族赋值
Mysource_addr.sin_port = htons(8000);//目的套接字地址的端口号赋值
Mysource_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/*创建UDP套接字*/
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
if(bind(sockfd,(struct sockaddr *)&Mysource_addr,sizeof(Mysource_addr)) != 0 )
{
perror("bind is error");
exit(1);
}
char recvBuff[512];
while(1)
{
while( recvfrom(sockfd,recvBuff,512,0,NULL,NULL) == 0 );
printf("%s\n",recvBuff);
}
}
模拟QQ情况
QQ既可以发信息也可以收消息,说明存在多进程或者多线程情况,一个进程或者线程收消息,而另外一个发消息
独立完成改代码