一、基础知识介绍

1、什么是UDP服务器

UDP服务器是一种使用用户数据报协议(UDP)作为传输协议的服务器。UDP是一种面向无连接的协议,它不保证传输的可靠性,但由于它的高速和低延迟特性,常用于需要快速传输数据的应用程序,如实时音视频传输、在线游戏等。

UDP服务器接收来自客户端的UDP数据报,并根据数据报中的信息进行相应的处理。UDP服务器通常不需要维护连接状态,因此可以处理大量的并发连接,适合于需要处理大量短暂请求的应用程序。

与TCP服务器相比,UDP服务器的实现相对简单,但需要应用程序自己处理传输过程中的错误和丢失问题,因此需要对传输的可靠性有一定的容忍度。

UDP服务器端和客户端均只需1个套接字

TCP中,套接字之间是一对一的关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字,即只需1个UDP套接字就可以向任意主机传输数据。

关于UDP协议

  • 无连接: 知道对端的IP和端口号就直接进⾏行传输, 不需要建立连接; 
  • 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息
  • 面向数据报: 不能够灵活的控制读写数据的次数和数量;

特别注意:一个UDP能传输的数据最大长度是64K(由UDP数据报结构决定的), 然而64K在当今的互联网环境下, 这是一个非常小的数字。如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装。UDP协议虽然具有接收缓冲区,但是其接收缓冲区不保证接收顺序,也就是未必和发送顺序一样,因此这个问题如何解决也是程序员在应用层要做的。

UDP协议至今都是传输层的两大协议之一就是因为它的应用仍然非常广泛,它虽然有一些上面说到的缺点(TCP都没有),但是也有自己不可取代的优点:首先,UDP比TCP要快,虽然TCP没有UDP的那些缺点,但是TCP的三次握手建立连接、四次挥手断开连接、确认、窗口、重传、拥塞控制等机制无疑会导致TCP的速度是比不上UDP的,其次,没有这些机制,UDP也更不容易被攻击,因为UDP不需要这么多机制,那么为了保证其安全需要注意的方面也会相对少一些,能被他们利用的漏洞也就更少,当然UDP也是无法避免被攻击的,只是相比TCP之下更不容易。

比如QQ中的大多数服务,都是用的UDP协议。

基于UDP的应用层协议

NFS: 网络⽂文件系统 
TFTP: 简单文件传输协议 
DHCP: 动态主机配置协议 
BOOTP: 启动协议(用于无盘设备启动) 
DNS: 域名解析协议 

2、什么是端口号

端口号是一种用于标识计算机网络中特定进程或服务的数字标识符。在计算机网络中,每个网络应用程序都需要通过端口号来与其他应用程序进行通信,从而实现网络通信和数据传输。

端口号是一个16位的无符号整数,可以取值范围从0到65535。其中0到1023的端口号被称为“系统端口”或“熟知端口”,通常用于标识一些已知的网络服务,比如HTTP服务使用的端口号是80,FTP服务使用的端口号是21等等。

在网络通信中,发送方和接收方必须使用相同的端口号进行通信,才能互相识别和交换数据。因此,端口号是网络通信中非常重要的一部分。

端口号:标识特定主机上唯一一个进程的标识符

是不是和进程id(pid)很像,没错,确实就是很像,因为进程id是也用来标识一个主机上唯一的一个进程,我们通过某个进程的进程id,可以对该进程进行几乎所有操作,如进程控制,进程间通信等等。而端口号和它的区别就在于:进程id所有进程一定都会有,而端口号却不是所有进程都会有,端口号是需要在不同主机间进行通信时才会绑定给参与通信的某个进程的一个标识符,也就是说,端口号是在两台主机间时才会有的概念和标识,但是进程id是在这个进程被创建出后一直伴随到它死亡(某种程度上就和一个人的名字一样),这个进程id才能被新创建的进程使用。

就比如在一个学校里,每个学生都拥有学号,并且是从你成为这个学校的学生那一刻起就有了,这个学号也能唯一的来标识你,对于学校的管理层来说,他们只要拿到你的学号,就可以得到你的所有信息,并且可以进行相关事宜的安排,这就像进程id。假设这个学校里有一批学生,他们是被学校任命去长期与其他学校学生进行交流的一批人,学校又给这一批人分配了一些编号,这些编号又能唯一标识这些人,这就像端口号。不知道你明白了没,反正我觉得很形象了(๑•̀ㅂ•́)و✧

二、服务器搭建

下面咱们开始做一个echo(回显)服务器,这个服务器要做到的是服务端接收客户端发来的内容并显示,并且把该内容发回客户端,客户端再显示一次

服务器要做的事情就是:接收内容,并显示,然后发回客户端

客户端要做的事情就:发送内容,再接收服务器的回显,再显示出来

那么服务器代码的思路就是这样:

创建套接字(socket)-->填充本地信息(sockaddr_in)-->把本地信息(套接字)与socket(这个我们在这区分一下就不叫它套接字了)一起绑定到操作系统内部,也就是把socket与IP地址加端口号绑定在一起(bind)-->接收(recvfrom)和回发(sendto)

  1. 安装必要的软件包:sudo apt-get update

    (更新软件源)和sudo apt-get install build-essential

    (安装编译工具)。

  2. 服务端代码:
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char* argv[])
    {
    
        int server_sock_fd;
        struct sockaddr_in server_addr, client_addr;
        char recv_buf[100];
        int nbytes = 0;
        socklen_t len = 0;
    
        /* 创建Server Socket */
        server_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (server_sock_fd < 0)
        {
            printf("服务端Socket创建失败");
            return -1;
        }
        printf("服务端Socket创建成功\n");
    
        /* 绑定ip和端口 */
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(atoi(argv[1]));//指定端口号
        bind(server_sock_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    
        printf("服务端Socket绑定成功\n");
        
        while(1)
        {
            /* 接收UDP客户端的数据 */
            printf("等待接收客户端数据:\n");
            len = sizeof(client_addr);
            nbytes = recvfrom(server_sock_fd, recv_buf, 100, 0, (struct sockaddr *)&client_addr, &len);
            printf("ok\n");
            recv_buf[nbytes] = '\0';
            printf("recv %d bytes:%s.\n", nbytes, recv_buf);
    
            //接收用户输入,发送给客户端
            printf("请输入要发送给客户端的数据:");
            fgets(recv_buf, 100, stdin);
            sendto(server_sock_fd,recv_buf,strlen(recv_buf),0,(struct sockaddr *)&client_addr,len);
        }
    
        return 0;
    }
    复制
  3. 客户端代码:创建套接字(socket)-->填充本地信息(sockaddr_in)-->发送(sendto)和接收回显(recvfrom)
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char* argv[])
    {
        int sock_fd;
        struct sockaddr_in server_addr;
        char recv_buf[100];
        int nbytes = 0;
        socklen_t len = 0;
    
        /* 创建Socket */
        sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_fd < 0)
        {
            printf("客户端Socket创建失败");
            return -1;
        }
    
        /* 绑定ip和端口 */
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = inet_addr(argv[1]);
        server_addr.sin_port = htons(atoi(argv[2]));//指定端口号
        while(1)
        {
    	    len = sizeof(server_addr);
            //接收用户输入,发送给服务端
            printf("请输入要发送给服务端的数据:");
    	    fgets(recv_buf, 100, stdin);
            sendto(sock_fd,recv_buf,100,0,(struct sockaddr *)(&server_addr),len);
            
            /* 接收UDP客户端的数据 */
            len = sizeof(server_addr);
            nbytes = recvfrom(sock_fd, recv_buf, 100, 0, (struct sockaddr *)&server_addr, &len);
            recv_buf[nbytes] = '\0';
            printf("recv %d bytes:%s.\n", nbytes, recv_buf);
        
        }
    
        return 0;
    }
    复制
  4. Makefile
    .PHONY:all
    all:udp_server udp_client
     
    udp_server:udp_server.c
    	gcc -o $@ $^ 
     
    udp_client:udp_client.c
    	gcc -o $@ $^ -static
     
    .PHONY:clean
    clean:
    	rm -f udp_client udp_server
    复制

    cc默认动态链接,因此我们的客户端代码要加上-static采用静态链接保证了客户端的可移植性,既是一个好习惯也方便我们可以在两台主机测试,不依赖本地库文件

    最后,用一台机器测试的用时候服务器和客户端的IP地址都用127.0.0.1(本地回环),端口号就都用8080(这个随意,只要是能用的就行)

    我写的这个是个非常简单的基于UDP的服务器,但是UDP协议的本质就是这样了,由这个你也能拓展出很多东西,比如,你把客户端发过来的又转发给另一个客户端,然后两个客户端之间互相回显,这就可以一对一聊天啦,服务器给所有连接的客户端回显,就是聊天室。

三、验证UDP服务是否建立成功

1、在旭日X3与PC直接的运行结果

服务端:

客户端: