udp部分的网络编程,主要分为3个部分,套接字的创建,端口号和ip地址的绑定,数据的读取和发送,我们一般创建两个文件,分别用来建立服务器和客户端。我们主要来看一下服务器的代码。
UDP服务器代码
服务器的编写可以分为:udp套接字的创建,绑定端口号和ip地址,等待接受数据并处理。这里我们简单的做一个回显服务器,就是服务器接收到客户端发来的数据后不进行什么处理,直接将原数据返回给客户端。
- udp套接字创建
说到网络编程,不得不提的就是网络套接字socket,无论你想要做什么,只要是涉及到网路编程的,都必须使用套接字,可以说套接字是整个网络编程的基础。
套接字的创建调用的是socket()
函数,函数原型如下:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数domain
指的的通行领域,主要包括AF_INET
IPV4和AF_INET6
IPV6。
参数type
表示协议种类,因为这里我们编写的是udp套接字,因此使用SOCK_DGRAM
(即支持数据包,无连接,不可靠,固定最大长度消息)。
参数protocol
目前可先不考虑,填0即可,即将协议指定为0;
因此我们的udp套接字创建时非常简单的,具体代码如下:
int sock = socket(AF_INET,SOCK_DGRAM, 0);
if(sock == -1){
perror("socket");
return 1;
}
socket的返回值:成功时返回套接字的文件描述符,失败是返回-1,并且会适当的设置错误码(errno)。因此我们在创建完套接字后,要人为的检测返回值,常看是否出错。
绑定端口号和IP地址
绑定端口的API是bind()函数,函数原型为:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
要绑定端口号和ip地址,就得有一个套接字,因此第一个参数就是套接字的文件描述符。第二个参数就是要绑定的端口号和ip地址,只不过是按照结构体的形式给出;第三个参数就是结构体的大小。
我们先来看一下这个结构体struct sockaddr
,它是我们网络编程中的一个通用结构体,我们一般定义struct sockaddr_in
这个结构体,它包括了16位的端口号,和32位的IP地址。只不过我们在使用的时候强制类型转换为struct sockaddr
。
它的返回值如大多数函数一样,成功返回零,失败返回-1;
绑定的里程如下:
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if( bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0 ){
perror("bind");
return 3;
}
我们先定义一个struct sockaddr_in
的变量local
,然后选择它的地址类型local.sin_family
为AF_INET
(AF_INET
为IPv4类型,AF_INET6
为IPv6类型的网络)。local.sin_port
为端口号, local.sin_addr.s_addr
为IP地址。argv[2]
,argv[1]
分别存放端口号,和IP地址。
需要主要的是,端口号和IP地址,并不是简单的赋值就可以了,因为这个argv数组里面存放的是字符串,因此在赋值时要特别注意。
在对端口号赋值时因为网络字节序是大端,而你的主机不指定是大端,因此有个通用的函数htons()
,如果你的主机的大端那它什么都不做,如果是小端,那他会转成大端,因此,一般的端口号的赋值为:local.sin_port = htons(PortNum);
如果你给的字符串形式,那就需要调用atoi()
将其转化成整形。
在对ip地址赋值时,我们所给的一般地址为点分十进制形式的,因此我们需要将其转换的网络字节序,这里就用到inet_addr()
函数。
我们在来看一下这个网络结构体的结构:
虽然socket api的接口是sockaddr,单是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里面主要有三部分:地址类型,端口号,ip地址。
in_addr结构体对ip的地址进行了一次封装,真正的IP地址存放在 s_addr里面。
接受数据并处理
接受数据就是从sock套接字中读取数据,调用函数recvfrom()
可以进行读取,函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
总共包含6个参数,前4个参数是普通类型的参数,而后面两个是输出型参数,因为我们作为服务器,肯定要知道数据是从哪里发过来的,因此将对端的信息保存在后面两个参数中。
要套接字读取数据,却少不了的就是套接字的文件描述符了,第二个参数是你自己定义的缓存区的地址,即读来的信息存放到哪里;第三个参数是缓存区的大小,第四个参数我们可以先不关注,填0即可。后面的两个参数一个是用来存放对端的ip地址和端口号,另外一个是这个结构体的大小,以指针的方式传递。
因为是回显服务器,所做的处理就是将从哪里得到的数据在发回到哪里。
向套接字发送数据使用的sendto()
函数,函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
前面的和读取函数类似,后面的两个就是服务器的地址和端口号,和结构体的长度。