一、UDP编程框架
分为客户端和服务端两部分。
- 服务端主要包含建立套接字socket()、将套接字与地址结构进行绑定bind()、读写数据recvfrom()和sendto()、关闭套接字close()等几个过程。
- 客户端包括建立套接字socket()、读写数据recvfrom()和sendto()、关闭套接字close()几个过程。
1、UDP编程框图
使用socket()建立套接字的类型与TCP不同,为数据报套接字。客户端和服务端之间的差别在于服务端必须使用bind()函数来绑定侦听的本地udp端口,而客户端可以不进行绑定,之间发送到服务器地址的某个端口地址。
UDP协议中服务端和客户端的交互存在于数据的收发过程中。与TCP协议的比较缺少了二者之间的连接。
2、UDP服务端编程框架
- 建立套接字文件描述符
int fd = socket(AF_INET, SOCK_SGRAM, 0);
建立一个AF_INET族的数据报套接字,UDP协议的套接字使用SOCK_DGRAM选项。
2. 设置服务器地址和侦听端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;//地址类型
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//任意本地地址
serverAddr.sin_port = htons(SERVERPORT);//服务器端口
bzero(&(addr.sin_zero),8);//sin_zero置为0
- 绑定侦听端口
bind(fd,(sockaddr*)&serverAddr,sizeof(serverAddr));
- 接收和发送数据,使用recvfrom()和sendto()
- 关闭套接字,使用close()
3、UDP客户端编程框架
- 建立套接字文件描述符,socket()
- 设置服务器地址和端口,sockaddr_in addr;
- 接收和发送数据,recvfrom()和sendto()
- 关闭套接字,使用close()
二、UDP协议程序设计常用函数
1、socket()和bind()
socket()与TCP一样,只是协议的类型使用SOCK_DGRAM而不是SOCK_STREAM。
bind()与TCP没有差别。
2、接收数据recvfrom() 和 recv()
- 函数介绍
#include <sys/socket.h>
#include <sys/types.h>
ssize_t recv(int s,void* buf,size_t len,int flags);
ssize_t recvfrom(int s,void* buf,size_t len,int flags,
sockaddr* from,socklen_t* fromlen);
- s:正在监听端口的套接字文件描述符,由函数socket()生成
- buf:接收数据缓冲区
- len:接收缓冲区大小
- from:指向sockaddr_in指针,接收数据时发送方的地址信息放在该结构中
- fromlen:from所值内容的长度,可用sizeof(sockaddr_in)获得
函数返回值在成功时返回接收到数据的长度,出错时返回-1,errno如下:
- recvfrom()示例
#define SERVERPORT 16123
int sockfd = socket(AF_INET, SOCK_SGRAM,0);//建立套接字文件描述符
if(-1 == sockfd)
{
printf("socket error\n");
printf("errno =%d,%s\n", errno ,strerror(errno));
return ;
}
//设置服务器地址和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;//地址类型
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//任意本地地址
serverAddr.sin_port = htons(SERVERPORT);//服务器端口
bzero(&(addr.sin_zero),8);//sin_zero置为0
//接收数据
sockaddr_in from;//发送方地址信息
char buf[128]={0};//接收缓冲区
socklen_t fromlen = sizeof(from);
int size = recvfrom(sockfd,buf,128,(sockaddr*)&from,&fromlen);
if(-1 == size)
{
perror("recvfrom");
exit(EXIT_FAILURE);
}
//处理数据
//...
上述例子在使用recvfrom()函数时没有绑定发送方的地址,所有在接收时不同发送方的发送的数据都可以到达接收方的套接字文件描述符。
3、发送数据sendto() 和 send()
- 函数介绍
#include <sys/socket.h>
#include <sys/types.h>
ssize_t send(int s,void* buf,size_t len,int flags);
ssize_t sendto(int s,void* buf,size_t len,int flags,
sockaddr* to,socklen_t tolen);
- s:正在监听端口的套接字文件描述符,由函数socket()生成
- buf:发送数据缓冲区
- len:发送缓冲区大小
- to:指向sockaddr_in指针,接收数据的主机的地址信息放在该结构中
- tolen:to所值内容的长度,可用sizeof(sockaddr_in)获得
函数返回值在成功时返回发送成功数据的长度,出错时返回-1,errno如下:
- sendto()示例
#define SERVERIP "192.168.98.92"
#define SERVERPORT 16123
int sockfd = socket(AF_INET, SOCK_SGRAM,0);//建立套接字文件描述符
if(-1 == sockfd)
{
printf("socket error\n");
printf("errno =%d,%s\n", errno ,strerror(errno));
return ;
}
//设置服务器地址和端口
sockaddr_in toAddr;
toAddr.sin_family = AF_INET;//地址类型
toAddr.sin_addr.s_addr = htonl(SERVERIP);//服务器地址
toAddr.sin_port = htons(SERVERPORT);//服务器端口
bzero(&(toAddr.sin_zero),8);//sin_zero置为0
//发送数据
char buf[128]={0};//发送缓冲区
int size = sendto(sockfd,buf,128,0,(sockaddr*)&to,sizeof(toAddr));//发送数据
if(-1 == size)
{
perror("sendto");
exit(EXIT_FAILURE);
}
//处理过程
//...
在上面示例中,由于没有设置本地的IP地址和本地端口,而这些参数是网络协议栈发送数据时的必需条件,所以在UDP层网络协议栈会选择合适的端口。经过IP层时,会选出合适的本地IP地址进行填充。
三、UDP接收和发送示例
#include <stdio.h>
#include <stdlib.h>
#include<string>
#include<string.h>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVERIP "127.0.0.1"
#define SERVERPORT 16123
void udpServerProc(int s,sockaddr_in* clientAddr)
{
char buf[128] = {0};
while(1)
{
printf("waiting for msg\n");
socklen_t len = sizeof(*clientAddr);
//接收数据并获得客户端地址
int size = recvfrom(s,buf,128,0,(sockaddr*)clientAddr,&len);
if(0 == size)
{
printf("no msg\n");
return ;
}
printf("ser recv:%s\n",buf);
//将接收的数据发回客户端
sendto(s,buf,size,0,(sockaddr*)clientAddr,len);
printf("ser send:%s\n",buf);
}
return ;
}
int udpServerTest()
{//服务端编程框架
int sockfd = socket(AF_INET, SOCK_DGRAM,0);//建立套接字文件描述符
if(-1 == sockfd)
{
printf("socket error\n");
printf("errno =%d,%s\n", errno ,strerror(errno));
return 0;
}
//设置服务器地址和侦听端口,并初始化要绑定的网络地址结构
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;//地址类型
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//任意本地地址
serverAddr.sin_port = htons(SERVERPORT);//服务器端口
bzero(&(serverAddr.sin_zero),8);//sin_zero置为0
//绑定侦听端口
if(-1 == bind(sockfd,(sockaddr*)&serverAddr,sizeof(serverAddr)) )
{
perror("bind");
exit(EXIT_FAILURE);
}
//服务端处理
sockaddr_in clientAddr;//发送方地址信息
udpServerProc(sockfd,&clientAddr);
//关闭套接字
close(sockfd);
return 0;
}
void udpClientProc(int s,sockaddr_in* serverAddr)
{
char buf[128]="UDP TEST";//
socklen_t len = sizeof(*serverAddr);
sendto(s,buf,128,0,(sockaddr*)serverAddr,len);
printf("cli send:%s\n",buf);
memset(buf,0,128);
//从服务器接收数据
sockaddr_in fromAddr;
recvfrom(s,buf,128,0,(sockaddr*)&fromAddr,&len);
printf("cli recv:%s\n",buf);
return ;
}
int udpClientTest()
{
int sockfd = socket(AF_INET, SOCK_DGRAM,0);//建立套接字文件描述符
if(-1 == sockfd)
{
printf("socket error\n");
printf("errno =%d,%s\n", errno ,strerror(errno));
return 0;
}
//设置服务器地址和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;//地址类型
serverAddr.sin_addr.s_addr = inet_addr(SERVERIP);//服务器地址
serverAddr.sin_port = htons(SERVERPORT);//服务器端口
bzero(&(serverAddr.sin_zero),8);//sin_zero置为0
//客户端处理
udpClientProc(sockfd,&serverAddr);
//关闭套接字
close(sockfd);
}
int main()
{
pid_t pid = fork();
if(0 == pid)//子
udpClientTest();
else
udpServerTest();
return 0;
}
四、UDP协议程序设计中的几个问题
1、UDP报文丢失
在局域网内一般情况下数据接收方均能接收到发送方的数据,除非连接双方的主机发生故障。
而在Internet上,由于要经过多个路由器,路由器要对转发的数据进行存储、处理、合法性判定、转发等操作,容易出现错误,所以在转发过程中很容易出现丢失。当UDP报文丢失时,函数recvfrom()会一直阻塞直到数据到来。
解决方法:如TCP中对每个报文进行响应,超时进行数据重发。
2、UDP数据发送中的乱序
在网络上传输时,由于路由的不同和路由器的存储转发顺序不同,有可能造成数据的顺序更改,接收方的数据顺序和发送方发送的顺序不一致。
解决方法:发送端在数据段中加入数据报序号,接收端对接收的数据头端进行简单的处理恢复原始顺序。
3、UDP协议中的connect()函数
UDP协议的套接字描述符在进行了数据收发之后才能确定套接字描述符中所表示的发送方和接收方的地址,否则仅能确定本地的地址。
在UDP协议中使用connec()函数的作用仅仅表示确定了另一方的地址,并无其他含义。
connect()函数用于UDP协议中的副作用:
- 发送操作不能再使用sendto(),要使用write()、send()类函数
- 接收操作不能再使用recvfrom(),要使用read()、recv()类函数
- 多次使用connect()函数的时候会改变原来套接字绑定的地址和端口号,用新的地址和端口号代替,可以使用这种特点来断开连接
4、UDP缺乏流量控制
UDP接收数据时直接将数据放到缓冲区中。如果没有及时地从缓冲区中将数据复制出来,后面到来的数据会接着向缓冲区中放入。当缓冲区满的时候,后面到来的数据会覆盖之前的数据而造成数据的丢失。(类似循环队列)
解决方法:增大接收数据的缓冲区和接收方接收单独处理的方法来解决局部的udp数据接收缓冲区溢出的问题。
5、UDP协议中的外出网络接口
多网卡机器上由于不同的网卡连接不同的子网,用户发送的数据从其中一个网卡发出,将数据发送到特定的子网上。使用connect可以将套接字文件描述符与一个网络地址结构绑定。
6、UDP协议中的数据报文截断
接收缓冲区的大小小于到来的数据大小时,接收缓冲区会保存最大的可能接收到的数据,其他数据将会丢失,并且用MSG_TRUNC标志。
解决方法:服务器和客户端程序要配合,接收的缓冲区要比发送的数据大一些,防止数据丢失的现象。
注:
LINUX网络编程 第二版 第十章 读书笔记