詳細な UDP プログラミング

1.1 プログラミングの準備 - バイトオーダー、アドレス変換

1.1.1 エンディアンの概要

バイト順序の概念: マルチバイト データの格納順序を指します。

カテゴリ:
ビッグエンディアン形式: 下位バイトのデータを下位アドレスに格納
リトルエンディアン形式: 上位バイトのデータを下位アドレスに格納

注:
LSB: 下位アドレス
MSB: 上位アドレス

現在のシステムのバイト順序を確認する方法:

#include <stdio.h>

union un
{
    
    
	int a;
	char b;
}

int main()
{
    
    
	union un myun;
	myun.a = 0x12345678;
	printf("a = %#x\n",myun.a);
	printf("b = %#x\n",myun.b);
	
	if(myun.b == 0x78){
    
    
		printf("小端存储\n");
	}
	else{
    
    
		printf("大端存储\n");
	}
	
	return 0;
}

結果の表示:

1.1.2 バイトオーダー変換機能
  1. ネットワーク プロトコルは通信バイト オーダー (ビッグ エンディアン) を開発しました。
  2. エンディアンネスを考慮する必要があるのは、マルチバイト データを処理する場合のみです。
  3. 同じコンピュータ上で実行されているプロセスが相互に通信する場合、通常、バイト順序は考慮されません。
  4. 異種コンピュータ間で通信するには、独自のバイトオーダーをネットワークバイトオーダーに変換する必要があります

バイトオーダー変換が必要な場合、通常、特定のバイトオーダー変換関数が呼び出されます。

host --> network
1 -- htonl
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
功能:32位主机字节序数据转换成网络字节序数据
参数:
	hostint32:	待转换的32位主机字节序数据
返回值:
	成功:返回网络字节序的值

2 -- htons
#include<arpa/inet.h>
uint16_t htons(uint16_t hostint16);
功能:16位主机字节序数据转换成网络字节序数据
参数:
	hostint16:	待转换的16位主机字节序数据
返回值:
	成功:返回网络字节序的值

network --> host
3 -- ntohl
#include<arpa/inet.h>
uint32_t ntohl(uint32_t netint32);
功能:32位网络字节序数据转换成主机字节序数据
参数:
	netint32:	待转换的32位网络字节序数据
返回值:
	成功:返回主机字节序数据

4 -- ntohs
#include<arpa/inet.h>
uint16_t ntohs(uint16_t netint16);
功能:16位网络字节序数据转换成主机字节序数据
参数:
	netint16:	待转换的16位网络字节序数据
返回值:
	成功:返回主机字节序数据

場合:

#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    
    
	int a = 0x12345678;
	short a = 0x1234;
	
	printf("%#x\n",htonl(a));

	printf("%#x\n",htons(b));

	return 0;
}
1.1.3 アドレス変換機能

人間が認識するIPアドレスはドット付き10進数の文字列ですが、コンピュータやネットワーク上で認識されるIPアドレスは整数データなので変換する必要があります

1.1.3.1 inet_pton関数
#include <arpa/inet.h>
int inet_pton(int family,const char *strptr,void *addrptr);
功能:
	将点分十进制数串转换成32位无符号整数
参数:
	family	协议族
			AF_INET			ipv4网络协议
			AF_INET6		iPV6网络协议
	strptr	点分十进制数串
	addrptr	32位无符号整数的地址
返回值:
	成功返回1、失败返回其他

場合:

#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    
    
	char ip_str[] = "198.168.3.103";
	unsigned int ip_int = 0;
	unsigned char *ip_p = NULL;
	
	//将点分十进制ip地址转化为32位无符号整形数据
	inet_pton(AF_INET,ip_str,&ip_int);
	printf("ip_int = %d\n",ip_int;

	ip_p = (char *)&ip_int;
	printf("in_uint = %d,%d,%d,%d\n",*ip_p,*(ip_p+1),*(ip_p+2),*(ip_p+3));

	return 0;
}
1.1.3.2 inet_ntop() 関数
#include <arpa/inet.h>
const char * inet_ntop(int family,const void *addrptr,char *strptr, size_t len);
功能:32位无符号整数转换成点分十进制数串的ip地址
参数:
	family	协议族
			AF_INET			ipv4网络协议
			AF_INET6		iPV6网络协议
	addrptr	32位无符号整数的地址
	strptr	点分十进制数串
	len		strptr缓存区长度
			len的宏定义
				#define INET_ADDRSTRLEN  16  //for ipv4
				#define INET6_ADDRSTRLEN 46	 //for ipv6
返回值:
	成功返回1、失败返回其他

場合:

#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    
    
	unsigned char ip_int[] = {
    
    198,168,3,103};
	char ip_str[16] = ""; //"198.168.3.103"刚好16个字节
	inet_ntop(AF_INET,&ip_int,ip_str,16);
	printf("ip_s = %s\n",ip_str);
	return 0;
}
1.1.3.3 inet_addr()和inet_ntoa()

これら 2 つの機能は、IPv4 アドレスの変換でのみ使用できます。

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

in_addr_t inet_addr(const char *cp);
功能:
	将点分十进制ip地址转化为整型数据
参数:
	cp:点分十进制的ip地址
返回值:
	成功:整型数据
	
char *inet_ntoa(struct in_addr in);
功能:将整型数据转化位点分十进制的ip地址
参数:
	in:	保存ip地址的结构体
返回值:
	成功:点分十进制的ip地址

1.2 UDP の導入、プログラミング プロセス

1.2.1 UDPの概要

UDP プロトコルは
コネクションレス型のユーザー データグラム プロトコルであり、データを送信する前に接続を確立する必要がなく、宛先ホストのトランスポート層は UDP メッセージの受信後に確認を行う必要がありません。

UDPの機能

  1. TCP よりも比較的高速です。
  2. 単純な要求/応答アプリケーションでは UDP を使用できます。
  3. UDP は大量のデータ転送には使用しないでください。
  4. ブロードキャストおよびマルチキャスト アプリケーションは UDP を使用する必要があります。

UDP アプリケーション
DNS (ドメイン名解決)、NFS (ネットワーク ファイル システム)、RTP (ストリーミング メディア) など。
一般的な音声やビデオのおとぎ話は UDP 通信を使用します。

1.2.2 ネットワークプログラミングインターフェースソケット

ソケットの役割: 異なるホスト上のプロセス間の通信を提供します。

ソケットの特徴:

  1. ソケットは「ソケット」とも呼ばれます。
  2. 通信パイプラインのエンドポイントを表すファイル記述子です。
  3. ファイル操作と同様に、読み取り、書き込み、クローズなどの関数を使用してネットワーク データを収集し、ソケットに送信できます。
  4. ソケットソケット (記述子) を取得するメソッドは、socket() を呼び出します。

ソケット分類:
SOCK_STREAM、ストリーム ソケット、TCP に使用、
SOCK_DGRAM、データグラム ソケット、UDP に使用、
SOCK_RAW、生のソケット、このタイプは他の層プロトコル操作に必要です

1.2.3 UDPプログラミングC/Sアーキテクチャ

UDP ネットワーク プログラミング プロセス:

サーバー:
ソケットを作成します


クライアント:
ソケットの作成socket()
データの送信sendto()
データの受信recvfrom()
ソケットのクローズclose()

1.2.4 UDP プログラミング - ソケットの作成
1.2.4.1 ソケットの作成
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
	创建一个套接字,返回一个文件描述符;
参数:
	domain:通信域,协议族;
			AF_UNIX		本地通信
			AF_INET		ipv4网络协议
			AF_INET6	ipv6网络协议
			AF_PACKET	底层接口
	type:	套接字的类型
			SOCKET_STREAM	流式套接字(TCP)
			SOCKET_DGRAM	数据报套接字(UDP)
			SOCKET_RAW		原始套接字(用于链路层)
	特点:
			创建套接字时,系统不会分配端口
			创建的套接字默认属性是主动的,即主动发起服务的请求,当作为服务器时,往往需要修改为被动的。
1.2.4.2 UDP ソケットのデモを作成する
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types>
int main(int argc, char const *argv[]){
    
    
	//使用socket函数创建套接字
	//创建一个用于UDP网络编程的套接字
	int sockfd = 0;
	sockfd = socket(AF_INET,SOCKE_DGRAM,0);
	if(sockfd < 0){
    
    
		perror("socket");
		exit(-1);
	}
	printf("sockfd = %d\n",sockfd);//任何一个进程在开启或创建的时候会分配三个文件描述符(0,1,2)
}
1.2.5 UDP プログラミング - データの送信、バインド、受信
1.2.5.1 ipv4ソケットアドレス構造

ネットワークプログラミングでよく使われる構造体

#include <netinet/in.h>

struct in_addr
{
    
    
	in_addr_t s_addr; // 4字节
}

struct sockaddr_in
{
    
    
	sa_family_t sinfamily; //2字节;
	in_port_t 	sin_port;  //2字节;
	struct in_addr	sin_addr; //4字节;
	char sin_zero[8]		//8字节
}



異なる形式のアドレスをソケット関数に渡すことができるようにするには、そのアドレスを共通のソケット アドレス構造体に変換する必要があります。その理由は、状況によって使用される構造体は異なりますが、呼び出される関数は同じなので、一般的な構造を定義します。特定の機会に使用する場合は、要件に従って特定の構造を渡すだけです。

一般構造 sockaddr

头文件#include <netinet/in.h>
struct sockaddr
{
    
    
	sa_family_t sa_family;	//2字节
	char sa_data[14]		//14字节
}
//注意,以上3个结构在Linux系统中已经定义
1.2.5.2 2 つのアドレス構造の適用場面

送信元アドレスと宛先アドレスの構造を定義するときは、struct sockaddr_in を選択します。
例:

struct sockaddr_in my_addr;

当调用编程接口函数,且该函数需要从传入地址结构时需要用struct sockaddr进行强制转换
例:

bind(sockfd,struct sockaddr*)&my_addr,sizeof(my_addr));
1.2.5.3 发送数据—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
1.2.5.4 向“网络调试助手”发送消息

设置网络调试助手(在windows下运行的软件)中的属性
注意:ip地址不能随意设置,必须是当前windows的ip地址

ubuntu下客户段的代码编写:

#include <stdio.h>			//printf
#include <stdlib.h>			//exit
#include <sys/types.h>
#include <sys/socket.h>		//socket
#include <netinet/in.h>		//sockaddr_in
#include <arpa/inet.h>		//htons	inet_addr
#include <unisted.h>		//close
#include <string.h>

int main()
{
    
    
	int sockfd;
	if((sockfd = socket(AF_INET,SOCK_DGRAM,0)) == -1)
	{
    
    
		perrpr("fail to socket");
		exit(1);
	}
	printf("sockfd = %d",sockfd);
	
	//第二步:填充服务器网络信息结构体	sockaddr_in(一般不需要,系统会自动分配)
	struct sockaddr_in	severaddr;
	serveraddr.sin_family = AF_INET;	//协议族	AF_INET:ipv4网络协议
	serveraddr.sin_addr.s_addr = inet_addr("192.168.3.78");	//ip地址
	serveraddr.sin_port = htons(8080);
	socklen_t addrlen = sizeof(serveraddr)
	
	//第三步:发送数据
	char buf[128] = "";
	while(1)
	{
    
    
		fgets(buf,128,stdin);
		buf[strlen(buf) - 1] = '\0'; 	//把buf字符串中的\n转化为\0
		if(sendto(sockfd,buf,N,0,(struct sockaddr *)&serveraddr,addrlen));
		{
    
    
			perror("fail to sendto");
			exit(1);
		}
	}
	
	//第四步:关闭套接字文件描述符
	close(socketfd);
	
	return 0;
}
1.2.5.5 绑定bind函数

UDP网络程序想要收取数据需要什么条件?
确定的ip地址
确定的port

怎么完成上面的条件呢?
​ 接收端:使用bind函数,来完成地址结构与socket套接字的绑定,这样ip、port就固定了;
​ 发送端:在sendto函数中指定接收端的ip、port,就可以发送数据了。

由于服务器是被动的,客户端是主动的,所以一般先运行服务器,后运行客户端,所以服务器需要固定自己的信息(ip地址和端口号),这样客户端才可以找到服务器并与之通信,但是客户端一般不需要bind绑定,因为系统会自动给客户端分配ip地址和端口号。

int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
功能:
	将本地协议地址与sockfd绑定
参数:
	sockfd:		文件描述符,socket的返回值
	addr:		网络信息结构体
					通用结构体(一般不用)
						struct sockaddr
					网络信息结构体	sockaddr_in
						#include<netinet/in.h>
						struct sockaddr_in
	addrlen:	addr的长度
返回值:
	成功:0
	失败:-1
	

示例:

#include <stdio.h>			//printf
#include <stdlib.h>			//exit
#include <sys/types.h>
#include <sys/socket.h>		//socket
#include <netinet/in.h>		//sockaddr_in
#include <arpa/inet.h>		//htons	inet_addr
#include <unisted.h>		//close
#include <string.h>

int main(int argc, char **argv)
{
    
    
	if(argc < 3){
    
    
		fprintf(stderr,"Useage: %s ip port\n",argv[0]);
		exit(1);
	}
	
	//第一步:创建套接字
	int sockfd;
	if((sockfd = socket(AF_INET,SOCK_DGRAM,0))== -1)
	{
    
    
		perror("fail to socket");
		exit(1);
	}
	//第二步:将服务器的网络信息结构绑定前进行填充
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(agrv[1]);
	serveraddr.sinport = htons(atoi(argv[2]));
	
	//第三步:将网络信息结构体与套接字绑定
	if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))==-1){
    
    
		perror("fail to bind");
		exit(1);
	}
	
	return 0;
}
1.2.5.6 接收数据—recvfrom函数
ssize_t recvfrom(int sockfd,void *buf,size_t nbytes,int flags, struct sockaddr *from,socklen_t *addrlen);
功能:
	接收UDP数据,并将源地址信息保存在from指向的结构中
参数:
	sockfd:		套接字
	buf:		接收数据缓冲区
	nbytes:		接收数据缓冲区的大小
	flags:		套接字标志(常为0)
					0				阻塞
					MSG_DONTWAIT	非阻塞
	from:		源地址结构体指针,用来保存数据的来源
	addrlen:	from所指内容的长度
注意:
	通过from和addrlen参数存放数据来源信息
	from和addrlen可以为NULL,表示不保存数据来源
返回值:
	成功:接收到的字符数
	失败:-1
1.2.5.7 接收“网络调试助手”的数据

此时网络调试助手作为客户端,ubuntu的程序作为服务器

设置客户端(网络调试助手)

设置服务器(ubuntu程序)

#include <stdio.h>			//printf
#include <stdlib.h>			//exit
#include <sys/types.h>
#include <sys/socket.h>		//socket
#include <netinet/in.h>		//sockaddr_in
#include <arpa/inet.h>		//htons	inet_addr
#include <unisted.h>		//close
#include <string.h>

int main(int argc, char **argv)
{
    
    
	if(argc < 3){
    
    
		fprintf(stderr,"Useage: %s ip port\n",argv[0]);
		exit(1);
	}
	
	//第一步:创建套接字
	int sockfd;
	if((sockfd = socket(AF_INET,SOCK_DGRAM,0))== -1)
	{
    
    
		perror("fail to socket");
		exit(1);
	}
	//第二步:将服务器的网络信息结构绑定前进行填充
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(agrv[1]);			//192.168.3.103
	serveraddr.sinport = htons(atoi(argv[2]));					//9999
	
	//第三步:将网络信息结构体与套接字绑定
	if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))==-1){
    
    
		perror("fail to bind");
		exit(1);
	}
	
	//接收数据
	char buf[128] = "";
	struct sockaddr_in clientaddr;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	while(1){
    
    
		if(recvfrom(sockfd,128,0,(struct sockaddr *)&clientaddr,&addrlen) == -1){
    
    
			perror("fail to recvfrom");
			exit(1);
		}
	}
	
	//打印数据
	//打印客户端的ip地址和端口号
	printf("ip:%s,port:%d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
	//打印接收到数据
	printf("from client:%s\n",buf);
	
	
	return 0;
}
1.2.6 UDP编程—client、server

其中在网络编程开发中client和server双方既可以有发送数据还可以接收数据;一般认为提供服务的一方为server,而接受服务的另一方为client。

1.2.6.1 C/S架构回顾
1.2.6.2 UDP客户端注意点

1.本地IP、本地端口(我是谁)
2.目的IP、目的端口(发给谁)
3.在客户端的代码中,我们只设置了目的IP、目的端口

客户端的本地ip、本地port是我们调用sendto的时候linux系统底层自动给客户端分配的;分配端口的方式为随机分配,即每次运行系统给的port不一样

//UDP客户端的实现
#include <stdio.h>			//printf
#include <stdlib.h>			//exit
#include <sys/types.h>
#include <sys/socket.h>		//socket
#include <netinet/in.h>		//sockaddr_in
#include <arpa/inet.h>		//htons	inet_addr
#include <unisted.h>		//close
#include <string.h>

int main(int argc,char **argv)
{
    
    
	if(argc < 3){
    
    
		fprintf(stderr,"Usage: %s <ip> <port>\n",agrv[0]);
		exit(1);
	}
	
	int sockfd;		//文件描述符
	struct sockaddr_in serveraddr;		//服务器网络信息结构体
	socklen_t addrlen = sizeof(serveraddr);
	
	//第一步:创建套接字
	if((sockfd = socket(AF_INET,SOCK_DGRAM,0)) < 0){
    
    
		perror("fail to socket");
		exit(1);
	}
	
	//客户端自己指定自己的ip地址和端口号,一般不需要,系统会自动分配
	struct sockaddr_in clientaddr;
	clientaddr.sin_family = AF_INET;
	clientaddr.sin_addr.s_addr = inet_addr(argv[3]); 	//客户端的ip地址
	clientaddr.sin_port = htons(atoi(argv[4]));			//客户端的端口号
	
	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制字符串ip地址转换为整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));
	
	//第三步:进行通信
	char buf[32] = "";
	while(1){
    
    
		fgets(buf,sizeof(buf),stdin);
		buf[strlen(buf) - 1] = '\0';
		
		if(sendto(sockfd,buf,sizeof(buf),(struct sockaddr *)&serveraddr,sizeof(serveraddr)))
		{
    
    
			perror("fail to sendto");
			exit(1);
		}
		
		char text[32] = "";
		if(recvfrom(sockfd,text,sizeof(text),0,(struct sockaddr *)&serveraddr,&addrlen) == -1){
    
    
			perror("fail to recvfrom");
			exit(1);
		}
		printf("from server:%s\n",text);
	}
	
	//第四步:关闭文件描述符
	close(sockfd);
	
	return 0;
}
1.2.6.3 UDP服务器注意点

1.服务器之所以要bind是因为它的本地port需要是固定的,而不是随机的
2.服务器也可以主动地给客户端发送数据
3.客户端也可以用bind,这样客户端的本地端口就是固定的了,但一般不这样做。

//UDP服务器的实现
#include <stdio.h>			//printf
#include <stdlib.h>			//exit
#include <sys/types.h>
#include <sys/socket.h>		//socket
#include <netinet/in.h>		//sockaddr_in
#include <arpa/inet.h>		//htons	inet_addr
#include <unisted.h>		//close
#include <string.h>

int main(int argc,char **argv)
{
    
    
	if(argc < 3){
    
    
		fprintf(stderr,"Usage: %s <ip> <port>\n",agrv[0]);
		exit(1);
	}
	
	int sockfd;		//文件描述符
	struct sockaddr_in serveraddr;		//服务器网络信息结构体
	socklen_t addrlen = sizeof(serveraddr);
	
	//第一步:创建套接字
	if((sockfd = socket(AF_INET,SOCK_DGRAM,0)) < 0){
    
    
		perror("fail to socket");
		exit(1);
	}
	
	//第二步:填充服务器网络信息结构体
	//inet_addr:将点分十进制字符串ip地址转换为整型数据
	//htons:将主机字节序转化为网络字节序
	//atoi:将数字型字符串转化为整型数据
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));
	
	//第三步:将套接字与服务器网络信息结构体绑定
	if(bind(sockfd,(struct sockaddr *)&serveraddr, addrlen) < 0){
    
    
		perror("fail to bind");
		exit(1);
	}
	
	while(1){
    
    
		//第四步:进行通信
		char text[32] = "";
		struct sockaddr_in clientaddr;
		if(recvfrom(sockfd,text,sizeof(text),0,(struct sockaddr *)&clientaddr,&addrlen) == -1){
    
    
			perror("fail to recvfrom");
			exit(1);
		}
		printf("[%s - %d]: %s\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port),text);
		
		strcat(text,"*_*");
		
		if(sendto(sockfd,text,sizeof(text),0,(struct sockaddr *)&clientaddr,addrlen)){
    
    
			perror("fail to sendto");
			exit(1);
		}
	}
	
	//第四步:关闭文件描述符
	close(sockfd);
	
	return 0;
}

执行结果:

おすすめ

転載: blog.csdn.net/AAAA202012/article/details/127233159