1. 程式人生 > 實用技巧 >UDP伺服器併發思路

UDP伺服器併發思路

UDP併發與TCP併發的區別

在TCP併發程式設計中,通常使用one loop per thread的併發模型,也就是使用多個執行緒,每個執行緒中都有一個epoll loop,無論是使用epoll還是poll或select,在觀察有無資料就緒時,都是針對多個檔案描述符。如果只有一個檔案描述符,那麼程序只要觀察那一個檔案描述符即可。

網路程式設計中,一個Socket對應一個檔案描述符。在TCP的併發中,伺服器在監聽埠初始化一個socket套接字描述符,接受客戶端後就與每個客戶端的連線有一個不同的檔案描述符,所以TCP併發中有多個socket套接字描述符。但是,UDP協議的伺服器沒有真正意義上的“連線”的概念。在訊息監聽埠和響應請求都只有一個socket套接字描述符。

UDP怎麼考慮併發?

《UNP · 卷1》中UDP章節描述了一句話:一般來說大多數TCP伺服器是併發的,而大多數UDP伺服器是迭代的。也就是伺服器等待客戶端的請求,然後讀取請求後處理,再發迴響應。如果是簡單的處理響應還可以,如果每個處理都很耗時,那麼就不得不考慮在UDP伺服器做併發處理。

併發常見的思路就是多執行緒。伺服器讀取一個新的請求後,可以交給一個執行緒處理,該執行緒在處理之後直接將響應內容發給客戶端。

雖然可以多執行緒處理讀取的每個訊息,但如果UDP伺服器與多個客戶端互動,卻沒有多個socket,這樣效率並不是很高。典型的解決方法就是:在伺服器為每個客戶端建立一個新的socket套接字並繫結一個新的埠,客戶端以後就需要以這個新的socket套接字與伺服器通訊。

總的來說:UDP併發伺服器針對多個客戶端,可以建立多個socket。針對多個請求,可以使用多執行緒(執行緒池)進行處理。

UDP 併發程式設計模型

  • 多個socket虛擬碼
for (; ;)
{
    //等待新的客戶端連線
    recvfrom(&from_addr);
    //每有一個新的客戶端,建立一個執行緒
    pthread_create(&tid, NULL, thread_fun, &from_addr);
}

//執行緒函式
void* thread_fun(void* arg)
{
    peer = socket(AF_INET, SOCK_DGRAM, 0);
    servaddr.sin_port = htons(0);	//繫結埠
    bind(peer, (struct sockaddr*)&servaddr, sizeof(servaddr));
    //將這個套接字和客戶端地址連線,之後就可以使用write/read或send/recv這些函式,且不用再關心客戶端地址
    connect(peer, (struct sockaddr*)&from, sizeof(from));
    //處理請求的loop
}
  • 使用epoll進行處理虛擬碼
  1. UDP伺服器建立socket,並設定socket為REUSEADDRREUSEPORT和非阻塞同時再bind伺服器地址local_addr:
listen_fd = socket(PF_INET, SOCK_DGRAM, 0);
fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFD, 0)|O_NONBLOCK)
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(listen_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
  1. 建立epoll fd,並將listen_fd新增到epoll中,並監聽其可讀事件:
epoll_fd = epoll_create(100);
ep_event.events = EPOLLIN | EPOLLET;
ep_event.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ep_event);
while (1) 
{
    in_fds = epoll_wait(epoll_fd, in_events, 1000, 5000);
  1. epoll_wait返回時,如果返回的是listen_fd, 呼叫recvfrom接受client第一個UDP包,並根據recvfrom返回client地址,建立一個新的socket套接字new_fd,設定new_fd為REUSEADDRREUSEPORT和非阻塞,同時bind本地地址local_addr然後connect上recvfrom返回的client地址:
    for (i = 0; i < in_fds; i++)
    {
        if(in_events[i].data.fd = listen_fd)
        {
            recvfrom(listen_fd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &client_len);
            new_fd = socket(PF_INET, SOCK_DGRAM, 0);
            fcntl(new_fd, F_SETFL, fcntl(new_fd, F_GETFD, 0)|O_NONBLOCK);
            setsockopt(new_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            setsockopt(new_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
            bind(new_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
            connect(new_fd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr));
  1. 將新建立的new_fd加入到epoll中並監聽其可讀事件:
            client_ev.events = EPOLLIN;
            client_ev.data.fd = new_fd;
            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &client_ev);
        }
        else if (in_events[i].events & EPOLLIN)
        {
  1. 當epoll_wait返回時,如果返回的是new_fd,那麼呼叫recvfrom來接收特定client的UDP包:
            recvfrom(new_fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&client_addr, &client_len);
            data->fd = new_fd;
            data-> ptr= process(recvbuf); /*data中包括socket資訊*/
            ev.data.ptr = data;
            ev.events = EPOLLOUT | EPOLLET;
            epoll_ctl(epoll_fd,EPOLL_CTL_MOD,new_fd,&ev);
        }
        else if (in_events[i].events & EPOLLOUT)
        {
            sockfd = data->fd;
            send( sockfd, data->ptr, strlen((char*)data->ptr), 0 );
            ev.data.ptr = data;
            ev.events = EPOLLIN | EPOLLET;
            epoll_ctl(epoll_fd,EPOLL_CTL_MOD,sockfd,&ev);
        }
        else
        {
        }
	}
}