linux c socket编程学习笔记(4)(UDP通信)

本文详细介绍了UDP协议的特点,包括其无连接、不可靠传输、面向报文等特性,并提供了创建套接字、发送接收数据等核心函数的使用说明及示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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和端口。
addrlenmy_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的校验和重传等行为,该丢的包还是会丢。

由于书上没有给出例程,网上找也找不到资料,后面的我再研究一下再放上代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值