基于UDP协议的服务器

在撸我们的服务器之前,先好好给大家好好理一理套接字这块的一些知识和概念。


你如果学过网络,问问你自己,什么是端口号?这个概念可不是一个模模糊糊的东西

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

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


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


关于端口号再扩展一点重要的东西

端口号的划分:

0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的,也就是你不能用的。

扫描二维码关注公众号,回复: 2351242 查看本文章

1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的,也就是你能用的。

这些也算是一个程序员的常识了,记不住也混个眼熟吧
ssh服务器, 使⽤用22端⼝
ftp服务器, 使⽤用21端⼝ 
telnet服务器, 使⽤用23端⼝ 
http服务器, 使⽤用80端⼝ 
https服务器, 使⽤用443 

再来说说啥是IP地址呢?

不不不,我可不是要你告诉我IP地址咋划分,子网掩码和IP地址有什么关系之类的,那都是IP地址的组成,我想问的是IP地址到底是个用来干什么的东西呢?为什么要有它?

IP地址:用来标识网络中,接入公网的唯一一台主机(注意,这里的公网请简单的理解,就是指的你要网络通信的那一块范围,不然又有人要和我IP地址也会变啊,怎么标识??所以说我们说的公网不是说全世界的那个互联网哦)

这个时候,你就毫无疑问的知道,一台主机的IP地址加上其上的一个端口号,是不是就可以在互联网中唯一的标识特定主机上的一个特定进程!套接字(socket)是一个翻译过来的词,由于网络中的套接字技术本身也是一种抽象出来的概念,其理解没有一个绝对的说法,其最好的理解就是:IP地址+端口号

再把UDP协议好好理一理

说到UDP当然是先好好说说它的三大特点

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

我们知道,在通信技术不发达的时代,寄信是人类最常用的通信方式,UDP协议某些程度上和寄信挺像的

无连接:写信的时候,你知道知道对方的邮政编码,对方知道寄信人,就能写信了,别的都不用管

不可靠:写信这种通信方式是相当不可靠的(我们的UDP还是比写信可靠很多很多的),如果邮递员遇到恶劣天气无法前往就放弃送这封信了,或者邮递员在路上把你的信弄丢了,都会到导致信寄不到,也就是导致通信的失败,而邮递员也不会去告诉你,信没送到,毕竟那么多封信嘛,他根本就知道,这样,寄信人就傻傻的发,收信人就傻傻的收,有时候后果还挺严重啊

关于面向数据报这一特点在寄信中不好类比,在我们的网络通信中是这样:

无论应用层交给UDP多长的报文, UDP都原样发送, 既不会拆分, 也不会合并;

比如用UDP传输100个字节的数据: 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100 个字节; 而不能循环调⽤用10次recvfrom, 每次接收10个字节;


特别注意:一个UDP能传输的数据最大长度是64K(由UDP数据报结构决定的,我们再次去细说,反正告诉你就是这样), 然而64K在当今的互联网环境下, 这是一个非常小的数字。如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装(尴尬的是UDP的接收缓冲区也不能保证收的顺序和你发的顺序一致,因此UDP的应用场景最好不要超过一次64K的传输数据大小,不然后果你自己想呗)

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

比如QQ语音这样的一个应用,你想想是不是就能用UDP,打电话嘛,有一点点噪声也不影响嘛,只要及时听到对方说话就好啦

基于UDP的应用层协议

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

当然还有咱们自己写基于UDP程序时自定义的应用层协议

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

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

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

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

创建套接字(socket)-->填充本地信息(sockaddr_in)-->把本地信息同套接字一起绑定到操作系统内部-->(接收和回发)

#include <stdio.h>                                                                                                                          
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h>


int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage %s [ip] [port]\n",argv[0]);
        return 1;
    }

    int sock = socket(AF_INET,SOCK_DGRAM,0);//协议IPV4 udp传输协议
    if(sock < 0)
    {
        perror("socket");
        return 2;
    }

    printf("sock: %d\n",sock);

    //填充本地信息
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));  //把主机序列的字符串端口号转换成整型  然后转换为二子皆网络端口号 因为网络端口号是两个字节所以用s
    local.sin_addr.s_addr = inet_addr(argv[1]);  //把点分十进制字符串IP地址转换为网络字节序的四字节整型IP地址
    
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)    //把用户栈上的local绑定到操作系统内部
    {
        perror("bind");
        return 3;
    }

    char buf[1024];
    while(1)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        //收到的东西放到buf里
        ssize_t s = recvfrom(sock, buf, sizeof(buf)-1,0,(struct sockaddr*)&client,&len);//通过client带回的信息知道是谁连接了我,我后面要发送
        if(s > 0)
        {
            buf[s] = 0;                                                                  
            //打印出客户端的IP地址和网络端口号,并用这两个函数将其转换成我们想看到的样子,并打印出client发过来的内容
            printf("[%s:%d]: %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
            sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));//把buf中的内容发回客户端                               
        }
    }

}

客户端代码的思路:

创建套接字(socket)-->填充本地信息(sockaddr_in)-->(发送和接收回显)

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <stdlib.h>


int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage %s [ip] [port]\n",argv[0]);
        return 1;
    }

    int sock = socket(AF_INET,SOCK_DGRAM,0);//协议IPV4 udp
    if(sock < 0)
    {
        perror("socket");
        return 2;
    }

    printf("sock: %d\n",sock);

    //填充本地信息
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));  //把主机序列的字符串端口号转换成整型  然后转换为二子皆网络端口号 因为网络端口号是两个字节所以用short
    local.sin_addr.s_addr = inet_addr(argv[1]);  //把点分十式十进制字符串IP地址转换为网络字节序的四字节整型IP地址
    
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)    //把用户栈上的local绑定到操作系统内部
    {
        perror("bind");
        return 3;
    }

    char buf[1024];
    while(1)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        //收到的东西放到buf里
        ssize_t s = recvfrom(sock, buf, sizeof(buf)-1,0,(struct sockaddr*)&client,&len);//通过client带回的信息知道是谁连接了我,我后面要发送给谁
        if(s > 0)
        {
            buf[s] = 0;                                                                  
            //打印出客户端的IP地址和网络端口号,并用这两个函数将其转换成我们想看到的样子,并打印出client发过来的内容
            printf("[%s:%d]: %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
            sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));//把buf中的内容发回客户端
        }
    }

}

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

客户端代码采用静态链接保证了客户端的可移植性,不依赖本地库文件

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

咱们这个是个非常简单的基于UDP的服务器,但是由这个你也能拓展出很多东西,比如,你把客户端发过来的又转发给另一个客户端,然后两个客户端之间互相回显,这就可以一对一聊天啦,服务器给所有连接的客户端回显,就是聊天室啦,是不是挺有意思的,还有什么方式你可以自己去想想。

代码点我

猜你喜欢

转载自blog.csdn.net/weixin_38333555/article/details/80780206