Linux C语言程序设计(十九)——基于UDP的网络编程

来源:互联网 发布:淘宝进货网下载 编辑:程序博客网 时间:2024/06/06 08:24

1、UDP协议

        UDP协议是英文User Datagram Protocol的缩写,即用户数据报协议。它是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此UDP协议是一种不可靠的协议。

UDP适用于一次只传送少量数据、对可靠性要求不高的应用和环境。由于没有建立连接的过程,所以它的通信效率高。使用UDP传输,需要发送端和接收端。

UDP特点:无连接的,不可靠的,数据报包小于等于64k,效率高。


2、UDP编程框架

2.1 框架图示

        使用UDP进行程序设计可以分为客户端和服务器端两部分。服务器端主要包含建立套接字、将套接字与地址结构进行绑定、读写数据、关闭套接字几个过程。客户端包括建立套接字、读写数据、关闭套接字几个过程。服务器端和客户端两个流程之间的主要差别在于对地址的绑定(bind())函数,客户端可以不用进行地址和端口的绑定操作。



2.2 服务器端流程

服务器流程主要分为下述6个部分,即建立套接字、设置套接字地址参数、进行端口绑定、接收数据、发送数据、关闭套接字等。

1)建立套接字文件描述符,使用函数socket(),生成套接字文件描述符,例如:

int s = socket(AF_INET, SOCK_DGRAM, 0);

2)设置服务器地址和侦听端口,初始化要绑定的网络地址结构,例如:

struct sockaddr addr_serv;addr_serv.sin_family = AF_INET;                    /*地址类型为AF_INET*/addr_serv.sin_addr.s_addr = htonl(INADDR_ANY);     /*任意本地地址*/addr_serv.sin_port = htons(PORT_SERV);             /*服务器端口*/

3)绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定,例如:

bind(s, (struct sockaddr*)&addr_serv, sizeof(addr_serv));  /*绑定地址*/

4)接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。

5)向客户端发送数据,使用sendto()函数向服务器主机发送数据。

6)关闭套接字,使用close()函数释放资源。


2.3 客户端流程

        UDP协议的客户端流程分为套接字建立、设置目的地址和端口、向服务器发送数据、从服务器接收数据、关闭套接字5个部分。与服务器端的框架相比,少了bind()部分,客户端程序的端口和本地的地址可以由系统在使用时指定。在使用sendto()和recvfrom()的时候,网络协议栈会临时指定本地的端口和地址,流程如下:

1)建立套接字文件描述符,socket();

2)设置服务器地址和端口,struct sockaddr;

3)向服务器发送数据,sendto();

4)接收服务器的数据,recvfrom();

5)关闭套接字,close()。


3、UDP常用函数

UDP协议常用的函数有recv()/recvfrom()、send()/sendto()、socket()、bind()等。事实上这些函数在TCP协议中也是最常用的。

3.1 socket()和bind()

UDP协议建立套接字的方式同TCP方式一样,使用socket()函数,只不过协议的类型使用SOCK_DGRAM,而不是SOCK_STREAM。例如,下面是建立一个UDP套接字文件描述符的代码。

int s;s =  socket(AF_INET, SOCK_DGRAM, 0);

3.2 recvfrom()/recv()

        当客户端成功建立了一个套接字文件描述符并构建了合适的struct sockaddr结构或者服务器端成功地将套接字文件描述符和地址结构绑定后,可以使用recv()或者recvfrom()来接收到达此套接字文件描述符上的数据,或者在这个套接字文件描述符上等待数据的到来。

        recv()函数和recvfrom()函数的原型如下:

#include <sys/types.h>#include <sys/socket.h>ssize_t recv(int s, void*buf, size_t len, int flags);ssize_t recvfrom(int s, void*buf, size_t len, int flags, struct sockaddr*from, socklen_t*fromlen);

第1个参数s表示正在监听端口的套接口文件描述符,它由函数socket()生成。

第2个参数buf表示接收数据缓冲区,接收到的数据将放在这个指针所指向的内存空间中。

第3个参数len表示接收数据缓冲区的大小,系统根据这个值来确保接收缓冲区的安全,防止溢出。

第4个参数from是指向本地的数据结构sockaddr_in的指针,接收数据时发送方的地址信息放在这个结构中。

第5个参数fromlen表示第4个参数所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。

        在UDP中使用recvfrom()函数的时候,可以不绑定发送方的地址。所以在接收数据的时候要判断发送方的地址,只有合适的发送方才能进行相应的处理,因为不同的发送方发送的数据都可以到达接收方的套接字文件描述符,这是由于UDP协议没有按照连接进行区分造成的。



3.3 sendto()/send()

        当客户端成功地建立了一个套接字文件描述符,并构建了合适的struct sockaddr结构或者服务器端成功地将套接字文件描述符和地址结构绑定后,可以使用send()或者sendto()函数来发送数据到某个主机上。

        send()函数和sendto()函数的原型如下:

