1、UDP的特性:
参考自:https://www.cnblogs.com/fundebug/p/differences-of-tcp-and-udp.html
(1)udp
是面向无连接的,不需要像tcp
一样客户端调用connect
,不需要服务器accept
接受应答,tcp
中的recv()
和send()
在udp
中改成使用sendto()
和recvfrom()
。
(2)udp
没有重传机制。udp
的端到端的服务是一种尽力而为交付的服务,不保证通过udp
发送的数据能够完全到达目的地。这就意味着,使用udp
套接字的程序必须准备处理消息的丢失和重新排列。
(3)udp
是面向报文的,它的套接字会保留消息边界。发送方的udp
对应用程序交下来的报文,在添加首部后就向下交付IP
层。udp
对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。
(4)udp
不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 udp
提供了单播,多播,广播的功能。
(5)头部开销小,传输数据报文时是很高效的。udp
头部包含了以下几个数据:
- 两个十六位的端口号,分别为源端口(可选字段)和目标端口。
- 整个数据报文的长度。
- 整个数据报文的检验和(
IPv4
可选 字段),该字段用于发现头部信息和数据中的错误。 - 因此
udp
的头部开销小,只有八字节,相比tcp
的至少二十字节要少得多,在传输数据报文时是很高效的。
2、UDP函数:
参考自:https://www.cnblogs.com/skyfsm/p/6287787.html,做了一些补充。
(1)创建套接字:
int socket(int domain, int type, int protocol);
domain
:用于设置网络通信的域,socket
根据这个参数选择信息协议的族。
type
:
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. //用于TCP
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages ). //用于UDP
SOCK_RAW Provides raw network protocol access. //RAW类型,用于提供原始网络访问
protocol
:置0即可。
返回值
:成功:非负的文件描述符。失败:-1。
(2)发送数据:
将数据由指定的socket 传给对方主机。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd
:正在监听端口的套接口文件描述符,通过socket获得。
buf
:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据。
len
:发送缓冲区的大小,单位是字节。
flags
:填0即可。
dest_addr
:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程。
addrlen
:表示第五个参数所指向内容的长度。
返回值
:成功:返回发送成功的数据长度。失败: -1。
(3)接收数据:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
:正在监听端口的套接口文件描述符,通过socket
获得。
buf
:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据。
len
:接收缓冲区的大小,单位是字节。
flags
:填0
即可。
src_addr
:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的。
addrlen
:表示第五个参数所指向内容的长度。
返回值
:成功:返回接收成功的数据长度。失败: -1。
(4)绑定端口:
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
sockfd
:正在监听端口的套接口文件描述符,通过socket
获得。
my_addr
:需要绑定的IP
和端口。
addrlen
:my_addr
的结构体的大小。
返回值
:成功:0。失败:-1。
(5)关闭套接字:
int close(int fd);
3、UDP的字符串通信代码:
代码是参考TCP/IP Socket编程(c语言实现)
书上的代码。书上的代码实现的是一个echo
服务器,所以我做了一些修改和补充。与之前一样,还是在本机电脑上回环。
头文件Practical.h
代码:
#include <stdio.h>
#include <stdlib.h>
void DieWithUserMessage(const char *msg,const char *detail)
{
fputs(msg,stderr);
fputs(":",stderr);
fputs(detail,stderr);
fputc('\n',stderr);
exit(1);
}
void DieWithSystemMessage(const char *msg)
{
perror(msg);
exit(1);
}
服务器端udp2.c
代码:
//服务器代码
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include "Practical.h"
#define size 8192
#include <stdbool.h>
bool SockAddrsEqual(const struct sockaddr *addr1
, const struct sockaddr *addr2) { //判断两个地址是否相同
if (addr1 == NULL || addr2 == NULL)
return addr1 == addr2;
else if (addr1->sa_family != addr2->sa_family)
return false;
else if (addr1->sa_family == AF_INET) {
struct sockaddr_in *ipv4Addr1 = (struct sockaddr_in *) addr1;
struct sockaddr_in *ipv4Addr2 = (struct sockaddr_in *) addr2;
return ipv4Addr1->sin_addr.s_addr == ipv4Addr2->sin_addr.s_addr && ipv4Addr1->sin_port == ipv4Addr2->sin_port;
}
else if (addr1->sa_family == AF_INET6) {
struct sockaddr_in6 *ipv6Addr1 = (struct sockaddr_in6 *) addr1;
struct sockaddr_in6 *ipv6Addr2 = (struct sockaddr_in6 *) addr2;
return memcmp(&ipv6Addr1->sin6_addr, &ipv6Addr2->sin6_addr, sizeof(struct in6_addr)) == 0 && ipv6Addr1->sin6_port== ipv6Addr2->sin6_port;
}
else
return false;
}
void PrintSocketAddress(const struct sockaddr *address
, FILE *stream) { //输出套接字地址到流文本中
if (address == NULL || stream == NULL)
return;
void *numericAddress;
char addrBuffer[INET6_ADDRSTRLEN];
in_port_t port;
switch (address->sa_family) {
case AF_INET:
numericAddress = &((struct sockaddr_in *) address)->sin_addr;
port = ntohs(((struct sockaddr_in *) address)->sin_port);
break;
case AF_INET6:
numericAddress = &((struct sockaddr_in6 *) address)->sin6_addr;
port = ntohs(((struct sockaddr_in6 *) address)->sin6_port);
break;
default:
fputs("[unknown type]", stream);
return;
}
if (inet_ntop(address->sa_family, numericAddress, addrBuffer,sizeof (addrBuffer)) == NULL)
fputs("[invalid address]", stream);
else {
fprintf(stream, "%s", addrBuffer);
if (port != 0)
fprintf(stream, "-%u", port);
}
}
int main(int argc,char *argv[])
{
char *service = "8000"; //端口
struct addrinfo addrCriteria;
memset(&addrCriteria,0,sizeof(addrCriteria)); //0填充
addrCriteria.ai_family = AF_UNSPEC; //地址族
addrCriteria.ai_flags = AI_PASSIVE;
addrCriteria.ai_socktype = SOCK_DGRAM;
addrCriteria.ai_protocol = IPPROTO_UDP;
struct addrinfo *servAddr;
int rtnVal = getaddrinfo(NULL,service,&addrCriteria,&servAddr); //解析地址
if(rtnVal != 0)
DieWithUserMessage("地址信息解析失败",gai_strerror(rtnVal));
int sock = socket(servAddr->ai_family,servAddr->ai_socktype,servAddr->ai_protocol); //创建套接字
if(sock<0)
DieWithSystemMessage("创建套接字失败");
if(bind(sock,servAddr->ai_addr,servAddr->ai_addrlen)<0) //绑定端口
DieWithSystemMessage("端口绑定失败");
freeaddrinfo(servAddr);
puts("开始监听");
for(;;){ //开始监听
struct sockaddr_storage clntAddr;
socklen_t clntAddrLen = sizeof(clntAddr);
char buffer[size];
ssize_t numBytesRcvd = recvfrom(sock,buffer,size,0,(struct sockaddr *)&clntAddr,&clntAddrLen); //接收数据 //返回数据长度
if(numBytesRcvd<0)
DieWithSystemMessage("数据接收失败");
printf("收到数据:%s\n",buffer); //将从客户端接收到的内容输出
ssize_t numBytesSent = sendto(sock,buffer,numBytesRcvd,0,(struct sockaddr *)&clntAddr,sizeof(clntAddr)); //将接收到的数据发回客户端
printf("发送数据:%s\n",buffer);
if(numBytesSent<0) //发送失败
DieWithSystemMessage("数据发送失败");
else if (numBytesSent != numBytesRcvd) //发送数据长度与接收数据长度不一致
DieWithUserMessage("sendto()","数据字节丢失");
}
}
客户端udp1.c
代码:
//客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include "Practical.h"
#include <stdbool.h>
#define size 8192
bool SockAddrsEqual(const struct sockaddr *addr1
, const struct sockaddr *addr2) { //比较两个地址是否相同
if (addr1 == NULL || addr2 == NULL)
return addr1 == addr2;
else if (addr1->sa_family != addr2->sa_family)
return false;
else if (addr1->sa_family == AF_INET) {
struct sockaddr_in *ipv4Addr1 = (struct sockaddr_in *) addr1;
struct sockaddr_in *ipv4Addr2 = (struct sockaddr_in *) addr2;
return ipv4Addr1->sin_addr.s_addr == ipv4Addr2->sin_addr.s_addr&& ipv4Addr1->sin_port == ipv4Addr2->sin_port;}
else if (addr1->sa_family == AF_INET6) {
struct sockaddr_in6 *ipv6Addr1 = (struct sockaddr_in6 *) addr1;
struct sockaddr_in6 *ipv6Addr2 = (struct sockaddr_in6 *) addr2;
return memcmp(&ipv6Addr1->sin6_addr, &ipv6Addr2->sin6_addr,sizeof(struct in6_addr)) == 0 && ipv6Addr1->sin6_port== ipv6Addr2->sin6_port;
} else
return false;
}
int main(int argc, char *argv[])
{
char *server = "192.168.1.5"; //服务器地址
char *echoString = "你好!我是客户端!";
size_t echoStringLen = strlen(echoString);
if(echoStringLen>size) //检验输入格式
DieWithUserMessage(echoString,"字符串超长");
char *servPort = "8000"; //服务器端口
struct addrinfo addrCriteria;
memset(&addrCriteria,0,sizeof(addrCriteria)); //0补齐
addrCriteria.ai_family = AF_UNSPEC; //地址族
addrCriteria.ai_socktype = SOCK_DGRAM;
addrCriteria.ai_protocol = IPPROTO_UDP;
struct addrinfo *servAddr;
int rtnVal = getaddrinfo(server,servPort,&addrCriteria,&servAddr); //输入地址和端口,分别返回地址和端口的链表
if (rtnVal !=0)
DieWithUserMessage("地址端口解析失败",gai_strerror(rtnVal));
int sock = socket(servAddr->ai_family,servAddr->ai_socktype,servAddr->ai_protocol); //创建套接字
if (sock < 0)
DieWithSystemMessage("创建套接字失败");
ssize_t numBytes = sendto(sock,echoString,echoStringLen,0,servAddr->ai_addr,servAddr->ai_addrlen); //数据写入缓冲区 //返回发送数据长
printf("已发送数据:%s\n",echoString);
if(numBytes<0)
DieWithSystemMessage("数据发送失败");
else if (numBytes != echoStringLen)
DieWithUserMessage("sendto() error","发送数据有字节丢失");
struct sockaddr_storage fromAddr;
socklen_t fromAddrLen = sizeof(fromAddr);
char buffer[size +1];
numBytes = recvfrom(sock,buffer,size,0,(struct sockaddr *) &fromAddr, &fromAddrLen); //从缓冲区读数据
if (numBytes <0)
DieWithSystemMessage("接收数据失败");
else if(numBytes != echoStringLen)
DieWithUserMessage("recvfrom() error","接收到的数据有字节丢失");
printf("已接收数据:%s\n",buffer);
if(!SockAddrsEqual(servAddr->ai_addr,(struct sockaddr *)&fromAddr)) //比较发送数据的地址和接收到数据的地址是否相同
DieWithUserMessage("recvfrom()","发送数据地址与接收数据数据地址不符");
freeaddrinfo(servAddr);
buffer[echoStringLen] = '\0';
close(sock);
exit(0);
}
编译两段程序:
gcc udp1.c -o udp1
gcc udp2.c -o udp2
先执行服务器端程序udp2.c
,再执行客户端程序udp1.c
,客户端程序每运行一次,就向服务器发送一次字符串。
./udp1
./udp2
运行结果:
4、UDP中使用connect():
udp
调用connect()
用来与目的地址构建连接,构建连接后,udp
就可以使用send()
和recv()
代替recvfrom()
和sendto()
来收发数据报。在未建立连接时,使用recvfrom()
和sendto()
需要指定数据发送的目标地址,构建连接后,就不需要指定目标地址了。
连接后,udp
就只能给构建了连接的源地址发送信息,和从连接的源地址接收信息。此外,connect()
只是构建了一个数据传输的通道,不会改变udp
的行为方式,不会多出tcp
的校验和重传等行为,该丢的包还是会丢。
由于书上没有给出例程,网上找也找不到资料,后面的我再研究一下再放上代码。