简单的UDP网络编程
本文,我们将编写一个基于UDP实现的简单网络服务器与客户端。
一. UDP协议初识
UDP协议又叫
用户数据报协议,是传输层的一种协议。根据该协议在进行数据传输时,两台主机之间不需要相互连接,直接根据对方的IP地址和端口号进行数据传送。所以不用花费时间去连接主机,因此根据该协议进行传输时,速度会相对TCP协议快一点;但同时数据传送错误的概率相对TCP协议会较高;而且基于UDP协议进行数据传输,传输的基本单位是数据报,即源主机一次发送了多少字节的数据,目的主机一次就应将该数据全部接收。
所以UDP协议的特点有:
(1)传输层协议;
(2)无连接;
(3)不可靠传输;
(4)面向数据报。
二. socket编程接口
我们之前提到过,“IP地址+端口号”也就是socket可以唯一标识网络上某一主机的某一进程。所以我们要实现网络通信,我们要利用到socket。这里先介绍一些与socket有关的函数接口与结构。
1. sockaddr结构
socket API即套接字接口是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4和IPv6及UNIX Domain Socket。然而,各种网络协议的地址格式并不相同,所以就有以下几种地址格式。
这些结构体都可以用于储存套接字等的相关信息。其中,sockaddr结构是泛型接口,适用于各种套接字;sockaddr_in结构适用于网络通信;sockaddr_un适用于不跨网络不跨主机间的通信。
(1)IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型、16位端口号、32位IP地址;
(2)
IPv4和IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容;
(3)socket API可以都有struct sockaddr*类型表示,在使用的时候强转为sockaddr_in。这样程序就比较
通用,可以接收各种类型的sockaddr结构体指针作为参数。
我们用到的都是IPv4类型的,所以我们详细介绍一下sockaddr_in结构体。
struct sockaddr_in { sa_family_t sin_family; /* Address family:地址类型(套接字类型) */ __be16 sin_port; /* Port number:16位端口号 */ struct in_addr sin_addr; /* Internet address:封装32位IP地址的结构体 */ /* Pad to size of `struct sockaddr'. : 8字节填充字段(这里不讨论)*/ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)]; };
struct in_addr { __be32 s_addr;//32位的IP地址 };
2. socket相关接口函数
(1)socket
函数原型:
1)函数功能:创建套接字,也可以理解为打开网卡。因为Linux下一切皆文件,且在网络上发送数据时,数据到目的主机首先到的就是网卡。
2)函数参数:
domain :
主机的套接字类型标识,sockaddr_in结构体的第一个成员。若为IPv4类型的套接字,则为AF_INET
type:服务类型。若依据UDP协议提供服务,该参数是SOCK_DGRAM;若是TCP,则为SOCK_STREAM
protocol:缺省为0
3)函数返回值:成功返回文件描述符,失败返回-1
(2)bind函数
函数原型:
1)函数功能:绑定套接字,将文件信息与网络信息连接起来
2)函数参数:
scokfd:socket函数返回的文件描述符
addr:要绑定的套接字的相关信息,它们用sockaddr结构体保存
addrlen:第二个参数的大小
3)函数返回值:成功返回0,失败返回-1
(3)recvfrom函数
函数原型:
1)函数功能:将套接字文件sockfd从结构体src_addr读到的内容放到buf缓冲区中,是UDP个性化的接收数据函数接口
2)函数参数:
scokfd:已连接套接字的文件描述符,即socket函数返回值
buf:接收数据的缓冲区
len:接收缓冲区的大小,希望读到的数据大小
flag:调用操作方式,设置为0时表示读不到数据时就阻塞等待
src_addr:存放发送消息的套接字的相关信息
addrlen:参数src_addr的长度
3)函数返回值:成功返回接收到的数据的大小(字节数),失败返回-1
(3)sendto函数
函数原型:
1)函数功能:将buf中的数据放入套接字sockfd中,然后发给dest_addr,是UDP的个性化发送数据的函数接口
2)函数参数:
sockfd:socket函数返回值
buf:用于存放数据的缓冲区
len:参数buf的大小
flags:调用操作方式,设置为0表示读不到数据时阻塞等待
dest_addr:指向目的套接字
addrlen:参数dest_addr的大小
3)返回值:成功返回发送数据的字节数,失败返回-1
(4)地址转换函数
我们常见的IP地址为“点分十进制”的地址,而在网络中传送的为四字节的IP地址。所以在网络上发送与接收数据时我们要对IP地址进行转换。
1)点分十进制转四字节(字符串转in_addr)函数
函数原型:
函数参数:
cp:要转换的字符串IP地址
函数返回值:转换后的整型结果
2)四字节转点分十进制函数
函数原型:(头文件与上面点分十进制转四字节函数相同)
函数参数:要转换的整型IP地址
函数返回值:转换后的结果
三.UDP的简单网络程序实现
1. 服务器实现
实现过程:
(1)先创建一个套接字,用来接收网络传来的数据
(2)利用bind函数将创建好的套接字与提供服务的套接字连接起来
(3)开始接收客户端发来的请求
(4)处理请求之后将处理结果发给客户端
(5)服务器一直重复(3)(4)的过程,因为服务器应该做到“一直提供服务”
实现代码如下:
//UDP服务器 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h>//close #include <arpa/inet.h>//网络字节序和主机字节序库转换 #include <stdlib.h>//atoi #include <netinet/in.h> #include <string.h> #define SIZE 128 void service(int sock) {//这里我们提供的服务是:将客服端的消息拿到并打印,再将消息发回去 char buf[SIZE]; for( ; ; )//服务器应该周期性的一直提供服务,可用守护进程实现,这里先用死循环实现 { struct sockaddr_in peer; socklen_t len = sizeof(peer);//socklen_t 可认为简单整形 //udp不用read、write读写,它有自己个性化的接口 ssize_t s = recvfrom(sock, buf, sizeof(buf)-1, 0 , (struct sockaddr*)&peer, &len); if(s > 0)//读成功 { buf[s] = 0;//保证以\0结尾,上面参数sizeof-1就是给\0留位置 if(strcmp("quit", buf) == 0) { printf("client quit\n"); break; } printf("[%s:%d]# %s\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port), buf);//将客户端发来的消息打印出来 sendto(sock, buf, strlen(buf), 0, (struct sockaddr*)&peer, len);//将内容给客户端发回去 } } } //./udp_server 192.168.3.55 8080 int main(int argc, char* argv[]) { if(argc != 3)//命令行输入格式不正确 { printf("Usage: %s [IP] [port]\n", argv[0]); return 3; } //1.创建套接字(这里是数据报的套接字,因为UDP面向数据报) int sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock < 0)//创建套接字失败 { printf("socket error\n"); return 1; } struct sockaddr_in local; local.sin_family = AF_INET;//地址类型 local.sin_port = htons(atoi(argv[2]));//端口号;命令行的字符串”808"转换为整数的8080;主机序列转换为网络序列,16位 local.sin_addr.s_addr = inet_addr(argv[1]);//IP地址;将点分十进制转换为四字节地址(因为该字段在结构体中就是四字节); //2.绑定(将文件信息与网络信息关联起来),绑定之后就可提供服务了 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) { printf("bind error\n"); return 2; } //3.提供服务 service(sock); close(sock);//Linux下一切皆文件,能打开就要关闭 return 0; }
2. 客户端实现
实现过程:
(1)创建一个套接字,用来发送接收消息
(2)因为客户端的端口号不需要固定,它由内核随机分配,所以不需要绑定过程
(3)向服务器端发送请求
(4)接收服务器端发送回来的结果并处理
实现代码:
//UDP客户端 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h>//close #include <arpa/inet.h>//网络字节序和主机字节序库转换 #include <stdlib.h>//atoi #include <netinet/in.h> #include <string.h> //./udp_client 127.0.0.1 8080 int main(int argc, char* argv[]) { if(argc != 3) { printf("Usage:%s [ip] [port]\n", argv[0]); return 1; } //客户端不需要绑定,因为它的端口号不需要特定 int sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock < 0) { printf("sock error\n"); return 2; } char buf[128]; struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(atoi(argv[2])); server.sin_addr.s_addr = inet_addr(argv[1]); socklen_t len = sizeof(server); for( ; ; ) { buf[0] = 0;//清零 //1.从标准输入读数据 ssize_t s = read(0, buf, sizeof(buf)-1); if(s > 0)//读成功 { buf[s-1] = 0;//会把回车读进来,这里不要回车 //2.将读到的数据发出去 sendto(sock, buf, strlen(buf), 0, (struct sockaddr*)&server, len);//将数据发到socket if(strcmp("quit", buf) == 0) { printf("client quit\n"); break; } //3.将发出的数据再收回来 s = recvfrom(sock, buf, sizeof(buf)-1, 0, NULL, NULL);//这里知道是谁发的消息,所以参数设置为NULL即可 if(s > 0) { buf[s] = 0; printf("server echo# %s\n", buf); } } } close(sock); return 0; }
3. 测试过程
服务器端和客户端都通过命令行格式,输入所需要的参数:IP地址、端口号。因为在网络上我们可以通过这两个参数唯一地确定某一主机上的某一进程。我们是通过本地环回来测试的,本地环回常用于测试网络服务器,在有条件的情况下大家尽量通过网络用两台主机去测试。所以测试结果如下:
图左为客户端,右边为服务器端。127.0.0.1即为本机的本地环回,端口号我们使用了8080,端口号可以任意指定,但是要避开知名端口号。