#include <sys/types.h>#include <sys/socket.h>ssize_t send(int s, const void*buf, size_t len, int flags);ssize_t  sendto(int  s,  const  void*buf, size_t len, int flags, const struct sockaddr*to, socklen_t tolen);

第1个参数s是正在监听端口的套接口文件描述符,通过函数socket获得。

第2个参数buf是发送数据缓冲区,发送的数据放在此指针指向的内存空间中。

第3个参数len是发送数据缓冲区的大小。

第4个参数to指向目的主机数据结构sockaddr_in的指针,接收数据的主机地址信息放在这个结构中。

第5个参数tolen表示第4个参数所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。

send()函数和sendto()函数的返回值在调用出错的时候返回-1;在调用成功的时候,返回发送成功的数据长度,数据的长度可以为0,因此函数返回值为0的时候是合法的。

        下面是一个使用sendto()函数发送数据的简单例子。在这个例子中,先调用socket()函数产生一个数据报类型的套接字文件描述符;然后设置发送数据的目的主机的IP地址和端口,将这些数值赋给地址结构;当地址结构设置完毕后,调用sendto()函数将需要发送的数据通过sendto()函数发送出去。

#include <string.h>#include <sys/types.h>#include <sys/socket.h>int main(int argc, char*argv[]){    int s;                                     /*套接字文件描述符*/    struct sockaddr_in to;                     /*接收方的地址信息*/    int n;                                     /*发送到的数据长度*/    char buf[128];                             /*发送数据缓冲区*/    s = socket(AF_INET, SOCK_DGRAM, 0); /*初始化一个IPv4族的数据报套接字*/    if (s == -1) {                             /*检查是否正常初始化socket*/        perror("socket");        exit(EXIT_FAILURE);    }    to.sin_family = AF_INET;                   /*协议族*/    to.sin_port = htons(8888);                 /*本地端口*/    to.sin_addr.s_addr = inet_addr("192.168.1.1");                                       /*将数据发送到主机192.169.1.1上*/    n = sendto(s, buff, 128, 0, (struct sockaddr*)&to, sizeof (to));                                       /*将数据buff发送到主机to上*/    if(n == -1){                       /*发送数据出错*/        perror("sendto");        exit(EXIT_FAILURE);    }    /*处理过程*/    …}

sendto()函数发送的过程比较简单,如图:



4、简单案例

一个简单的UDP通信过程,如下图所示:



4.1 UDP服务器

UDP的服务器端与TCP服务器端十分相似,不过流程要简单得多。服务器的代码如下,其步骤为:

1)建立一个套接字文件描述符s。

2)填充地址结构addr_serv,协议为AF_INET,地址为任意地址,端口为PORT_SERV(8888)。

3)将套接字文件描述符s绑定到地址addr_serv。

4)调用udpserv_echo()函数处理客户端数据。

/* udp_server.c*/#include <stdio.h>#include <string.h>#include <netinet/in.h>#include "wrap.h"#define MAXLINE 80#define SERV_PORT 8000int main(void){struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int sockfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN];int i, n;sockfd = Socket(AF_INET, SOCK_DGRAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);Bind(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr));printf("Accepting connections ...\n");while (1) {cliaddr_len = sizeof(cliaddr);n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);if (n == -1){perr_exit("recvfrom error");}printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));for (i = 0; i < n; i++){buf[i] = toupper(buf[i]);}n = sendto(sockfd, buf, n, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));if (n == -1){perr_exit("sendto error");}}}


4.2 UDP客户端

UDP客户端向服务器端发送数据UDP TEST,然后接收服务器端的回复信息,并将服务器端的数据打印出来。客户端的代码如下,其步骤为:

1)建立一个套接字文件描述符s。

2)填充地址结构addr_serv,协议为AF_INET,地址为任意地址,端口为PORT_SERV(8888)。

3)将套接字文件描述符s绑定到地址addr_serv。

4)调用udpclie_echo()函数和服务器通信。

/* udp_client.c*/#include <stdio.h>#include <string.h>#include <unistd.h>#include <netinet/in.h>#include "wrap.h"#define MAXLINE 80#define SERV_PORT 8000int main(int argc, char *argv[]){struct sockaddr_in servaddr;int sockfd, n;char buf[MAXLINE];char str[INET_ADDRSTRLEN];socklen_t servaddr_len;sockfd = Socket(AF_INET, SOCK_DGRAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT);while (fgets(buf, MAXLINE, stdin) != NULL) {n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));if (n == -1){perr_exit("sendto error");}n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);if (n == -1){perr_exit("recvfrom error");}Write(STDOUT_FILENO, buf, n);}Close(sockfd);return 0;}

4.3 测试UDP程序

先编译文件,如下

$gcc -o udp_server udp_server.c$gcc -o udp_client udp_client.c

先运行服务器程序,这时UDP服务器会在8888端口等待数据到来。

$ ./udp_server

再运行客户端的程序,客户端向服务器端发送字符串UDP TEST,并接收服务器的信息反馈。

$ ./udp_client

5、总结

由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现。


0 0
原创粉丝点击