传输层协议包括TCP协议和UDP协议。UDP协议被称为是用户数据报协议,该协议规定,用户在进行数据传送时,两台主机之间不需要事先建立好连接,也因此,UDP协议提供的是无连接不可靠的服务。使用UDP协议进行传送时,是根据对方主机的IP地址和端口号进行数据传送的。使用该协议进行数据传输时,速度较快(与使用TCP协议进行数据传送时相比)。
基于UDP协议传输的特点是:
(1)无连接,两台主机传输数据前不需要建立连接
(2)提供不可靠的服务
(3)提供面向数据报的服务
根据UDP协议编写一个服务器端和客户端的程序代码使两者之间可以进行通信。
在编写该程序代码之前,需要了解一些接口函数:
一、sockaddr结构体
该结构体用于存储套接字等相关信息。套接字是由IP地址和端口号两部分组成的。由于IP协议的不同,IP地址的格式也不同,其所对应的套接字的类型也不同,因此不同的套接字类型对应一个不同的sockaddr结构体。
1、16位地址类型可理解为是标识某个套接字类型的
2、结构体struct sockaddr_in用于存放的是IPv4类型的套接字,套接字的类型是AF_INET(AF_INET6存放的是IPv6类型的套接字),该结构体的第二个成员为套接字的16位端口号;第三个成员为套接字的32位IP地址。
3、结构体struct sockaddr_un存放的是域间套接字,该种类型的套接字主要是用于同一主机内两进程之间的通信。
4、结构体struct sockaddr是一种通用的结构体,它可以存放或接收任意类型的套接字类型,该结构体类似于一种泛型接口,可以存放IPv4或IPv6套接字类型。
具体介绍struct sockaddr_in结构体:
struct sockaddr_in {
sa_family_t sin_family; //地址类型(套接字类型)
__be16 sin_port; //16位端口号
struct in_addr sin_addr; //封装32位IP地址的结构体
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];//8字节填充
};
上面嵌套的结构体定义如下,存放32位的IP地址:
struct in_addr{
_be32 s_addr; //32位的IP地址
};
下面介绍在使用UDP协议进行通信时需要使用的一些接口函数:
socket API
1、socket函数
当客户端向服务器端发送请求时,根据冯诺依曼体系结构,数据的传送过程如下:
客户端内存-->客户端适配器-->网络-->服务器端适配器-->服务器端内存
在Linux中,几乎所有的操作都是通过文件实现的,因此对网卡(适配器)写入数据,就需要将相关的网卡文件打开,进行操作,因此可以调用如下接口:
#include<sys.types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
功能:相当于打开网卡,创建socket文件
参数说明:
(1)domain:该主机的套接字类型标识,结构体的第一个成员,对于IPv4套接字类型来说,参数应填AF_INET
(2)type:服务类型,如果根据UDP协议提供服务,则该参数应设置为SOCK_DGRAM;若根据TCP协议提供服务,则该参数应设置为SOCK_STREAM
(3)protocol:默认置为0
(4)函数返回值:成功打开,则返回文件描述符;否则,返回-1
2、bind函数
绑定相应的套接字,当客户端将指定的套接字(某台主机上指定好的IP地址和端口号)传过来时,根据指定的端口号和IP地址来绑定相应的套接字文件。服务器端会根据该套接字找到相应的主机上的某进程来处理客户端的请求,该函数的原型如下:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int socket,const char* sockaddr* address,socklen_t address_len);
功能:绑定相关的端口号,将网络信息和文件信息关联起来
(1)socket:表示上面socket函数的返回值
(2)address:表示指定的套接字的相关信息,相关的信息存放在该结构体变量中
(3)address_len:第二个参数结构体的大小
返回值:成功绑定时返回0,失败则返回-1
3、recvfrom函数
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,\
struct sockaddr *src_addr,socklen_t *addrlen);
功能:将文件sockfd从src_addr中接收到的消息写入到buf中
(1)sockfd:上面使用socket函数创建的socket文件返回的文件描述符
(2)buf:是存放输出的参数,用于存放接收到的消息
(3)len:标识buf的长度
(4)flags:状态标志位,置flag为0,
(5)src_addr:用于存放发送消息的套接字等相关的信息
(6)addrlen:存放src_addr的长度
成功时,返回实际收到的字节数,失败则返回-1
4、send_to函数
#include<sys.types.h>
#include<sys/socket.h>
ssize_t sendto(int sockfd,const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr,socklen_t addrlen);
功能:将buf中的内容先放入到文件标识符为sockfd的文件中,再通过该文件将内容发送给dest_addr
(1)sockfd:socket函数返回的文件描述符
(2)buf:缓冲区
(3)len:缓冲区的长度
(4)flags:状态标志位,置为0
(5)dest_addr:将数据发送给该变量指定的套接字
(6)addrlen:dest_addr的长度
返回值:成功时返回实际发送的字节数,失败时返回-1
3、地址转换函数
该程序使用到的都是IPv4类型的IP地址,所以下面使用的地址转换函数都是基于IPv4类型的
我们在计算机网络中接触到的IP地址,通常都是以“点分十进制”的字符串形式来表示的,而在网络传送时,需要将其转化为整型格式来传输,所以在发送和接收IP地址时,都要进行IP地址的转换,将其转化为整型格式。
(1)“点分十进制”转化为整型格式
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
in_addr_t inet_addr(const char *cp);
cp:这个参数表示需要转化的字符串,转化后的结果直接由返回值带回,返回值的类型是整型
(2)整型形式转化为“点分十进制”字符串
char *inet_ntoa(struct in_addr in);
in:表示要转化的整形地址(为结构体形式),转化后的结果同样以返回值带回
了解了以上知识后,我们开始编写简单的基于UDP协议的网络程序。
UDP服务器
(1)刚开始,服务器端会使用socket函数打开一个套接字文件,使用该套接字文件接收网络中传来的数据
(2)使用bind函数,将服务器端打开的套接字文件与提供服务的套接字文件进行绑定
(3)开始接收来自客户端发来的请求
(4)处理完请求后,将结果发送给客户端(该程序较简单,实现为不处理客户端发来的信息,直接原样发回去)
(5)服务器端不断的重复(3)~(4)的工作
我们将以下提供服务的进程端口号设置为8080(可自己指定),该程序所在的主机ip地址由ifconfig查找。在程序中,要对该套接字进行绑定,因此,要将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类型,故参数为AF_INET;服务类型为UDP,故参数为SOCK_DGRAM,打开成功时,返回的是文件描述符
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)//打开失败,返回-1,输出提示信息
{
printf("socket error\n");
return 2;
}
/*将上述套接字文件和命令行指定的套接字进行绑定,IP地址和套接字以命令行参数的形式传入
因为bind函数的第二个参数为结构体变量(存放着套接字文件的相关信息),所以先定义结构体变量,存放套接字的相关信息*/
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)//绑定失败,返回值为-1
{
printf("bind error\n");
return 3;
}
//绑定完成后,开始接收和发送数据
char buf[128]; //定义缓冲区数组
struct sockaddr_in client;//定义sockaddr_in结构体来存放IPv4类型的套接字
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)接收服务器端发回的请求处理的结果并将其打印出来
(5)客户端在发送请求之前,首先要知道服务器端的套接字,才能将请求送达,所以该信息通过命令行参数的形式传入
#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>
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)//创建失败,返回-1
{
printf("sock error\n");
return 2;
}
//根据命令行参数来确定服务器端的套接字
struct sockaddr_in server; 定义结构体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; //定义结构体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函数向服务器端发送请求
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,len);
}
else if(s < 0)
{
break;
}
//接收服务器端传回的结果,将文件标识符sock标识的文件接受到的信息写入到buf中
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;
}
分别在两个终端运行服务器端程序和客户端程序,得到的结果如下图所示:
客户端:
服务器端: