网络编程------UDP协议网络程序
以下有关IP地址,端口号,网络字节序列相关知识见:网络编程------IP地址,端口号,套接字,网络字节序
这里要先简单认识UDP协议。
UDP协议
UDP协议也称为用户数据报协议。它是传输层的一种协议。根据该协议在进行数据传送时,两台主机之间不需要相互连接,直接根据对方的IP地址和端口号进行数据传送。所以不用花费时间去连接两台主机,因此根据该协议进行传输时,速度会相对TCP协议快(关于TCP协议的相关知识见:网络编程------TCP协议网络程序)。但与此同时数据传送错误的概率相对TCP协议会较高。同时,根据该协议数据在传送的基本单位是数据报,即源主机一次发送了多少字节的数据,目的主机一次就应将该数据全部接收。
因此,根据UDP协议传输的特点为:
(1)它是一种传输层的协议
(2)无连接
(3)不可靠
(4)面向数据报
下面根据UDP协议来编写一个服务器端和客户端代码来使二者之间进行通信。
在此之前先认识一些接口函数:
sockaddr结构体
该结构体用于存储套接字等相关信息。我们知道,由于IP协议的不同,IP地址的格式也不同,其所对应的套接字类型也不同。因此不同的套接字类型对应一个不同的sockaddr结构体,如下图:
在上述三个结构体中的第一个成员:16位地址类型可以理解为是标识某个套接字类型的。
上图中的第二个结构体struct sockaddr_in用于存放IPv4类型(IP地址是遵照IPv4协议的)的套接字。它的地址类型(或套接字类型)为AF_INET。该结构体的第二个成员即为该套接字中的16位端口号,第三个成员即为该套接字中的32位的IP地址。第三个成员为8字节填充(这里不讨论)。另外,地址类型为AF_INET6表示某结构体中存放的是IPv6类型的套接字(这里没有具体给出该结构体)。
上图中的第三个结构体struct sockaddr_un用于存放域间套接字。它的地址类型(套接字类型)为AF_UNIX。这种类型的套接字主要用于同一主机内的两进程通信。
上图中的第一个结构体struct sockaddr是一种通用的结构体,它可以存放或接收任意类型的套接字类型。比如若该结构体的第一个成员为IPv4类型的标识地址AF_INET,则该结构体存放的是IPv4类型的套接字。若该结构体的第一个成员为AF_INET6,则该结构体中存放的是IPv6类型的套接字。该结构体类似于void*类型,是一种泛性化接口。在以下各接口函数的使用中为保证程序的通用性,统一使用的均是该类型的结构体。对于具体的应用场合在对其进行强制转换即可。
以下的相关socket API中使用的都是IPv4类型的套接字类型,所以,下面具体介绍struct 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)];
};
上述结构体中嵌套了一个封装32位IP地址的结构体,该结构体定义如下:
struct in_addr {
__be32 s_addr;//32位的IP地址
};
下面介绍根据UDP协议在进行通信时需要使用的一些接口。
socket API
1. socket函数
当客户端向服务器端发送请求时,根据冯诺依曼体系结构,数据会在这几个设备之间进行传送:
客户端内存->客户端网卡->网络->服务器端网卡->服务器端内存。
所以要想将数据从内存中写入网卡。而我们知道在Linux中,一切皆文件,要对网卡写入数据,就需要将网卡文件打开,因此可以调用如下接口:
int socket(int domain,int type,int protocol);//头文件<sys.types.h> <sys/socket.h>
函数功能:相当于打开网卡,创建socket文件。
参数说明:
domain:该主机的套接字类型标识,即为上述结构体的第一个成员。若为IPv4类型的套接字,则该参数为AF_INET。
type:服务类型。如果根据UDP协议提供服务,则该参数是SOCK_DGRAM。如果是TCP,则为SOCK_STREAM。
protocol:该参数默认为0。
返回值:成功返回文件描述符,失败返回0。
2. bind函数
服务器要通过某主机中的某特定进程来提供特定的服务,所以必须为该进程指定一个端口号和IP地址(套接字)来绑定套接字文件。当客户端将指定的套接字传送过来时,服务器就根据该套接字找到对应的进程来处理请求。该函数原型如下:
int bind(int socket,const char* sockaddr* address,socklen_t address_len);//头文件:<sys/types.h> <sys/socket.h>
函数功能:绑定端口号,将网络信息和文件信息关联起来
参数:
socket:表示上述socket函数的返回值。
address:表示要绑定的套接字等相关信息,这些信息存放在该结构体变量中
address_len:第二个参数结构体的大小
返回值:成功返回0,失败返回-1。
3. recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,\
struct sockaddr *src_addr, socklen_t *addrlen);//头文件:<sys/types.h> <sys/socket.h>
函数功能:将文件sockfd从src_addr中接收到的消息写入buf中
参数:
sockfd:socket函数创建的文件描述符
buf:输出型参数,用于存放接收到的消息
len:buf的长度
flags:状态位,为0表示当sockfd文件为空时,阻塞等待
src_addr:输出型参数,用于存放发送消息的套接字等相关信息
addrlen:src_addr的长度
返回值:成功返回实际获取的字节数,失败返回-1。
4. sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);//头文件<sys.types.h> <sys/socket.h>
函数功能:将buf中存放的内容先放入sockfd表示的文件中,通过该文件将内容发送给dest_addr。
参数:
socket:sockfd函数返回的文件描述符
buf:缓冲区
len:buf的长度
flags:为0表示sockfd文件为满时,阻塞等待
dest_addr:数据发送给该变量指定的套接字
addrlen:dest_addr的长度
返回值:成功返回实际发送的字节数,失败返回-1。
3. 地址转换函数
以下程序用到的都是IPv4类型的IP地址,所以下面两个地址转换函数都是基于IPv4类型的。
我们人识别的IP地址一般是“点分十进制”的字符串类型。在在网络中进行传送时,要将其转换为整数的格式来传输。所以在发送和接收IP地址时,都要进行相应的转换。转换函数如下:
“点分十进制”字符串转换为整型格式:
in_addr_t inet_addr(const char *cp);//头文件为<sys/socket.h> <netinet/in.h> <arpa/inet.h>
cp:表示要转换的字符串
转换后的结果直接由返回值带回,注意返回值的类型。
整型格式转换为“点分十进制”字符串
char *inet_ntoa(struct in_addr in);
in:表示要转换的整型地址(注意它的类型为结构体)
转换后的字符串直接由返回值带回。
下面来编写简单的UDP网络程序。
UDP服务器
(1)首先,服务器要用socket函数打开一个套接字文件用于接收网络中传来的数据
(2)利用bind函数将上述打开的套接字文件与提供服务的套接字进行绑定
(3)开始接收来自客户端发来的请求
(4)处理请求后,将结果发送给客户端(这里简单处理为将客户端发来的信息在回发过去)
(5)服务器端不断的重复(3)~(4)的工作。
我们将以下提供服务的进程端口号设置为8080(可自己指定),该程序所在的主机IP地址由ifconfig查找,如:192.168.3.95。在程序中要对该套接字进行绑定,因此要将IP地址和端口号以命令行参数的形式传入。
代码如下:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
//该程序运行时的格式为:./a.out 192.168.3.95 8080
int main(int argc,char* argv[])
{
if(argc != 3)//如果传入的参数不为3,则返回用法说明
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
//打开套接字文件:套接字类型为IPv4类型,服务类型为UDP
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)//打开失败
{
printf("socket error\n");
return 2;
}
//将上述套接字文件和命令行指定的套接字进行绑定
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(argv[1]);
local.sin_port = htons(atoi(argv[2]));
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定失败
{
printf("bind error\n");
return 3;
}
//开始接受和发送数据
char buf[128];
struct sockaddr_in client;
while(1)
{
socklen_t len = sizeof(client);//每次接受到得客户端可能不同,所以,要在循环中求长度
//接收来自客户端的请求
ssize_t s = recvfrom(sock,buf,sizeof(buf) - 1,0,(struct sockaddr*)&client,&len);
if(s > 0)
{
buf[s] = 0;
printf("%s %d say# %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
}
//处理请求并将结果发给客户端,这里请求的处理是原样发给客户端
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,len);
}
return 0;
}
UDP客户端
(1)首先客户端也要打开一个套接字文件来接收和发送信息
(2)因为客户端不需要固定的端口号,因此不需要调用bind进行绑定。客户端的端口号由内核自动分配。
(3)向服务器端发送请求
(4)接收服务器端发回的请求处理结果并将其打印出来。
客户端在发送请求时,首先要知道服务器端的套接字,才能将请求送到,所以该信息通过命令行参数的格式传入。
代码如下:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
//程序执行的格式为:./a.out 192.168.3.95 8080
int main(int argc,char* argv[])
{
if(argc != 3)//如果传入的参数不为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;
}
//根据命令行参数来确定服务器端的套接字
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
socklen_t len = sizeof(server);
//开始发送和接受数据
char buf[128];
struct sockaddr_in peer;
while(1)
{
socklen_t len1 = sizeof(peer);
printf("please enter#");
fflush(stdout);
buf[0] = 0;
ssize_t s = read(0,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = 0;
//发送请求
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,len);
}
else if(s < 0)
{
break;
}
//接受服务器端传回的结果
ssize_t ss = recvfrom(sock,buf,sizeof(buf) - 1,0,(struct sockaddr*)&peer,&len1);
if(ss > 0)
{
buf[ss] = 0;
printf("server# %s\n",buf);
}
}
//当不再请求时,关闭套接字文件
close(sock);
return 0;
}
可以通过scp命令将客户端代码远程拷贝到另一主机,然后在两台主机之间进行通信。
scp 本地客户端代码所在的路径 客户端用户名@客户端主机的ip:客户端主机存放客户端代码的路径
结果如下:
服务器端:
客户端: