ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

linux UDP并发服务器

2021-01-22 18:01:52  阅读:180  来源: 互联网

标签:UDP struct 队列 数据包 head linux 服务器 recv listen


摘要:
本文将讨论UDP的并发实现机制。给出了两种实现方法。第一种是最为常见的,TFTP传输的方式。
第二种是对UDP进一步封装,以达到并发的可能。主要是采用队列、多线程的方法。后面会给出一个简单的实现例子,以供大家参考。功能方面较为简单,以后会慢慢完善。

众所周知,通常所见的的TCP服务器都是并发实现的,即服务同时处理多个请求,
而不是等待前一个完成再处理下一个请求,这个实现得益于TCP的listen()与connect()的分工处理机制。
而对于 UDP 没有这种监听和连接机制,所以它必须等待前一处理完成才能继续处理下一个客户的请求。
但并不是说UDP实现并发服务器是不可能的,只是与上面的实现稍有不同。

UDP服务器并发的两种方法:
一、比较常用的处理方法是:
服务器(知名端口)等待一下客户的到来,当一个客户到来后,记下其IP和port,然后同理,
服务器fork一个子进程,建立一个socket再bind一个随机端口,然后建立与客户的连接,
并处理该客户的请求。父进程继续循环,等待下一个客户的到来。在tftpd中就是使用这种技术的。

大概的实现如下:
        for ( ; ; )
        {
           /* 等待新的客户端连接 */
            recvform( &from_addr)

            /* 创建一个新的进程,由该进程去处理 */
           if (fork() == 0)
                break; //子进程跳出循环   
        }

        //child now here
        peer = socket(AF_INET, SOCK_DGRAM, 0);
        //绑定一个随机端口
        myaddr.sin_port = htons(0); 
        bind(peer,(struct sockaddr *)&myaddr, / 
                                        sizeof myaddr)
        /* 
            把这个套接字跟客户端的地址连接起来
            这也就意味之后之后套接字使用 send recv这些函数时
            都是直接跟指定的客户端进行通信的
        */
        connect(peer, (struct sockaddr *)&from, sizeof from)

以上方式简单实用,但是每来个客户端都需要创建一个新的 socket,为每个客户端分配一个新的临时端口,然后客户端,之后的通信需要跟新的端口进行数据传输。
二、如果对上述不满意。我们可以采用新的策略。对UDP进行封装,以此实现类型TCP的功能。 我们来看下一个简单 TCP 服务器的原型:12
       int main()
        {
            /* 初始化socket套接字 */
            sockfd = init_socket();             /* 开始监听 */
            if(listen(sock_fd, BACKLOG) == -1)
            {                perror("listen is error\r\n");
                exit(1);
            }            while(1)
            {                /* 等待新的客户端连接 */
                if((new_fd = accept(sock_fd, (struct sockaddr *)&their_addr, &sin_size)) == -1)
                {                    perror("accept");
                    continue;
                }                /* fork出一个进程,由该进程去处理这个连接 */
                if(!fork())
                {                }            }        }
我们封装出几个跟上面的TCP相似的函数接口。使用这些接口,可以很简单写出一个UDP并发服务器。例如:
/* 主函数 */
        int main(int argc, char *argv[])
        {
            /* 定义一个listen指针。该结构体是自己定义的 */
            struct listen *_listen;
            /* 初始化socket,这个初始化过程跟普通的UDP初始化 socket套接字一样 */
            sockfd = init_socket();             /*
                开始监听这个socket. 最大的连接数为10,也就是说最多只有10个客户端
                封装好的一个函数,功能有点类似于 TCP协议中的 listen 函数
            */
            server_listen(&sockfd, 10);
            while(1)
            {                /* 
                获得一个连接。类似于TCP的 accept 函数 
                需要注意的是,如果没有连接, server_accept 函数将进入休眠状态,直到有一个新的客户端数据
                客户端只有在第一次发生数据过来的时候,才会创建一个新的 listen ,并唤醒 server_accept 函数
                之后,这个客户端的所有数据都将发送到 这个新的 listen 的数据队列中。
                所以。通过这个 listen ,我们可以创建一个进程,由该进程去处理这个客户端之后的请求
                这里,listen 有点像 TCP 协议中的 accept 函数新建的 sockfd
                */
                _listen = server_accept();                /* 
                虽然说 server_accept 会进入休眠,但是仍然会被其它信号唤醒,所以要做个判断
                判断下是否为 NULL 。为 NULL 则说明没有新的连接 
                */
                if(_listen == NULL){
                    continue;
                }                printf("new client \r\n");
                /* 
                启动一个 listen_phread 线程,并且,由该线程去处理这个连接
                类似于TCP 的fork
                */
                listen_pthread(_listen, listen_phread);            }        }
listen_phread 线程简单实现:
void *listen_phread(void *pdata)
        {
            int ret;
            char buf[1204];
            struct sockaddr_in clientaddr;
            /* 获得 listen */
            struct listen *_listen;
            _listen = (struct listen *)pdata;            while(1)
            {                /*
                recv_from_listen 也是一个封装好的函数,功能是从这个 lsiten 中获取数据
                最后一个参数表示无数据时休眠的时间
                -1 表示永久休眠。知道有数据为止
                */
                ret = recv_from_listen(_listen, &clientaddr, buf, 1204, -1);
                if(ret == -1)
                {                    printf("%p recv is err \r\n", _listen);
                }else{
                    printf("%p recv %d byte data is [%s]\r\n", _listen, ret, buf);
                    if((ret = sendto(sockfd, buf, ret, 0, (struct sockaddr *)(&(_listen->addr)), 
                                            sizeof(struct sockaddr))) == -1)
                    {                        perror("sendto :");
                    }                    printf("sento [%s]\r\n", buf);
                }            }            /* 关闭连接,会释放内存,注意,一个listen 被创建后,需要使用这个函数释放内存 */
            listen_close(_listen);        }
lsiten 结构体原型:
12
        struct listen{      
            struct sockaddr addr;       /* 数据包地址信息 */
            int data_num;               /* 数据包数量 */
            int list_flg;               /* 是否已经被监听了 */
            pthread_mutex_t mutex;  /* 线程锁 */
            /* 这两个条件变量相关的 */
            pthread_mutex_t recv_mtx;
            pthread_cond_t recv_cond;
            struct list_head head;      /* 数据包队列 */
            struct list_head listen_list;       /*接收的线程队列 */
        };实现原理:    这个接口函数是基于队列、多线程实现的。这里简单地说下原理,稍后有时间我会对代码进一步分析1.  listen 队列:
    系统会创建一个队列,该队列的成员为一个 listen ,每个 listen 的 addr 元素会记录下自己要接收的    客户端。    之后,server_listen 创建一个线程,由该线程去接收数据。    接收到网络数据后,会遍历 listen 链表,找到一个想要接收这个数据的 listen 。    如果没有,会创建一个新的 listen ,并将这个 listen 加入到 listen 队列中去2   数据包队列
    找到 listen 后,每个 listen 其实就是一个 数据包队列头。系统会把数据放到 这个 listen 数据包队列中去    然后唤醒 recv_from_listen 也就是说,系统的队列结构如下listen 队列    listen(1) -> listen(2) -> listen(3) -> listen(4) -> .......
        |           |           |      data(1)     data         data
        |           |      data(1)     data
每个listen本身就是一个数据包队列头recv_from_listen 函数会试图去从一个 listen 的数据包队列中获取数据,如果没有数据,则进入休眠状态。
lsiten 结构体原型:
12
        struct listen{      
            struct sockaddr addr;       /* 数据包地址信息 */
            int data_num;               /* 数据包数量 */
            int list_flg;               /* 是否已经被监听了 */
            pthread_mutex_t mutex;  /* 线程锁 */
            /* 这两个条件变量相关的 */
            pthread_mutex_t recv_mtx;
            pthread_cond_t recv_cond;
            struct list_head head;      /* 数据包队列 */
            struct list_head listen_list;       /*接收的线程队列 */
        };实现原理:    这个接口函数是基于队列、多线程实现的。这里简单地说下原理,稍后有时间我会对代码进一步分析1.  listen 队列:
    系统会创建一个队列,该队列的成员为一个 listen ,每个 listen 的 addr 元素会记录下自己要接收的    客户端。    之后,server_listen 创建一个线程,由该线程去接收数据。    接收到网络数据后,会遍历 listen 链表,找到一个想要接收这个数据的 listen 。    如果没有,会创建一个新的 listen ,并将这个 listen 加入到 listen 队列中去2   数据包队列
    找到 listen 后,每个 listen 其实就是一个 数据包队列头。系统会把数据放到 这个 listen 数据包队列中去    然后唤醒 recv_from_listen 也就是说,系统的队列结构如下listen 队列    listen(1) -> listen(2) -> listen(3) -> listen(4) -> .......
        |           |           |      data(1)     data         data
        |           |      data(1)     data
每个listen本身就是一个数据包队列头recv_from_listen 函数会试图去从一个 listen 的数据包队列中获取数据,如果没有数据,则进入休眠状态。

主要函数接口:

void listen_head_init(struct list_head *head)
初始化一个 链表头
int listen_add(struct list_head *head, listen_t *listen)
将要监听的 listen 添加到这个链表头
recv_from_listen_head
从链表中获取数据12345678

示例:

//我们创建两个 listen_head 
    struct list_head poll_head_1, poll_head_2;    int main(int argc, char *argv[])
    {        int poll_num = 0;
        struct listen *_listen;
        /* 初始化socket */
        sockfd = init_socket();         /*
            开始监听这个socket. 运行最大的连接数为10
            该函数类似于TCP协议中的 int listen(SOCKET sockfd, int backlog)
        */
        server_listen(&sockfd, 10);
        /* 初始化这个poll 机制 */
        listen_head_init(&poll_head_1);        listen_head_init(&poll_head_2);        while(1)
        {            /* 获得一个连接。类似于TCP的
            int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 
            */
            _listen = server_accept();            if(_listen == NULL){
                continue;
            }            printf("new client \r\n");
            if(poll_num < 5)
            {                /* 前面五个连接者添加到 poll_head_1 */
                poll_num ++;                listen_add(&poll_head_1, _listen);            }else{
                /* 添加到 poll_head_2 */
                poll_num ++;                listen_add(&poll_head_2, _listen);            }        }    }```
 然后我们就可以从中两个 listen_head 中读取数据了
​```
    while(1)
    {        /*
        从 poll_head_1 中读取数据。
        此时,前面五个 listen 被挂钩到这个 poll_head_1,所以这五个listen中任何一个有了数据
        recv_from_listen_head 都会返回,而且将 _listen 指向这个 listen 
        这样,我们就可以知道是哪个listen有数据了
        */
        ret = recv_from_listen_head(poll_head_1, &_listen, (struct sockaddr *)&clientaddr, buf, 1204, -1);
        if(ret == -1)
        {            printf("%p recv is err \r\n", _listen);
        }else{
            printf("__ poll %p recv %d byte data is [%s]\r\n", _listen, ret, buf);
            if((ret = sendto(sockfd, buf, ret, 0, (struct sockaddr *)(&(_listen->addr)), 
                                    sizeof(struct sockaddr))) == -1)
            {                perror("sendto :");
            }            printf("sento [%s]\r\n", buf);
        }    }

 

标签:UDP,struct,队列,数据包,head,linux,服务器,recv,listen
来源: https://www.cnblogs.com/lidabo/p/14314612.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有